Linux高性能服务器编程(4)TCP协议详解

2023-10-26

Linux高性能服务器编程(4)

TCP协议

TCP服务的特点

TCP协议更靠近应用层,在应用程序中有更好的可操作性。

信息 作用
TCP头部 TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口号、目的端口号,管理TCP连接,控制两个方向的数据流
TCP状态转移过程 TCP连接的任意一端都是一个状态机。在TCP连接到断开的整个过程中,连接两端的状态机将经历不同的状态变迁。理解TCP状态转移对于调试网络应用程序有较大帮助。
TCP数据流 通过分析TCP数据流,我们可以从网络应用程序外部来了解应用层通信协议和通信双反交换的应用程序数据。这一部分将讨论两种类型的TCP数据流:交互数据流和成块数据流。TCP数据流中有一种特殊的数据,称为紧急数据。
TCP数据流的控制 为保证可靠传输和提高网络通信质量,内核需要对TCP数据流进行控制。超时重传和拥塞控制。

TCP模块开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。TCP模块发送出的TCP报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系。
当接收端收到多个报文段时,,TCP模块将它们携带的应用程序数据按照TCP报文段的序号依次放入TCP接受缓冲区中,并通知应用程序读取数据。接收端可以选择是否一次全读取,也可分多次读取,这取决于用户指定的应用程序缓冲区的大小。应用程序执行读操作次数和TCP模块接收到的TCP报文段个数之间没有固定的数量关系。
发送端执行写操作次数和接收端执行读操作次数之间没有任何数量关系,这就是字节流的概念:应用程序对数据的发送和接受是没有边界限制的。UDP不然,发送端每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送。接收端必须及时针对每一个UDP数据报执行读操作,否则就会丢包(经常发生在较慢的服务器上)。并且,如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。

TCP头部结构

在这里插入图片描述

字段 含义
16位端口号 告知主机该报文段来自那(源端口)以及传给那个上层协议或应用程序(目的端口)的
TCP通信时,客户端通常使用系统自动选择的端口号,服务端则使用知名服务端口号
32位序号 一次通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。
32位确认号 用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1
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流量控制的一个手段。这里说的窗口,指的是接收通告窗口。
它告诉对方本端接收缓冲区还能容纳多少字节数据,这样对方就可以控制发送数据的速度。
16位校验和 由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏
注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP通信可靠的一个重要保障。
16位紧急指针 是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。
确切的说,这个字段是紧急指针相对当前序号的偏移,称为紧急偏移。TCP的紧急指针是
发送端发送紧急数据的办法。

TCP头部的最后一个选项字段是可变长的可选信息,最长为40字节。在这里插入图片描述

选项的第一个字段kind说明选项的类型。有的TCP选项没有后面两个字段,仅包含1字节的kind字段。第二个字段length指定该选项的总长度,该长度包括kind字段和length字段占据2字节。第三个字段info是选项的具体信息。常见TCP选项有7种:在这里插入图片描述

kind = 0是选项表结束选项
kind = 1是空操作(nop)选项,没有特色意义,一般用于将TCP选项的总长度填充为4字节的整数倍。
kind = 2是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(MSS)。TCP常常将MSS设置为(MTU-40)字节(减掉的40字节包括IP头部的20字节和TCP头部的20字节)。这样TCP报文段的IP数据报的长度就不会超过MTU,从而避免本机发生IP分片。对以太网而言,MSS值是1460(1500-40)字节。
kind = 3是窗口扩大因子选项。TCP连接初始化时,通信双方使用该协议来协商接收通告窗口的扩大因子。在TCP头部中,接收通告窗口大小是用16位表示的,故最大为65535字节,但实际上TCP模块允许的接收通告窗口大小远不止(为了提高TCP通信的吞吐量)。
kind = 4是选择性确认选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有额报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP模块只重新发送丢失的TCP报文段,不用发送所有的未被确认的TCP报文段。
kind = 5是SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而发送端可以据此检查并重发丢失的数据块。每个块边沿参数包含一个4字节的序号。其中左边沿块表示不连续的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。
kind = 8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间的方法,从而为TCP流量控制提供重要信息。

TCP连接的建立和关闭

1、客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。
2、次握手完成两个重要的功能,既要双方做好发送数据的准备工作双方都知道彼此已准备好

