从socket看网络编程:
最近开始做了一段时间php,就是自己做页面并且写接口之后才发觉作为一个客户端开发人员,由于各种原因,对计算机网络的知识确实是比较薄弱的。刚做php的时候因为各种不懂真是很痛苦。可是网络对我们真是一个无比重要的东西。以下我就从socket讲讲与客户端开发比较密切的网络编程吧。希望能从网络的软件编程给大家普及一些计算机网络的知识。
我们知道网络编程有建立连接,传送数据和断开连接这几步,其中建立连接又分为三次握手,断开连接分为四次握手。当然这些都是理论,那么在计算机中的建立连接,传送数据和断开连接用编程是如何实现的呢,Then show you the code~!
下面是socket编程的流程图,左边是客户端需要做的,右边是服务端需要做的,典型的CS(client-server)架构。不懂没关系,我会慢慢介绍:
先来介绍一下socket编程主要五个函数,分别是 socket,connect,bind,listen,accept:
先讲 socket 函数,这个函数就是创建一个socket(即在系统注册一个I/O接口)下图是 socket 函数的原型:
其中第一个参数 domain 是需要使用的协议,其中 AF_INET 宏代表 ipv4 地址(即32位地址);AF_INET6宏代表 ipv6 地址(即128位地址)。
第二个参数 type 用来区分这个 socket 的作用,可以是 SOCK_STREAM 宏,表示这个套接字是连接的一个端点。
第三个参数 protocol 代表使用的应用层协议 (比如HTTP)。
返回值是一个半打开的描述符,还不能用于读写(关于描述符大家可以对比一下文件描述符,我们需要读写一个文件的时候也会给我们一个文件描述符用于读写。因为对计算机来说获取网络数据也属于I/O的一种,所以操作系统对I/O统一使用文件描述符来做操作来简化它),还需要配合后面的函数才能读写。
由此看 socket 就是告诉系统我需要一个:使用ipv4/ipv6地址结构的,用于客户端/服务器的,基于HTTP/SMTP...协议的网络 I/O 描述符。
然后讲 connect 函数,这个函数的作用是和服务器建立连接,由此可以猜出它是客户端使用的函数,下图是函数原型:第一个参数就是上一个函数 socket 函数的返回值,即一个 I/O 描述符。
第二个参数是一个地址串,你可以想想它就是一个IP地址(比如域名:www.baidu.com的IP地址是 111.13.100.91),一般情况下一个域名就一个地址,但是很多网站是一个域名有多个IP地址的,或者多个域名多个IP地址,其实百度的就是,让我们看看百度的:
命令行输入: NSLOOKUP www.baidu.com
看 answer 部分,可以看到域名是 www.a.shifen.com ,这个可以看作是一个外壳,防攻击用的。然后可以看到 有 111.13.100.92 和 111.13.100.91 两个Ip地址,所以这里的 addr 是一个链表,当连接的时候会对同一域名下的多个IP逐个尝试直到连接成功。
第三个参数就是地址长度了。
返回值仍然是一个 I/O 描述符。但是如果连接成功,这个 I/O 描述符就是可以读写了的。由此可知客户端部分已经完成了,即客户端到这一步已经是可以和服务器传数据了。
以上是客户端需要的函数,下面讲服务端需要的函数:
注意,无论客户端还是服务端都必须先调用 socket 函数获得一个 I/O 描述符,其它的函数都是针对这个 I/O 描述符进行的操作(可以想象成装饰器)。
第一个是 bind 函数,它用来给服务器绑定一个IP地址和端口号,也许你要问为什么服务端需要这个函数而客户端不需要,因为服务端一旦开启一个服务,它的地址和端口就是固定的,这样别人才可以通过这个地址和端口找到这个服务器的某个程序。而客户端就不需要了,客户端甚至可以使用即使分配的IP地址和端口号。就像寄信一样,你给某个大V写信需要知道他的确切地址才能给别人送信,而别人给你回信只需要用你送过去信上写的地址就可以了。
下图是 bind 函数的原型:
和 connect 函数一样需要一个 I/O 描述符,一个 addr 地址的链表和一个地址长度。bind 函数之后操作系统就会为这个 I/O 描述符绑定一个 I/P 地址和一个端口号,这样从网络来的数据才能用这个 I/O 描述符进行操作。
接下来是 listen 函数:
顾名思义,就是监听的意思,想想我们的 observer 模式,这个函数的作用就是监听是否有客户端连接请求,有的时候告诉操作系统去做一些事情。
下图是函数原型:
第一个参数也是 I/O 描述符,第二个参数是用来限制最大连接数量用的。
最后一个是 accept 函数,当有客户端的连接请求被 listen 监听到之后,如果同意这个连接,就会调用 accept 函数建立连接,如果 accept 成功了那和客户端的连接就成功了。
下图是函数原型:
和 connect 函数一样,只不过这里的意义是:同意使用这个 I/O 描述符,和来自 addr 地址链表中长度为 addrlen 地址的客户端建立连接。
以上就是socket的所有函数,其实最主要的就这5个。需要澄清的就是 listen 函数一般只在服务端开启服务的时候调用一次,然后不断监听,但是 accept 是会调多次的,每一个连接都会调一次然后开一个子服务去执行与这个连接相关的事情,主服务仍然在监听来自客户端的请求来实现并发。
我们平时都是习惯使用域名来访问服务器,比如 www.baidu.com ,但是实际访问的时候是有一次转换的,会先把 www.baidu.com 这个域名转换成它的 IP 地址比如 111.13.100.91 然后再用这个 IP 地址去对百度进行访问,这个转换也是通过一次网络请求实现的,一般是向离你最近的服务商服务器请求(如果是电信,就是向电信的DNS请求)。过程就是向这个DNS发送 www.baidu.com 然后DNS给你返回对应的IP地址,并且离你越近的DNS服务器返回给你的IP地址优先使用(其实最近的是本机的 hosts 文件,路径是 /etc/hosts )。
基于以上,我们讲讲两个用于域名/IP和方便 socket 连接使用的结构体 addrinfo 相互转化的函数:
先附上 addrinfo 结构体:
字段比较多,不懂没关系,只要知道这个结构体的作用是方便我们调用 socket 函数就行了。
第一个是使用域名获取 IP 地址的 getaddrinfo 函数,以下是函数原型:
一共四个参数,
第一个参数 host 是个可选参数,内容是域名或者十进制的 IP 地址
第二个参数也是可选参数,代表服务名或者是端口号(比如HTTP服务,和本机的80端口),但是第一个和第二个参数必填一个(如果是某个确定的HTTP服务或者端口,端口在运输层,HTTP在应用层,一个确定的高层服务必定包含了低层的内容所以就可以不需要域名或IP地址了)。
第三个参数是一个 addrinfo 结构体其中包含了是 ipv4/ipv6,以及这是一个客户端还是服务端用的地址信息。
第四个参数是一个空的二级指针,用于接收转化好的地址结构体链表。
返回值 0 是成功,非 0 是失败。
然后是 getnameinfo 函数,这个函数是把 addrinfo 结构体转化成域名/IP地址或者HTTP服务名等,以下是函数原型:
第一个参数是需要转化的 addrinfo 结构体(只支持单个结构体),第二个参数是 addrinfo 地址长度,第三个和第四个参数是用来接收结果 host 域名的指针,如果是 HTTP等服务的话就需要填第五第六个参数,第七个参数 flags 代表需要的结果是域名还是I/P地址;
同样返回值0代表成功,非0代表失败。
需要用的函数都讲完了,接下来将两个应用,一个是 open_clientfd(char *hostname, char *port) 函数,用于客户端向服务器建立一个连接并在成功后返回一个 I/O 描述符。另一个 open_listenfd(char *port) 函数,用于在服务器创建一个监听端口等待客户端连接并在创建成功后返回一个I/O 描述符。以下是函数实现:
其实只需要关注我用箭头标出来的三个地方就好了
可以看到,输入是域名和端口号,然后其实就几步:
第一步使用域名等信息获取 addrinfo 结构体链表(因为一个域名多个I/P等情况);
第二步就是遍历这个 addrinfo 结构体链表;
第三步就是每次遍历中建立一个socket;
第四步就是用这个 socket 去建立连接,只要成功就跳出这个循环然后释放资源返回就行(CF里的东西是需要自己释放资源的);
这就完成了客户端与服务器的连接,很简单吧,剩下的就是使用这个函数返回值的 I/O 描述符和服务端进行通信了,通信完毕后需要断开连接(就和读写文件遇到 EOF 就结束一样,这里也有一个和 EOF 一样的结束符,遇到这个结束符就代表通信完毕)。
接下来讲服务端要做的事,将你的电脑看作一台服务器,想想你要等待客户端连接并通信需要做什么:
以下是服务端要用的 open_listenfd(char *port) 函数原型:
需要关注的我也标出来了,同样就几步:
第一步转化出需要的 addrinfo 结构体链表;
第二步遍历这个链表;
第三步在遍历中创建 socket 并给 I/O 描述符绑定一个IP地址和端口号,如果成功就跳出循环。
第四步将这个I/O描述符设置为监听客户端连接的状态,如果都成功了就返回。
其实服务端要和客户端建立通信连接也就这几步,遇到 EOF 就中断连接。但是其实网络是不稳定的,所以在此之外有大量的网络异常状况需要处理,比如突然断网等等情况。
下面将两个完整应用,一个是客户端获取用户输入传给服务器,另一个是服务器获取客户端数据并输出;
下图是客户端程序:
下图是服务端程序:
就三步,开启监听程序,建立连接,和客户端进行读写的 echo 函数.
下图是 echo 函数:
到此socket编程其实已经讲完了,希望对大家有帮助,后面的内容属于拓展,有兴趣的朋友可以了解以下。
拓展:首先讲讲服务器静态内容和动态内容(其实对我们来说大部分情况就是H5的静态页面和动态页面)。
静态内容其实就是磁盘文件,访问静态内容其实就是服务器把它磁盘上的一个文件传给你。比如:
http://www.google.com:80/index.html 就是从80端口访问谷歌网站根目录下的 index.html 文件(一般服务端的入口文件都是这个,如果只填http://www.google.com:80/ 或者 http://www.google.com:80 一般访问的也都是这个文件)。
以下是一些静态内容格式:
动态内容其实就是你告诉服务器执行服务器某个路径下的一个程序,然后把执行的结果返回给你:
比如:http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213 就是从8000端口访问 http://bluefish.ics.cs.cmu.edu 根目录下 cgi-bin/adder 路径的程序,参数是 15000 和 213
然后讲讲HTTP请求
其实一个HTTP请求很简单,就是 URL,请求行,请求头,请求体几部分。一个最简单的GET请求如下
GET / HTTP/1.1
Host: www.aol.com
一个 HTTP 请求头以 一个请求行开始,中间多个请求抱头(需要什么根据业务而定),最后以一个空行结束
请求行格式为: method URI HTTPVersion
这这里就是: GET / HTTP/1.1
GET代表GET请求, / 代表根目录 , HTTP/1.1 则是1.1版本的HTTP协议。
其中支持的有GET, POST, OPTIONS, HEAD, PUT, DELETE, TRACE等方法。
最常用的就是GET和POST,GET用于简单的获取,POST用于一些保密性数据上传,大量信息的上传等。
OPTIONS方法用途主要两个:1、获取服务器支持的HTTP请求方法;2、用来检查服务器的性能。
HEAD方法用来获取请求头信息,一般我们做文件下载的时候会先获取请求头信息再做下载决定。
PUT是用来提交信息的不常用,DELETE方法用于删除。
最后讲讲响应报文,和请求报文一样有响应行,响应报头和响应主体。如下图:
响应行的格式是:HTTPversion statusCode statusMessage
这里就是 HTTP/1.0 200 OK
200是状态码,代表成功,OK是附加的消息,可以在服务器设置。下图是一些常见状态码:
然后是响应头,其中我们只需要关注 Content-Type: 代表传输数据的类型, 以及 Content-Length:代表返回数据的长度。
最后是响应体:响应体就是返回的数据,如果是浏览器访问这就是要渲染的HTML界面的代码。如果是客户端的接口这里就是JSON数据。
最后讲讲服务器怎么执行动态内容:
比如:http://bluefish.ics.cs.cmu.edu:8000/cgi-bin/adder?15000&213 这个请求,服务器收到这个请求后,调用 fork 函数创建一个子进程,然后在这个子进程中调用 execve 函数执行 /cgi-bin/adder 路径的程序,然后一个叫CGI(通用网关接口)的程序把环境变量设置为 15000&213 这样程序执行过程中就可以使用这两个参数了。同时CGI程序还把子进程的输出重定向到与客户端连接的 I/O 描述符,这样输出内容就会直接发送到客户端了。对于POST请求,CGI程序会将子程序输入重定向到和客户端连接的 I/O 描述符以读区POST请求体中的信息。
以上就是socket编程的内容了,下一期我将针对iOS的AFN框架讲讲网络编程。希望对大家有帮助。