三、TCP
TCP通过多种方式确保可靠性的机制
1. 应用数据被分割成TCP认为最适合发送的数据块。
2. 当TCP发布一个段后,它启动一个定时器,等待目的端确认收到这报文段。如果不能及时收到一个确认,将重发 这个报文段。
3. 当TCP收到来自发送端的数据,它将发送一个确认。这个确认不是立即发送,一般推迟几分之一秒(大概200ms)
4. TCP将保持它的首部和数据的校验和
5. 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果 必要,TCP将对收到数据进行重新排序,将收到的数据以正确的顺序交给应用层。
6. TCP还能提供流量控制,TCP连接的每一方都有固定的缓冲空间。TCP的接收端只允许另一端发送接收到缓冲区所 能接纳的数据。这将防止较快主机致使减慢主机的缓冲区溢出
1. 两个应用程序通过TCP连接交换 8 bit 字节构成字节流。TCP不在字节流中插入记录标识符。我们将这称为字节流服务(byte stream service)。如果另一方的应用程序先传 10 byte,又传输 20 byte, 再传 50 byte, 连接的另一方将无法了解发送方每次发送了多少个字节。收方可以分4次发送这80个字节,每次接收20字节。一端将字节流放到TCP连接上,同样的字节流将出现在TCP连接的另一端。
2. 另外,TCP对字节流的内容不做任何解释。TCP不知道传输的数据字节流是二进制数据还是ASCII字符、EBC、DIC字符或者其他类型数据。对字节流的解释由TCP连接双方的应用层解释。
源端口和目的端口,各占2个字节,分别写入源端口和目的端口。
每个TCP段都包含源端口和目的端口号,用于寻找发端和收端应用进程。他两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接
一个IP地址和一个端口号也称为一个插口(socket)插口对(socketpair)
确认号,占4个字节,是期望收到对方下一个报文的第一个数据字节的序号。例如,B收到了A发送过来的报文,其序列号字段是701,而数据长度是200字节,这表明B正确的收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701;
发送ACK无需占用任何序号,因为32bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。
TCP为应用层提供全双工服务。这意味数据能在两个方向上独立进行传输。因此连接的每一端必须保持每个方向上的传输数据序号。
TCP可以表述为一个没有选择确认和否认的滑动窗口协议。我们说TCP缺少确认是因为TCP首部中的确认序号所表示发方收到字节,但还不包括确认序号所指的字节。当前还无法对数据流中选定的部分进行确认。例如: 如果在 1~1024字节已经成功收到,下一报文段中包含序号从2049~3072的字节,收端并不能确认这个新的报文段。它所能做的就是发挥一个确认序号为1025的ACK,它也无法对一个报文段进行否认。例如: 如果收到包含1025~2048字节的报文段,但它的检验和错,TCP接收端所能做的就是发回一个确认序列号为1025的ACK。
当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接。
一般来说,无论何时一个报文段发往基准的连接出现错误,TCP都回发出一个复位报文段。
1. 发送到不存在的端口的连接请求,IP存在,但是PORT不存在的时候,服务器就会发出RST包
2. 异常终止一个连接的时候,差不多强制断开了,没有四次挥手的过程了。
1) 终止一个连接的正常方式是一方发送FIN。有时这也称为有序释放。
2) 有可能发送一个复位报文段而不是FIN来中途释放一个连接。有时候称这为异常释放。
3) 异常终止一个连接对应用程序有两个优点:
①: 丢弃任何代发数据并立即发送复位报文段。
②: RST的接收方会区分另一端执行的异常关闭还是正常关闭。应用程序使用的API必须提供产生异常关闭而不是正常 关闭的手段
在某些书中,将它看做是可"协商"选项。它并不是任何条件下都可以协商的。当建立一个连接时候,每一方都有用于通告它期望接收到MSS选项(MSS选项只能出现在SYN报文段中)。如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节。
一般来说,如果没有分段发生,MSS还是越大越好。报文段越大允许每个报文段传送的数据就越多,相对IP和TCP首部有更高的网络利用率。当TCP发送一个SYN时,或者是因为一个本地应用进程向发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将MSS值设置为外出接口上的MTU长度减去固定的IP首部和TCP首部的长度。对于一个以太网,MSS值可达到1460字节。
如果目的IP地址为"非本地的(nonlocal)",MSS通常默认值536字节。而区分地址是本地还是非本地是很简单的,如果目的IP地址的网络号和子网号都和我们相同,则是本地的,如果目的IP地址的网络号与我们是不相同的,则是非本地的; 如果目的IP地址的网络号与我们的相同而子网号与我们不同,则可能是本地的,也有可能是非本地的。大多数TCP实现版都提供了一个配置选项,让系统管理员说明不同的子网是属于本地的还是非本地的,这个选项的设置将确定MSS可以选择尽可能的大(达到外出接口的MTU长度)或者默认值536。
3.1 三次握手
3.1.1 三次握手的含义
2. TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
如下图: 首部长度到终止比特FIN共占用2个字节,数据为 8002 (1000 0000 0000 0010),其中SYN为0x002,即二进制数据的倒数第二位,也就是 1。SYN=1, seq=0
3. TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号
如下图: SYN=1, ACK=1, seq=0
4. TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
如下图: SYN=0, ACK=1, seq=1
3.2 四次挥手
1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
FIN=1, seq=4, Ack=4
2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
ACK=1, Ack=5, seq=4
3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
FIN=1, Ack=5, Seq=4
5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗ *∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
ACK=1, Ack=5, Seq=5
3.2.3 为什么客户端最后还要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
说明: 客户执行主动关闭并进入TIME_WAIT是正常的。服务器通常执行被动关闭,不会进入TIME_WAIT状态。这暗示我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地接口。这不会带来什么问题,因为客户端使用本地端口,而并不关心这个端口号是什么。 对于服务器,情况有所不同,因为服务器使用的指明端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不会把它的者指明端口复制给它的断电没因为那个端口是处于2MSL连接的一部分。在这个重新启动服务器程序前,需要等待 1~4分钟。
1. 对于来自某个连接的焦躁替身的迟到报文段,2MSL等待可防止将它解释称使用相同插口对的新的连接部分。但是这只是在处于2MSL等待连接中的主机处于正常工作状态时间才有效。
2. 如果使用处于2MSL等待端口的主机出现故障,它会在MSL秒内重新启动,并立即使用故障前仍处于2MSL的插口对来建立一个新的连接吗?如果是这样,在故障前从这个连接发出而迟到的报文段会被错误地当做属于重启后新连接的报文段。无论如何选择重启后新连接的初始信号,都会发生这种情况。
3. 为了防止这种情况, RFC 793指出TCP在重启动后端 MSL秒内不能建立任何连接。这就是平静时间。
4. 只是极少的显现版遵守这一原则,因为大多数主机重启动的事件都比MSL秒要长。
1. 在 FIN_WAIT_2 状态我们已经发出了PIN,并且另一端也已对它进行确认。除非我们实现半关闭,否则将等待另一端的应用层意识到它已收到一个文件结束符的说明,并向我们发一个FIN来关闭另一端的连接。只有当另一端的进程完成这个关闭,我们这端才会从FIN_WAIT_2状态进入到TIME_WAIT状态。
2. 这意味着我们这端可能永远保持这个状态。另一端也将处于CLOSE_WAIT状态,并一直保持这个状态知道应用层决定进行关闭。
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
TCP提供了连接的一端在结束它的发送后还能接收来自另一端的数据的能力,我们称之为半关闭。
出现半关闭的原因可以理解为: 没有半关闭,需要其他的一些技术让客户通知服务器客户端已经完成了它的数据发送,但是仍然要接收来自服务器端的数据,这是一个全双工的状态,使用两个TCP连接可以作为一个选择。
4.1 TCP的交互数据流
通常每一次按键都会产生一个数据分组,每个数据分组会产生4个报文段
(1)来自客户的交互按键
(2)来自服务器的按键确认
(3)来自服务器的按键回显
(4)来自客户的按键回显确认
我们一般可以将报文段2和3进行合并,称其为经受时延的确认。因为有时候发送的字节会很小,比如说只有10个字节,但是IP首部和TCP首部已经占据40个字节了,这样多发送ACK包出来就会浪费资源了。当我们发送的字节数很大的时候,如1000个字节,就远远大于TCP前面的40个字节,这样的话服务器的ACK包就会发送出来了。
Nagle算法的代价是产生了时延,有时候我们需要关闭Nagle算法。一个典型的例子就是X窗口系统服务器: 小消息(鼠标移动)必须无延时的发送,以便为进行某种操作的交互使用提供实时的反馈。
在一个交互注册过程中键入中断的一个特殊功能键。这个功能键通常可以产生多个字符序列。通常从ASCII码的转移(escape)字符开始。如果TCP每次得到一个字符,它很可能会发送序列中的第一个字符,然后缓存其他字符并等待对该字符的确认,这就会经常触发服务器的经受延时的确认算法,表示剩下的字符没有在200ms内发送。这对交互用户而言,
4.2 TCP的成块数据流
1. 窗口左边沿向右边沿靠近为窗口合拢 ,该现象发生在数据被发送和确认时。
2. 窗口右边沿向右移动时允许发送更多的数据,称为窗口张开,该现象发生在另一端的接收进程读取已经确认的数据并释放了TCP的接收缓存时。
3. 窗口右边沿向左移动时称为窗口收缩。
4. 窗口左边沿不会向左移动,因为窗口左边沿受另一端发送的确认序号的控制,如果接收到一个这样的指示的话会被认为是一个重复ACK被丢弃。注意:
1. 发送方不必发送全窗口大小的数据
2. 接收方在发送一个ACK前不必等待窗口被填满
1. 发送方不必发送一个全窗口大小的数据
2. 来自接收方的一个报文段确认数据并把窗口左边沿向右滑动。这是因为窗口的大小是现对于确认序号的。
3. 窗口的大小可以减小,但是窗口的右边沿却不能够向左移动。
4. 接收方在发送一个ACK前不必等待窗口被填满。我们有时候可以看见许多实现每收到两个报文段就会发送一个ACK。
由接收方提供的窗口的大小由接收进程控制,会影响TCP性能。
如: 发送和接受缓冲区的大小为4096个字节。
用途:发送方用该标志通知接收方将所收到的数据(包括与PUSH一起传送的数据以及接收方TCP已经为接收进程收到的其他数据)全部提交给接收进程。
现状:大部分API不向应用程序提供通知其TCP设置PUSH标志的方法,因为一个好的TCP实现能够自行决定何时设置该标志。
TCP采用慢启动算法来降低一开始就发送过多的数据到网络。
1. 慢启动为发送方的TCP增加了一个另一个窗口: 拥塞窗口,记为 cwnd。当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为 1 个报文段(即另一端通告的报文段大小)。没收到一个ACK,拥塞窗口就增加一个报文段(cwnd以字节为单位,但是慢启动以报文段为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
2. 发送方开始时发送一个报文段,然后等待ACK。当收到该ACK时,拥塞窗口侧泳1增加到2,即可以发送两个报文段。当收到这两个报文段的ACK时,拥塞窗口就增加到4.这是一种指数增加的关系。
3. 在某些点可能达到了互联网的容量,于是中间路由器开始丢弃分组,发送方检测到丢包,相当于得到通知: 发送方的拥塞窗口开得过大,需要调整。这就是TCP的重传机制。
通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。
拥塞窗口(cwnd) 初始化为1个报文段,每收到一个ACK,拥塞窗口增加一个报文段(cwnd以字节为单位,但慢启动以报文段为单位进行增加),发送方取拥塞窗口与通告窗口中的最小值作为发送上限。
发送一个分组的时间取决于两个因素:
1. 传播时延,由光速、传输设备等待时间引起
2. 发送时延,取决于媒体速率(媒体每秒可传输的比特数)对于给定的两个接点之间的通路,传播时延固定,发送时延取决于分组大小。
会导致拥塞的情况:
1. 数据从大管道(如快速局域网)流向小管道(较慢的广域网)
2. 多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和
1. TCP提供了紧急方式,它使一段可以告诉另一端有些具有某种方式的 "紧急数据" 已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流中,由接收方决定如何处理。可以通过设置TCP首部中的两个字段来发出这种从一端到另一端的紧急数据已经被防止在数据流中的通知。URG比特被置为1,并且一个16bit的紧急指针被置为一个正的偏移量,该偏移量必须与TCP首部中的序号字段相加,以使得出紧急数据的最后一个字节的序号。
2. TCP必须通知接收进程,已接收到一个紧急数据指针。接收进程读取数据流,并被告知合适碰到紧急数据指针。只要从接收方当前读取位置到紧急数据指针之间有数据存在,确认为应用程序处于紧急方式。在紧急指针通过之后,应用程序便转回到正常方式。
3. TCP本身对紧急数据知之甚少。没有办法指明紧急数据从数据流的何处开始。TCP通连接传送的唯一信息就是紧急方式已经开始(TCP的首部中的URG比特)和只想晋级数据最后一个字节的指针。其他的事情留给应用程序去处理。
4. 紧急方式有什么用?比较常见的是 Telnet 和 Rlogin。 当交互用户键入中断键时候,就会使用紧急方式来完成这个功能,另一个是 FTP,当用户放弃一个文件的传输时,也使用紧急方式。Telnet 和 Rlogin从服务器到客户使用晋级方式是因为在这个方向上的数据流很可能要被客户的 TCP停止(即通告了一个大小为0的窗口)。但是如果服务器进程进入了紧急方式,尽管它不能够发送任何数据,服务器TCP也会立即发送紧急指针和URG标志。当客户TCP接收到这个通知时就会通知客户进程,于是客户可以从服务器读取其输入、打开窗口并使数据流动。
在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端就难于分辨出来了,必须提供科学的拆包机制。
保护消息边界:把数据看成一条独立的消息在网上传输,接收端也只能接收独立的消息;而流则是无保护消息边界的,它可以一次发送多个数据,接受端也能一次接收多个数据,依据缓冲区而定。
如:有三个数据大小分别为2k,4k,6k,如果是TCP流传输,则只要缓冲区大小为12k以上,就能一次发送完毕,但如果是UDP传输,则要分3次才能传输完成
1、发送方:发送使用了Nagle算法,将多个数据整合在一起发出,就有可能出现粘包
2、接收方:接收方收到发送方的数据,需要传到应用层处理,应用层没来得及处理,导致缓冲区的数据包粘在了一起
1、如果发送的数据正好是同一文件里数据,那就不需要处理
2、如果发送的数据没有任何关系,就需要处理
1、发送方:主动选择关闭Nagle算法
2、接收方:接收方不能处理这个问题,得交给应用层处理
3、应用层:只要可以在接受时,知道每个数据的长度就可以了
a. 每条数据的首位加上特殊标记,接收方可以分辨
b. 发送数据时附带上数据的长度
c. 发送固定长度的数据
# 在发送方加上 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) # 经过测试,不对,具体该怎么处理后续解决。
对每个连接,TCP管理4个不同的定时器。
1. 重传定时器: 用于当发送一个报文时,在规定时间内,发送方需要收到另一端发送的接收报文确认。
2. 坚持(persist)定时器: 使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。
3. 保活(keepalive)定时器: 用于检测一个空闲连接的另一端是否依然还保持连接。
4. 2MSL定时器: 测量一个连接处于 TIME_WAIT 状态的时间。
6.2 异常数据的的传输过程
当发送端将数据发出后,会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。否则,数据可能已经丢失。
在一定时间内没有等到确认应答,发送端会认为数据已经丢失,并进行重发。这样,即使有丢包,仍能保证数据达到对端,实现可靠传输。
TCP首部有一个确认字段,该字段实现TCP传输过程中的确认功能,该字段有两个作用:
1)表示确认字段之前的序列号的字节流均已被成功接收。
2)表示期望收到的下一个报文段的序列号,即下一个报文段的数据部分的第一个字节的序号。
超时重传指的是在发送数据报文段后开始计时,到等待确认应答到来的那个时间间隔。如果超过这个时间间隔,仍未收到确认应答,发送端将进行数据重传。这个等待时间称为RTO(Retransmission Time-Out,超时重传时间)。
还有一个时间叫RTT(Round Trip Time,报文段的往返时间),这个时间间隔是指数据报文段发出的时间戳与收到确认应答的时间戳的时间之差。
超时重传的概念很简单,但是重传时间的选择却是TCP最复杂的问题之一。因为网络环境的不同,时间会有差异。如果把超时重传的时间间隔设置得太短,就会引起很多报文段的不必要的重传,使网络负荷增大;如果把超时重传设置得多长,则又使网络的空闲时间增大,降低了传输效率。
那么,TCP的超时计时器的超时重传时间究竟应设置为多大呢?
TCP采用了一种自适应的算法,它记录一个报文段发出的时间戳,以及收到相应的确认应答的时间戳。这两个时间戳之差就是该报文段的往返时间RTT。
算法步骤如下:
(1)计算一个加权平均往返时间RTTs(这也称为平滑的往返时间,s 表示 smoothed。因为进行的是加权平均,因此得到的结果更加平滑)。
第1次测量得到的RTT样本值,作为RTTs的初始值。从第2次开始,新的RTTs值使用如下的公式计算得到
新的RTTs = (1 - α) × (旧的RTTs) + α × (新的RTT样本)
<说明1> 新的RTT样本需要重新测量得到。
<说明2> 系数 α 的取值范围为[0, 1)。
<说明3> RFC 6298文档推荐的 α 值为 1/8,即 0.125。
(2)基于计算得到的RTTs时间设置超时重传时间RTO。显然,超时计时器设置的超时重传时间RTO应略大于上面得到的加权平均往返时间RTTs。
RFC 6298 建议使用下式计算RTO:
RTO = RTTs + 4 × RTTd
<说明> RTTd 是 RTT的偏差的加权平均值,它与 RTTs 和 新的 RTT 样本之差有关。RFC 6298 建议这样计算RTTd。当第一次测量时,RTTd的值取为测量到的RTT样本值的一半。在以后的测量中,则使用下式计算加权平均的RTTd:
新的RTTd = (1 - β) × (旧的RTTd) + β × |RTTs - 新的RTT样本值|
<说明1> 系数 β 的取值范围为[0, 1),它的推荐值为 1/4,即 0.25。
上面所说的RTT样本的测量,实现起来相当复杂。示例如下:
如下图所示,发送方发出一个报文段后,设定的重传时间到了,还没有收到确认应答。于是重传报文段。经过一段时间后,收到了确认报文段。现在的问题是:如何判定此确认报文段是对先发送的报文段的确认还是对后来重传的报文段的确认?由于重传的报文段和原来的报文段完全一样,因此源主机在收到确认后,就无法做出正确的判断,而正确的判断对确定加权平均RTTs的值关系很大。
若收到的确认是对重传报文段的确认,但却被源主机当成是对原来的报文段的确认,则这样计算出的RTTs和超时重传时间RTO就会偏大。若后面再发送的报文段又是经过重传后才收到确认报文段,则按照此方法得出的超时重传时间RTO就会越来越长。
同样的道理,若收到的确认是对原来的报文段的确认,但被当成是对重传报文段的确认,则由此计算出的RTTs和RTO都会偏小。这就必然导致报文段过多地重传。这样就有可能使RTO的值越来越短。
根据以上所述,Karn 提出了一个算法:在计算加权平均 RTTs 时,只要报文段重传了,就不采用其往返时间样本。这样得出的加权平均 RTTs 和 RTO就比较准确。
但是,这又可能引起新的问题。设想出现这样的情况:报文段的时延突然增大了很多。因此,在原来得出的重传时间内,不会收到确认报文段。于是就重传报文段。但根据 Karn 算法,不考虑重传的报文段的往返时间样本。这样,超时重传时间就无法更新。
因此需要对 Karn 算法进行修正。方法是:报文段每重传一次,就把超时重传时间RTO增大一些。典型的做法是取新的重传时间为旧的重传时间的 2倍。当不再发生报文段的重传时,再根据上面给出的公式计算超时重传时间RTO的值。实践证明,这种策略较为合理。
总之,Karn 算法能够使TCP区分开有效的和无效的往返时间样本,从而改进了往返时间RTT的估测,使超时重传时间的计算更加合理。
当然,数据不会被无限、反复地重发。当达到一定重发次数后,如果仍然没有任何确认应答返回,就会判断为网络或对端主机发生了异常,TCP模块就会强制关闭连接,并且通知上层应用通信异常强行终止。
问题:如果接收方收到的报文段无差错,只是未按序到达,中间还缺了一些序号的字节数据,那么能否设法只重传缺少的数据而不重传已经正确到达接收方的数据呢?
答案是可以的,选择确认就是一种可行的处理方法。下面我们用一个例子来说明选择确认的工作原理。
当TCP的接收方在接收对方发送过来的数据字节流的序号不连续时,结果就形成了一些不连续的字节块,如下图所示。
可以看出,序号 1 ~ 1000 已收到了,但序号 1001 ~ 1500 没有收到。接下来的字节流又收到了,可是又缺少了 3000 ~ 3500。再后面从 4501 起又没有收到。也就是说,接收方收到了和前面的字节流不连续的两个字节块。如果这些字节块都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据。
从上图可以看出,和前后字节不连续的每一个字节块都有两个边界:左边界和右边界。因此在图中用四个指针标记这些边界。请注意,第一个字节块的左边界 L1=1501,而右边界 R1 = 3001 而不是 3000。这就是说,左边界指出字节块的第一个字节的序号,但右边界减 1 才是字节块的最后一个字节序号。同理,第二个字节块的左边界 L2=3501,而右边界 R2 = 4501。
我们知道,TCP报文段的首部没有哪个字段能够提供上述这些字节块的边界信息。RFC 2018 规定,如果要使用选择确认 SACK 功能,那么在建立TCP连接时,就要在TCP 首部的选项字段(即Options)中加上“允许SACK”的选项,而通信双方必须都事先约定好。如果使用选择确认,那么原来首部中的“确认号”字段的用法仍然不变。只是以后在TCP报文段的首部中的“选项”字段中都增加了SACK选项,以便报告收到不连续的字节块的边界。
由于TCP首部的“选项”可选字段的长度最多只有 40 字节,而指明一个边界就要用掉4字节(因为序号有32位,需要使用4个字节表示),因此在选项中最多只能指明4个字节块的边界信息。这是因为4个字节块有8个边界,因为需要用 8 * 4 =32个字节来描述。另外还需要两个字节,一个字节用来指明是 SACK 选项,另一个字节是指明这个SACK选项要占用多少字节。如果要报告五个字节块的边界信息,那么至少需要 42 个字节。这就超过了选项字段的40字节的上限。具体是如何规定的,可以参考 RFC 2018。示例表示如下:
kind = 4:占1字节,表示选项类别为 SACK。length:占1字节,表示SACK选项总共占用的字节数。该长度包括了kind字段和length字段占据的2个字节。
kind = 5:占1字节,表示这是SACK实际工作的选项。legnth:N 表示字节块数量;每个字节块有两个边界信息,因此需要8字节;+2 表示加上kind和length这两个字节。
TCP通过让接收方指明希望从发送方接受的窗口大小来进行流量控制。设置窗口大小为0可以组织发送方传送数据,直至窗口变为非0为止。
如果接收方向发送方通告了一个为0的接口,然后又向发送方通告了窗口更新,恰好这个确认丢失了,那么接收方等待接收数据,发送方等待允许他继续发送数据的窗口更新,就会形成死锁。为了防止这种死锁,发送方使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否增大。这些从发送方发出的报文段称为窗口探查。
计算坚持定时器的定时时间使用了普通的TCP指数退避。窗口探查包含一个字节的额数据,TCP总是允许在关闭连接前发送一个字节的数据。返回的窗口为0的ACK不是确认该字节,因此该字节被持续重传。
接收方通告一个小窗口,发送方通过这个小窗口发送少量的数据,这个数据量甚至小于报文段的长度,TCP的传输效率低到了极点。
避免措施
接受方:
①接受方不通告小窗口。通常的做法是除非窗口可以增加一个报文段或者可以增加接受方缓存的一半,否则不予通告窗口更新。
发送方-满足下述条件之一再发送数据:
① 可以发送一个满长度的报文段
②可以发送至少是接受方通告窗口大小一半的报文段
③ 可以发送任何数据并且不希望接收ACK(没有未被确认的数据)或者该连接上不能使用Nagle算法
1. 发送端收到0窗口通告后,就启动坚持定时器,并在定时器溢出的时候向客户端查询窗口是否已经增大。
2. 在定时器未到,就收到非零通告,则关闭该定时器,并发送数据。
3. 若定时器已到,还没有收到非零通告,就发探查报文。
4. 如果探查报文ACK的通告窗口为0,就将坚持定时器的值加倍,TCP的坚持定时器使用1,2,4,8,16……64秒这样的普通指数退避序列来作为每一次的溢出时间,重复1、2、3步,如果通告窗口非零,发送数据,关闭定时器。
6.7 TCP的保活定时器
6.7.1 保活定时器的由来
现实中可能存在一种空闲的TCP连接--连接的双方都没有向对方发送数据,则在两个TCP模块之间不交换任何信息,这意味我们可以启动一个客户和服务器建立连接,然后离去很长时间,而连接依然保持。而且中间的路由器可以崩溃或重启,只要两端的主机没有重启,则依然保持连接建立。
服务器为了知道客户机是否崩溃或关机等情况,从而引入了保活定时器来探查这种情况。
如果一个给定的连接在2小时内没有任何动作,那么服务器就向客户发送一个探查报文段。客户主机必须处于以下4个状态之一:
1. 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方的正常工作的,服务器在2小时内将保活定时器复位。
2. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应,服务器将不能收到对探查的响应,并在75秒后超时,总共发送10个探查,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
3. 客户主机崩溃并已经重新启动。这是服务器将收到一个对其保活探查的响应,但这个响应是一个RST复位,使得服务器终止这个连接。
4. 客户主机正常运行,但是从服务器不可达。这与状态2相同,因为TCP不能够区分状态4与2之间的区别,它所能发现的就是没有收到探查的响应。服务器不用关注客户主机被关闭或者重新启动的情况。当客户机被关闭之后,所有的应用进程也被终止,这会使客户的TCP在连接上发出一个FIN。接收到FIN会使服务器的TCP向服务器进程报告文件结束,从而服务器检测到了这种情况。
LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)