第一次握手: 客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(B确认了A的发信能力和B的收信能力)
第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;(A确认了A的收信和发信能力,确认了B的发信能力)
第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。(告知B,B的发信能力)
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP连接都将被一直保持下去。

序号 SYN=0,ACK=0 简写 操作
第一次握手 SYN=1,ACK=0 SYN=1 连接请求
第二次握手 SYN=1,ACK=1 SYN=1,ACK=1 请求确认
第三次握手 SYN=0,ACK=1 ACK=1 连接确认

SYN:请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1
ACK:表示响应,确认号是否有效,一般置为1
FIN: 希望断开连接,表示关闭连接

四次挥手

第一次挥手:客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态。
(A:任务处理完成,希望断开连接)
第二次挥手:服务端收到FIN后,发送一个ACK给客户端,服务端进入CLOSE_WAIT状态。
(B:我知道了,我准备一下)
第三次挥手: 服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态。
(B:我准备好了,可以断开连接)
第四次挥手:客户端收到FIN后,客户端进入TIME_WAIT状态,发送ACK给服务端,服务端进入CLOSED状态,完成四次握手。
(A:确认断开)

序号 fin=0,ack=0 简写 操作
01 FIN=1,ACK=0 FIN=1 断开请求
02 FIN=1,ACK=1 FIN=1,ACK=1 请求确认
03 FIN=0,ACK=1 ACK=1 断开确认

TCP如何实现数据的可靠性

通过校验和、序列号、确认应答、超时重传、连接管理、流量控制、拥塞控制等机制来保证可靠性。

半关闭状态
TCP连接是全双工的,它允许两端的数据传输被独立关闭。如,通信一段可以发送结束报文段给对方,告诉它本端已经完成了数据传送,但允许继续接受来自对方的数据,直到对方也发送结束报文段以关闭连接。

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

TCP状态转移

在这里插入图片描述

粗虚线表示典型的服务端连接状态转移;粗实线表示典型的客户端。CLOSED是一个假象的起点,并不是一个实际的状态。

最开始的时候,主动请求端和被动端都处于CLOSED状态。

主动发起连接

第一阶段:当主动请求端发送SYN(同步报文段)标志位后,其状态变化为SYN_SENT;

第二阶段:此时被动方收到主动方的请求后,会向主动方发送SYN标志位以及确认信息ACK,主动方收到信息后,向被动方发送ACK确认信息,此时主动方的状态变化为ESTABLISHED,完成TCP连接的三次握手。

SYN_SEND一般不容易见到

状态变化:
CLOSE – 发送SYN – SEND_SYN – 接收 ACK、SYN – SENT_SYN – 发送 ACK – ESTABLISHED(数据通信态)

主动关闭连接

在主动发发起关闭请求前,两方都处于ESTABLISHED状态。

第一阶段:主动方发起关闭连接请求,发送FIN,此时其状态变化为FIN_WAIT_1,当其收到被动方的ACK确认之后,其状态变化为FIN_WAIT_2,这也就是半关闭状态。

第二阶段:主动方收到被动方的关闭连接请求(FIN),其向被动方发送ACK确认,状态变化为TIME_WAIT。

第三阶段:要真正进入到CLOSED状态,需要等待2MSL时长,不同的系统时间不一样。

状态变化:

ESTABLISHED(数据通信态) – 发送 FIN – FIN_WAIT_1 – 接收ACK – FIN_WAIT_2(半关闭)-> - 接收对端发送 FIN – FIN_WAIT_2(半关闭)-- 回发ACK – TIME_WAIT(只有主动关闭连接方,会经历该状态) – 等 2MSL时长 – CLOSE

被动方接受连接

第一阶段:被动接受连接一般都是服务器,因此一开始就处于LISTEN监听状态,的当其收到主动方的SYN标志位后,再向主动方发送SYN标志位和ACK确认信息后,其状态变化为SYN_RCVD。

第二阶段:当被动方收到主动方发送的ACK确认信息后,其状态变化为ESTABLISHED。

状态变化:

CLOSE – LISTEN – 接收 SYN – LISTEN – 发送 ACK、SYN(发送成功) – SYN_RCVD – 接收> > ACK – ESTABLISHED(数据通信态)

如果被动方没有收到主动方的ACK确认信息,在被动方一直向主动方发送SYN和ACK。

被动关闭连接

第一阶段:主动方和被动方都处于ESTABLISHED阶段,当被动方收到主动方发送的FIN结束标志,并且被动方向主动方发送ACK确认信息后,被动方状态变化为CLOSE_WAIT,其与主动方的半关闭状态对应。

