CXD Linux Engineer

TCP协议中的几个问题

2017-11-08
   

TCP的建立与终止

TCP建立连接的三次握手

  1. 请求端(通常称为客户)发送一个SYN(同步序号)段指明客户打算连接的服务器的端口,以及初始序号(ISN - 系统随机生成的,在这个例子中为1415531521)。这个SYN段为报文段1。
  2. 服务器发回包含服务器的初始序号的SYN报文段(报文段2)作为应答。同时,将确认序号(ACK)设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号。
  3. 客户必须将确认序号设置为服务器的ISN加1以对服务器的SYN报文段进行确认(报文段3)。
  4. 当连接请求队列已满,TCP将不理会传入的SYN,也不发回RST(复位连接)作为应答,迫使客户TCP随后重传SYN,以等待请求队列有空间接收新的连接。
  5. 当TCP客户端的SYN没有收到服务器的响应,则等待6秒在发送一个,若仍无响应则等待24秒在发送一个,总共等待75秒后返回ETIMEDOUT错误。

TCP建断开连接的四次挥手

  1. 建立一个连接需要三次握手,而终止一个连接要经过4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。
  2. 收到一个FIN只意味着在这一方向上没有数据流动。一个TCP连接在收到一个FIN后仍能发送数据。而这对利用半关闭的应用来说是可能的,尽管在实际应用中只有很少的TCP应用程序这样做。
  3. 调用close是将该套接字的引用计数减一,为0时才关闭套接字。而shutdown可以不管引用计数就激发TCP的正常连接终止序列,并且可以通过参数关闭指定方向的字节流(读半部或者写半部)。

tcp_1
图1 - TCP的三次握手和四次挥手

TCP的数据传输

滑动窗口

  1. 窗口的大小在TCP建立连接的时候确认,TCP中的ACK是累积的--它表示接收方已经确认收到了一直到确认序列号减1的所有字节数。(注:在wireshark或tcpdump中为了便于观察都是将原始序列号显示为0,这样ACK数值的大小就是接收方确认接收到的字节总数加1,实际的ACK数值是原始序列号加上接收到的字节数再加1,可以在wireshark的首选项中设置显示ACK的实际数值)
  2. 如上图所示,发送方可以发送的最大未被确认的字节数为窗口的大小(窗口的大小实际上就是接收方的缓冲区大小),当收到接收方的ACK确认后,根据ACK的数值将窗口向右移,当接收方的缓冲区变小则窗口左边沿向右移窗口变小,说明接收方的接收缓冲区中还有数据没有处理。
  3. 当接收方的缓冲区被填满时,发送方的窗口大小变为0,此时发送方将不能在发送数据了,必须等待接收方处理缓冲区中的数据后再次发送ACK确认包并告知它当前窗口的大小。

tcp_2
图2 - 滑动窗口

超时重传

当一个数据包发送除去之后,TCP会启动一个重传定时器,第一次发送后所设置的超时时间为1.5秒,1.5秒过后如果还没有接收到ACK就认为被超时,并重传这个数据包,然后将定时器设置为3秒。一次类推超时时间分别为1、3、6、12、24、48和最长的64秒。可以看到此后该时间在每次重传时增加1倍直到64秒。这个倍乘关系被称为“指数退避”,如果总共超时时间超过了9分钟就会发送一个RST复位报文,并关闭连接。

慢启动和拥塞避免算法

  1. 拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。在实际中这两个算法通常在一起实现。
  2. 拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh。这样得到的算法的工作过程如下:
    • 对一个给定的连接,初始化cwnd为1个报文段, ssthresh为65535个字节。
    • TCP输出例程的输出不能超过cwnd和接收方通告窗口(滑动窗口)的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
    • 当拥塞发生时(数据包超时或收到重复确认ACK),ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(再次进入慢启动,只有数据包超时才会进入慢启动)。
    • 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到cwnds的值等于ssthresh时才停止(我们在步骤3发生拥塞时将ssthresh被设置为当前窗口大小的一半),然后转为执行拥塞避免。
    • 慢启动算法初始设置cwnd为1个报文段,此后每收到一个确认就增加1陪,也就是乘以2。正如2 0 . 6节描述的那样,这会使窗口按指数方式增长:发送1个报文段,然后是2个,接着是4个⋯⋯
    • 拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加比起来,这是一种线性增长(additive increase)。我们希望在一个往返时间内最多为cwnd增加1个报文段(不管在这个RTT(往返时间)中收到了多少个ACK),然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。

