可靠数据传输
Segment(段)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 源端口(source port):2字节。
- 目标端口(destination port):2字节。
- 序列号(sequence number):4字节,表示大小约4GB数据
- 确认号(acknowledgment number):4字节。
- 数据偏移(Data Offset):4bit, 这个值乘以四等于标头中的字节数,它必须始终是四的倍数。
- 保留位(Reserved):6bit。
- 控制位:6bit
- URG
- ACK
- PSH
- RST
- SYN
- FIN
- 窗口(Window):2字节,表示最大64kB。发送方愿意从接收方接受的字节数,即是发送该段的设备当前接收窗口大小,也是该段的接收者的发送窗口。
- 校验和(Checksum):2字节
- 紧急指针(Urgent Pointer):2字节,与控制位URG一起使用
- 选项(Option):不定长
- Option-kind:1字节
- Option-Length:1字节,选项内容最大256字节
- Option-Data:长度由Option—Length决定
- 填充(Padding):不定长,如果选项字段的长度不是 32 位的倍数,则添加足够的零来填充标头,使其成为 32 位的倍数。
- 数据(Data):不定长
常用options
TCP 校验和计算
TCP伪标头
为了计算 TCP 段报头的校验和字段,首先构造 TCP 伪报头,并在逻辑上放在 TCP 段之前。然后在伪标头和 TCP 段上计算校验和。然后丢弃伪标头。
- 源地址:4字节,取自IP报头
- 目标地址:4字节,取自IP报头
- 保留位:1字节
- 协议:1字节,TCP=6
- TCP长度:2字节,TCP段的长度包括标头和数据,它是计算出来的。
校验和结果放置
当 TCP 段到达其目的地时,接收 TCP 软件执行相同的计算。它形成伪标头,将其附加到实际的 TCP 段,然后执行校验和(将校验和字段设置为零以进行计算)。如果其计算结果与源设备放在校验和字段中的值不匹配,则表明发生了某种错误并且该段通常被丢弃。
注意:校验和字段本身是 TCP 标头的一部分,因此是计算校验和的字段之一,从而产生了一种“鸡与蛋”的情况。在计算校验和期间,假定该字段全为零。
伪标头的优点
加入伪标头后的校验和现在不仅可以防止 TCP 段字段中的错误,还可以防止:
- 不正确的段传递:如果源指定的目标地址与使用该段的目标地址之间的目标地址不匹配,则校验和将失败。如果源地址不匹配,也会发生同样的情况。
- 不正确的协议:如果数据报被路由到实际上属于不同协议的TCP,无论出于何种原因,都可以立即检测到。
- 不正确的段长度:如果TCP段的一部分被意外遗漏,则源和目标使用的长度将不匹配,校验和将失败。
伪标头的巧妙之处在于,通过将其用于校验和计算,我们可以提供这种保护,而无需实际发送伪标头本身中的字段。这消除了在 TCP 标头内复制伪标头中使用的 IP 字段,这将是冗余和浪费带宽。
但它违反了 TCP 设计者在拆分 TCP 和 IP 时寻求遵守的架构分层原则。对于校验和,TCP 必须知道技术上它“不应该”知道的 IP 信息。
TCP流与报文段
流分段的依据:
- MSS:防止在 IP 层进行不必要的分段。
- 流控:考虑接收端的能力
TCP 段由 IP 处理,就像所有其他要传输的离散消息一样。它们被放入 IP 数据报中并传输到目标设备。接收者解包这些段并将它们传递给 TCP,TCP 将它们转换回字节流以发送给应用程序。
MSS:Max Segment Size
- 定义:参见 RFC879
- MSS 选择目的
- 尽量每个Segment报文段携带更多的数据,以减少头部空间占用比率(过小)
- 防止 Segment 被某个设备的IP层基于MTU拆分(过大)
- 默认 MSS:536 字节
- 默认MTU:576 字节, 由于规定所有网络都必须能够处理这种大小的IP数据报而不会分片。
- 20 字节 IP 头部,20 字节 TCP 头部
- 这个值的选择是一种折衷。当使用这个数字时,这意味着大多数 TCP 段将通过 IP 互联网不分片地发送。但是,如果使用任何 TCP 或 IP 选项,这将导致超过 576 的最小 MTU 并发生碎片。
- 指定非默认 MSS 值
- 握手阶段通过在SYN消息中包含MSS选项:设备的接收者告知设备的发送者可以接收的 最大报文段长度(即RMSS)。
- 由于是全双工,所以两个设备都可能作为接收者,但可以选择默认或者带TCP选项的MSS。
- MSS小不包括 TCP/IP 标头和选项, 仅表示payload长度
- MSS 分类
- 发送方最大报文段 SMSS:SENDER MAXIMUM SEGMENT SIZE
- 这个值可以基于网络的最大传输单元,路径MTU 发现 [RFC1191, RFC4821] 算法
- RMSS
- 其他因素
- 接收方最大报文段 RMSS:RECEIVER MAXIMUM SEGMENT SIZE
- 接收方在连接建立时,握手中的MSS选项指定
- 发送方最大报文段 SMSS:SENDER MAXIMUM SEGMENT SIZE
序列号
由于 TCP 是面向流的,面向的是单个字节的数据而不是离散的消息,因此它必须使用在字节级别工作的标识方案来实现其数据传输和跟踪系统。这是通过为每个字节的TCP进程分配一个序列号来实现的。
ISN
每个 TCP 设备在启动连接时为连接选择一个 32 位的初始序列号 (ISN)。每个设备都有自己的 ISN,它们通常不会相同。
传统上,每个设备通过使用定时计数器来选择 ISN,就像某种时钟,每 4 微秒递增一次。这个计数器在 TCP 启动时被初始化,然后它的值每 4 微秒增加 1,直到它达到可能的最大 32 位值(4,294,967,295),此时它“环绕”到 0 并继续增加。每当建立新连接时,ISN 都会从该计时器的当前值中获取。由于以每增量 4 微秒计算从 0 到 4,294,967,295 需要 4 个多小时,这实际上保证了每个连接不会与之前的任何连接发生冲突。
序列号复用
序列号回绕
PAWS (Protect Against Wrapped Sequence numbers,防止序列号回绕)
通过使用option选项中的timestamp判断segment发送的时间先后。
滑动窗口
发送窗口
指针:
- SND.WND:发送窗口大小
- SND.UNA: 未确认字节的第一个字节
- SND.NXT:将要发送的下一个字节
不同类别的指针位置:
- 类别#1:SND.UNA结束
- 类别#2:SND.UNA开始
- 类别#3:SND.NXT开始
- 类别#4:SND.UNA+SND.WND开始
- 可用窗口大小:SND.UNA+SND.WND-SND.NXT
- 类别#1:已发送并收到 Ack 确认的数据:1-31 字节
- 类别#2:已发送未收到 Ack 确认的数据:32-45 字节
- 类别#3:未发送但总大小在接收方处理范围内:46-51 字节
- 类别#4:未发送但总大小超出接收方处理范围:52及之后字节
接收窗口
- RCV.WND:通知给对端设备的窗口大小 ,通常是为接收此连接分配的缓冲区的大小
- RCV.NXT:对端设备将要发送的下一个字节序列号1
- 类别#1+2:接收和确认的 字节数,1~31字节
- 类别#3:接收者已经准备好的字节数,32~51字节
- 类别#4:接收者尚未准备好尚未发送的字节数,52及之后字节
每个设备维护一组接收指针,它们是发送指针的补充。设备的发送指针跟踪其传出数据,其接收指针跟踪传入数据。SND和REV指针信息都保存在TCB中。
TCP Segment 中交换指针信息的字段
TCP 段格式中的三个基本字段用于实现滑动窗口系统:
- 序列号字段:指示正在传输的数据的第一个字节的编号,通常等于发送端SND.UNA指针的值。
- 确认号:用于确认发送该段的设备接收到的数据,通常等于发送端的RCV.NXT指针。
- Window字段:告诉段的接收者它应该设置它的发送窗口的大小。
窗口滑动
TCP 确认是累积的,并告诉发送器直到确认中指示的序列号的所有字节都已成功接收。因此,如果字节被乱序接收,则在接收到所有前面的字节之前,它们不能被确认。
简化的滑动窗口示例
客户端角度
服务端角度
实际上,现实世界的连接更加复杂:
- 重叠传输:客户端和服务器可能会快速连续地相互发送许多请求和响应。客户端将确认从服务器接收到的段,其中段本身包含新请求
- 多个段的确认
- 流量控制的波动窗口大小
- 网络丢包
- 避免小窗口问题:傻窗综合症
- 拥塞处理和避免
重传与确认(closing the loop)
为提供发送数据的基本可靠性,每个设备的 TCP 实现都使用重传队列。每个发送的段都放在队列中,并为它启动一个重传计时器。当收到对段中数据的确认时,将其从重传队列中删除。如果计时器在收到确认之前关闭,则重新传输该段并重新启动计时器。且我们不希望TCP永远保持重传,所以TCP在断定存在问题并终止连接之前只会重传丢失的段一定次数。
PAR基本概念
使用带有重传的肯定确认 (PAR) 提供基本可靠性:
缺点:发送器在第一个消息被确认之前不能发送第二个消息。
通过消息识别和发送限制提高 PAR 的实用性:
优点:通过识别要发送的每条消息来增强基本 PAR 可靠性方案一次传输多条消息。发送限制的使用允许该机制还提供流量控制功能。
缺点:标识符是消息id,在流式消息传输时对应于每个字节,效率低下。
TCP使用滑动窗口的累积确认方法带来的问题
由于TCP滑动窗口使用的累积确认的方法,它虽然满足了基本的流量控制和高效率的确认重传机制(相比于PAR),但是它无法有效地处理接收到的非连续TCP段。
原因是TCP的确认系统是累积的。这意味着如果一个段在传输过程中丢失,则在丢失的段被重传并成功接收之前,不能确认后续段。
存在未确认段时处理重传的策略(两种):
- 仅重传超时段(保守,乐观)
- 较少带宽
- 可能造成较大延迟
- 重新传输所有未完成的段(激进,悲观)
- 较大带宽
- 延迟低
带有重传的TCP事务示例
保守,乐观方法:
激进,悲观方法:
TCP非连续确认处理: 选择性确认 (SACK)
前提:
- 这是一个可选的功能
- 需要两端的设备都支持
- 在建立连接时的SYN段中,启用(SACK-Permitted)选项。
开启成功后:
- 允许任一设备在常规TCP段中包含选择性确认 (SACK)选项。
- 两端设备都会修改其重传队列,以便每个段都包含一个标志,如果该段已被选择性地确认,则该标志设置为 1—— SACK位
- 重传时设备采用上述“积极”方法的修改版本:
- 在重传一个段时,所有后续段也将被重传
- 除了设置了SACK为1的段
重传计时器设置策略
重传定时器持续时间选择的难点:
理想情况下的重传计时器时间应该设置为稍大于RTT值,然而实际情况下RTT值是不固定且波动的主要原因:
- 连接距离的差异
- 瞬态延迟和可变性:在任何两个设备之间发送数据所需的时间量会因互联网上的各种事件而随时间变化:流量波动、路由器负载等。
RTT(Round Trip Time)
往返时间
测量RTT:重传情况下应该通过使用timestamp中的发送时间和回显时间计算RTT
RTO(Retransmission Time Out)
超时重传时间
在实际使用中,一般RTT波动较大,一般采用第二种方式。Linux中也采用第二种方式:
- 方法一:平滑 RTO【RFC793】,降低瞬时变化
- SRTT (smoothed round-trip time) = ( α * SRTT ) + ((1 - α) * RTT)
- α 从 0到 1(RFC 推荐 0.9),越大越平滑
- RTO = min[ UBOUND, max[ LBOUND, (β * SRTT) ] ]
- 如 UBOUND为1分钟,LBOUND为 1 秒钟, β从 1.3 到 2 之间
- 不适用于 RTT 波动大(方差大)的场景
- 方法二:追踪 RTT 方差【RFC6298(RFC2988)】,其中α = 1/8, β = 1/4,K = 4,G 为最小时间颗粒:
- 首次计算 RTO,R为第 1 次测量出的 RTT
- SRTT(smoothed round-trip time) = R
- RTTVAR(round-trip time variation) = R/2
- RTO = SRTT + max (G, K*RTTVAR)
- 后续计算 RTO,R’为最新测量出的 RTT
- SRTT = (1 - α) * SRTT + α * R’
- RTTVAR = (1 - β) * RTTVAR + β * |SRTT - R’|
- RTO = SRTT + max (G, K*RTTVAR)
- 首次计算 RTO,R为第 1 次测量出的 RTT
参考资料
- tcp/ip Guide: http://www.tcpipguide.com/free/t_toc.htm
- https://time.geekbang.org/course/intro/100026801?tab=catalog