第二阶段:被动方向主动方发送FIN结束标志后,其状态变化为LAST_ACK。

第三阶段:当被动方收到主动方的ACK确认后,其状态变化为CLOSE。

状态变化:

STABLISHED(数据通信态) – 接收 FIN – ESTABLISHED(数据通信态) – 发送ACK
– CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) – 发送FIN – LAST_ACK – 接收ACK – CLOSE

2MSAL保证最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)

一定出现在【主动关闭连接请求端】。 — 对应 TIME_WAIT 状态。

上半部分是TCP三路握手过程的状态变迁,下半部分是TCP四次挥手过程的状态变迁。

  1. CLOSED:起始点,在超时或者连接关闭时候进入此状态,这并不是一个真正的状态,而是这个状态图的假想起点和终点。
  2. LISTEN:服务器端等待连接的状态。服务器经过 socket,bind,listen 函数之后进入此状态,开始监听客户端发过来的连接请求。此称为应用程序被动打开(等到客户端连接请求)。
  3. SYN_SENT:第一次握手发生阶段,客户端发起连接。客户端调用 connect,发送 SYN 给服务器端,然后进入 SYN_SENT 状态,等待服务器端确认(三次握手中的第二个报文)。如果服务器端不能连接,则直接进入CLOSED状态。
  4. SYN_RCVD:第二次握手发生阶段,跟 3 对应,这里是服务器端接收到了客户端的 SYN,此时服务器由 LISTEN 进入 SYN_RCVD状态,同时服务器端回应一个 ACK,然后再发送一个 SYN 即 SYN+ACK 给客户端。状态图中还描绘了这样一种情况,当客户端在发送 SYN 的同时也收到服务器端的 SYN请求,即两个同时发起连接请求,那么客户端就会从 SYN_SENT 转换到 SYN_REVD 状态。
  5. ESTABLISHED:第三次握手发生阶段,客户端接收到服务器端的 ACK 包(ACK,SYN)之后,也会发送一个 ACK 确认包,客户端进入 ESTABLISHED 状态,表明客户端这边已经准备好,但TCP 需要两端都准备好才可以进行数据传输。服务器端收到客户端的 ACK 之后会从 SYN_RCVD 状态转移到 ESTABLISHED 状态,表明服务器端也准备好进行数据传输了。这样客户端和服务器端都是 ESTABLISHED 状态,就可以进行后面的数据传输了。所以 ESTABLISHED 也可以说是一个数据传送状态。

上面就是 TCP 三次握手过程的状态变迁。结合第一张三次握手过程图,从报文的角度看状态变迁:SYN_SENT 状态表示已经客户端已经发送了 SYN 报文,SYN_RCVD 状态表示服务器端已经接收到了 SYN 报文。

下面看看TCP四次挥手过程的状态变迁。

  1. FIN_WAIT_1:第一次挥手。主动关闭的一方(执行主动关闭的一方既可以是客户端,也可以是服务器端,这里以客户端执行主动关闭为例),终止连接时,发送 FIN 给对方,然后等待对方返回 ACK 。调用 close() 第一次挥手就进入此状态。
  2. CLOSE_WAIT:接收到FIN 之后,被动关闭的一方进入此状态。具体动作是接收到 FIN,同时发送 ACK。之所以叫 CLOSE_WAIT 可以理解为被动关闭的一方此时正在等待上层应用程序发出关闭连接指令。前面已经说过,TCP关闭是全双工过程,这里客户端执行了主动关闭,被动方服务器端接收到FIN 后也需要调用 close 关闭,这个 CLOSE_WAIT 就是处于这个状态,等待发送 FIN,发送了FIN 则进入 LAST_ACK 状态。
  3. FIN_WAIT_2:主动端(这里是客户端)先执行主动关闭发送FIN,然后接收到被动方返回的 ACK 后进入此状态。
  4. LAST_ACK:被动方(服务器端)发起关闭请求,由状态2 进入此状态,具体动作是发送 FIN给对方,同时在接收到ACK 时进入CLOSED状态。
  5. CLOSING:两边同时发起关闭请求时(即主动方发送FIN,等待被动方返回ACK,同时被动方也发送了FIN,主动方接收到了FIN之后,发送ACK给被动方),主动方会由FIN_WAIT_1 进入此状态,等待被动方返回ACK。
  6. TIME_WAIT:从状态变迁图会看到,四次挥手操作最后都会经过这样一个状态然后进入CLOSED状态。共有三个状态会进入该状态
  • 由CLOSING进入:同时发起关闭情况下,当主动端接收到ACK后,进入此状态,实际上这里的同时是这样的情况:客户端发起关闭请求,发送FIN之后等待服务器端回应ACK,但此时服务器端同时也发起关闭请求,也发送了FIN,并且被客户端先于ACK接收到。
  • 由FIN_WAIT_1进入:发起关闭后,发送了FIN,等待ACK的时候,正好被动方(服务器端)也发起关闭请求,发送了FIN,这时客户端接收到了先前ACK,也收到了对方的FIN,然后发送ACK(对对方FIN的回应),与CLOSING进入的状态不同的是接收到FIN和ACK的先后顺序。
  • 由FIN_WAIT_2进入:这是不同时的情况,主动方在完成自身发起的主动关闭请求后,接收到了对方发送过来的FIN,然后回应 ACK。

