TCP协议基础知识(二)

2021-10-11
学习

可选项

20字节的固定头部字段之后是可选的头部字段,这部分的字段名和字段个数是不固定的,因此其大小也是不固定的,为了标识每个字段的大小和种类。可选项的格式是以下形式:
TCP-header-Options.png
每个选项的头一个字节代表种类,种类后面是选项的值,不同种类的值有不同的长度,常见的可选项及其长度有以下几种:

种类kind 长度:len(字节) 名称 描述
0 1 EOL 选项列表结束
1 1 NOP 无操作
2 4 MSS 最大段大小
3 3 WSOPT 窗口缩放因子
4 2 SACK-Permitted 发送者支持SACK选项
5 可变 SACK 选择确认选项
8 10 TSOPT 时间戳选项

可选项的总长度是kind+len字节,EOL的作用时标记可选项列表的结尾,说明无需再对可选项进行处理。NOP的作用是填充可选项字段,因为TCP头部的总长度必须是32比特(4字节)的倍数,所以当TCP头部字段的长度不满足这个要求时,可以利用该选项填充。接下来具体解释几种常见的可选项。

窗口缩放选项

上面介绍固定头部字段时提到了窗口大小字段,该字段本身占用2个字节,所以他能表示的最大值为2^16 - 1,即接收窗口的最大值为65535字节(640KB)。但是计算机技术的不断发展,原先设计的值已经不能满足需要,所以TCP协议的设计者们又引入了窗口缩放选项。窗口缩放选项表示的是缩放的比例因子,其值的范围是0~14,它可以将窗口大小扩大到原来的2^N倍,即通过窗口缩放选项,窗口大小的最大值可以从16位增加值30位。真正的窗口大小是由上面提到的窗口大小字段和窗口缩放选项共同决定的。窗口缩放选项有以下特点:

  • 两个方向上的窗口缩放因子可以不同。
  • 比例因子的大小是三次握手时确定的。
  • 当主动打开连接的一方声明了非零的缩放因子,但是却没有接收到对方的缩放因子,他会将自己的缩放因子设置为零。
  • 缩放因子的大小有接收方的缓存大小决定,缓存的大小是由系统设定的。

下面通过抓包演示这个选项的工作方式。
窗口缩放的比例因子在三次握手时确定,下图是三次握手的第一个包,他的Window scale为6,所以缩放的倍数WS为2^6=64。
window-scale.png

后面数据传输时窗口的大小会根据上面的值来计算,图中Window为2061,缩放比例因子Window size scaling factor为64,所以其计算后的窗口大小Win为2061 * 64 = 131904。
window-size.png

MSS

TCP头部另外一个重要的选项是最大段大小最大段大小(Max Segment Size,MSS)。但是在讲解MSS的含义含义之前需要先把最大传输单元(Maximum Transmission Unit, MTU)讲清楚。

我们知道网络分层的实质就是在发送数据时,不同的层向上层发来数据添加自己的数据头信息,接收数据时,不同的层再把这些头信息剥离,并向上层传递。
TCP-header-OSI.png
上图演示了数据处理的大致模型,数据链路层负责数据的传递,但是数据链路层对传输的数据帧的体积是有限制的,网络层不能把一个太大的包直接塞给数据链路层,需要对超过这个限制的包进行切分再传递,这个限制称为最大传输单元。
数据链路层的以太网数据帧的体积最小为64字节,最大为1518字节。数据帧的格式如下:
TCP-header-datalink.png
可以看到头部占用14字节,尾部占用4字节,因此来自网络层数据的有效载荷范围为46~1500字节,即MTU值为1500字节。

需要声明的是,上图的数据链路层以太网数据帧格式并不准确,只是为了帮助理解。数据链路层的数据帧格式根据不同的标准有所不同,目前比较流行的802定义了局域网和城域网的工作过程,其标准包括802.3(以太网)和802.11(WLAN/WI-FI),他们的格式可统称为Ethernet II。

如果网络层有100KB的数据需要传输,则这100KB的数据至少需要(100 * 1024 / 1500) = 69个以太网数据帧。需要注意的是1500字节是大多数网卡的默认值,实际的值可能根据设备和操作系统不同而不同。并且由于数据传输需要经过多个网络,所以MTU的值有木桶效应。通过执行netstat -i可以查看本机网卡的MTU值。

