更多文章分享在个人微信公众号:极客熊猫
欢迎扫码关注:

引言

在《TCP的三次握手与四次挥手》中,我们已经了解了一个TCP连接的建立与终止的规则及这个过程中发送的各个类型的报文段。这些决定TCP应该做什么的规则其实是由当前TCP连接所属的状态决定的。当前的状态会在各种触发条件下发生改变。常见的触发条件如:

  • 传输或接收到某报文段;
  • 计时器超时;
  • 客户端/服务端应用程序的读写操作;
  • 来自其他层的信息。

这些规则可以概括为TCP的状态转换图。

本文就以TCP状态转换为出发点,再探TCP的连接管理相关内容。

TCP状态转换图

TCP定义了11种状态,状态名字基于netstat命令所输出的名称。

CLOSED状态作为开始状态点和终止状态点,但它并不能算一个“官方”状态。

典型TCP过程

所谓典型TCP过程,这个词是我定的,即上图中黑线表示的过程的有序集合。这些黑线组成的典型TCP过程不考虑同时打开与关闭、重置等特殊情况。本节描述这个过程。

可以看到除了CLOSING,其他10种状态均在典型TCP过程中。

  • 开始时,客户端/服务端均处于CLOSED状态;
  • 服务端进程启动,调用listen函数后,服务端由CLOSED转换为LISTEN;
  • 客户端进程启动,调用connect函数后,TCP三路握手建立连接过程被激起,客户端发送第一路握手请求的SYN报文段,然后由CLOSED转换为SYN_SENT;
  • 服务端接收到客户端发来的SYN报文段,发起第二路握手请求,向客户端发送SYNACK报文段,然后由LISTEN转换为SYN_RCVD;
  • 客户端接收到服务端发来的SYNACK报文段,发起第三路握手请求,向服务端发送ACK报文段,然后由SYN_SENT转换为ESTABLISHED,至此,客户端已完成连接;
  • 服务端收到客户端发来的ACK报文段,也由SYN_RCVD转换为ESTABLISHED,至此双方连接建立完成;
  • ESTABLISHED是通信双方双向传输数据的状态;

尽管双方均可发起主动关闭操作,但我们以客户端负责执行主动关闭为例。

  • 数据传输结束,客户端调用close函数关闭套接字描述符,激起TCP四路握手关闭连接的过程,客户端发送第一路握手的FIN报文段,然后由ESTABLISHED转换为FIN_WAIT_1;
  • 服务端收到客户端发来的FIN报文段,发起第二路握手,向客户端发送ACK报文段,然后由ESTABLISHED转换为CLOSE_WAIT;
  • 客户端收到服务端发来的ACK报文段,什么都不发送,由FIN_WAIT_1转换为FIN_WAIT_2;
  • 服务端调用close函数,发起第三路握手,向客户端发送FIN报文段,由CLOSE_WAIT转换为LAST_ACK;
  • 客户端收到服务端发来的FIN报文段,发起第四路握手,向服务端发送ACK报文段,由FIN_WAIT_2转换为TIME_WAIT;
  • 服务端收到客户端发来的ACK报文段,什么都不发送,由LAST_ACK转换为CLOSED,至此服务端关闭;
  • 客户端等待2MSL,计时器超时后,客户端由TIME_WAIT转换为CLOSED,至此双方连接彻底关闭。

非典型TCP过程

TCP状态转换图中还有一些非典型过程,在图中用蓝色表示。下边我们描述一下这些部分。这些非典型过程包括以下内容:

  • 连接建立超时
  • 同时打开
  • 同时关闭
  • 重置报文段(RST)

连接建立超时

有时会存在连接不能建立的情况,比如服务器关闭的情况。

当客户端发送SYN报文段,但迟迟得不到回应的时候,客户端就会频繁地发送SYN报文段,直到达到限定的次数,客户端放弃与服务端进行连接,由SYN_SENT转换为CLOSED

Linux系统默认重试次数为5次。

指数回退:首个SYN报文段发送后3秒发送第二个SYN报文段,第二个报文段发送后6秒后发送第三个SYN报文段,第三个报文段发送后12秒后发送第四个SYN报文段,以此类推,即每一次回退数值都是前一次的两倍。

同时打开过程

TCP支持双方同时打开的情况,要实现同时打开,有两个要求:

  • 通信双方均有彼此的套接字地址结构sockaddr_in;(正常情况下,只有客户端知道服务端的套接字地址,而服务端不知道客户端的。);

  • 通信双方在收到来自对方的SYN报文段之前必须先发送一个SYN报文段。

由于双方均同时扮演了客户端与服务端的角色,所以不能将任何一方称为客户端或服务端。

如上图所示,同时打开的过程如下:

  • 双方在CLOSED状态时,通过调用connect函数,均在接收到对方的SYN报文段之前,自己就先发送了一个SYN报文段,双方均进入SYN_SENT状态;
  • 在接收到对方发来的SYN报文段后,双方均向彼此发送SYNACK报文段,并进入SYN_RCVD状态;
  • 双方在接收到彼此的SYNACK报文段后,均进入ESTABLISHED状态,连接建立完成。

