(Linux 高性能服务器) – TCP 协议笔记

TCP 协议笔记

传输协议

传输协议主要有两种:TCP 和 UDP。TCP 的特点是面向连接、字节流和可靠传输

  • 使用 TCP 协议的通信双方必须建立连接,然后才能开始数据读写。双方必须为该连接分配必要的内核资源以管理连接状态和数据。同时 TCP 连接是全双工的,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
  • TCP 一对一连接,而 UDP 适用于广播和多播(目标十多个主机地址)

  • 字节流:发送多执行的写操作和接收端执行的读操作的次数上没有数量关系。应用程序对数据的发送和接收是没有边界限制的。UDP则不然。发送端应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操作(通过recvfrom系统调用),否则就会丢包(这经常发生在较慢的服务器上)。并且,如果用户没有指定足够的应用程序缓冲区来读取 UDP 数据,则 UDP 数据将被截断。
  • TCP 采用了发送应答机制:发送的每个 TCP 报文端都必须得到接收端的应答,才认为传输成功。
  • TCP 超时重发,如果未收到应答那么它将重发。
  • 因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层。

UDP 提供的是不可靠的服务。

TCP 结构

TCP 头部结构

字段 意义
16位源端口号 主机包位来自哪个端口(IP 已经明确地址了)
16位目的端口号 传递给那个上层协议或者应用程序
32位序号 依次 TCP 通信过程中一个传输方向字节流的每个字节的编号。编号的组成部分为 ISN(初始序号值) + 传输的相同 ISN 的字节流的偏移字节数。
32位确认号 用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1。假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同时携带自己的序号和对A发送来的报文段的确认号。
4位头部长度 标识该TCP头部有多少个32bit字(4字节)。因为4位最大能表示15,所以TCP头部最长是60字节。
6位标志 URG: 紧急状态指针是否有效;ACK:确认是否有效(确认报段);PSH:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间;RST:表示要求对方重新建立连接(复位报段);SYN:请求建立一个连接(同步报文段);FIN:通知对方本端要关闭连接了(结束报段)
16位窗口大小 接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
16位校验和 接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。保障了 TCP 的可靠性
16位紧急指针 和序号字段中的值相加表示紧急数据最后一个字节的序号。如果URG为1,则紧急指针标志着紧急数据的结束。其值是紧急数据最后1字节的序号,表示报文段序号的偏移量。例如,如果报文段的序号是1000,前8个字节都是紧急数据,那么紧急指针就是8。紧急指针一般用途是使用户可中止进程。

TCP 头部选项

TCP 头部 20 字节固定。加上 TCP 头部选项最长的 40 字节可以组成 60字节。

length = kind(字段1字节) + length(字段1字节)+ info的字节数。

kind 取值:

意义
0 选项表结束
1 空操作,一般用于将TCP选项的总长度填充为4字节的整数倍。
2 最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size,MSS)。TCP模块通常将 MSS 设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS 值是1460(1500-40)字节。
3 是窗口扩大因子选项。在TCP的头部中,接收通告窗口大小是用16位表示的,故最大为65 535字节,但实际上TCP模块允许的接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。假设TCP头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小是N*2^M,或者说 N 左移 M 位。注意,M的取值范围是 0~14。
4 选择性确认(Selective Acknowledgment,SACK)选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。
5 该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。
6 时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改 /proc/sys/net/ipv4/tcp_timestamps 内核变量来启用或关闭时间戳选项。

所以在 SACK 中,segment 501-600 没有被确认,那么Left Edge of 1st Block = 501,Right Edge of 1st Block = 600,TCP的选项不能超过40个字节,所以边界不能超过4组。

使用 Wireshark 抓包分析:

