前言

这个系列的博客是针对阅读《网络是怎样连接的》([日] 户根勤, 译 [中] 周自恒, ISBN: 9787115441249)一书的记录与思路整理。
有些概念是做纯路由交换的网络工程师很少接触但又很重要的部分,因此整理到这里以做分享。

协议栈委托全貌

应用程序做网络通讯需要委托操作系统协议栈来搭建数据通道并发送数据,这一过程需要按一定顺序调用socket 库中一系列的组件。

收发数据的两台计算机之间连接了一条数据通道,数据沿着这条通道流动,最终到达目的地。我们可以把数据通道想象成一条管道,将数据从一端送入管道,数据就会到达管道的另一端然后被取出。数据可以从任何一端被送入管道,数据的流动是双向的。

对于一个TCP 应用来说,可以总结成以下4个通讯过程:

  1. 创建套接字
  2. 将管道连接到服务器端的套接字上
  3. 收发数据
  4. 断开管道并删除套接字

前面这 4 个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序并不会自己去做连接管道、放入数据这些工作,而是委托协议栈来代劳。此外,这些委托的操作都是通过调用 Socket 库中的程序组件来执行的,但这些数据通信用的程序组件其实仅仅充当了一个桥梁的角色,并不执行任何实质性的操作,应用程序的委托内容最终会被原原本本地传递给协议栈。因此,我们无法形象地展示这些程序组件到底完成了怎样的工作,与其勉强强调 Socket 库的存在,还不如将 Socket 库和协议栈看成一个整体并讲解它们的整体行为让人更容易理解。

创建套接字阶段

客户端创建套接字的操作非常简单,只要调用Socket 库中的socket 程序组件就可以了。调用socket 之后,控制流程会转移到socket 内部并执行创建套接字的操作,完成后控制流程又会移交回应用程序。

“书中出现了 Socket、socket、套接字(英文也是 socket)等看起来非常容易混淆的词,其中小写的 socket 表示程序组件的名称,大写字母开头的 Socket 表示库,而汉字的“套接字”则表示管道两端的接口。”

套接字创建完成后,协议栈会返回一个描述符,应用程序会将收到的描述符放到内存中。描述符是用来识别不同的套接字的。

计算机中会同时进行多个数据的通信操作,例如可以打开两个浏览器窗口,同时访问两台Web 服务器。这时,计算机内部有两个数据收发操作在同时进行,也就需要创建两个不同的套接字,进一步思考就必须有一种手段来识别不同的套接字,这个方法就是描述符。
想象一下在酒店寄存行李的场景,酒店服务人员会分给你一个号码牌,向服务人员出示号码牌,就可以取回自己寄存的行李,描述符的远离和号码牌差不多。

连接阶段

应用程序通过调用Socket 库中的connect 程序,委托协议栈将客户端创建的套接字与服务器的套接字进行连接。

调用connect 需要三个参数:

  1. 描述符,在创建套接字时由协议栈返回的;协议栈通过描述符来判断应该对哪个套接字进行操作
  2. 服务器IP 地址,在进行数据收发操作时,双方必须知道对方的IP 地址并告知协议栈。
  3. 端口号,对端服务器套接字拥有的端口号。

既然可以用创建套接字时,协议栈返还的描述符来标识每个套接字,那么访问远程服务器上的一个套接字为什么还需要用到端口呢?
先说结论,不能用描述符来替代端口号。
这是因为,描述符是和委托创建套接字的应用程序进行交互时使用的,另一方无法得知应用程序从协议栈获取了什么描述符(本地有效)。所以,我们需要另外一个对客户端也同样适用的机制,而这个机制就是端口号。

端口号其实也是一个本地有效的编号,但是我们通过将常用服务的端口号实现规定好的方法来使之可以供客户端远程访问。例如HTTP 的端口号默认是80,HTTPS 的端口号默认是443。

同样的,既然确定连接对象的套接字需要使用端口号,那么服务器也必须知道客户端套接字的端口号才行。
客户端在创建套接字的时候,协议栈会为这个套接字随机分配一个端口号。接下来,当协议栈执行连接操作时,会将这个随机分配的端口号通知给服务器。

描述符:应用程序用来识别套接字的机制;
IP 地址和端口号:客户端和服务器之间用来识别对方套接字的机制。

通信阶段 - 消息传递

当套接字连接起来之后,剩下的事情就是将数据送入套接字。应用程序需要调用Socket 库中的write 程序来执行这个操作。

数据发送

首先应用程序需要在内存中准备好要发送的数据。
在HTTP 的例子中,根据用户输入的网址生成的HTTP 请求消息就是我们要发送的数据。

接下来应用程序调用write 时,需要制定描述符和发送数据,然后协议栈就会将数据通过指定的协议栈通道发送到服务器。由于套接字中已经保存了已连接的通信对象的相关信息,所以只要通过描述符指定套接字,就可以识别出通信对象,并向其发送数据了。

接着,发送的数据会通过网络到达我们要访问的服务器。

数据返回

服务器执行接受操作,解析收到的数据内容并执行相应的操作,然后向客户端返回响应消息。

接收消息的操作是应用程序通过Socket 库中的read 程序组件委派协议栈完成的。

调用read 时需要指定用于存放接收消息的内存地址,这一内存地址被称之为缓冲区。于是,当服务器返回响应消息时,read 就会负责将接收到的响应消息存放到缓冲区中。
而由于缓冲区是一块位于应用程序内部的内存空间,因此当消息被存放到缓冲区中时,就相当于数据已经转交给了应用程序。

断开阶段

当数据收发完毕,服务器和客户端会有一方主动发起断开。应用程序需要调用Socket 库中的close 程序来完成此操作。最终,连接在套接字之间的管道会断开,而客户端的套接字本身也会被删除。

“断开的过程如下。Web 使用的 HTTP 协议规定,当 Web 服务器发送完响应消息之后,应该主动执行断开操作,因此 Web 服务器会首先调用 close 来断开连接。断开操作传达到客户端之后,客户端的套接字也会进入断开阶段。接下来,当浏览器调用 read 执行接收数据操作时,read 会告知浏览器收发数据操作已结束,连接已经断开。浏览器得知后,也会调用 close 进入断开阶段。” - HTTP 1.0

“这就是 HTTP 的工作过程。HTTP 协议将 HTML 文档和图片都作为单独的对象来处理,每获取一次数据,就要执行一次连接、发送请求消息、接收响应消息、断开的过程。因此,如果一个网页中包含很多张图片,就必须重复进行很多次连接、收发数据、断开的操作。对于同一台服务器来说,重复连接和断开显然是效率很低的,因此后来人们又设计出了能够在一次连接中收发多个请求和响应的方法。在 HTTP 版本 1.1 中就可以使用这种方法,在这种情况下,当所有数据都请求完成后,浏览器会主动触发断开连接的操作。”