计算机网络运输层详解(4)——TCP
TCP是面向连接的。在一个应用进程可以开始向另外一个应用进程发送数据之前,这两个进程必须先相互“握手”,即他们必须相互发送某些预备报文段,以建立确保数据传输的参数。
TCP连接提供的是全双工服务,允许数据在两个方向上同时传输。
一旦建立起一条TCP连接,两个应用进程之间就可以相互发送数据了。客户进程通过套接字传递数据流。数据一旦通过该门,它就由客户中的TCP控制了。TCP将这些数据引导到该连接的发送缓存中,发送缓存是三次握手初期设置的缓存之一。接下来TCP会时不时从发送缓存里取出一块数据。TCP可以从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(Maximum Segment Size,MSS)。MSS通常根据最初确定的由本地发送主机发送的最大链路层帧长度,即所谓的最大传输单元(Maximum Transmission Unit,MTU)来设置。该设置MSS要保证一个TCP报文段加上TCP/IP首部长度(通常40字节)将适合单个链路层帧。以太网和PPP链路层的协议都具有1500字节的MTU,因此MSS的典型值为1460字节。
TCP报文段结构
当TCP发送一个大文件,例如Web页面上的一个图片时,TCP通常是将该文件划分成长度为MSS的若干块。然而,交互式应用通常传送长度小于MSS。对于像Telnet这样的远程登录应用,其TCP报文段的数据字段经常只有一个字节。由于TCP首部一般是20字节,所以Telnet发送的报文段也许只有21字节。
TCP报文段包含以下字段:
- 源端口号、目的端口号,用于多路复用/分解来自或送到上层应用的数据。
- 32比特的序号和32比特的确认号。这些字段被TCP发送方和接收方用来可靠数据传输服务。
- 16比特的接收窗口,该窗口用于流量控制。
- 4比特的首部长度,该字段只是了以32比特(必须是32比特的倍数)的字为单位的TCP首部长度。由于TCP选项字段的原因,TCP首部的长度是可变的。TCP首部的典型长度就是20字节。
- 可选与变长的选项字段。
- 6比特的标记字段。
序号和确认号
这是TCP首部中最重要的两个字段,是可靠数据传输的关键部分。TCP把数据看成一个无结构的、有序的字节流。一个报文段的序号是该报文段首字节的字节流编号。举个栗子,假设主机A上的一个进程想通过一条TCP连接向主机B上的一个进程发送一个数据流。主机A中的TCP将隐式地对数据流中的每一个字节编号。假定数据流由一个包含500000字节的文件组成,MSS为1000字节,数据流的首字节编号为0,那么第一个报文段的序号为0,第二个为1000,第三个为2000,以此类推。事实上,一条TCP连接的双方都可以随机地选择初始序号。
TCP是全双工的,主机A在向B发送数据的同时,也许在接收来自B的数据。从主机B到达的每一个报文段中都有一个序号用于从B流向A的数据。主机A填充进报文段的确认号是主机A期望从主机B收到的下一个字节的序号。TCP使用累计确认的机制(见上一章GBN协议)。
三次握手
假设运行在一台主机上的一个进程想与另一台主机上的一个进程建立一条连接。客户进程首先通知客户TCP,它想建立一个与服务器上某个进程之间的连接。客户端TCP会按以下方式与服务器中的TCP建立一条连接:
- 第一步:客户端的TCP首先向服务端的TCP发送一个特殊的TCP报文段。该报文段不包含应用层数据,但在报文段的首部中的一个标志位SYN比特被置为1。因此,这个特殊的报文段被称为SYN报文段。另外,客户端会随机地选择一个初始序号(client_isn)( initial sequence number ISN)放于该SYN报文段的序号当中。该报文段会被封装在一个IP数据报中并发给服务器。
- 第二步:一旦该SYN报文段到达服务器,服务器将为该TCP连接分配TCP缓存和变量,并向客户TCP发送允许连接的报文段,这个报文段也不包含应用层数据。该报文段的首部包含了三个重要信息。首先,SYN比特被置为1。其次,确认号字段被设置为 client_isn + 1。最后服务器随机选择自己的初始序号(server_isn)放到序号字段中。这个允许连接的报文段实际上表明了:“我收到了你发起建立连接的SYN分组,该分组带有初始序号client_isn。我同意建立该连接,我的初始序号是server_isn。”这个报文段有时被称为SYNACK报文段。
- 第三步:在收到SYNACK报文段后,客户端也要给该连接分配缓存和变量。客户主机向服务器发送另外一个报文段,这个报文段对SYNACK报文段进行了确认(客户将server_isn + 1 放到确认号字段里)。因为连接已经建立了,所以SYN比特被置为0。三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据。
一旦完成这3个步骤,客户和服务器就可以互相发送包括数据的报文段了。
为什么是三次握手?
为什么需要三次握手,而不是两次呢?主要有两点原因:
- 一,也是最重要的一点,双方要互相告知自己的起始序列号,并确认对方已收到自己的序号的必经步骤。TCP 的可靠连接是靠 seq 进行重传或接收,避免连接复用时无法分辨出 seq 是延迟或者是旧链接的 seq,因此需要三次握手来约定确定双方的 ISN。两次握手后,客户主机 A 知道了对方已经收到了自己的初始序号。如果缺少第三次握手,那么服务主机 B 无法确定主机 A 是否收到了自己的初始序号,将无法保证传输的可靠性。
- 二,两端都要知到自己和另外一个人的收发能力是否都正常。客户主机 A 向主机 B 发送SYN报文段,如果 B 收到了SYN报文段,那么主机 B 知道了主机 A 的发送能力是正常的,B接着发送一个SYNACK报文段。如果A收到了SYNACK报文段,那么它能确定自己和 B 的发送和接收都正常。如果仅有两次握手,那么接收方B无法确定自己发送SYNACK是否被正确接收,也就是自己的发送功能和 A 的接受功能是否正常,第三次握手能使 B 确认自己的发送和 A 的接收是正常的。
那为什么不需要四次握手呢?因为三次握手就足够了,一般而言四次握手其实就是把三次握手的第二个报文拆成两个报文发送,这两次握手当然可以合并成一次了。
第三次握手失败了会发生什么?
客户端行为
在第二次握手之后,客户TCP已经确认对方收到了我的起始序号,也确认了双方的发送和接收功能都是正常的。因此,第二次握手结束后,客户TCP的状态就变成了established,认为连接已经建立成功,并且可以单方面地向服务端TCP发送数据。
服务端行为
如果服务端迟迟没有收到ACK,那么它的状态为 SYN_RCVD ,并且根据TCP的超时重传机制,会等待3秒、6秒、12秒后会重新发送第二次握手的分组,以便客户端重新发送第三个分组。如果若干次后还没有收到ACK应答,服务器将关闭这个连接。如果在第三次握手的ACK丢失的情况下客户端向服务端发送了数据,那么服务器将以RST包响应。
四次挥手
四次挥手的具体流程如下图。
为什么挥手要四次?
握手只需三次,因为可以把四次握手的两次合成一次。那为什么不能把挥手的第二步和第三步也合并成一个呢?
第一次挥手是告诉服务端,客户端不需要发送数据了;第二次挥手表明服务器知道了客户端不需要发送数据了,但此时服务端可能还要数据需要发送,因此不能立刻发送 FIN; 当服务器的数据也发送完时便可以发送 FIN 了;第四次握手表明客户端也知道服务器不再发送数据了,但此时还需等待2MSL才能关闭TCP客户端。
为什么客户TCP要等待2MSL(maximum sequence lefttime)
MSL是报文在网络中最长生存时间,这是一个工程值(经验值),是规定,不是算出来的,不同的系统中可能不同,一般是分钟级别。
为什么要等待?如果最后一个ACK没有抵达服务端,那么服务器将保持 LAST_ACK 状态,无法正常关闭。但由于服务端会超时重发第三次挥手的 FIN,让客户TCP尝试重发最后一个 ACK,如果客户端不等待而直接关闭了,那么服务端将无法正常地进入关闭状态。
为什么是2MSL?客户端发送完ACK后并不知道服务端是否正确收到,此时有两种情况,一服务端正确地收到了ACK,并再也不会发送报文段;二是服务端没有收到ACK,客户端再次收到了重传的FIN数据,并且重新发送ACK。客户端要取这两种情况等待地最大值,以应付最坏的情况发生。这种最坏情况是,第一次发送的ACK用了MSL的时间才到达客户端,超过这个时间服务端会重发FIN,重发的FIN最多用MSL的时间才到达客户端。因此,只要服务端重发了FIN,且没有网络故障,那么客户端一定能在2MSL内收到。
往返时间的估计
TCP采用超时/重传机制来处理报文段的丢失,那么一个关键的问题就是超时时间间隔长度的设置。显然,超时间隔必须大于该连接的往返时间(RTT)
RTT:从某报文被发出到收到对该报文段的确认之间的时间量。
大多数的TCP实现是在某个时刻做一次样本测量(sampleRTT),并不是为每个报文都做测量。另外,TCP决不为已被重传的报文段估计sampleRTT,原因如下。主机 A 发送了一个P1报文段,但一直未收到ACK,超时后进行重传一个和P1完全相同的报文段P2,但发送完不久后就收到了P1的ACK,如果此时对P2进行了采样,就会获取到一个错误的采样值。
为了估计一个典型的RTT,要对采取的SampleRTT取平均,TCP会维持一个SampleRTT均值,称为EstimatedRTT。一旦获取到一个新SampleRTT时,就会按照以下公式更新EstimatedRTT。这种平均叫做指数加权移动平均。α的参考值在一些标准中是 0.125。
$$ EstimatedRTT = (1 - α) * EstimatedRTT + α * SampleRTT $$
除了估算RTT,测量RTT的变化也是有价值的。RTT的偏差DevRTT,用于估算SampleRTT一般会偏离EstimatedRTT的程度:
$$ DevRTT = (1 - β) * EstimatedRTT + α * | SampleRTT - EstimatedRTT | $$
如果SampleRTT的波动比较小,那么DevRTT的波动就小,波动大,那么DevRTT的值就很大。 β 的推荐值为 0.25。
现在我们已经有了 EstimatedRTT 和 DevRTT,超时时间应该大于等于 EstimatedRTT,否则将造成不必要的重传,但也不能大太多,否则当报文段丢失的时候,TCP不能很快的重传,导致数据传输时延大。因此要将超时间隔设为 EstimatedRTT 加上一个余量。当 SampleRTT 波动大一些时,余量应该大一些,波动小一些时,余量应该减少些。最终可以确定使用以下,能考虑到所有方面:
$$ TimeoutInterval = EstimatedRTT + 4 * DevRTT $$
初始的TimeoutInterval设置为 1 秒。当超时后,TimeoutInterval将加倍。一旦收到报文段并更新 EstimatedRTT 后,TimeoutInterval 就又使用上面的公式计算了。
超时与快速重传
超时,间隔加倍。每当超时事件发生时,TCP重传具有最小序号的还未被确认的报文段。只是每次重传时都会将下一次的超时间隔设为先前值的两倍,而不是用从 EstimatedRTT 和 DevRTT 推算出的值。
快速重传。 超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组,因而增加了端到端时延。但发送方通常可以在超时事件发生之前通过注意冗余ACK来检测丢包情况。
冗余ACK(duplicate ACK)就是再次确认某个报文段的ACK,而发送方先前已经收到对该报文段的确认。
一旦收到某个数据报的3个冗余ACK,TCP就进行快速重传,立刻重传该报文段。
是回退N步还是选择重传
考虑下TCP的差错恢复机制是一个GBN协议还是一个SR协议。上面说过,TCP使用了累计确认的机制,TCP发送方只需要维护已发送的但未被确认的最小的序号和下一个要发送的报文段的序号,在这个方面上,TCP看起来像一个GBN风格的协议。
但TCP协议还是和GBN协议有一些区别的,假如有一百个分组 1,2,3,···,100,第 30 个分组的确认报文段丢失,其余所有分组的ACK都分别在超时之前到达了发送端。在该例中,GBN会重传30到100的所有分组,而TCP由于快速重传,最多只会重传第30个报文段。因此,TCP的差错恢复机制也许最好被分类为GBN协议和SR协议的混合体。
流量控制
一条TCP连接每一侧主机都为该连接设置了接收缓存。当TCP连接收到正确、按序的字节后,就把数据放到接收缓存。相关联的应用进程会从该缓存中读取数据,但不必是数据刚一到达就读取,应用进程也可能在忙于其他事,要很长时间才能读取数据缓存。因此,如果发送方发地太快而接收方读取比较慢,那么接收缓存就很可能溢出。
TCP提供了流量控制服务以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率和接收方应用程序的读取速度相匹配。
TCP 通过让发送方维护一个称为接收窗口(receive window)的变量来提供流量控制。接收窗口用于给发送方一个提示——该接收方还有多少可用的缓存空间。假设主机A通过TCP向主机B发送一个大文件。主机B为该连接分配了一个接收缓存,并用 RcvBuff 来表示。主机B上的进程不时地从该缓存中读取数据。我们定义以下变量:
- LastByteRead:主机B上的进程从缓存读出的数据流的最后一个字节的编号。
- LastByteRcv:从网络中到达并且已放入主机B接收缓存中的数据流的最后一个字节的编号。
如果TCP不允许已分配的缓存溢出,那么下式必须成立:
$$ LastByteRcv - LastByteRead <= RcvBuff $$
接收窗口用 rwnd 表示,根据缓存可用空间的数量来设置:
$$ rwnd = RcvBuff - (LastByteRcv - LastByteRead) $$
由于该空间是随着时间变化的,所以 rwnd 是动态的。
主机 B 通过把当前的 rwnd 值放入它发给主机 A 的报文段的接收窗口字段,通知主机 A 还有多少可用空间。开始时,主机 B 设定 rwnd = RcvBuff。
主机 A 跟踪两个变量, LastByteSend 和 LastByteAcked,注意到这两个变量的差值 LastByteSend - LastByteAcked,这就是主机 A 发送到网络中,B 可能还没收到的数据的字节数(ACK可能在来的路上),这个差值是主机 B rwnd 尚未减去的数值。例如 B 发给 A 的接收窗口是 100,A 检查 LastByteSend 和 LastByteAcked 的差值,发现也是 100,那此时 A 不能再发送数据了,因为可能一会之后,这100字节的数据就到达 B ,B的 rwnd 就会被更新为0。因此,主机 A 在连接的整个周期必须保证:
$$ LastByteSend - LastByteAcked <= rwnd $$
拥塞控制
实践中,丢包一般是当网络变得拥塞时由于路由器缓存溢出引起的。分组重传因此作为网络拥塞的征兆来对待,但却无法处理导致网络拥塞的原因,因为太多的源想以过高的速率发送数据。因此,为了处理网络拥塞原因,需要一个机制以在面临网络拥塞时遏制发送发。
拥塞的代价
- 当分组的到达速率接近链路容量时,分组经历巨大的排队时延。
- 发送方必须重传以补偿因为缓存溢出而丢失(丢弃)的分组。
- 发送方在遇到大时延时所进行的不必要重传会引起路由器利用其链路带宽来转发不必要的分组副本。
- 当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费掉了。
TCP拥塞控制
TCP使用段对端拥塞控制而不是使用网络辅助的拥塞控制,因为IP层不向端系统提供显式的网络拥塞反馈。如果TCP发送方感知从它到目的地之间的路径上没什么拥塞,则TCP发送方增加其发送速度;如果发送方感知沿着该路径有拥塞,那么发送方就会降低其发送速率。这种方法提出了三个问题。第一,一个TCP发送方如何限制它向其连接发送流量的速率呢?第二,一个TCP发送方如何感知它到目标地之间的路径存在拥塞呢?第三,当发送方感觉到拥塞时,采用什么算法来改变其发送速率呢?
TCP的每一端都是由一个发送缓存、一个接受缓存和几个变量(LastByteRcv、LastByteRead等)组成的。运行在发送方的TCP需要跟踪一个额外的变量,即拥塞窗口(congestion window)。拥塞窗口表示为 cwnd,它对TCP发送方向网络发送流量的速率做了限制,我们将更新上面提到的一个公式为:
$$ LastByteSend - LastByteAcked <= min(rwnd, cwnd) $$
当发送方出现超时,或者受到来自接收方的3个冗余 ACK ,那么发送方就认为在这个路径上出现了拥塞。
一个丢失的报文段意味着拥塞,因此当丢失报文段时应当降低TCP发送方的速率。
一个确认报文段指示该网络正向接收方交付发送方的报文段。因此当对先前未确认的报文段的确认到达时,能够增加发送方的速率。确认的到达被认为是一切顺利的隐含提示,即报文段正从发送方成功地交付给接收方,网络不拥塞,窗口长度因此能增加。如果确认以相当慢的速度到达,则拥塞窗口会以相当慢的速度增加;如果确认以高速度到达,那么拥塞窗口将会更迅速地增加。
TCP拥塞控制算法包括三个主要部分: ①慢启动;②拥塞避免;③快速恢复。慢启动和拥塞避免是TCP的强制部分,快速恢复是推荐部分,对TCP发送方不是必须的。
慢启动
当一条TCP连接开始时,cwnd 的值通常初始设置为一个 MSS 的较小值,这样初始发送速率大约为 MSS / RTT。由于对 TCP 发送方而言,可用带宽可能比 MSS / RTT 大的多,TCP 发送方希望迅速找到可用带宽的数量。因此,在慢启动状态,cwnd 的值以 1 个 MSS 开始并且每当传输的报文段首次被确认就增加一个 MSS。在此阶段下,每过一个 RTT,发送速率就翻倍。因此,TCP 发送速率起始慢,但在慢启动阶段以指数增长。
结束这种指数增长的阶段有三种方式。首先,如果发生一次超时(即拥塞),TCP 发送方会将 cwnd 的值设置为 1 并重新开始慢启动过程,同时它还将第二个状态变量的值 ssthresh(慢启动阈值)设置为 cwnd / 2。第二种情况,当 cwnd 的值等于 ssthresh 时,结束慢启动并转移到拥塞避免模式。最后一种情况是,如果检测到 3 个冗余 ACK,这时 TCP 执行快速重传丢失的报文段并进入快速恢复模式。
拥塞避免
一旦进入拥塞避免状态,cwnd 的值大约是上次遇到拥塞时的一半,即距离拥塞可能并不遥远!因此,TCP 无法每过一个 RTT 就将 cwnd 的值翻倍,而是采用比较保守的方法,每个 RTT 只将 cwnd 的值增加一个 MSS。一种通用方法是对于 TCP 发送方无论何时到达一个新的确认,就将 cwnd 的值增加一个 MSS * (MSS / cwnd)
在此阶段,cwnd是线性增长的(每 RTT 1 MSS)。当出现超时时,拥塞避免的算法慢启动一样。当检测到 3 个冗余 ACK 时,行为也与慢启动一样。
快速恢复
在快速恢复中,对于引起 TCP 进入快速恢复状态的缺失报文段,对收到的每个冗余的 ACK, cwnd 的值增加一个 MSS。最终,当对丢失报文段的一个 ACK 到达时, TCP 在降低 cwnd 后进入拥塞避免状态。如果出现超时,其行为和慢启动一样。