【计算机网络自顶向下方法】手把手带你设计一个可靠且高效的数据传输协议介绍了校验和、计时器、序列号、ACK 应答确认、超时重传以及管道化传输等机制,以及如何通过这些机制来保证一般意义上数据传输可靠性的同时提高数据传输效率。因此,建议读者在尝试继续往下阅读之前,先通读上述文章对相关机制有所了解。
实际上,在互联网的 TCP/IP 协议栈中,传输层的 TCP 协议也无外乎利用了上述提到的这些机制,然而作为 TCP/IP 协议栈中举足轻重的协议,TCP 协议在利用上述上述机制实现可靠且高效数据传输时,在很多方面仍然有很多特别之处。这些特别之处主要集中在序列号、ACK 应答、计时器以及超时重传这几方面。接下来,本文将就以上几个方面,对 TCP 协议进行深入探究。
1. 序列号
根据之前的博文可知,序列号不仅可以让任何可靠数据传输协议中的接收方区分收到的数据段究竟是包含新的数据还是重传的数据,同时还可以让接收方能将多个数据段按照顺序组装还原成原始数据。
虽然 TCP 协议的数据段中也使用了序列号,但 TCP 协议放入序列号字段的值却不是按照数据段的数量编号来计算的,下面将对此做具体阐述。
实际上,TCP 协议将经其传输的数据视为无序的字节流,这一点体现在了 TCP 对于数据段序列号的定义上,即对于某一个 TCP 数据段来说,其序列号是当前数据段传输的这部分字节流中第一个字节所拥有的编号。
对此,下面通过一个具体例子来进行说明。假设主机 A 上的某进程希望通过一个 TCP 连接发送一个文件至主机 B 上的某进程;同时假定该文件包含
500000
500000
500000 个字节,最大数据段大小(MSS :
Maximum?Segment?Size
\text{Maximum Segment Size}
Maximum?Segment?Size)为
1000
1000
1000 ,且数据流的第一个字节编号为
0
0
0 。
因此,如下图所示,最终 TCP 协议将发送
500
500
500 个数据段,根据上述定义,第一个数据段序列号即为
0
0
0 ,第二个数据段序列号为
1000
1000
1000 ,第三个数据段序列号为
2000
2000
2000 ,依次类推。
2. ACK 确认值
接下来讨论 TCP 协议的 ACK 确认值。相较于 TCP 协议的数据段序列号, ACK 确认值要稍显复杂一些。具体地,由于 TCP 是全双工的,这意味着主机 A 在向主机 B 发送数据的同时也可能在从主机 B 接收数据。在主机 A 发往主机 B 的数据段中,TCP 协议在其中填写的 ACK 确认值是主机 A 期望从主机 B 接收到的下一个数据段的第一个字节所具有的编号。
上面这句话乍一听比较拗口,下面来看几个具体的例子来帮助理解。
假定主机 A 已经从主机 B 收到了编号从
0
0
0 到
535
535
535 的所有字节,同时主机 A 将要向主机 B 发送一个数据段。由于主机 A 正在等待接收主机 B 一侧的数据流中从编号为
536
536
536 往后的字节,因此这时主机 A 向主机 B 发送的数据段中,ACK 确认值应该为
536
536
536 。
再举一个例子,假定主机 A 已经从主机 B 收到了编号从
0
0
0 到
535
535
535 以及编号从
900
900
900 到
1000
1000
1000 的所有字节,但还没有收到编号从
536
536
536 到
899
899
899 的字节。在这样的情况下,主机 A 发往主机 B 的下一个数据段将包含的 ACK 确认值为
536
536
536 。
因此,由于 TCP 协议只会针对截至第一个未收到的字节发送 ACK 确认,因此可以说 TCP 采用的是累积确认机制。
需要说明的是,第二个例子也指出了一个重要的点。主机 A 在收到第二个数据段(字节编号从
900
900
900 到
1000
1000
1000)之前先收到了第三个数据段(字节编号从
536
536
536 到
899
899
899),那么现在的问题是:主机 A 的 TCP 协议应该如何处理先到达的第三个数据段。
有趣的是,由 IETF 制定的 TCP 协议标准并没有强制规定此时应该怎么做,而是将选择权留给了 TCP 协议的具体实现人员。实际上,对此大体上无外乎两种选择:
- 接收方立刻丢弃没有按照序列号顺序抵达的数据段,正如在先前博文中提及的,此举可以简化接收端的设计;
- 接收方保留没有按照序列号顺序抵达的数据段,接着等待应该按照顺序抵达的数据段到达后,将这些数据段一并发送给上层的应用。
很显然,就网络带宽角度而言,上述第二种方式更加高效,这也是实际 TCP 协议实现时采用的方式。
这里,最后需要特别指出的一点是,上图假定数据段的初始序列号为
0
0
0 。在实际中,一个 TCP 连接的收发两端都会随机选择一个起始序列号。此举是为了避免这样一种可能的情况,即两台主机可能先前建立了一个 TCP 连接,该连接目前虽然已断开,但网络中还存在二者之间发送的某数据段,如果此时二者又使用了和之前相同的端口号建立了新的连接,那么该数据段就有可能误认为是本次连接传输的,从而造成意料之外的情况。
以 Telnet 协议为例
为了更好的理解 TCP 协议对于序列号和 ACK 确认值的使用,下面以 Telnet 协议为例进行讲解。
Telnet 协议是由 IETF 在 RFC 854 规范中定义的一个用于用户远程登录的应用层协议,该协议使用 TCP 作为其传输层协议。
假定由主机 A 发起和主机 B 的 Telnet 会话,由于会话由主机 A 发起,因此主机 A 一般被称为客户端,而主机 B 一般被称为服务端。
Telnet 协议是交互式的,其工作方式是这样的:用户在客户端上键入的每一个字符都会被发送至服务端;服务端会发回每个字符的拷贝然后将其显示在客户端的显示器上(这也被称为“回显”)。Telnet 协议的这个机制确保了用户在客户端显示器上看到的所有字符都已经在服务端被成功接收和处理了。因此,在用户键入字符和在显示器上看到字符这段时间内,每个字符都经网络传输了两次。
假设用户在客户端上键入字符 'C' ,且客户端和服务器的数据段起始序列号分别为
42
42
42 和
79
79
79 。由于一个数据段的序列号是数据部分第一个字节在数据流中的编号,因此从客户端发往服务器的第一个数据段具有的序列号为
42
42
42 ,而从服务器发往客户端的第一个数据段具有的序列号为
79
79
79 。
又由于 ACK 确认值是主机期望收到的下一个字节的编号,因此在 TCP 连接建立之后且无任何数据发送之前,客户端在等待服务器编号为
79
79
79 的字节,服务器在等待客户端编号为
42
42
42 的字节。
因此,如上图所示客户端和服务器之间共计发送了三个数据段:
- 第一个数据段由客户端发送至服务器,在数据部分包含了一个字节的字母
'C' ,如前所述显然其序列号应该为
42
42
42 ,由于客户端至此还未收到来自服务器的数据,其期望收到的下一个数据段中应该包含从
79
79
79 开始的一系列字节,因此数据段中的 ACK 确认值应该为
79
79
79 ; - 第二个数据段由服务器发送至客户端,该数据段有两个作用,首先是服务器通过在
ACK 确认值位置填写
43
43
43 的方式告知客户端自己已经成功接收了截至编号为
42
42
42 的所有字节,同时在等待编号从
43
43
43 开始往后的字节;其次服务器向客户端“回显”字符 'C' ,由于这是服务端向客户端发送的第一个字节,因此数据段的序列号位置应该填写
79
79
79 ; - 第三个数据段又是由客户端发送至服务器,该数据段的唯一作用是向服务器确认已经收到“回显”字符,该数据段的
ACK 确认值是
80
80
80 ,这是因为客户端已经成功接收了截至编号为
79
79
79 的所有字节,同时在等待编号从
80
80
80 开始往后的字节。
对于上述第三个数据段,有些人可能会觉得有疑问,即此时数据段中并没有包含应用层数据,为什么还要有序列号?这是因为在 TCP 的数据段结构中,序列号是必填的数据域。
2. 计时器机制
在先前的博文中,为了解决丢包问题而引入了计时器机制。同样,TCP 协议也使用同样的机制来应对同样的问题,只是在具体实现上 TCP 协议并非针对每个正在传输但未被确认的数据包都设置了一个计时器,因为计时器的设置是需要额外开销的。因此, RFC 6298 Computing TCP’s Retransmission Timer 中推荐在任意时刻都仅使用一个计时器,尽管实际可能有很多已发送但未被确认的数据包。
下面基于类似【计算机网络自顶向下方法】手把手带你设计一个可靠且高效的数据传输协议采用的渐进方式,结合两个模型来逐步说明 TCP 是通过仅使用一个计时器来应对丢包的:
- 一个高度简化的模型,其中
TCP 的发送方仅基于超时与否来确定是否重传数据包; - 一个更加完备的模型,其中
TCP 除了基于超时还使用双重 ACK 确认(类似于前述博文中的 rdt2.2 相较于 rdt2.1 的优化)来确定是否重传数据包。
下面是第一个模型中 TCP 发送方的伪代码。可以看到其中和报文发送或重传相关的事件有三类,即:
NextSeqNum=InitialSeqNumber
SendBase=InitialSeqNumber
loop (forever) {
switch(event)
event: data received from application above
create TCP segment with sequence number NextSeqNum
if (timer currently not running)
start timer
pass segment to IP
NextSeqNum = NextSeqNum + length(data)
break;
event: timer timeout
retransmit not-yet-acknowledged segment with
smallest sequence number
start timer
break;
event: ACK received, with ACK field value of y
if (y > SendBase) {
SendBase = y
if (there are currently any not-yet-acknowledged segments)
start timer
}
break;
}
- 从上方应用程序收到待发送数据。此时
TCP 协议会先将数据封装进数据段中,然后将数据段传递至 IP 协议。显然,如前所述,每个数据段中都包含一个序列号,其数值为数据段数据部分中第一个字节在字节流中的编号。同时,更重要的是,如果此时没有处于运行中的计时器,则在将数据段传递给 IP 协议时会启动一个计时器。实际上,可以将这样的计时器理解成和最早已发送但未收到 ACK 确认的数据包是关联起来的。 - 计时器超时。此时
TCP 协议会先重传导致计时器超时的数据段,然后重启计时器。 - 收到
ACK 确认。此时 TCP 协议会将 ACK 确认值 y 和变量 SendBase 进行比较,其中后者是最早发送但仍未被确认的字节在数据流中的编号,即 SendBase - 1 是发送方已知被接收方按序成功接收的字节中的最后一个在数据流中的编号。由于前面说 TCP 使用累积确认机制,即当发送方收到的数据段中 ACK 确认值为 y ,这表示所有编号小于 y 的字节都已被接收方成功接收。因此,如果 y > SendBase ,这表明此次 ACK 确认了一个或一个以上先前发送但未被确认的数据段。此时发送方会更新 SendBase 的值,与此同时,如果此时仍有任何已发送但未被确认的数据段,则发送方还会重启计时器。
实际上,上述模型虽然简化了,但实际上也还是有一些微妙之处值得探究,下面通过几个案例来让读者对该简化模型有更深的理解:
- 案例
1
1
1 ,如下图所示,主机
A 向主机 B 发送一个数据段,该数据段序列号为
92
92
92 且其中包含
8
8
8 个字节应用数据,之后主机 A 会等待主机 B 返回具有 ACK 值为
100
100
100 的数据段。假设 A 发送的数据段 B 的确成功接收了,但后者的 ACK 却丢失了。在这种情况下,在计时器超时后,主机 A 会重传一个相同的数据段。当主机 B 收到重传的数据段后,根据序列号识别出这个数据段中数据先前已经成功接收了,那么主机 B 就会直接丢弃重传的数据段。
- 案例
2
2
2 ,如下图所示,主机
A 连续向主机 B 发送了两个数据段,其中第一个数据段的序列号为
92
92
92 且包含
8
8
8 个字节应用数据,第二个数据段的序列号为
100
100
100 且包含
20
20
20 个字节应用数据,之后两个数据段均安然无恙先后抵达主机 B ,接着主机 B 也先后返回了 ACK 值分别为
100
100
100 和
120
120
120 的数据段作为应答。假定此后两个应答均未能在计时器超时之前到达主机 A ,那么在超时发生后,主机 A 会重传序列号为
92
92
92 的数据段并重启计时器。此时,只要接收方针对前述第二个数据段的 ACK 在新计时器超时前到达主机 A ,那么第二个数据段就不会被重传。需要注意的是,由于 TCP 采用累积确认机制,因此当重传数据段到达主机 B 时,此时主机 B 针对重传数据段的确认数据段所具有的 ACK 是
120
120
120 而非
100
100
100 。
- 案例
3
3
3 ,假设此时主机
A 像上一种情况一样发送了两个数据段,此后在计时器超时前,虽然针对第一个数据段的 ACK 确认发生了丢包,但是主机 A 及时收到了 ACK 值为
120
120
120 的确认数据段。同样,由于 TCP 协议采取累积确认机制,主机 A 将不会重传任何数据段。
倍增超时间隔
需要特别指出的是,在大多数实际的 TCP 实现中,计时器的超时间隔并非一成不变的。当超时发生时,虽然 TCP 的确会重传所有已发送但未确认的数据段中序列号最小的那个,但每次都会将超时时间设置为先前的两倍。例如:假设协议栈根据默认算法计算出针对某数据段的初始超时时间间隔为
0.75
0.75
0.75 秒,当第一次超时发生后,TCP 在重传该数据段后会将超时时间间隔设置为
1.5
1.5
1.5 秒,如果该数据段传输期间又发生超时,那么 TCP 再次重传该数据段后会将超时时间间隔设置为
3.0
3.0
3.0 秒,依次类推。
然而,需要特别指出的是,当触发计时器启动的事件是从上层应用接收到数据或者收到 ACK 确认时,此时超时时间间隔还是由协议栈根据默认算法计算得出的。
实际上,上述机制提供了一种有限的拥塞控制,即计时器超时大概率是有此时网络拥堵所导致的,这很大可能意味着在收发两端的路由上,由于很多主机在同时收发数据包,导致一个或多个路由器的存储转发队列已满,进而造成了大量丢包或排队很长的排队时延。如果此时路由上的发送端仍然以相同的速率持续发送数据包,则可能进一步加剧网络拥堵。
3. 快速重传机制
针对基于是否超时确定是否重传的机制,存在的一个问题是超时的时间可能相对较长。当一个数据段丢失后,较长时间的超时间隔会提高收发两端之间的时延。
对此,发送方可以借由所谓的重复确认(duplicate ACK)来实现在计时器超时前提前探测到丢包。所谓重复确认是指发送方收到接收方针对同一个数据段具有相同 ACK 值的确认。
为了更容易理解为什么重复确认可以提前探测到丢包,这里需要先介绍为什么接收方需要发送重复确认。下表是 IETF 的 RFC 5681 TCP Congestion Control 规范中给出的 TCP 协议接收方应该在哪些情况下生成 ACK 。
事件 | TCP 接收方行为 |
---|
收到按序到达的数据段,数据段中序列号为期望序列号。所有截至期望序列号的数据都已经确认。 | 延迟发送 ACK 。等待最长
500
500
500 毫秒,如果在此期间下一个序列号按序排列的数据段未到达,则发送 ACK 确认。 | 收到按序到达的数据段,数据段中序列号为期望序列号。同时上一个按序到达的数据段处于
500
500
500 毫秒等待期内。 | 立刻发送一个累积确认的 ACK ,以确认两个按序到达的数据段。 | 收到非按序到达的数据段,数据段中的序列号大于期望的序列号,即探测到数据段序列号和期望序列号有间隔。 | 立刻发送重复 ACK ,表明期望收到的下一个数据段所应该具有的序列号。 | 收到序列号在上述间隔以内的数据段,该数据段的序列号可能部分或全部覆盖了上述间隔。 | 如果此时数据段的序列号为间隔的左边界,则立刻发送一个累计确认性质的 ACK ,否则立刻发送重复 ACK 。 |
当一个 TCP 的接收方收到了一个数据段,且该数据段的序列号大于下一个期望按序到达数据段的序列号,接收方此时就可以认为之前可能发生了丢包或者后发送的数据段早于先发送的数据段到达。此时 TCP 不会发送明确的否定应答,而是会发送一个重复的确认应答,其中的 ACK 值对应的是迄今为止最后一个按序到达的数据段。
由于一个 TCP 发送端经常会连续发送多个数据段,如果一个数据段发生丢包,那发送方就有可能会连续收到多个 ACK 重复的确认应答。一般来说,如果 TCP 发送方连续收到三个针对同一个数据段的重复 ACK ,发送方就会认为该数据段已经丢失了,在这种情况下,发送方就会执行所谓的快速重传,即虽然此时计时器未超时也依然对该数据段进行重传。下图就描述了这样一个场景:
基于上述分析,前面的代码段需要修改如下,其中仅修改了针对最后一个事件的逻辑。
NextSeqNum=InitialSeqNumber
SendBase=InitialSeqNumber
loop (forever) {
switch(event)
event: data received from application above create TCP segment with sequence number NextSeqNum
if (timer currently not running)
start timer
pass segment to IP
NextSeqNum = NextSeqNum + length(data)
break;
event: timer timeout
retransmit not-yet-acknowledged segment with smallest sequence number
start timer
break;
event: ACK received, with ACK field value of y
if (y > SendBase) {
SendBase=y
if (there are currently any not yet acknowledged segments)
start timer
}
else {
increment number of duplicate ACKs received for y
if (number of duplicate ACKs received for y == 3)
resend segment with sequence number y
}
break;
}
4. 滑动窗口还是选择重传协议
【计算机网络自顶向下方法】手把手带你设计一个可靠且高效的数据传输协议中向读者介绍两种一般性的可靠数据传输协议——滑动窗口协议和选择重传协议。那么 TCP 协议究竟属于哪一种呢?
如本文之前所述,TCP 协议的 ACK 应答是累积确认性质的,TCP 协议也不会针对正确接收但未按序列号顺序到达的数据段进行挨个确认,从这个角度来看 TCP 协议似乎比较像滑动窗口协议。
实际上,TCP 协议和滑动窗口协议还是有以下两个很大的不同之处:
- 大多数的
TCP 协议实现都会缓存正确接收但未按序列号顺序到达的数据段,而滑动窗口协议则会直接丢弃这样的数据段; - 相较于滑动窗口协议,
TCP 协议在一些情况下可以大幅降低报文重传的必要性。例如:假设发送方连续发送了序列号分别为
1
,
2
,
?
?
,
N
1,2,\cdots,N
1,2,?,N 的数据段,且所有数据段都安然无恙到达了接收方,而针对这些数据段的所有确认 ACK 中,只有针对序列号为
n
<
N
n<N
n<N 的数据段的确认 ACK 发生了丢包。在这样的情况下,如果是滑动窗口协议,那么发送方不仅会重传序列号为
n
n
n 的数据段,还会重传所有序列号为
n
+
1
,
n
+
2
,
?
?
,
N
n+1,n+2,\cdots,N
n+1,n+2,?,N 的数据段;然而,对于 TCP 协议而言,发送方最多会重传序列号为
n
n
n 的数据段,如果接收方针对序列号为
n
+
1
n+1
n+1 的数据段的确认 ACK 赶在计时器超时前到达发送方,那么发送方连序列号为
n
n
n 的数据段都不会重传。
那么是不是说 TCP 协议就是选择重传类型的协议呢?查阅网络资料可知, IETF 的 RFC 2018 TCP Selective Acknowledgment Options 规范中提出所谓的选择性确认机制,即 TCP 协议的接收方可以针对未按序列号顺序到达的数据段进行单独确认,而非总是进行累积式确认,同时结合选择重传机制,即不针对那些被选择性确认过的数据段进行重传。从这个角度来看,TCP 协议似乎和选择重传协议又有一些类似。
综上,TCP 协议属于结合了滑动窗口协议和选择重传协议二者有点的一个可靠数据传输协议。
|