下面来看看这个看似有点多余的TIME_WAIT状态:从上面进入TIME_WAIT状态的三个状态动作来看(可以直接看状态变迁图)都是主动方最后回应一个ACK(CLOSING实际上前面的那个FIN_WAIT_1状态就已经回应了ACK)。

先考虑这样的一个情况,假如这个最后回应的ACK丢失了,也就是服务器端接收不到这个ACK,那么服务器将继续发送它最终的那个FIN,因此客户端必须维护状态信息(TIME_WAIT)允许它重发最后的那个ACK。如果没有这个TIME_WAIT状态,客户端处于CLOSED状态(开头就说了CLOSED状态实际并不存在,是我们为了方便描述假想的),那么客户端将响应RST,服务器端收到后会将该RST分节解释成一个错误,也就不能实现最后的全双工关闭了(可能是主动方单方的关闭)。所以要实现TCP全双工连接的正常终止(两方都关闭连接),必须处理终止过程中四个分节任何一个分节的丢失情况,那么主动关闭连接的主动端必须维持TIME_WAIT状态,最后一个回应ACK的是主动执行关闭的那端。从变迁图可以看出,如果没有TIME_WAIT状态,我们将没有任何机制来保证最后一个ACK能够正常到达。前面的FIN,ACK正常到达均有相应的状态对应。

还有这样一种情况,如果目前的通信双方都已经调用了 close()或shutdown(),都到达了CLOSED状态,没有TIME_WAIT状态时,会出现这样一种情况,现在有一个新的连接被建立起来,使用的IP地址和端口和这个先前到达了CLOSED状态的完全相同,假定原先的连接中还有数据报残存在网络之中,这样新的连接建立以后传输的数据极有可能就是原先的连接的数据报,为了防止这一点,TCP不允许从处于TIME_WAIT状态的socket 建立一个连接。处于TIME_WAIT状态的 socket 在等待了两倍的MSL时间之后,将会转变为CLOSED状态。这里TIME_WAIT状态持续的时间是2MSL(MSL是任何IP数据报能够在因特网中存活的最长时间),足以让这两个方向上的数据包被丢弃(最长是2MSL)。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已经在网络中消逝了。

综上来看:TIME_WAIT存在的两个理由就是

  1. 可靠地实现TCP全双工连接的终止;
  2. 保证让迟来的TCP报文段有足够的时间被识别丢弃
复位报文段的产生

在某些特殊条件下,TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接。

1.访问不存在的端口
当客户端程序访问一个不存在的端口的时候,目标主机将给它发送一个复位报文段。收到复位报文段的一端应该关闭连接或重新连接,而不能回应复位报文段。
实际上,当客户端程序向服务器的某个端口发起连接,而该端口仍处于TIME_WAIT状态的连接所占用时,客户端程序也将收到复位报文段。

2.异常终止连接
TCP提供了异常终止一个连接的办法,即给对方发送一个复位报文段,一旦发送复位报文段,发送端所有排队等候发送的数据都将被丢弃。

3.处理半打开连接
服务器(或客户端)关闭或异常终止连接,而对方没有接收到结束报文段(比如网络故障),此时,客户端(或服务器)还维持着原来的连接,而服务器(或客户端)即使重启,也没有该连接的任何信息了。这种状态被称为半打开状态,处于该状态的连接称为半打开连接。如果客户端(或服务器)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。

