数据通信网络实践:基础知识与交换机技术
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.4.2 TCP详解

TCP的IP协议号是6,是传输层一个面向连接、可靠的传输协议。通信的双方在传输数据前先同步相关的参数,并拥有确认机制,数据传输的可靠性比较高,因此也需要更多的标识字段,协议开销也比较大,其通信效率相对于UDP较低,默认的TCP报头长度是20 B。TCP多应用在需要高可靠传输的场景,广域网大多采用TCP来传输数据。

TCP最早是在RFC 761中描述的,其文档名称为DoD Standard Transmission Control Protocol。RFC 761发布于1980年1月,TCP后来被RFC 793重新描述,因此RFC 761被废弃了。RFC 761的文档链接地址是https://datatracker.ietf.org/doc/rfc761/?include_text=1。

RFC 793替代了RFC 761,其文档名称是Transmission Control Protocol Darpa Internet Program Protocol Specification,发布于1981年9月,最近一次更新时间是2016年4月8日,文档等级是互联网标准(Internet Standard)。RFC 761的文档链接地址是https://datatracker.ietf.org/doc/rfc793/?include_text=1。

后来,RFC 9293又替代了RFC 793,其文档名称Transmission Control Protocol (TCP),发布于2022年8月,最近一次更新时间是2022年8月18日,文档等级是互联网标准(Internet Standard)。RFC 9293的文档链接地址是https://datatracker.ietf.org/doc/rfc9293/?include_text=1。与RFC 793相比,RFC 9293在报文格式的描述方面发生了一些变化,如增加了CWR、ECE两个控制位(Flag),这两个控制位是在RFC 3168中描述的。

1.TCP的报头格式

TCP报头格式如图2-2所示,具体说明如下:

 TCP报头宽度是32 bit。

 Source Port:源端口,长度为16 bit。

 Destination Port:目的端口,长度为16 bit。

 Sequence Number:顺序号或序列号,长度为32 bit。

 Acknowledgment Number:确认号,长度为32 bit。

 Rsrvd:保留,未使用,长度为4 bit。

 Control Bit:控制位,也称为Flag,可以是CWR、ECE、URG、ACK、PSH、RST、SYN或FIN,用来建立和维护TCP连接等。

 Window:窗口大小,表示在确认了字节之后还可以发送多少字节,长度为16 bit,最大为65536,可以通过SYN报文中的窗口扩展选项扩展。

 Checksum:校验和,长度为16 bit。

 Urgent Pointer:紧急指针,长度为16 bit。

 Options:可选项,长度为0~32 bit。

在没有添加任何选项的情况下,TCP的报头大小是20 B,也是TCP报头的最小长度,在实际中通常都使用这个长度。

图2-2 TCP的报头格式(摘自RFC 9293)

2.TCP报文示例

在Wireshark中显示的TCP报头示例如图2-3所示。

图2-3 在Wireshark中显示的TCP报头示例

3.TCP连接的建立

TCP连接是一个状态机,根据不同状态下收到的消息而变化。从报文交换的视角来看TCP连接状态的转换,可以看到TCP连接的建立需要经过三次报文交换,因此称为TCP三次握手(TCP Three Way Hand Shake),交互过程如图2-4所示。

图2-4 TCP三次握手交互过程

TCP连接的建立过程如下:

(1)服务器(Server)创建好了socket(socket(),bind(),listen(),accept()),状态置为LISTENING,等待客户端连接。

(2)客户器(Client)创建好socket(socket(),connect()),向服务器(Server)发送一个SYN报文,携带序列号x,将连接状态置为SYN_SENT。

(3)服务器收到SYN报文后,回复一个序列号为x+1的ACK报文,这个报文同时被置SYN位,携带序列号y,将连接状态置为SYN_RCVD。

(4)客户端收到服务器回应的SYN+ACK报文后,检查ACK的序列号是否正确,如正确则回复一个序列号为y+1的ACK报文,将连接状态置为ESTABLISHED。

(5)服务器收到客户回应的ACK报文后,检查序列号是否正确,如正确则将连接状态置为ESTABLISHED。

至此,TCP连接建立成功,接下来客户端会根据自己的应用程序来构造数据请求,通过write()函数将数据发送到缓冲区,再转发到服务器。服务器收到用户发送的数据请求报文,通过read()函数从缓冲区读取数据内容,确认(以ACK报文的形式)收到数据,并回复(构造数据内容,写入write()发送缓冲区)给客户端想要的数据。如此经过若干轮的数据发收后,客户端完成数据发送后主动发起连接拆除(close())请求,进入连接关闭流程。

4.TCP连接的建立(三次握手)示例

TCP连接建立示例(客户端SYN报文)如图2-5所示。

图2-5 TCP连接建立示例(客户端SYN报文)