快速重传与快速恢复算法

  1. TCP按序收到正常数据后,接收TCP将数据交给用户进程,并将当前ACK加上收到的字节大小返回给发送方,但是当收到的报文段是失序的(收到的数据序列号不是期望的序号,这个期望的序号是根据发送数据包的序列号加上此数据包大小确定的,例如当前发送的数据包序列号为seq=5597,数据包大小为len=1400,则下一个数据包的序列号应该是6997,如果不是则说明数据包失序了),TCP会保存这个数据包,并返回一个已成功接收正常数据的最大序号加1的ACK,也就是这个失序的数据包不会改变ACK的值(也就是重复发送ACK)。
  2. 当收到第3个重复的ACK时,就假定一个报文段已经丢失并重传丢失的数据报文段,而无需等待超时定时器溢出。这就是Jacobson的快速重传算法。
  3. 快速重传之后接下来执行的不是慢启动而是拥塞避免算法,这就是快速恢复算法。整个算法的流程如下:
    • 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
    • 每次收到另一个重复的ACK时, cwnd增加1个报文段大小并发送1个分组(如果新的cwnd允许发送)。
    • 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。

tcp_3
图3 - 流量控制

TCP中的几种定时器

坚持定时器

前面我们提到当接收方的缓冲区被填满时,发送方的滑动窗口大小变为0,此时发送方将不能在发送数据了,必须等待接收方处理缓冲区中的数据后再次发送ACK确认包并告知它当前窗口的大小。这里就有一个问题了:当这个再次发送的通知窗口大小的ACK包丢掉了会出现什么现象?此时双方就有可能因为等待对方而使连接终止:接收方等待接收数据(因为它已经向发送方通告了一个非0的窗口),而发送方在等待允许它继续发送数据的窗口更新。为防止这种死锁情况的发生,发送方使用一个坚持定时器(persist timer)来周期性地向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称为窗口探查(windowprobe)。当接收方报告滑动窗口为0后,发送方不再发送数据,并启动坚持定时器,设置定时时间为5秒,5秒过后发送方就会发送一个窗口探查包,如果接收方回复ACK的窗口仍然为0时,发送方再次将时间间隔设置为6秒,6秒之后再次发生探查包,这些时间间隔一次为比5、6、12、24、48和最大为60,这个过程和超时重传类似,一直到窗口被打开,或者应用进程使用的连接被终止。

保活定时器

保活定时器主要用来检测一个空闲连接的另一端是否崩溃或者重启了。如果一个给定的连接在两个小时之内没有任何动作,则服务器就向客户发送一个探查报文段(我们将在随后的例子中看到这个探查报文段看起来像什么)。客户主机必须处于以下4个状态之一:

  1. 客户主机依然正常运行,并切服务器可已收到回复。客户的TCP响应正常,而服务器也知道对方是正常工作的。服务器在两小时以后将保活定时器复位。如果在两个小时定时器到时间之前有应用程序的通信量通过此连接,则定时器在交换数据后的未来2小时再复位。
  2. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务器将不能够收到对探查的响应,并在75秒后超时。服务器总共发送10个这样的探查,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
  3. 客户主机崩溃并已经重新启动。这时服务器将收到一个对其保活探查的响应,但是这个响应是一个复位,使得服务器终止这个连接。
  4. 客户主机正常运行,但是服务器接收不到回复。这与状态2相同,因为TCP不能够区分状态4与状态2之间的区别,它所能发现的就是没有收到探查的响应。

2MSL定时器

当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIMEWAIT状态停留的时间为2倍的MSL(Maximum Segment Lifetime - 报文段最大生存时间,一般为2分钟)。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。

重传定时器

这个定时器已经在上面讲过就不多说了。

TCP中的几种状态

  • ESTABLISHED数据传送状态,表示连接双方能够进行双向数据传递的状态。进入ESTABLISHED状态的变迁对应于打开一个连接的动作,从ESTABLISHED状态离开的变迁对应关闭一个连接的动作。
  • LISTEN监听状态,当服务器调用了listen套接字系统调用时,服务器就会进入LISTEN状态,等待与客户端建立连接。
  • SYN_SENT状态,表示客户端发送了SYN报文后,在和服务器建立连接时的状态。
  • STN_RCVD状态,表示服务器端接收到客户端的SYN报文后,在和客户端建立连接时的状态。
  • TIME_WAIT状态,上面讲2MSL定时器的时候讲过,就不重复了。

TCP中的几种报文类型

在TCP首部中有6个标志比特。它们中的多个可同时被设置为1。每个标志代表的意思:

  • URG 紧急指针(urgent pointer)有效(见20.8节)。
  • ACK 确认序号有效。
  • PSH 接收方应该尽快将这个报文段交给应用层。
  • RST 重建连接。
  • SYN 同步序号用来发起一个连接。这个标志和下一个标志将在第1 8章介绍。
  • FIN 发端完成发送任务。

tcp_3
图2 - TCP报文格式

参考

《TCP/IP协议详解》

唠叨几句:一直对TCP协议的实现细节非常陌生,近几天决定把《TCP/IP协议详解》中关于TCP的几个章节好好看一遍,为了加深理解和记忆遂写了这篇博文,这篇文章中的绝大部分都是摘抄自《TCP/IP协议详解》书中的内容,有些地方附加了自己的理解,修正了我觉得书中表述错误的几个地方。


Comments

Content