TCP交互数据流

TCP报文段所携带的应用程序数据按照长度分为两种:交互数据和成块数据。交互数据仅包含很少的字节。使用交互数据的应用程序(或协议)对实时性要求高,比如telnet,ssh等。成块数据的长度则通常为TCP报文段允许的最大数据长度。使用成块数据的应用程序(或协议)对传送效率要求高,比如ftp。

延迟确认:它不马上确认上次收到的数据,而是在一段延迟时间后产看本端是否有数据需要发送,如果有,则和确认数据一起发出。

因为服务器对客户请求处理很快,所有它发送确认报文段的时候总有数据一起发送。延迟确认可以减少发送TCP报文段的数量。而由于用户的输入速度明显慢于客户端的处理速度,所以客户端的确认报文段总是不携带任何应用程序。TCP连接和断开过程中,也可以发生延迟确认。

局域网类似本地回路运行结果,广域网不同,广域网的交互数据流可能有很大延迟,携带交互数据的微小TCP报文数量很多(一个按键输入就导致一个TCP报文段),这些因素都可能导致拥塞发生。Nagle算法是解决问题的一个简单有效的方法。它要求一个TCP报文段的确认达到之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发送的微量数据,并确认到来时以一个TCP报文段将它们全部发出。这样就极大减少了网络上微笑TCP报文段的数量。另一个优点在于其自适应性:确认达到得极快,数据也就发送得越快。

TCP成块数据流

当传输大量大块数据时,发送方会连续发送多个TCP数据报,接收方可以一次性确认所有这些报文段。发送方能够连续发送多少TCP数据报由接收通告窗口(还需要考虑拥塞窗口)的大小决定的。

带外数据

有些传输层协议具有带外数据的概念,用于徐速通告对方本端发生的重要事件。因此,带外数据比普通数据(也称带内数据)有更高的优先级,它应该总是立即被发送,而不论缓冲区是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。

UDP没有实现带外数据传输,TCP也没有真正的带外数据。不过TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供一种紧急方式。TCP的紧急方式利用传输普通数据的连接来传输紧急数据。这种紧急数据的含义和带外数据类似,因此后面紧急数据称为带外数据。

发送带外数据的过程
1.紧急数据是插在正常数据流中进行传输的 。
2.一个紧急指针只指向一个字节的带外数据的后一个字节位置
3.假如由于发送窗口的关系,导致该发送缓冲区中的数据(1,2,3,4,5,6,7,8,X)分为多次或者两次发送。接收端记下接受的字节数并且发现紧急指针指向的紧急数据没有到达,所以继续等待下一个包,下一个包(紧急指针还是10)发过来 7,8,X ,接收端发现紧急指针指向的紧急数据在这个包里,所以将紧急数据进行处理即可。
4.带外字节会被标记为OOB
5.即使发送端TCP因流量控制而暂停发送数据(接受缓冲区的套接字接受缓冲区已满,导致其TCP向发送端通告了一个值为0 的窗口),紧急通知照样不伴随任何数据的发送。也就是说:即使数据的流动会因为TCP的流量控制而停止,紧急通知却总是无障碍的发送到对端TCP

接收带外数据的过程

TCP模块接收带外数据的默认方式:TCP接收端只有在接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将它读入应该特殊的缓存中。这个缓存只有一字节,称为带外缓存。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据将覆盖它。

1.当接受到一个设置了URG标志的分节时,接受端检查紧急指针,确定它是否指向新的带外数据,比如:前面发送了两个包,只有第一个才会通知接受进程有新的带外数据到达。
2.当有新的紧急指针到达时,接受进程被通知到。首先,内核会给接受套接字的属主进程发送SIGURG信号,前提是接受进程调用了 fcntl或者ioctl为这个套接字建立了属主,并且该属主进程为该信号建立了信号处理函数 。
3.只有一个OOB标记,如果新的OOB字节在旧的OOB字节之前到达,旧的OOB字节就会被丢弃。
4.当由紧急指针指向的实际数据字节到达接受端TCP时,数据字节会有两个存储地区:一个是和普通数据一样的在线留存,另外一个是独立的单字节带外缓冲区,接受进程从这个单字节带外缓冲区读入数据的唯一方法是指定MSG_OOB调用recv,recvfrom,recvmsg。如果放在和普通数据一起的带内区域,接受进程就得通过检查该连接的带外标记OOB来获悉何时访问带这个数据字节。两个区域的使用通过套接字选项SO_OOBLINE来使用,默认情况下将带外数据字节放入独立的单字节带外缓冲区内。