0000   28 6c 07 ec d0 a7 00 db df cc a8 c6 08 00 45 00   (l.ìЧ.Ûß̨Æ..E.
0010   00 34 5f 17 40 00 80 06 d8 84 c0 a8 01 7b 73 9c   .4_.@...Ø.À¨.{s.
0020   8d 68 1d 9e 1e 00 bf b0 f1 39 00 00 00 00 80 02   .h....¿°ñ9......
0030   fa f0 c4 6e 00 00 02 04 05 b4 01 03 03 08 01 01   úðÄn.....´......
0040   04 02                                             ..

由 Flags 可知是 SYN 标志,所以这个是一个同步报文。源端口号:7582,目标端口号:7680。TCP 报文从 21 字节之后(0x1d)开始。这个数据包请求建立连接。

报文的解析:

字段
16位源端口号 0x1d9e = 7582
16位目的端口号 0x1e00 = 7680
32位序号 0xbfb0f139
32位确认号 0x00000000
4位头部长度 0x8 = 8 个字(4字节),共 32 字节
6位标志 0x002(高6个位保留),所以 SYN 置位
16位窗口大小 0xfaf0 = 64240 字节
16位校验和 0xc46e
16位紧急指针 0x0000(URG 没有置位,无意义)

TCP 选项部分:

第一组:

由 0x0204 组合可知,应该是 7 种选项的第三种。

字段
kind 0x02 = 2
length 0x04 = 4
info 最大报文段数 = 0x05b4 = 1460(即以太网的 MTU - IP 报文 - TCP 报文)

第二组:

由 0x01 组合可知,应该是 NOP 操作

第三组:

由 0x0303 组合可知,应该是 7 种选项的第四种。

字段
kind 0x03 = 3
length 0x03 = 3
info 移位数 = 0x08,为了提高吞吐量,窗口左移位 << 8

第四组:

由 0x01 组合可知,应该是 NOP 操作

第五组:

由 0x01 组合可知,应该是 NOP 操作

第六组:

由 0x0402 组合可知,是 SACK 确认。SACK 必须在 TCP 建立的时候指定,也就是说 SYN 必须置位。

TCP 的建立

TCP 的建立就是三次握手的过程:Win 主机:192.168.137.1,Linux 虚拟机:192.168.137.2。Win 主机使用 telnet 尝试远程登录。

  • 第一次握手的数据包,根据之前的分析流程,这是 SYN 同步,代表 Win 主机尝试连接 telnet 服务。32 位的序号(0x0018 开始):0x727fe59f
IP 192.168.137.1.12152 > 192.168.137.2.telnet: Flags [S], seq 1920984479, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
	0x0000:  4500 0034 1d95 4000 8006 49da c0a8 8901
	0x0010:  c0a8 8902 2f78 0017 727f e59f 0000 0000
	0x0020:  8002 faf0 591c 0000 0204 05b4 0103 0308
	0x0030:  0101 0402
  • 第二次握手:目标需要确认连接(确认同步报文段连接,ACK 设置为第一次握手的序号 + 1。虽然序号用来区分字节位置,同步报文特殊)并发送连接确认的数据包。32 位序号 0x3657d4d9,32 位确认序号 0x727fe5a0 = 0x727fe59f + 0x1
IP 192.168.137.2.telnet > 192.168.137.1.12152: Flags [S.], seq 911725785, ack 1920984480, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
	0x0000:  4500 0034 0000 4000 4006 a76f c0a8 8902
	0x0010:  c0a8 8901 0017 2f78 3657 d4d9 727f e5a0
	0x0020:  8012 7210 937b 0000 0204 05b4 0101 0402
	0x0030:  0103 0307
  • 第三次握手:对第二个数据包报文的确认。32 位序号 0x727fe5a0,32 位确认序号 0x3657d4da。由标志 0x010 可知是 ACK 置位。
IP 192.168.137.1.12152 > 192.168.137.2.telnet: Flags [.], ack 1, win 2053, length 0
	0x0000:  4500 0028 1d96 4000 8006 49e5 c0a8 8901
	0x0010:  c0a8 8902 2f78 0017 727f e5a0 3657 d4da
	0x0020:  5010 0805 8199 0000 0000 0000 0000

由于 TCP 是全双工的,每个方向全部关闭。关闭需要进行 4次挥手。

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。标准文档RFC 1122的 MSL 的建议值是2 min。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

TCP 超时

1.21:23:35.612136 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
2.21:23:36.613146 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
3.21:23:38.617279 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
4.21:23:42.625140 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
5.21:23:50.641344 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0
6.21:24:06.673331 IP 192.168.1.109.39385>192.168.1.108.telnet:Flags[S],seq 1355982096,length 0

保留了时间戳,可以发现客户端发出的 TCP 数据包的时间间隔基本上是 1s,2s,4s,8s和 16s。总共发送了 6 次建立的请求。最终服务端没有响应,显示 timeout。

TCP 状态转移

TCP 状态转移图:

四次握手的客户端-服务端关闭就满足这种状态转移。连接停留在 FIN_WAIT_2 状态的情况可能发生在:客户端执行半关闭后,未等服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称之为孤儿连接(和孤儿进程类似)。Linux为了防止孤儿连接长时间存留在内核中,定义了两个内核变量:/proc/sys/net/ipv4/tcp_max_orphans和/proc/sys/net/ipv4/tcp_fin_timeout。前者指定内核能接管的孤儿连接数目,后者指定孤儿连接在内核中生存的时间。

TIME_WAIT 状态

客户端连接在收到服务器的结束报文段(LAST_ACK 发送的数据报)之后,并没有直接进入CLOSED 状态,而是转移到 TIME_WAIT 状态。客户端连接要等待一段长为2MSL的时间,才能完全关闭。TIME_WAIT 存在的原因:

  • 可靠地终止TCP连接。例如:用于确认服务器(LAST_ACK 状态)结束报文段的 TCP报文段丢失,那么服务器将重发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像刚刚丢失的TCP报文段那样的确认报文段。
  • 保证让迟来的TCP报文段有足够的时间被识别并丢弃。

在 Linux,一般情况下端口一个时刻只能被一个程序占用(socket 可使用 SO_REUSEADDR 强制系统使用 TIME_WAIT 状态的端口)。等待 2MSL 可以确保服务端不会接收到原来连接的数据报(在转发路由中被丢弃)。

TCP 复位报文段

三种情况导致发送 RST 置位的复位报文段:

  • 访问不存在的端口或者是尝试连接处于 TIME_WAIT 的服务段端口。例如:telnet 访问不存在的端口,窗口大小为 0。收到回复的一端应该关闭而不是回应。
1.IP 192.168.1.109.42001>192.168.1.108.54321:Flags[S],seq 21621375,win 14600,length 0
2.IP 192.168.1.108.54321>192.168.1.109.42001:Flags[R.],seq 0,ack 21621376,win 0,length 0
  • 异常终止连接
  • 处理半打开连接。服务端或者客户端任何一方异常退出(即使重启,也丢失原来的连接信息),将会导致对方保存着一个处于半大开的连接。,对方法发送的任何数据,异常方在正常运行之后都会发送复位数据报。

TCP 成块数据流

2.IP 127.0.0.1.20>127.0.0.1.39651:Flags[.],seq 205799425:205815809,ack 1,win 513,length 16384
17.IP 127.0.0.1.39651>127.0.0.1.20:Flags[.],ack 205815809,win 30084,length 0
18.IP 127.0.0.1.39651>127.0.0.1.20:Flags[.],ack 206045185,win 27317,length 0

假设 17 和 18 数据段已经分别确认了某个同步数据报段,我们假设接收的窗口扩大因子是 6(scale)。由 17 可知客户端还可以接收(30084<<6 = 1925376 字节 )。18 可接受 27317<<6 = 1748288 字节。可用的缓冲区变少,服务端可以选择性发送标志 PSH 的数据报要求客户端尽快读取缓冲区的数据。18 之后的数据报最多还有 1748288 / 16384 = 106 个。

带外数据

些传输层协议具有带外(Out Of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。TCP 可以使用 URG 实现。

发送端一次发送的多字节的带外数据中只有最后一字节被当作带外数据(字母c),而其他数据(字母a和b)被当成了普通数据。如果TCP模块以多个TCP报文段来发送图3-10所示TCP发送缓冲区中的内容,则每个TCP报文段都将设置 URG 标志,并且它们的紧急指针指向同一个位置(数据流中带外数据的下一个位置),但只有一个TCP报文段真正携带带外数据。这个带外数据将会被写入大小为 1字节的特殊缓存中。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据(如果有的话)将覆盖它。 如果 TCP 连接使用 SO_OOBINLINE,那么程序也可以像将带外数据作为普通的数据读取。信号 SIGURG

TCP 超时重传

TCP服务必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就是TCP的重传策略。

Linux有两个重要的内核参数与TCP超时重传相关:/proc/sys/net/ipv4/tcp_retries1/proc/sys/net/ipv4/tcp_retries2。前者指定在底层IP接管之前TCP最少执行的重传次数,默认值是3。后者指定连接放弃前TCP最多可以执行的重传次数,默认值是15(一般对应13~30 min)。

拥塞控制

标准文档 RFC5681。

SWND(Sender Window) 表示发送端向网络段一次性连续写入(收到的第一个数据的确认之前)的数据量。不过最终使用 TCP 报文段发送数据,所以 SWND 限定了发送端能连续发送的 TCP 报文段数量。这些 TCP 报文段的最大长度(数据段)为 SMSS(Sender Segment Size)。

发送端需要合理地选择SWND的大小。如果 SWND太小,会引起明显的网络延迟;反之,如果SWND太大,则容易导致网络拥塞。前文提到,接收方可通过其接收通告窗口(RWND)来控制发送端的SWND。但这显然不够,所以发送端引入了一个称为拥塞窗口(Congestion Window,CWND)的状态变量。实际的 SWND 值是 RWND 和 CWND 中的较小者。

拥塞控制包含四个部分:

慢启动和拥塞避免

慢开始法

当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 CWND 设置为一个最大报文段 MSS 的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS 的数值。用这样的方法逐步增大发送方的拥塞窗口 CWND ,可以使分组注入到网络的速率更加合理。

为了防止拥塞窗口 CWND 增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量:

  1. 当 CWND < ssthresh 时,使用上述的慢开始算法。
  2. 当 CWND > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
  3. 当 CWND = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

拥塞避免算法

让拥塞窗口 CWND 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 CWND 加1,而不是加倍。这样拥塞窗口 CWND 按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

实例

  1. 当TCP连接进行初始化时,把拥塞窗口 CWND 置为1。前面已说过,为了便于理解,图中的窗口单位不使用字节而使用报文段(SMSS)的个数。慢开始门限的初始值设置为16个报文段,即 CWND = 16 。
  2. 在执行慢开始算法时,拥塞窗口 CWND 的初始值为1。以后发送方每收到一个对新报文段的确认ACK,就把拥塞窗口值另1,然后开始下一轮的传输(图中横坐标为传输轮次)。因此拥塞窗口 CWND 随着传输轮次按指数规律增长。当拥塞窗口 CWND 增长到慢开始门限值 ssthresh 时(即当 CWND=16 时),就改为执行拥塞控制算法,拥塞窗口按线性规律增长。
  3. 假定拥塞窗口的数值增长到24时,网络出现超时(这很可能就是网络发生拥塞了)。更新后的ssthresh值变为12(即变为出现超时时的拥塞窗口数值24的一半),拥塞窗口再重新设置为1,并执行慢开始算法。当CWND=ssthresh=12时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过一个往返时间增加一个MSS的大小。

强调:“拥塞避免”并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免”是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞

快重传和快恢复

快重传

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。

M3 算法送的确认报段丢失后,发送端继续传递 M4、M5 和 M6 数据报,接收方确认数据报发送回去。快重传算法还规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段 M3,而不必继续等待 M3 设置的重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。

快恢复算法

其过程有以下两个点:

  • 当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法
  • 由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口 CWND 现在不设置为 1),而是把CWND 值设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

下图给出了快重传和快恢复的示意图,并标明了“TCP Reno版本”。

也有的算法把开始时的重传的拥塞窗口值设置为 ssthresh + 3 * MSS。这样做的理由是:既然发送方收到三个重复的确认,就表明有三个分组已经离开了网络。这三个分组不再消耗网络 的资源而是停留在接收方的缓存中。可见现在网络中并不是堆积了分组而是减少了三个分组。因此可以适当把拥塞窗口扩大了些。

在采用快恢复算法时,慢开始算法只是在 TCP 连接建立时和网络出现超时时才使用。

采用这样的拥塞控制方法使得TCP的性能有明显的改进。

总结

  • 接收方根据自己的接收能力设定了接收窗口rwnd,并把这个窗口值写入TCP首部中的窗口字段,传送给发送方。因此,接收窗口又称为通知窗口。因此,从接收方对发送方的流量控制的角度考虑,发送方的发送窗口一定不能超过对方给出的接收窗口rwnd 。
  • 发送方窗口的上限值(SWND) = Min(RWND, CWND)
  • 当 RWND < CWND 时,是接收方的接收能力限制发送方窗口的最大值。
  • 当 CWND < RWND 时,则是网络的拥塞限制发送方窗口的最大值。

参考&引用