通信--TCP从连接到关闭

98 阅读7分钟

计算机的不同部分承担不同功能,和通信相关的有如下层级。

  • 应用程序:包括浏览器、邮件客户端、Web服务器、邮件服务器等,有网络功能的应用程序具有封装好的 Socket 库用于网络传输。

  • 操作系统:包括协议栈,协议栈包括协议(TCP、UDP) 和 IP(传送网络包、确定路由)。

  • 驱动程序:网卡驱动程序(控制网卡)。

  • 硬件:网卡。

浏览器与服务器使用 TCP 传输数据的过程如下:

1. 创建套接字

协议栈的内部有一块用于存放控制信息的内存空间,记录了通信对象的 IP 地址、端口号(Web 服务器默认 80)、通信操作的进行状态,这些控制信息就是套接字,或者说这一块内存空间就是套接字。协议栈中可能存在多个套接字,不同套接字用套接字的描述符区分。

  • 浏览器通过 Socket 库的 socket 程序申请创建套接字;

  • socket 委托给协议栈;

  • 协议栈分配存放一个套接字的内存空间;

  • 向内存空间中写入初始状态的控制信息;

  • 将表示这个套接字的描述符告知浏览器。

2. 连接服务器

创建套接字之后,浏览器就会调用 Socket 库的 connect 程序,随后协议栈会将客户端的套接字与服务器的套接字进行连接。

  • 浏览器调用 connect(<描述符>, <服务器 IP 地址和端口号>,……);

  • connect 委托给客户端协议栈的 TCP 模块;

  • 客户端协议栈的 TCP 模块与服务器的 TCP 模块交换控制信息,创建 TCP 头部(第一次握手);

  1. 客户端创建一个包含表示开始数据收发操作的控制信息的头部,包含发送方和接收方的端口号,客户端的套接字就找到了服务器的套接字;
  2. 将头部的控制位的 SYN 比特设为 1,表示连接;
  3. 设置适当的序号和窗口大小;
  • 客户端 TCP 模块委托客户端 IP 模块发送信息;

  • 服务器 IP 模块接收到数据传递给服务器 TCP 模块;

  • 服务器 TCP 模块从 TCP 头部找到端口号和对应的服务器套接字;

  • 服务器套接字中写入相应信息,并将等待连接改为正在连接;

  • 服务器在 TCP 头部中设置发送方和接收方端口号以及控制位的SYN比特,并把控制位ACK比特设为 1(第二次握手);

  • 服务器 TCP 模块委托服务器 IP 模块返回响应信息;

  • 网络包通过客户端 IP 模块到达客户端 TCP 模块;

  • 通过 TCP 头部的信息确认连接服务器是否成功(SYN为 1 表示成功);

  • 客户端套接字中写入服务器的 IP 地址、端口号等信息,将状态改为连接完毕;

  • 客户端将 TCP 头部的ACK比特设为 1 并发送给服务器(第三次握手);

  • 服务器收到这个包之后,连接操作才算完成,控制流程从 connect 交回到浏览器。

3. 收发数据

当控制流程从 connect 回到浏览器之后,接下来就进入了数据收发阶段。

  • 浏览器调用 Socket 库的 write 将要发送的数据交给协议栈;

  • 将 HTTP 请求消息交给协议栈:

浏览器这种会话型应用程序会使用协议栈的直接发送选项(然而通常来说协议栈收到数据后并不立即发出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据,直到接近填满一个最大传输单元MTU 或经过一段时间之后发送网络包。协议栈的开发者自行决定是长度优先还是时间优先。协议栈也给应用程序保留了控制发送时机的余地,应用程序在发送数据时可以指定一些选项,如浏览器就指定了不等待缓冲区填满直接发送。);

  • 对较大的数据进行拆分:

当发送数据的长度超过了一个网络包所能容纳的数据量(以太网一个最大传输单元MTU 大概 1500 个字节,其中头部一般占 40 个字节,最大分段大小MSS 就是1460 个字节。),发送缓冲区中的数据会被以 MSS 长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中,当需要发送这些数据时,就在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给 IP 模块来执行发送数据的操作。

  • 使用 ACK 号确认网络包已收到:

网络包传输过程中可能会丢失,因此接收方需要返回一个确认收到的信息给发送方。TCP 模块在拆分数据时会先计算好每一块数据相当于从头开始的第几个字节,将算好的字节数写在 TCP 头部中。接收方用整个网络包的长度减去头部长度得到数据的长度,接收方根据序号和长度可以判断出网络包有没有遗漏,如果确认没有遗漏,接收方会将接收到的数据长度加起来,计算出一共收到了多少个字节,然后将这个数值写入 TCP 头部的 ACK 号中发送给发送方(返回 ACK 号时,出了要设置 ACK 号的值以外,还需要将控制位的 ACK 比特设为 1,这代表 ACK 号字段有效,接收方也就可以知道这个网络包是用来告知 ACK 号的。)。

  • 接收 HTTP 响应消息:

浏览器在委托协议栈发送请求消息之后,会调用 Soceket 库的 read 程序来获取响应消息,read 程序委托给协议栈。协议栈尝试从接收缓冲区中取出数据并传递给浏览器,然而这时候请求刚发送出去可能还没有返回响应消息。协议栈会将委托暂时挂起,等服务器返回的响应消息到达之后再继续执行接收操作。当数据到达后,协议栈会检查收到的数据块和 TCP 头部的内容,判断是否有数据丢失,如果没有问题则返回 ACK 号。然后协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始数据,最后把数据复制到浏览器指定的内存地址中,然后就将控制流程交回浏览器。

4. 从服务器断开连接并删除套接字

断开连接

  • 服务器调用 Socket 库的 close 程序;

  • 服务器的协议栈生成包含断开信息的 TCP 头部(将控制位中的 FIN 比特设为 1);

  • 服务器的协议栈委托 IP 模块向客户端发送数据,同时服务器的套接字中也会记录下断开操作的相关信息;

  • 客户端收到含有 FIN 为 1 的 TCP 头部时,客户端协议栈会将自己的套接字标记为进入断开操作状态;

  • 客户端会向服务器发送一个确认收到 FIN 为 1 的包的 ACK 号,之后协议栈就等待浏览器来取数据;

  • 浏览器调用 read 来读取数据,协议栈会告知浏览器来自服务器的数据已全部收到,浏览器会调用 close 来结束数据收发操作;

  • 客户端的协议栈生成一个控制位的 FIN 比特为 1 的 TCP 包,然后委托 IP 模块发送给服务器;

  • 服务器返回 ACK 号,客户端和服务器的通信全部结束。

删除套接字

和服务器的通信结束之后,用来通信的套接字也就不会再使用了,然而套接字不会被立刻删除,而是会等几分钟再被删除,这样是为了防止一些误操作。比如服务器发送 FIN 为 1 的包之后,客户端收到了,如果客户端在此时删掉了套接字,释放出的端口号又被其它新建的套接字使用了。然而客户端返回的 ACK 号丢失了,服务器一直没收到 ACK 号,那么服务器重发的 FIN 到达时,则又删除了客户端新建的套接字。

参考: 《网络是怎样连接的》 第2章 用电信号传输TCP/IP数据