TCP超时重传

TCP服务必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。下一次重传的超时时间如何选择,最多执行多少次重传,就是TCP重传策略。

拥塞控制

TCP模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。这就是拥塞控制。
TCP拥塞控制的四个部分:慢启动、拥塞避免、快速重传、快速恢复。

拥塞控制的最终受控变量是发送端向网络一次连续写入(收到其中第一个数据的确认之前)的数据量,我们称为SWND(发送窗口)。不过,发送端最终以TCP报文段来发送数据,所有SWND限定了发送端能连续发送的TCP报文段数量。这些TCP报文段的最大长度(仅指数据部分)称为SMSS(发送者最大段大小),其值一般等于MSS。
发送端要合理地选择SWND的大小。如果SWND太小,会引起明显的网络延迟;如果SWND太大,容易导致网络拥塞。接收方可通过其接收通告窗口(RWND)来控制发送端的SWND。着显然不够,所有发送端引入一个称为拥塞窗口的状态变量。实际SWND值是RWND和CWND中较小者。如图显示了拥塞控制的输入和输出(是一个闭环反馈控制)。

在这里插入图片描述

慢启动和拥塞避免

TCP建立完成后,CWND将被设置成初始值IW,其大小为2~4个SMSS。但新的Linux内核提高了该初始值,以减小传输滞后。此时发送端最多能发送IW个字节的数据。此后发送端每接收到一个确定,其CWND(拥塞窗口)就按照公式增加:
C W N D + = m i n ( N , S M S S ) CWND+=min(N,SMSS) CWND+=min(N,SMSS)
其中N是此次确认中包含的之前未被确认的字节数。这样一来,CWND将按照指数形式扩大,这就是所谓的慢启动。慢启动的理由是:TCP模块刚开始发送数据时并不知道网络的实际情况,需要用一种试探的方式平滑的增大CWND的大小。TCP拥塞控制中定义了慢启动门限(ssthresh),用于防止CWND无限制的膨胀而导致网络拥塞。当CWND的大小超过该值时,TCP拥塞控制将进入拥塞避免阶段。
拥塞避免算法使得CWND按照线性方式增加,从而减缓其扩大。
①每个RTT时间按照上面公式计算新的CWND,而不论该RTT时间内发送端收到了多少个确认。
②每收到一个对新数据的确认报文段,就按照下面的公式来更新CWND。
C W N D + = S M S S ∗ S M S S / C W N D CWND+=SMSS*SMSS/CWND CWND+=SMSSSMSS/CWND
MSS:报文段
RTT:往返时间 (表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认,不包含数据传输时间,总共经历的时间)。
SWND: 发送窗口 (发送端向网络一次连续写入的数据量,窗口大小就是无需等待确认应答而可以继续发送数据的最大值)
CWND: 拥塞窗口 指某一源端数据流在一个RTT内可以最多发送的数据包数
RWND: 接收窗口

之前我已经介绍过滑动窗口,它的其中一个作用就是进行流量控制可以避免发送方过载接收方。但是却无法避免过载网络,这是因为接收窗口只反映了服务器个体的情况,却无法反映网络整体的情况。

发送方维持一个拥塞窗口 CWND 的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口。

发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。

慢开始算法:
拥塞窗口和接收窗口共同决定了发送者的发送窗口
当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。
较好的方法是 先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值
通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。
如果不施加手段进行控制,慢启动必然使得CWBD很快膨胀,为防止拥塞窗口cwnd的增长引起网络拥塞,还需要另外一个变量,慢开始门限ssthresh
   
CWND <ssthresh时,进行慢开始算法。  
CWND >ssthresh时,进行拥塞避免算。
CWND = ssthresh时,两者皆可

拥塞避免算法:
让拥塞窗口CWND 缓慢地增大,即每经过一个往返时间RTT就把发送方的CWND 拥塞窗口CWND 加1 ,而不是加倍CWND 。这样拥塞窗口CWND 按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。
不论是慢开始还是拥塞避免只要网络出现拥塞(没有按时到达)时,就把ssthresh的值置为出现拥塞时的拥塞窗口的一半(但不能小于2),以及CWND 置为1,进行慢开始。 目的是迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。

