TCP协议的超时详解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP协议的超时详解相关的知识,希望对你有一定的参考价值。

参考技术A

众所周知,TCP协议是一个可靠的的协议。TCP的可靠性依赖于大量的 Timer 和 Retransmission 。现在咱们就来细说一下TCP协议的那些Timer

在TCP三次握手创建一个连接时,以下两种情况会发生超时:

1.client发送SYN后,进入SYN_SENT状态,等待server的SYN+ACK。
2.server收到连接创建的SYN,回应SYN+ACK后,进入SYN_RECD状态,等待client的ACK。
当超时发生时,就会重传,一直到75s还没有收到任何回应,便会放弃,终止连接的创建。但是在Linux实现中,并不是依靠超时总时间来判断是否终止连接。而是依赖重传次数:

当三次握手成功,连接建立,发送TCP segment,等待ACK确认。如果在指定时间内,没有得到ACK,就会重传,一直重传到放弃为止。Linux中也有相关变量来设置这里的重传次数的:

当一方接受到TCP segment,需要回应ACK。但是不需要 立即 发送,而是等上一段时间,看看是否有其他数据可以 捎带 一起发送。这段时间便是 Delayed ACK Timer ,一般为200ms。

如果某一时刻,一方发现自己的 socket read buffer 满了,无法接受更多的TCP data,此时就是在接下来的发送包中指定通告窗口的大小为0,这样对方就不能接着发送TCP data了。如果socket read buffer有了空间,可以重设通告窗口的大小在接下来的 TCP segment 中告知对方。可是万一这个 TCP segment 不附带任何data,所以即使这个segment丢失也不会知晓(ACKs are not acknowledged, only data is acknowledged)。对方没有接受到,便不知通告窗口的大小发生了变化,也不会发送TCP data。这样双方便会一直僵持下去。

TCP协议采用这个机制避免这种问题:对方即使知道当前不能发送TCP data,当有data发送时,过一段时间后,也应该尝试发送一个字节。这段时间便是 Persist Timer 。

TCP socket 的 SO_KEEPALIVE option,主要适用于这种场景:连接的双方一般情况下没有数据要发送,仅仅就想尝试确认对方是否依然在线。目前vipbar网吧,判断当前客户端是否依然在线,就用的是这个option。

具体实现方法:TCP每隔一段时间(tcp_keepalive_intvl)会发送一个特殊的 Probe Segment,强制对方回应,如果没有在指定的时间内回应,便会重传,一直到重传次数达到 tcp_keepalive_probes 便认为对方已经crash了。

当主动关闭方想关闭TCP connection,发送FIN并且得到相应ACK,从FIN_WAIT_1状态进入FIN_WAIT_2状态,此时不能发送任何data了,只等待对方发送FIN。可以万一对方一直不发送FIN呢?这样连接就一直处于FIN_WAIT_2状态,也是很经典的一个DoS。因此需要一个Timer,超过这个时间,就放弃这个TCP connection了。

TIME_WAIT Timer存在的原因和必要性,主要是两个方面:

主动关闭方发送了一个ACK给对方,假如这个ACK发送失败,并导致对方重发FIN信息,那么这时候就需要TIME_WAIT状态来维护这次连接,因为假如没有TIME_WAIT,当重传的FIN到达时,TCP连接的信息已经不存在,所以就会重新启动消息应答,会导致对方进入错误的状态而不是正常的终止状态。假如主动关闭方这时候处于TIME_WAIT,那么仍有记录这次连接的信息,就可以正确响应对方重发的FIN了。
一个数据报在发送途中或者响应过程中有可能成为残余的数据报,因此必须等待足够长的时间避免新的连接会收到先前连接的残余数据报,而造成状态错误。
但是我至今疑惑的是:为什么这个超时时间的值为2MSL?如果为了保证双方向的TCP包要么全部响应完毕,要么全部丢弃不对新连接造成干扰,这个时间应该是:

被动关闭方LAST_ACK的超时时间 + 1MSL

因为被动关闭方进入LAST_ACK状态后,假设一直没有收到最后一个ACK,会一直重传FIN,一直重传次数到达TCP_RETRIES放弃,将这个时间定义为「被动关闭方LAST_ACK的超时时间」,接着必须等待最后一个重传的FIN失效,需要一个MSL的时间。这样才能保证所有重传的FIN包失效,不干扰新连接吧。

