TCP(Transmission Control Protocol)全称为传输控制协议, 它工作在网络七层模型中的第四层-传输层, 是一种面向连接的可靠的数据传递协议。 对于IP和UDP协议, 它们会在接收到数据后根据数据的校验值来对数据的有效性进行判断, 对于无效的数据会直接丢弃, 而不会去纠正。 相比于UDP协议, TCP协议显得更“安全”, 它在数据失效时会进行“重传”以确保数据的正确性。本文主要讨论TCP数据传输过程中所涉及到的一些基本知识,包括报文的格式,TCP连接建立和TCP数据包发送过程的分析,并对滑动窗口和SACK做了相关介绍。
传输队列中的TCP数据包常被称为”分组“,因此下文用”分组“来代指TCP数据包。
在发送TCP报文时, 需要对TCP报文进行层层的封装, 由于最终的报文是以以太网帧的形式发送出去的, 因此TCP报文要经历TCP数据报→IP数据报→以太网帧
的封装。 TCP在IP数据报中的封装如下图所示。 TCP报文的头部不带选项的话为20个字节,带选项可达60字节。
TCP头部的结构如下图所示, 每个TCP头部都包含了16位的源端口和目的端口。 TCP头部中的端口号和IP头部中的IP地址唯一地标示了一个连接, IP地址和端口的组合也被称为端点(endpoint)
或套接字(socket)
。
头部长度字段指出了TCP头部的长度,它是以32位为单位的(即4字节),其最小值为5,代表头部长度为4*5=20
字节。
SEQ(序列号):该字段代表着该数据报中所携带在数据中的第一个字节在数据流中的位置, 它是一个32位的无符号数, 在到达 2 32 − 1 2^{32}-1 232−1后再循环到0。
ACK(确认号):字段代表着接收方期望接收的下一个序列号。
SYN(同步位):该标志位用于TCP连接的建立, 详见下文中的TCP连接。
FIN(结束位):发送方已经结束报文的发送。
RST(重置位):重置连接。
TCP的连接通常分为三个阶段:启动, 数据传输和退出。 TCP连接的建立分为三个阶段, 需要发送三个报文, 因此该连接方式也被称为三次握手协议,步骤如下:
SYN_SEND
, 即同步位已发送状态。ISN(c)+1
, SEQ字段被设置为一个初始值, 这里将其记为ISN(s)。此时S的状态由一开始的LISTEN(监听)
变为SYN_REV(同步位已收到)
。ACK=ISN(s)+1
, SYN被设置为0。 段3在发送后, C端的状态变为ESTABLISHED(已连接)
, S段在收到该报文后状态也变为ESTABLISHED(已连接)
。至此, 三次握手完成, 连接建立。TCP分组: TCP是可靠传输协议,通过超时与重传机制,来保证收到的数据是完整的。因为TCP是可靠传输协议,如果要传输的数据大于 1480 - 20(tcp头部) =1460Byte时,在ip层被分片,而ip层分片会导致,如果其中的某一个分片丢失,因为tcp层不知道哪个ip数据片丢失,所以就需要重传整个数据段,这样就造成了很大空间和时间资源的浪费,为了解决这个问题,就有了tcp分组和MSS(最长报文大小)概念,利用tcp三次握手建立链接的过程,交互各自的MTU,然后用小的那个MTU-20-20 , 得到MSS,这样就避免在ip层被分片。
在TCP连接建立后, TCP便进入了数据传输的状态, 数据发送的步骤如下图所示,具体为:
F(x)
。 设置F(x)
分组的SEQ为其携带的数据的首字节的序号,这里假设其为100; ACK设置为发送方上一次接收到的分组的SEQ+数据长度+1
,这里假设其为1。F(x+1)
。 该报文的ACK字段为F(x).SEQ+LEN
, 即希望接收的下一个分组的序列号,这里为100+60=160
(注意该分组的最后一个字节的序列号为159)。在上述传输中, 若F(x+1)
无法顺利到达或者延迟到达发送方, 则会导致F(x)
的重发。 这种情况可能导致接收方收到多个F(x)
的副本,接收方通过SEQ来判断是否接收过该分组, 并丢弃重复的分组。
延迟确认:
接收端在收到数据包后不立马进行ACK数据包的发送,而是等待一定的时间,并统一对收到的多个数据包进行ACK,这种方法称为延迟确认。延迟确认能够减少数据包的发送,节约资源。当接收队列中存在失序分组时,延迟确认将不起作用:收到任意数据包时都将立马进行ACK。
在上述的TCP分组发送过程中, 上一个分组发送成功并得到确认后, 下一个分组才能发送, 而这中间的“等待”会造成效率的降低。 如果同时允许多个分组进入网络又会引发一系列的问题。 为解决这一些列的问题, 滑动窗口的概念被提出。
建立TCP的两端都维护着一个发送窗口
结构和接收窗口
结构。发送窗口
指的是将发送的TCP分组按照其序列号顺序放置到一个窗口
中,窗口
左边为已确认的分组,右边为待发送的分组,窗口里为已发送但还未确认的分组,如下图所示。图中,2、3为已确认的分组,10、11为待发送的分组,4-9为窗口中的已发送待确认的分组。
此时当我们收到4号分组的ACK,10号分组会被发送从而进入
窗口,4号分组得到确认从而退出
窗口,这个过程仿佛窗口往右滑动了一段,因此称为滑动窗口
。
接收窗口
的实现逻辑与发送窗口类似,这里不再赘述。
窗口中的分组数量称为窗口大小
,可以简单理解为图中窗口的宽度
,其实就是窗口中的所有分组所携带的数据大小。窗口过大会导致占用较大的缓存,浪费内存;窗口过小将影响传输的效率,因此窗口大小要设置在一个合理的范围内。TCP通讯双方通过TCP报文中的16位的窗口大小
字段来设置对方发送窗口的大小,16位导致窗口最大只能设置为65535,为了提高效率,增大窗口大小,可以中TCP头部的选项
中设置窗口缩放
字段(16位),最终的窗口大小为窗口大小*窗口缩放
。
在分组传输过程中,窗口的大小是动态变化的,接收方会通过窗口通告
发送分组告知发送方采用多大的窗口大小(简称为:目标窗口大小)。当目标窗口大小小于当前窗口大小时,发送窗口的右边界不动,左边界右移,实现窗口的“缩小”;当目标窗口大小大于当前窗口大小时,发送窗口的左边界不动,右边界右移,实现窗口的“放大”。
在数据发送过程中,当接收方跟不上发送方的速度时,需要告知发送方慢下来,这称为流量控制
。使用滑动窗口能够很好的进行流量控制:接收方通过通告较小的窗口大小来降低发送方的发送速度。当接收方忙碌时,可以将目标窗口大小设置为0,从而使发送方停止发送。此时发送方将定期向接收方发送探测报文(keep-alive)来查看接收方窗口的状态,一旦查询到目标窗口大小为非零值,将继续进行分组的发送。
当发送方发送的分组长时间得不到确认,收不到该分组的ACK时,就认为该分组已经超时
,需要对其进行重新发送。目前常用的重传方法有定时器重传和超时重传。
定时器重传指的是为窗口中每个已发送的报文设置一个超时定时器,一旦定时器超时后还没有收到该报文的ACK,则进行重传。重传超时RTO的设置一般为动态设置,根据RTT的值来确定(过程比较复杂)。
快速重传指的是在接收端接收到非预期的报文(未按照预期顺序到达的报文),ACK不进行延迟,而是立马返回以请求缺失的报文。发送端在接收到该ACK后,若其请求的报文未超时,则不立马进行重传,而是记录请求该报文的重复的ACK的数量,当到达一定的阀值(一般为3)后则进行重传。
Nagle算法是为了应对小报文在数据发送过程中产生数据浪费,如发送1字节的数据会发送64字节的报文,造成63字节的浪费。其基本思想是:在收到前一个报文的ACK前,不发送下一个报文,而是将这段时间内添加到发送队列中的小报文重组为一个报文(不超过1500字节),然后在收到前一个报文的ACK后将其发送出去。这种机制会导致报文传输的一定延迟,对于实时性要求比较大的场景不适用,可通过内核的TCP_NODELAY
选项进行禁用。
TCP的ACK是一种累积的确认方法,因此在进行ACK时只能发送已连续到达的序列号最大的分组的ACK。下图为在网络丢包或者分组乱序到达时的接收方的接收窗口情况,红色报文代表已到达报文。很明显,此时4、7、8、9报文很可能已经发生了网络丢包的情况,但在进行申请重传时只能发送3号报文的ACK以申请4号报文的重传,无法申请7、8、9报文的重传。这种串行的重传机制降低了重传的效率,在网络丢包严重的情况下将十分影响数据的传输。
为了解决这一问题,SACK(Selective Acknowledgment,选择确认)被提出。在TCP连接时可以通过报文选项
中SACK-Permitted Option
来设置是否启用SACK。SACK与普通的ACK报文相同,只不过在选项
中设置了额外的SACK确认信息,其格式如下图所示。kind=5
代表着这个选项是SACK确认选项,length
代表选项的长度;块1代表着最新到达的分组所在数据块,左边界为该块最左边数据的序列号,右边界为最右边数据的序列号+1;块2为第二新的数据块。假设上图中分组到达的顺序为05、10、06,则块1为05、06分组,块2为10分组。块1的左边界为05分组的序列号,右边界为06分组的ACK。由于TCP报头长度的限制(选项最大长度为40字节),SACK中最多只能包含三个确认块(时间戳等信息也要占用选项空间)。从上述分析我们可以看出SACK是可以重复确认的,即多次确认同一个数据块,这从一定程度上提高了网络传输的容错性。收到SACK后,发送方把对应的分组状态设置为SACKed,在进行超时时不会重发。
发送方在接收到SACK后,可以推断出空缺的分组,并在合适的时机对其进行重发。从上图中我们可以看出07、08、09号分组为空缺分组。空缺也可以理解为被SACKed最右边的分组左边的所有未被SACKed的分组。SACK重发流程如下图所示。
下面将以案例的形式来讲解一下SACK数据具体的发送与接收流程(案例来自RFC2018)。假设发送窗口左边界(最左边分组的序列号)为5000,窗口中有8个分组,每个分组长度为500字节。
情景1:8个分组中的前4个分组顺利到达,此时没有产生失序分组,接收方会返回正常的不带SACK选项的ACK数据包,该数据包的ACK为7000(5000+4*500)。
情景2:1号分组丢失,后面7个分组顺利到达。因为后面七个分组都是失序分组,因此都会触发ACK数据包的立刻发送。假设后面七个数据包是按照正常顺序到达的,则接收方返回的ACK数据包如下:
触发的分组 | ACK | 块1左边界 | 块1右边界 | 块2左边界 | 块2右边界 | 块3左边界 | 块3右边界 |
---|---|---|---|---|---|---|---|
5500 | 5000 | 5500 | 6000 | ||||
6000 | 5000 | 5500 | 6500 | ||||
6500 | 5000 | 5500 | 7000 | ||||
7000 | 5000 | 5500 | 7500 | ||||
7500 | 5000 | 5500 | 8000 | ||||
8000 | 5000 | 5500 | 8500 | ||||
8500 | 5000 | 5500 | 9000 |
情景3:第2、4、6、8个分组丢失,第1、3、5、7分组正常到达。由于1号分组属于正常到达的分组,因此会返回正常的ACK。而3号及以后的分组属于失序分组,会触发ACK的立刻返回并携带SACK信息。
触发的分组 | ACK | 块1左边界 | 块1右边界 | 块2左边界 | 块2右边界 | 块3左边界 | 块3右边界 |
---|---|---|---|---|---|---|---|
5000 | 5500 | ||||||
6000 | 5500 | 6000 | 6500 | ||||
7000 | 5500 | 7000 | 7500 | 6000 | 6500 | ||
8000 | 5500 | 8000 | 8500 | 7000 | 7500 | 6000 | 6500 |
假设此时4号分组到达了(可能是因为网络延迟而导致其晚到达,也可能触发了重发),此时的ACK信息如下:
触发的分组 | ACK | 块1左边界 | 块1右边界 | 块2左边界 | 块2右边界 | 块3左边界 | 块3右边界 |
---|---|---|---|---|---|---|---|
6500 | 5500 | 6000 | 7500 | 8000 | 8500 |
再假设此时2号分组也到达了,则ACK信息如下:
触发的分组 | ACK | 块1左边界 | 块1右边界 | 块2左边界 | 块2右边界 | 块3左边界 | 块3右边界 |
---|---|---|---|---|---|---|---|
5500 | 7500 | 8000 | 8500 |
食言:
当接收端丢弃被SACK的分组时,食言
的情况就发生了。当发送端检测到食言情况时,将取消所有分组SACKed的标识,并进行重发。因为SACK只是一种建议项,所以不能完全根据SACK来判断是否接收到数据包,ACK是唯一判断的依据。