可以看到,同时打开过程需要交换四个报文段,比普通的三路握手增加了一个。

同时关闭过程

在接收到对方发来的FIN报文段之前,双方均向对方发送FIN报文段,这会触发同时关闭过程。

如上图,同时关闭过程如下:

  • 双方在ESTABLISHED状态时,通过调用close函数,均在收到对方的FIN报文段之前,向对方发送了FIN报文段,双方均进入FIN_WAIT_1状态;
  • 双方并没有如预期收到对方的ACK报文段,而是收到了FIN报文段,双方均向对方回应ACK报文段,均进入CLOSING状态;
  • 双方收到对方发来的ACK报文段后,均进入TIME_WAIT状态,待2MSL超时后,进入CLOSED状态,至此连接彻底关闭。

FIN报文段还包含一个ACK段用于确认对方最近一次发来的数据。

可以看到同时关闭过程与正常关闭过程交换相同数量的报文段,二者的区别在于:

  • 正常关闭过程中报文段序列是不交叉的,一个发,另一个收到之后再发;
  • 同时关闭过程中报文段序列是交叉的,一个发的同时另一个也再发。

同时关闭过程用到了一个正常过程中没有的状态:CLOSING。

重置报文段(RST)

一个将TCP头部中的RST字段置1的报文段称为重置报文段,它用于关闭那些已经没有必要继续存在的连接。

以下是常见的产生重置报文段的场景:

  • 客户端发起一个连接请求,服务端却没有相应的进程在目的端口监听时,服务端就会给该客户端发送一个重置报文段;

  • 终止一条连接。在任何时刻均可以发送一个重置报文段替代FIN来终止一条连接,通过这种方式终止连接时,任何排队的数据都将被抛弃,重置报文段立即发送出去;

  • 在不告知另一方的情况下,通信的一端关闭或终止了连接,将导致这条TCP连接处于半开状态。这通常发生在通信一方的主机崩溃的情况下。这种情况下只要不尝试通过这条半开连接传输数据,正常工作的一端将不会检测到另一端已经崩溃(因为崩溃的一端连重置报文段或者FIN报文段都没办法发出去)。这时如果崩溃的一端重新连接,它对这条连接上另一端发送过来的数据一无所知,TCP规定此时崩溃一方将回复一个重置报文段以关闭这个连接。

  • TCP的TIME_WAIT状态的目的是让任何一个受制于与数据相关的关闭连接的数据被丢弃。在这段时期,等待的一方通常不需要任何操作,它只需要维持当前状态直到2MSL的计时结束。然而,如果它在这段时期内接收到来自于这条连接的一个重置报文段时,它的TIME_WAIT状态就会被破坏而提前进入CLOSED状态。

    为什么连接的被动关闭方会发送重置报文段呢?在连接的主动关闭方进入TIME_WAIT状态前,它回复一个ACK以告知被动关闭方自己已经接收到FIN报文段,被动关闭方收到这个ACK后随即进入CLOSED状态,此时主动关闭方还在TIME_WAIT状态等待2MSL计时结束。在这个时期,网络中可能存在延时,被动关闭方之前发送的对某数据的ACK在这个时候才姗姗来迟,此时这个ACK对处于TIME_WAIT状态的主动关闭方来说是旧的消息,因此它会发送一个ACK作为响应,其中包含了最新的序列号与ACK值。已经处于CLOSED状态的被动关闭方收到这个ACK后就会发送一个重置报文段作为响应。这会导致另一端的TIME_WAIT状态过早结束而进入CLOSED状态。解决这个错误的最简单方法就是让处于TIME_WAIT状态的TCP连接不对重置报文段做出响应。

TCP其他问题

TIME_WAIT状态

在TIME_WAIT状态中,TCP将会等待两倍于最大段生存期(MSL)的时间,MSL代表任何报文段在被丢弃前在网络中被允许存在的最长时间。

TIME_WAIT状态有两个存在的理由:

  • 可靠地实现TCP全双工连接的终止;

第一个理由可以通过断开连接时客户端最终发送给服务端的,以响应服务端的FIN报文段的ACK报文段丢失来解释。服务端迟迟收不到ACK,服务端就重发它的FIN报文段,如果没有TIME_WAIT状态,客户端就直接处于CLOSED状态了,那客户端就没有该连接的信息了,客户端就会回复给服务端一个重置报文段,这就不是正常断开连接的过程了;所以客户端需要有一个TIME_WAIT状态,来维护这个连接,从而可以发送ACK来响应服务端重传过来的FIN报文段。

  • 允许老的重复分组在网络中消逝。

第二个理由解释:如果一条TCP连接关闭之后,双方再以相同的四元组建立新连接,如果旧连接中有一些分组还在网络中,由于IP地址和端口号相同,这些分组就可能到达新连接,这样就会产生混乱。为了避免这个情况,设置TIME_WAIT状态,等待2MSL时间,在这段时间内,旧连接的四元组仍被占用,无法用相同的四元组建立新连接,这样就避免了新旧连接的数据混乱;同时,2MSL的时间足够让旧连接的分组在网络中消逝,这样再用相同的四元组建立连接时,就可以保证新连接不会接受到旧连接的分组。