TCP/IP Illustrated

TCP协议详解

TCP协议详解


TCP协议属于传输层协议。从通信和信息处理角度看,它属于面向通信部分的最高层,只有位于网络边缘的主机的协议栈才有传输层协议;同时也是用户功能中的最低层,一些重要的socket选项都和TCP协议相关。

TCP服务的特点

传输层协议主要有两个: TCP 协议和UDP协议。TCP协议相对于UDP协议的特点是:面向连接、字节流和可靠传输。
使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP连接是全双工的,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放系统资源。
TCP协议的这种连接是一对一的, 所以基于广播和多播(目标是多个主机地址)的应用程序不能使用TCP服务。而无连接协议UDP则非常适合于广播和多播。
TCP提供的字节流服务没有边界(长度)的限制,它源源不断地从通信的一端流入另一端。而UDP提供的数据报服务每个数据报都有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出,否则数据会被截断。
TCP传输是可靠的。首先,TCP 协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。其次,TCP协议采用超时重传机制,发送端在发送出一个TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。最后,因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层。UDP协议则和IP协议一样,提供不可靠服务。它们都需要上层协议来处理数据确认和超时重传。

TCP头部结构

TCP头部结构如下图所示,其中的诸多字段为管理TCP连接和控制数据流提供了足够的信息。

16位端口号:告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号。
32位序号: 一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化为某个随机值ISN ( Initial Sequence Number,初始序号值)。那么在该传输方向上(从A到B),后续的TCP报文段中序号值将被系统设置成ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如,某个TCP报文段传送的数据是字节流中的第1025 ~ 2048字节,那么该报文段的序号值就是ISN+1025。另外一个传输方向(从B到A)的TCP报文段的序号值也具有相同的含义。
32位确认号:用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1。假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。反之,B发送出的TCP报文段也同时携带自己的序号和对A发送来的报文段的确认号。
4位头部长度:标识该TCP头部有多少个32bit字(4字节)。因为4位最大能表示15,所以TCP头部最长是60字节。
6位标志位包含如下几项:

  • URG标志,表示紧急指针是否有效。
  • ACK标志,表示确认号是否有效。携带ACK标志的TCP报文段为确认报文段。
  • PSH标志,提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区中)。
  • RST标志,表示要求对方重新建立连接。搒带RST标志的TCP报文段为复位报文段。
  • SYN标志,表示请求建立一个连接。携带SYN标志的TCP报文段为同步报文段
  • FIN标志,表示通知对方本端要关闭连接了。携带FIN标志的TCP报文段为结束报文段。
    16位窗口大小:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。 它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
    16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。
    16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

TCP连接的建立和关闭(三次握手和四次挥手)


我们使用tcpdump抓取上面两台测试机器交换的TCP报文段来观察TCP连接和关闭的过程。
首先从ernest-laptop 上执行teInet命令登录Kongming20的80端口,然后抓取这一过程中客户端和服务器交换的TCP报文段。具体操作过程如下:

当执行telnet命令并在两台通信主机之间建立TCP连接后(telnet输出“Connectedto192.168.1.109”),输人Ctrl+]以调出telnet程序的命令提示符,然后在telnet命令提示符后输人quit以退出telnet客户端程序,从而结束TCP连接。整个过程中(从连接建立到结束),tcpdump输出的内容如代码清单3-2所示。

因为整个过程并没有发生应用层数据的交换,所以TCP报文段的数据部分的长度(length)总是0。为了更清楚地表示建立和关闭TCP连接的整个过程,我们将tcpdump输出的内容绘制成图3-6所示的时序图。