从图2-5可以看出如下信息:

 TCP的IP协议号是6;

 数据从源地址192.168.1.2发往目的地址104.20.0.85;

 源端口是本地随机分配的一个未被占用的端口(端口号为1280),目的端口是知名端口(端口号是80);

 流索引(Stream index)是16;

 分段长度(TCP Segment Len)是0;

 SYN序列号(Sequence number)是0,即syn.seq=0;

 标志位(Flags)被设置为0x02,表示这是一个SYN;

 窗口大小(Window size value)为65535;

 校验和(Checksum)是0x5272;

 校验状态(Checksum Status)是未校验(Unverified);

 紧急指针(Urgent pointer)未设置;

 选项(Options)字段的大小为12 B;

 数据分段的时间属性等内容,在图2-5中没有显示出来。

TCP连接建立示例(服务器SYN+ACK报文)如图2-6所示。

图2-6 TCP连接建立示例(服务器SYN+ACK报文)

从图2-6可以看出如下信息:

 TCP的IP协议号是6;

 数据从源地址104.20.0.85发往目的地址192.168.1.2,TTL是53;

 源端口是一个知名端口(端口号为80),目的端口是一个随机端口(端口号为1280);

 流索引是16,用来标识一个TCP连接;

 分段长度是0;

 SYN序列号是0,即syn.seq=0;

 ACK序列号是1,即ack.seq=1;

 SYN位和ACK位都被设置,即0x12,表示这是一个SYN+ACK;

 窗口大小为29200;

 校验和是0x29a4;

 校验状态是未校验;

 紧急指针未设置;

 选项字段的大小是12 B;

 TCP选项(TCP Option),最大分段大小选项字段设置为1412 B;

 TCP选项,是否支持SACK(选择性确认);

 TCP选项,扩展窗口规格为10,即210,这样TCP的实际窗口大小可达29200×210,这个字段也被称为窗口大小扩展因子,用来扩展发送窗口的大小;

 数据分段的时间属性等内容,在图2-6中没有显示出来。

TCP连接建立示例(客户端ACK报文)如图2-7所示。

图2-7 TCP连接建立示例(客户端ACK报文)

从图2-7可以看出如下信息:

 TCP的协议号是6;

 数据从源地址192.168.1.2发往目的地址104.20.0.85;

 源端口是一个随机端口(端口号为1280),目的端口是一个知名端口(端口号为80);

 流索引是16;

 分段长度是0;

 ACK序列号是1,即ack.seq=1;

 ACK位被设置,即0x10,表示这是一个ACK;

 窗口大小为32768;

 计算后得到的窗口大小是65536,因为窗口扩展因子是2;

 窗口大小扩展因子2;

 校验和是0x5c59;

 校验状态为未校验;

 紧急指针位未设置;

 其余的字段是数据分段的时间属性等。

至此,流索引为16的TCP连接就成功建立了。

5.TCP连接的拆除

TCP连接拆除的过程需要4个报文,但我们在抓包分析时看到的只有3个数据包,与TCP连接建立的过程非常相似,主要原因是发生了捎带(Piggybacking)。如果没有发生捎带,即每一个报文用一个数据包封装,则有4个数据包。捎带是指确认报文和应答报文一起发送,发生在服务器处理请求和应答总时间少于200 ms时。如果服务器(Server)处理FIN请求,则回复一个ACK进行确认,然后在200 ms之后,回复一个FIN应答,捎带就不会发生。这种情况很少发生,所以我们经常看到的TCP连接拆除过程和连接建立过程一样,也是三次报文交互。有捎带发生的TCP连接拆除过程如图2-8所示。

图2-8 有捎带发生的TCP连接拆除过程

(1)经过几轮的数据发送(通过write()函数实现)和接收(通过read()函数实现)后,客户端(Client)就完成了对服务器的数据访问。在大多数情况下,TCP连接拆除(或称为关闭)的请求是由客户端发起的,但并不是所有的TCP连接拆除都必须由客户端先发起,服务器也可以发起TCP连接拆除的请求。

(2)客户端向服务器发送一个FIN报文,携带序列号j,将自己的状态置为CLOSE_WAIT。

(3)服务器收到请求后,将自己的状态置为CLOSE_WAIT,回复一个序列号为j+1的ACK报文;这个报文的FIN位同时被置位,携带的序列号为k,一起回复给客户端,并将自己的状态置为LAST_ACK。

(4)客户端收到服务器发来的ACK报文后,检查序列号是否正确,如正确则将自己的状态置为TIME_WAIT,并回复一个序列号为k+1的ACK报文。

(5)服务器收到ACK报文后,检查序列号是否正确,如正确则进入CLOSED状态。至此,TCP连接拆除的报文交换过程就完成了。

(6)客户端等待2个MSL(Maximum Segment Lifetime)时间后也进入CLOSED状态。