发送端判断拥塞发生的依据:
①传输超时,或者说TCP报文重传定时器溢出
②接收到重复的确认报文段
拥塞控制对这两种情况有不同的处理方式。第一种仍然采用慢启动和拥塞避免。对第二种情况则使用快重传和快恢复。当第二种情况发生在重传定时器溢出后,则也被拥塞控制当成第一种情况来对待。
如果检测到拥塞发生是由于传输超时,那么它将执行重传并作如下调整:
s s t h r e s h = m a x ( F l i g h t S i z e / 2 , 2 ∗ S M S S ) C W N D < = S M S S ssthresh=max(FlightSize/2,2*SMSS) CWND <= SMSS ssthresh=max(FlightSize/2,2SMSS)CWND<=SMSS
其中FlightSize是已经发送但未收到确认的字节数。这样调整后,CWND将小于SMSS,那么也必然小于新的慢启动门限值sstresh,故而拥塞控制再次进入慢启动阶段。

快重传和快恢复

拥塞控制算法需要判断当收到重复的确认报文端时,网络是否真的发生了阻塞,或者说TCP报文端是否真的丢失了。具体的做法是:发送端如果连续收到3个重复的确认报文端,就认为是拥塞发生了。然后它启用快速重传和快速恢复算法来处理拥塞。

快速重传(Fast retransmit):要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时捎带确认。

如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK(其实是收到4个同样的ACK,第一个是正常的,后三个才是冗余的),发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出,大大提高了效率。这便是快速重传机制。

快速恢复(Fast retransmit)具体过程:

(1)当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。然后立即重传丢失的报文段,并将CWND设置为新的ssthresh(减半后的ssthresh)
请注意:接下去不执行慢开始算法

有些快重传实现是把开始时的拥塞窗口cwnd值再增大一点,即等于 ssthresh + 3 * MSS 。这样做的理由是:既然发送方收到三个重复的确认,就表明有三个分组已经离开了网络。这三个分组不再消耗网络 的资源而是停留在接收方的缓存中。可见现在网络中并不是堆积了分组而是减少了三个分组。因此可以适当把拥塞窗口扩大了些。

(2)由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

(3)每次收到一个重复的确认时,设置CWND=CWND+SMSS(拥塞窗口加1).此时发送端可以发送新的TCP报文段

(4)当收到新数据的确认时,设置CWND=ssthresh(ssthresh是新的慢启动门限值,由第一步计算得到)
原因是因为该ACK确认了新的数据,说明从重复ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

快速重传和快速恢复完成之后,拥塞控制将恢复到拥塞避免阶段.
不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

(3)每次收到一个重复的确认时,设置CWND=CWND+SMSS(拥塞窗口加1).此时发送端可以发送新的TCP报文段

(4)当收到新数据的确认时,设置CWND=ssthresh(ssthresh是新的慢启动门限值,由第一步计算得到)
原因是因为该ACK确认了新的数据,说明从重复ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

快速重传和快速恢复完成之后,拥塞控制将恢复到拥塞避免阶段.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux高性能服务器编程(4)TCP协议详解 的相关文章