第1个TCP报文段包含SYN标志,因此它是一个同步报文段,即enmest-laptop (客户端)向Kongming20 (服务器)发起连接请求。同时,该同步报文段包含一个ISN值为535734930的序号。第2个TCP报文段也是同步报文段,表示Kongming20同意与ernest-laptop建立连接。同时它发送自己的ISN值为2159701207的序号,并对第1个同步报文段进行确认。确认值是535734931,即第1个同步报文段的序号值加1。前面说过,序号值是用来标识TCP数据流中的每一字节的。但同步报文段比较特殊,即使它并没有携带任何应用程序数据,它也要占用一个序号值。第3个TCP报文段是erest-laptop对第2个同步报文段的确认。至此,TCP 连接就建立起来了。建立TCP连接的这3个步骤被称为TCP三次握手。
从第3个TCP报文段开始,tcpdump输出的序号值和确认值都是相对初始ISN值的偏移。当然,我们可以开启tcpdump的-S选项来选择打印序号的绝对值。
后面4个TCP报文段是关闭连接的过程。第4个TCP报文段包含FIN标志,因此它是一个结束报文段,即emest-laptop要求关闭连接。结束报文段和同步报文段一样, 也要占用一个序号值。Kongming20 用TCP报文段5来确认该结束报文段。紧接着Kongming20发送自己的结束报文段6, ernest-laptop 则用TCP报文段7给予确认。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确认报文段5是否出现在连接断开的过程中,取决于TCP的延迟确认特性。延迟确认将在后面讨论。

如果客户端访问一个距离它很远的服务器,或者由于网络繁忙,导致服务器对于客户端发送出的同步报文段没有应答,此时客户端程序将必然是先进行重连(可能执行多次),如果重连仍然无效,则通知应用程序连接超时。

TCP状态转移

TCP连接的任意一端在任一时刻都处于某种状态,当前状态可以通过netstat命令看。图3-8是完整的状态转移图,它描绘了所有的TCP状态以及可能的状态转换。

图3-8中的粗虚线表示典型的服务器端连接的状态转移;粗实线表示典型的客户端连接的状态转移。CLOSED是一个假想的起始点,并不是一个实际的状态。

服务器端的状态转移过程

服务器通过listen系统调用进入LISTEN状态,被动等待客户端连接,因此执行的是所谓的被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向客户端发送带SYN标志的确认报文段。此时该连接处于SYN_RCVD状态。如果服务器成功地接收到客户端发送回的确认报文段,则该连接转移到ESTABLISHED状态。ESTABLISHED状态是连接双方能够进行双向数据传输的状态。
当客户端主动关闭连接时(通过close 或shutdown系统调用向服务器发送结束报文段),服务器通过返回确认报文段使连接进入CLOSE_WAIT 状态。这个状态的含义很明确:等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接。这将使连接转移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了。

客户端的的状态转移过程

客户端通过connect系统调用主动与服务器建立连接。connect 系统调用首先给服务器发送一个同步报文段,使连接转移到SYN_SENT状态。此后,connect 系统调用可能因为如下两个原因失败返回:

  • 如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于TIME_WAIT状态的连接所占用,则服务器将给客户端发送一个复位报文段,connect调用失败。
  • 如果目标端口存在,但connect在超时时间内未收到服务器的确认报文段,则connect调用失败。

connect调用失败将使连接立即返回到初始的CLOSED状态。如果客户端成功收到服务器的同步报文段和确认,则connect调用成功返回,连接转移至ESTABLISHED状态。当客户端执行主动关闭时,它将向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态。若此时客户端收到服务器专门用于确认目的的确认报文段(比如图3-6中的TCP报文段5),则连接转移至FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态是可能发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入TIME_WAIT状态。
TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。换言之,通信的一端可以发送结束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。TCP连接的这种状态称为半关闭状态,如下图3-7所示。

图3-8还给出了客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路(不经过FIN_WAIT_2状态),前提是处于FIN_WAIT_1 状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)。这种情况对应于图3-6中的服务器不发送TCP报文段5。
前面说过,处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移至TIME_WAIT状态,否则它将一直停留在这个状态。如果不是为了在半关闭状态下继续接收数据,连接长时间地停留在FIN_WAIT_2状态并无益处。连接停留在FIN_WAIT_2状态的情况可能发生在:客户端执行半关闭后,未等服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称之为孤儿连接(和孤儿进程类似)。Linux 为了防止孤儿连接长时间存留在内核中,定义了两个内核变量: /proc/sys/net/ipv4/tcp_max_orphans 和/proc/sys/net/ipv4/tcp_fin_timeout。前者指定内核能接管的孤儿连接数目,后者指定孤儿连接在内核中生存的时间。

TIME_WAIT 状态