IP网络层的数据大于1500字节时,IP层会对数据进行切分再交给数据链路层传输,每个切分出来的数据包大小为1500字节,由于IPv4网络数据头的大小为20字节,这20字节包含了源IP、目标IP和该分片偏移量等信息。所以实际上IP层的有效数据载荷为1480字节。即每个IP数据包最多能传输1480字节的来自传输层数据。
接下来通过抓包演示以上理论,开启Wireshark抓包之后,使用ping命令发送3000字节数据-c选项设置请求次数,-s选项设置发送数据的大小,这里发送一次3000字节的数据包:

1
ping -c 1 -s 3000 www.baidu.com

利用返回的IP地址在Wireshark中输入显示过滤ip.addr == 112.80.248.75
tcp-icmp.png
点击ICMP协议包,查看数据包详情,可以看到IPv4总长度为3008字节(ping命令向数据包添加8字节数据),该数据包的分片数Fragment Count为3,三个分片的payload长度分别是1480,1480,48字节。双击查看第一个IP分片,可以看到其总长度为1500字节,头部长度Header Length为20字节,分片偏移Fragment Offset为1480,More fragments为Set状态,即表示后面有更多分片。
tcp-ping.pinng
以上根据MTU分片的过程如下:
TCP-header-MTU.png

传输层的TCP协议为了避免网络层根据MTU对数据分片会自己主动对数据分片这个分片的大小称为最大段大小

1
MSS = MTU - IP头 - TCP头

即MSS = 1500(MTU) - 20(IP头) - 20(TCP头)= 1460
以上理论的分片大小可以用以下图表示:
TCP-header-MSS.png
再次强调的是以上各个数据的大小并不是固定的,MTU的值取决于设备和操作系统,TCP头的大小也不总是20字节,当头部有选项数据时会大于20字节,此时MSS的值也会减小。MTU的值取传输路径上的最小值,这个值通过MTU路径发现来获得。有时MTU的值为1200,IP分片为1500,此时发送的数据包请求会被链路层告知下次传输需要以1200字节重新分片。

时间戳选项

时间戳选项分为两个部分,发送方再发送数据时,生成一个32位的数字作为第一部分,称作TSV或TSval。接收方收到数据后,在回复的ACK报文中将该数值原封不动的放入第二部分称为TSER或TSecr,同时也会自己的32位时间戳放入第一部分。时间戳选项长度为10字节,剩余的2个字节用于指明选项的长度可数值。时间戳选项不要求通信的双方进行时间同步。规范推荐发送者每秒钟将数值加1。

该选项可以用于估算一条TCP连接的往返时间。估算往返时间可以用于设置重传超时,重传超时用于发送方决定什么时候需要重新发送可能已经丢失的数据。不实用时间戳选项时,估算往返时间采用的是抽样计算的策略,而时间戳选项能够提高估算出的往返时间的精确度。

时间戳选项还可以用于防止序列号回绕,前面提到序列号是一个32位长的字段,达到最大值后从0开始。但是高速网络传输时,序列号回绕可能导致一些问题,比如发送方发送序列号为0G:1G的数据包因为网络原因很久才达到接收端,此时如果序列号已经回绕从0开始,接收端可能同时收到两个序列号为0G:1G的数据包,此时通过时间戳选项,接收端能够区分这两个数据包。
TCP-header-timestamp.png
通过抓包可以看到包需要为101的数据包中TSecr的值即为序号为95的数据包的TSVal值,同时它也将自己的时间戳选项放入ACK包的TSVal部分。

选择确认选项SACK与快速重传

TCP-header-SACK.png