随机推荐

  • 大厂笔试真题

    1 复数相乘 2 K个一组翻转链表 include
  • 浅谈 Python中if __name__ == ‘__main__‘:的工作原理

    为了理解if name main 的工作原理 我们需要先了解Python中的特殊变量 name 每个Python模块都有一个内置的变量 name 这个变量的值取决于如何执行模块 如果模块是被直接运行的 例如 你使用命令python mysc
  • 「网络安全」如何搭建MySQL恶意服务器读取文件?

    前言 注 本文不涉及对MySQL协议报文研究 仅讲解原理 并且做部分演示 搭建MySQL恶意服务器读取文件这件事 虽然直接利用门槛较高 但是由于在网上看到了一种比较新颖的利用方式 利用社会工程学引诱用户连接MySQL进而读取用户文件 个人觉
  • Day【4】字符串解码

    原题链接 思路 对于字符串k encoding 对于这样的形式 我们要加encoding重复k次 从左向右扫描字符串 如果说遇见字符 就直接将该字符添加到结果中 如果说 遇见 k encoding 的形式 我们首先要做的是就是将k和enco
  • GCC参数详解

    gcc and g 分别是gnu的c c 编译器 gcc g 在执行编译工作的时候 总共需要4步 1 预处理 生成 i的文件 预处理器cpp 2 将预处理后的文件不转换成汇编语言 生成文件 s 编译器egcs 3 有汇编变为目标代码 机器代
  • 9.OB4.0调用存储过程通过临时表返回多行记录

    MYSQL存储过程返回多行 1 表数据准备 drop table if exists t1 drop table if exists t2 drop table if exists t3 create table t1 id varchar
  • 任何程序都必须加载到什么中才能被cpu执行

    任何程序都必须加载到内存中才能被cpu执行 内存是计算机中的重要部件之一 它是外存与cpu进行沟通的桥梁 计算机中所有程序的运行都在内存中进行 内存性能的强弱影响计算机整体发挥的水平 任何程序都必须加载到内存中才能被cpu执行 学习视频分享
  • WPF 性能优化建议

    本章讲述 WPF 性能优化建议 20180930 WPF性能优化问题 运行软件发现CPU使用率很大 80 95 程序中含有委托 线程 定时器的处理 之前优化时 主要优化线程和定时器相关线程方面的处理 但是效果甚微 无意间看到博客中说程序界面
  • Android onInterceptTouchEvent与onTouchEvent调用关系

    概述 onInterceptTouchEvent 是用来拦截Touch事件 ViewGroup有 View没有 onTouchEvent 是Touch事件 ViewGroup与View都有 实例讲解 当一个Touch事件发生后 会由父布局开
  • connect错误:no route to host

    linux下 socket 用vmware装了两个虚拟机 分别运行客户端和服务器端 客户端连接的时候报错 connect error no route to host 但是在同一虚拟机下运行正常 我检查了socket返回值 正常 地址和端口
  • U-boot引导流程分析一

    U Boot 全称 Universal Boot Loader 即通用引导程序 是遵循GPL条款的开放源码项目 它的源码目录 编译形式与Linux内核很相似 事实上 不少U Boot源码就是相应的Linux内核源程序的简化 尤其是一些设备的
  • linuxptp源码研究

    目录 1 检查网卡是否支持相应的时间戳 2 linuxptp的目录架构 3 ptp4l的大致流程分析 4 gptp协议对应的sync follow up delay request delay response消息在代码的位置 5 slav
  • Deeplearning4j 实战 (13):基于TextCNN的文本分类实现

    Eclipse Deeplearning4j GitChat课程 Deeplearning4j 快速入门 专栏Eclipse Deeplearning4j 系列博客 万宫玺的专栏 wangongxi CSDN博客Eclipse Deeple
  • java native

    1 java lang Boolean中没有 native方法2 java lang Character中没有native方法3 java lang Byte中没有本地方法4 java labg Short中没有本地方法5 java lan
  • Go语言学习笔记(六)---map

    4 7 map map是key value数据结构 又称为字段或者给关联数组 类似其他编程语言的集合 映射 基本语法 var map变量名 map keytype valuetype keytype可以是bool int string 指针
  • ubuntu 忘记root密码

    方法一 如果用户具有sudo权限 那么直接可以运行如下命令 sudo su root 输入当前用户的密码 passwd 输入密码 再次输入密码 方法二 如果用户不具备sudo权限 则方法一不能用 并需进入GRUB修改kernel镜像启动参数
  • 如何用 Python 批量循环读取 Excel ?

    在使用 Python 批量处理 Excel 时经常需要批量读取数据 常见的方式是结合glob模块 可以实现将当前文件夹下的所有csv批量读取 并且合并到一个大的DataFrame中 df list for file in glob glob
  • 贪吃蛇(C语言)

    贪吃蛇项目 核心算法 循环数组 发牌算法 二维坐标一维化 编译环境 TC 2 0 准备工作 学习gotoxy 函数 了解bioskey 函数使用 知道bioskey 1 与bioskey 0 的区别 了解键盘扫描码 并且知道如何使用 核心工
  • Java读取ini文件

    Java读取ini文件 文章目录 Java读取ini文件 1 ini文件 2 代码示例 1 ini文件 src config config ini文件内容如下 login autorun n jls 2 url 10 10 1 29 por
  • Linux高性能服务器编程(4)TCP协议详解

    Linux高性能服务器编程 4 TCP协议 TCP服务的特点 TCP协议更靠近应用层 在应用程序中有更好的可操作性 信息 作用 TCP头部 TCP头部信息出现在每个TCP报文段中 用于指定通信的源端端口号 目的端口号 管理TCP连接 控制两