从图3-9来看,客户端连接在收到服务器的结束报文段(TCP 报文段6)之后,并没有直接进入CLOSED状态,而是转移到TIME_ WAIT 状态。在这个状态,客户端连接要等待一段长为2MSL (Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭。MSL是TCP报文段在网络中的最大生存时间,标准文档RFC 1122的建议值是2 min。
TIME_WAIT 状态存在的原因有两点:

  • 可靠地终止TCP连接
  • 保证让迟来的TCP报文段有足够的时间被识别并丢弃。

第一个原因很好理解。假设图3-9中用于确认服务器结束报文段6的TCP报文段7丟失,那么服务器将重发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像TCP报文段7那样的确认报文段。
在Linux系统上,一个TCP端口不能被同时打开多次(两次及以上)。当一个TCP连接处于TIME_WAIT 状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来思考,如果不存在TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具有相同的IP地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身,新的化身可能接收到属于原来的连接的、携带应用程序数据的TCP报文段(迟到的报文段),这显然是不应该发生的。这就是TIME_WAIT状态存在的第二个原因。
另外,因为TCP报文段的最大生存时间是MSL,所以坚持2MSL时间的TIME_WAIT状态能够确保网络上两个传输方向上尚未被接收到的、迟到的TCP报文段都已经消失(被中转路由器丢弃)。因此,一个连接的新的化身可以在2MSL时间之后安全地建立,而绝对不会接收到属于原来连接的应用程序数据,这就是TIME_WAIT状态要持续2MSL时间的原因。
有时候我们希望避免TIME_WAIT 状态,因为当程序退出后,我们希望能够立即重启它。但由于处在TIME_WAIT状态的连接还占用着端口,程序将无法启动(直到2MSL超时时间结束)。
对客户端程序来说,我们通常不用担心上面描述的重启问题。因为客户端一般使用系统自动分配的临时端口号来建立连接,而由于随机性,临时端口号一般和程序上一次使用的端口号(还处于TIME_WAIT状态的那个连接使用的端口号)不同,所以客户端程序一般可以立即重启。除非我们强制客户端使用固定端口。
但如果是服务器主动关闭连接后异常终止,则因为它总是使用同一个知名服务端口号,所以连接的TIME_WAIT状态将导致它不能立即重启。不过,我们可以通过socket选项SO_REUSEADDR来强制进程立即使用处于TIME_WAIT状态的连接占用的端口。

超时重传

在异常网络状况下(开始出现超时或丢包),TCP为了保证其承诺的可靠服务,TCP服务必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就是TCP的重传策略。
虽然超时会导致TCP报文段重传,但TCP报文段的重传可以发生在超时之前,即快速重传。

拥塞控制

TCP模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。这就是所谓的拥塞控制。
TCP拥塞控制的标准文档是RFC 5681,其中详细介绍了拥塞控制的四个部分:慢启动、拥塞避免、快速重传和快速恢复。
拥塞控制的最终受控变量是发送端向网络一次连续写入 (收到其中第一个数据的确认之前)的数据量,我们称为SWND (Send Window,发送窗口)。 不过,发送端最终以TCP报文段来发送数据,所以SWND限定了发送端能连续发送的TCP报文段数量。这些TCP报文段的最大长度(仅指数据部分)称为SMSS (Sender Maximum Segment Size,发送者最大段大小),其值一般等于MSS。
发送端需要合理地选择SWND的大小。如果SWND太小,会引起明显的网络延迟;反之,如果SWND太大,则容易导致网络拥塞。前文提到,接收方可通过其接收通告窗口(RWND)来控制发送端的SWND。但这显然不够,所以发送端引入了一个称为拥塞窗口(Congestion Window, CWND)的状态变量。实际的SWND值是RWND和CWND中的较小者。图3-11 显示了拥塞控制的输人和输出(可见,它是一个闭环反馈控制)。

以上是关于TCP协议的超时详解的主要内容,如果未能解决你的问题,请参考以下文章

TCP/IP传输层协议实现 - TCP的超时与重传(lwip)

TCP协议详解

计算机网络TCP协议详解

TCP协议详解(图解TCP协议格式TCP的各种可靠传输机制)

TCP协议详解(图解TCP协议格式TCP的各种可靠传输机制)

TCP UPD详解