由于网络的不稳定性,发送发按顺序发送的数据,到达接收端时有可能是上图这样的,接收端收到的数据有很多“空洞”,而接收端收到数据后回复的ACK报文只会回复最大的连续的包序号,例如上图中收到序列号为7,8,10包时,会在ACK确认报文中一直回复5,发送方收到ACK后可能会误以为序列号4以后的包丢失而重复发送。为了避免这种情况,TCP增加了选择确认的可选项SACK。
简而言之,在接收到乱序的数据时,SACK选项能够有效的描述这些乱序的数据,从而帮助发送方有效的进行重传。它包含已经成功接收包的序号范围,例如1:4,7:8。每个范围称为SACK块,每个块由一对32位序号表示。如果一个SACK选项有n个SACK块,则长度len=8字节*n+2字节。多出的两个字节用于保存SACK选项的种类和长度。TCP头部字段的长度时有限的,所以一个报文中最多只能包含3个SACK块。

快速重传指的是当接发送端接收到重复的ACK包会意识到某些分组的包已经丢失,此时便可以利用SACK选项进行快速重传以便填补数据“空洞”,而不用等到重传计时器超时再重传。

其他概念

滑动窗口

TCP数据是分组传输的,但是由于传输网络的不确定性,所以接收方收到的数据次序可能是杂乱无章的,即有可能是先发送的数据后抵达接收方,所以接收放需要有一个缓存空间将收到的数据先暂存起来,以便维护这些杂乱无章的数据,发送方也需要一套缓存机制,把已经发送的数据副本缓存下来,以便没有收到接收方的ACK时重新发送。由于接收方缓存大小有限制,所以发送方也不能无脑的以很高的速率发送数据,必须要有一套机制来实时的通知发送方来控制自己的发送速率。
这一整套工作机制需要通过上文提到的窗口大小和滑动窗口实现。发送端的滑动窗口的形式如下:
TCP-header-window.png

窗口有左边界和右边界,左边界左侧的值时已经发送并确认的数据,右边界右侧的数据是还没有发送的数据。发送端的窗口大小是右边界的值减左边界。窗口内的数据又被分为已经发送但未确认的部分和即将发送的部分。
接收端的接收窗口如下图所示:
TCP-header-rcv.window.png

左边界左侧的值是已经接收并确认的数据,如果收到左边界左侧的数据会被认为是重复数据而丢弃。右边界的右侧是不能接收的数据,如果收到这部分数据,则会被认为是超出处理范围,也会被丢弃。

理想情况下接收端接收到数据后,数据会立刻被消费掉,但是如果接收端由于某种原因没有立马消费掉接收的数据,接收端应该在回复的 ACK包里声明自己的窗口大小,以便接收端调整自己的发送窗口指针来控制发送速率,直到接收方声明自己的接收窗口大小为零时,发送方不能再发送数据。

如果发送方被告知接收窗口为零,自己将暂时不能发送数据,但是接收方的接收窗口大小回复以后,会一直等待发送的数据,为了避免这种死锁等待的问题,发送方被告知接收窗口为零后,会持续的向对方发送数据包探测对方窗口的大小是否仍然为零,这种机制叫做零窗口探测。

重置报文RST

前面介绍TCP固定头部字段时,看到RST标识位,该标识位设为1的报文称之重置连接报文,收到这个报文后表示某个连接因为某种原因需要重置,一个连接通常由TCP头部的源端口号,目标端口号和IP头部的源IP地址和目标IP地址确定。由于以下几种原因可能会导致连接需要重置:

  • 端口不存在。
  • 连接没有正常断开,连接正常断开要经过四次挥手,如果四次挥手的过程发生错误,比如FIN报文丢失。
  • 半开链接,如未告知对端的情况下,一段的连接被意外关闭或终止,对端在不知情的情况下发送数据,此时会回复RST报文。

超时重传机制

超时重传指发送端在发送数据时会设置一个计时器,如果到达一定时间后还没有收到ACK包,发送端会重新发送数据。计时器超时称为重传超时(RTO),在介绍时间戳选项时,提到他们可以用来估算往返时间RTT,基于计时器的重传机制会根据RTT来设置RTO。

超时重传机制

超时重传指发送端在发送数据时会设置一个计时器,如果到达一定时间后还没有收到ACK包,发送端会重新发送数据。计时器超时称为重传超时(RTO),在介绍时间戳选项时,提到他们可以用来估算往返时间RTT,基于计时器的重传机制会根据RTT来设置RTO。

参考资料