1.4 委托协议栈发送消息
要发送给Web服务器的HTTP消息是一种数字信息(digital data),因此也可以说是委托协议栈来发送数字信息。收发数字信息这一操作不仅限于浏览器,对于各种使用网络的应用程序来说都是共通的。因此,这一操作的过程也不仅适用于Web,而是适用于任何网络应用程序。
向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用Socket库中的程序组件。
建立管道的关键在于管道两端的数据出入口,这些出入口称为套接字。我们需要先创建套接字,然后再将套接字连接起来形成管道。实际的过程是下面这样的。首先,服务器一方先创建套接字,然后等待客户端向该套接字连接管道。当服务器进入等待状态时,客户端就可以连接管道了。
当数据全部发送完毕之后,连接的管道将会被断开。管道在连接时是由客户端发起的,但在断开时可以由客户端或服务器任意一方发起。其中一方断开后,另一方也会随之断开,当管道断开后,套接字也会被删除。到此为止,通信操作就结束了。
创建、连接、通信、关闭这4个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序并不会自己去做连接管道、放入数据这些工作,而是委托协议栈来代劳。
书中出现了Socket、socket、套接字(英文也是socket)等看起来非常容易混淆的词,其中小写的socket表示程序组件的名称,大写字母开头的Socket表示库,而汉字的“套接字”则表示管道两端的接口。
创建套接字阶段
调用socket之后,控制流程会转移到socket内部并执行创建套接字的操作,完成之后控制流程又会被移交回应用程序。
套接字创建完成后,协议栈会返回一个描述符,应用程序会将收到的描述符存放在内存中。
当创建套接字后,我们就可以使用这个套接字来执行收发数据的操作了。这时,只要我们出示描述符,协议栈就能够判断出我们希望用哪一个套接字来连接或者收发数据了。(一台计算机可能同时存在多个套接字)
应用程序是通过“描述符”这一类似号码牌的东西来识别不同套接字的。
连接阶段:把管道接上去
connect(描述符, 服务器 ip 地址, 端口号)
- 第1个参数,即描述符,就是在创建套接字的时候由协议栈返回的那个描述符。connect会将应用程序指定的描述符告知协议栈,然后协议栈根据这个描述符来判断到底使用哪一个套接字去和服务器端的套接字进行连接,并执行连接的操作。
- 第2个参数,即服务器IP地址,就是通过DNS服务器查询得到的我们要访问的服务器的IP地址。
- 第3个参数,即端口号。只要知道了IP地址,我们就可以识别出网络上的某台计算机。但是,连接操作的对象是某个具体的套接字,因此必须要识别到具体的套接字才行,而仅凭IP地址是无法做到这一点的。我们打电话的时候,也需要通过“请帮我找一下某某某”这样的方式来找到具体的某个联系人,而端口号就是这样一种方式。当同时指定IP地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。(服务器的端口号会根据应用的种类事先设定好;协议栈会为客户端的套接字随便分配一个端口号)
我们需要委托协议栈将客户端创建的套接字与服务器那边的套接字连接起来。应用程序通过调用Socket库中的名为connect的程序组件来完成这一操作。
描述符是和委托创建套接字的应用程序进行交互时使用的,并不是用来告诉网络连接的另一方的,因此另一方并不知道这个描述符。
如果说描述符是用来在一台计算机内部识别套接字的机制,那么端口号就是用来让通信的另一方能够识别出套接字的机制。
客户端在创建套接字时,协议栈会为这个套接字随便分配一个端口号。接下来,当协议栈执行连接操作时,会将这个随便分配的端口号通知给服务器。
描述符:应用程序用来识别套接字的机制
IP地址和端口号:客户端和服务器之间用来识别对方套接字的机制
通信阶段:传递消息
3.1 首先,应用程序需要在内存中准备好要发送的数据。根据用户输入的网址生成的HTTP请求消息就是我们要发送的数据。接下来,当调用write时,需要指定描述符和发送数据(图1.18③),然后协议栈就会将数据发送到服务器。由于套接字中已经保存了已连接的通信对象的相关信息,所以只要通过描述符指定套接字,就可以识别出通信对象,并向其发送数据。接着,发送数据会通过网络到达我们要访问的服务器。
3.2 接下来,服务器执行接收操作,解析收到的数据内容并执行相应的操作,向客户端返回响应消息。
3.3 当消息返回后,需要执行的是接收消息的操作。接收消息的操作是通过Socket库中的read程序组件委托协议栈来完成的(图1.18③')。调用read时需要指定用于存放接收到的响应消息的内存地址,这一内存地址称为接收缓冲区。于是,当服务器返回响应消息时,read就会负责将接收到的响应消息存放到接收缓冲区中。由于接收缓冲区是一块位于应用程序内部的内存空间,因此当消息被存放到接收缓冲区中时,就相当于已经转交给了应用程序。
断开阶段:收发数据结束
当浏览器收到数据之后,收发数据的过程就结束了。接下来,我们需要调用Socket库的close程序组件进入断开阶段(图1.18④)。最终,连接在套接字之间的管道会被断开,套接字本身也会被删除。
HTTP协议将HTML文档和图片都作为单独的对象来处理,每获取一次数据,就要执行一次连接、发送请求消息、接收响应消息、断开的过程。因此,如果一个网页中包含很多张图片,就必须重复进行很多次连接、收发数据、断开的操作。对于同一台服务器来说,重复连接和断开显然是效率很低的,因此后来人们又设计出了能够在一次连接中收发多个请求和响应的方法。在HTTP版本1.1中就可以使用这种方法,在这种情况下,当所有数据都请求完成后,浏览器会主动触发断开连接的操作。
服务器来说流程主要分为套接字初始化(socket()),套接字与端口的绑定(bind()),设置服务器的侦听连接(listen()),接受客户端连接(accept()),接收和发送数据(read()、write())并进行数据处理及处理完毕的套接字关闭(close())
客户端来说分为套接字初始化(socket()),连接服务器(connect()),读写网络数据(read()、write())并进行数据处理和最后的套接字关闭(close())过程。
所以两者的区别在与客户端在创建了套接字之后不进行地址绑定,而是直接连接服务器端。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回–1。
参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信的协议族,通信的协议族在文件sys/socket.h定义,包含下表所示的值,以太网应该设置PF_INET这个域,在程序设计的过程中会发现有的代码使用了AF_INET这个值,在头文件中AF_INET和PF_INET的值是一致的。
参数type用于设置套接字通信的类型,主要有SOCK_STREAM(流式套接字),SOCK_DGRAM(数据包套接字)。
函数socket()并不总是执行成功,有可能会出现错误,错误的原因有很多种,可以通过errno获得。在TCP中可以通过socket(AF_INET,SOCK_STREAM,0)返回一个TCP的套接字文件操作符。