listen函数的第二个参数

#include <sys/socket.h>
int listen(int sockfd, int backlog);
//成功返回0,出错返回-1

backlog参数规定了内核应为sockfd排队的最大连接个数,为了理解backlog参数,我们必须认识到内核为每一个监听套接字维护两个队列:

  • 未完成连接队列:那些处于SYN_RCVD状态的套接字组成的队列。即客户端发来的SYN报文段已到达服务端,服务端正在等待完成三路握手过程;
  • 已完成连接队列:那些处于ESTABLISHED状态的套接字组成的队列,即每个已完成三路握手过程的客户端对应该队列中的一项。

上述两队列之和不能超过backlog。

TCP半关闭

TCP支持半关闭操作。

伯克利套接字的API提供了半关闭操作,应用程序只需要调用shutdown函数来代替基本的close函数,就能实现上述操作。

初始序列号的选取

由于客户端和服务端之间的TCP连接是由客户端的IP地址及端口号和服务端的IP地址及端口号构成的四元组所确定的,因此当客户端出现了故障把这个TCP连接断开了,之后再以相同的四元组建立新的TCP连接(也就是说客户端和服务端两次建立TCP连接都是使用了相同的IP地址和端口号),就会出现数据乱序的问题。

换句话说,只要客户端发送了一个TCP报文段,且这个TCP报文段的四元组和序列号,和之前的TCP连接(四元组和序列号)相同的话,就会被服务端确认。这其实反映了TCP的脆弱性,如果TCP的这种缺点被一些恶意攻击者加以利用:选择合适的序列号、IP地址和端口号的话,就能伪造出一个TCP报文段,从而打断正常的TCP连接。那么通过谨慎选取初始化序列号的方式(通过算法来随机生成序列号)就会使序列号难以猜出,也就不容易利用这种缺点来进行一些恶意攻击行为。

Linux系统采用基于时钟的方案来选取初始化序列号。

TCP MTU的发现

MTU,即最大路径传输单元,指经过两台主机之间路径的所有网络报文段中最大传输单元的最小值。

当中间路由器的最大传输单元小于任何一个通信端的最大段大小(MSS)时,TCP就会执行路径最大传输单元发现过程:

IPv4头部中有一个3位的标志字段,目前只有低两位有意义,其中中间一位叫做DF位(Don’t Fragment),当该位置1时代表不能分片。

  • TCP发送端发送数据时将IP数据报中的DF位置1,这样中间路由器如果收到分片才能处理的过大的数据报时,中间路由器不会分片,而是将该数据报丢弃;
  • 路由器通过ICMP把链路上的MTU值通知TCP发送端;
  • TCP发送端获得ICMP所通知的MTU值以后,将它设置为当前的MTU。TCP发送端发送根据这个MTU对数据报进行分片处理。如此反复,直到数据报被发送到目标主机为止没有再收到任何ICMP,就认为最后一次ICMP所通知的MTU即是一个合适的MTU值。

TCP选项

TCP头部中包含了多个选项,常见的选项如下:

  • EOL:指出了选项列表的结尾,表示选项列表结束,说明无需对选项列表再进行处理。
  • NOP:允许发送者在必要的时候用多个4字节组填充某个字段。
  • MSS:最大段大小,即TCP协议所允许的从对方接收到的最大报文段。

一方把MSS发送给对方时,不是在商量,而是在通知对方,它表示在整个连接过程中都不愿意接收比MSS值大的报文段。

MSS的值是TCP数据载荷部分的字节数,而不包括TCP与IP头部。

建立一条TCP连接时,通信双方应该在SYN报文段的MSS选项中向对方说明自己允许的最大段大小。

  • MSS的默认值是536字节,加上TCP头部20字节和IPv4头部20字节,一共组成576字节的IPv4数据报,这是标准中规定的任何主机都应该能处理的IPv4数据报的最小大小;

  • IPv4中常见的MSS值为1460字节,加上TCP头部20字节和IPv4头部20字节,共组成1500字节的IPv4数据报,这正好是链路层中以太网的最大传输单元(MTU)。

  • 由于IPv6头部比IPv4头部大20字节,所以MSS值相应减20字节,为1440字节。

  • SACK:选择确认选项。接收方通过这个选项来描述乱序的数据(空洞),帮助发送方重传;
  • 窗口缩放选项:用于将TCP窗口大小字段的范围从16位增加至30位。该选项作为16位窗口大小的比例因子,最大比例数值为14,可将窗口大小的最大值由65535字节扩展至1GB;

窗口缩放选项只能出现在SYN报文段中,连接建立后比例因子与方向绑定,每个方向的比例因子可以各不相同。

  • 时间戳选项:发送方根据该选项通过每一个接收到的ACK来估算TCP连接的往返时间,并根据结果设置重传超时;
  • 用户超时选项:指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间;
  • 认证选项:用于增强连接的安全性。