CLOSE_WAIT状态的时间会因为不同系统的MSL而不同,可能会持续1~4 min,现在多数系统会持续1 min。虽然CLOSE_WAIT状态会占用系统资源,但它可以实现可靠的双向终止和允许迷路的重复报文在网络中消失,可以防止新建的连接被重置。

6.TCP连接拆除示例

客户端在完成对数据的访问后,如果要关闭TCP连接,就可以向服务器发送FIN报文。TCP连接拆除示例(客户端FIN报文)如图2-9所示,这里也发生了捎带,即同时也发送对最近接收数据确认的ACK报文。

图2-9 TCP连接拆除示例(客户端FIN报文)

图2-9所示的示例既可以用来确认最后收到的数据,也可以用于TCP连接的拆除,由客户端主动发出。从图2-9中可以看到如下信息:

 数据包的源地址是10.1.31.226,目的地址是104.16.45.99;

 数据包的源端口是一个私有端口(端口号为57942),目标端口是一个知名端口(端口号为443);

 流索引是0;

 分段长度为0;

 图中所示的是一个FIN+ACK报文,FIN序列号是582,ACK序列号是2503;

 窗口大小是512;

 校验和是0xb05d;

 紧急指针位未设置。

TCP连接拆除示例(服务端FIN+ACK报文)如图2-10所示,服务器发生了捎带,将用于确认FIN的ACK报文和回应关闭请求的FIN报文一起发送到了客户端。

从图2-10中可以看到如下信息:

 发送数据包的源地址是104.16.45.99,目标地址是10.1.31.226;

 数据包的源端口是知名端口(端口号为443),目标端口是私有端口(端口号为57943);

 流索引是0;

 分段长度是0;

 图中所示的是一个FIN+ACK报文,FIN序列号是2503,ACK序列号是583,即主动闭关方发送过来的FIN号+1;

 窗口大小是66;

 包校验和0xb21a;

 紧急指针位未设置。

图2-10 TCP连接拆除示例(服务器FIN+ACK报文)

TCP连接拆除示例(客户端ACK报文)如图2-11所示,图中所示的是一个ACK报文,是客户端对服务器FIN报文的应答。

图2-11 TCP连接拆除示例(客户端ACK报文)

从图2-11中可以看到如下信息:

 数据包的源地址是10.1.31.226,目的地址是104.16.45.99;

 数据包的源端口是一个私有端口57942,目标端口是一个知名端口443;

 流索引是0;

 分段长度是0;

 ACK序列号是2504,即被动闭关方发送过来的FIN号+1;

 窗口大小是512;

 校验和是0xb05c;

 紧急指针位未设置。

7.拥塞控制

TCP在收到确认(Acknowledgment,ACK)报文前可以发送多个分段(Segment),一次发送的数据量多少就是发送窗口(Sender Window,SWND)或滑动窗口(Sliding Window)的值,发送窗口使用接收窗口(Receiver Window,RWND)的值和拥塞窗口(Congestion Window,CWND)的值与SMSS的乘积比较后的最小值,即SWND=min(RWND, CWND×SMSS)。

滑动窗口是通过移动左右两个指针确定发送窗口边界的,边界内数据才会被发送,从而达到控制发送数据量的目的。接收窗口是该TCP连接的接收端所能通告的最大窗口大小。若发送端发送大于接收窗口大小的数据,则不会得到ACK报文。接收窗口在TCP连接建立时通过SYN报文被通告,SYN报文同时还通告了本端的最大分段大小(Maximum Segment Size,MSS),通信双方的MSS可能不同。接收端一次接收数据量不能大于接收缓冲区的大小,因为还要考虑数据的封装开销。鉴于通信双方计算能力的不同,接收窗口大小通常也不相同。

拥塞窗口(Congestion Window,CWND)由发送端维护,它表示报文个数而非具体的数据量,实际的数据量是拥塞窗口与发送端最大分段大小(Sender Maximum Segment Size,SMSS)的乘积,即CWND×SMSS,它的主要作用是确定线路的实际转发能力。为了避免太多未被确认的分段再次重传而引起雪崩式拥塞,发送端会维护一个拥塞窗口来限制不需要确认就可以发送的分段数量。

确定拥塞窗口大小的方法有很多,如TCP Tahoe、TCP Reno、TCP NewReno、TCP BIC、TCP CUBIC等,但基本上都遵循“加法增大,乘法减小”的模式。Google在2016年推出的BBR(Bottleneck Bandwidth and Round-trip propagation time)算法,并加入到了Liunx 4.9及更新的内核中。

TCP拥塞控制相关的描述是在RFC 5681中定义的,文档链接地址是https://datatracker.ietf.org/doc/rfc5681/。

如果网络中发生了拥塞,TCP的积减线增算法可能会导致TCP饥饿或TCP饿死,即UDP等其他协议快速恢复对网络带宽的占用,从而导致TCP实际可用带宽下降或无带宽可用,解决的办法是部署服务质量(Quality of Service,QoS)。