C++ TinyWebServer项目总结(3. TCP协议)

传输层的协议主要有两个:TCP 和 UDP 协议。TCP 协议相对于 UDP 协议的主要特点是:面向连接、面向字节流和可靠传输。

使用 TCP 通信的双方都必须建立连接,并分配必要的内核资源。TCP 连接是全双工的,双方的数据读写可以通过同一个连接进行。TCP 连接是一对一的。

TCP 协议采用发送应答机制,发送端发送的每个 TCP 报文段都必须得到接收方的应答,才认为传输成功。且采用超时重传机制,发送方在发出一个 TCP 报文段后启动定时器,定时时间内未收到应答,将重发。

由于 TCP 报文段会被封装成 IP 数据报发送,而 IP 数据报到达接收端可能会乱序、重复,因此 TCP 协议还会对接收到的 TCP 报文段重排、整理,再交付给应用层。

UDP 协议则和 IP 协议一样,提供不可靠服务。

特点

  • 面向连接:TCP协议在传输数据前需要建立连接,确保数据传输的可靠性。例如,网页浏览需要在浏览器和服务器之间建立TCP连接。
  • 全双工通信:TCP连接建立后,双方可以同时发送和接收数据。例如,视频通话过程中,双方可以同时发送和接收音视频数据。
  • 可靠性:TCP协议通过确认应答、重传机制和序号保证数据不丢失、无差错、不重复和按序到达。例如,文件传输需要确保文件完整性,TCP协议可以保证这一点。
  • 面向字节流:TCP协议将数据视为连续的字节流进行传输。例如,下载文件时,TCP协议将文件数据视为连续的字节流,确保数据顺序接收。

TCP 头部结构

标志位的含义

标志位含义:

  • URG Uregent:紧急位,URG=1,表示数据紧急,需要立即处理。
  • ACK Acknowledgement:确认位,ACK=1,确认号才生效,用于确认已收到的数据。携带 ACK 标志的称为确认报文段。
  • PSH Push:推送位,PSH=1,尽快将数据交付给应用层,而不是等待缓冲区满。
  • RST Reset:重置位,RST=1,重新建立连接,用于异常情况下重置连接。携带 RST 标志的称为复位报文段。
  • SYN Synchronization:同步位,SYN=1,表示连接请求报文,当SYN=1而ACK=0时,表明这是一个连接请求报文,若对方同意建立连接,则在相应报文中令SYN=1和ACK=1。携带 SYN 标志的称为同步报文段。
  • FIN Finish:终止位,FIN=1,表示释放连接,用于正常关闭连接。携带 FIN 标志的称为结束报文段。

TCP 连接的建立和关闭

TCP 连接的端口称为套接字 (socket)。

socket = (主机 IP 地址, 端口号)

建立连接过程(三次握手)

建立连接前客户端、服务器都处于关闭状态(CLOSED),直到客户端主动打开连接,服务器才被动打开连接(处于监听状态LISTEN),等待接受客户端的请求。

  1. 第一次握手:将同步标志位设为1(SYN=1),随机选择一个序号(seq=x)。客户端进入SYN_SEND状态。
  2. 第二次握手:服务器收到请求连接报文段后,若同意建立连接,则将同步标志位设为1(SYN=1),确认标记为设为1(ACK=1),随机选择一个序号(seq=y),确认收到的序号(ack=x+1),服务器进入同步已接收状态(SYN_RCVD)。
  3. 第三次握手:客户端收到确认报文段后,向服务器再次发出连接确认报文段,确认标记位设为1(ACK=1),序号(seq=x+1),确认收到的序号(ack=y+1)。客户端先进入ESTABLISHED状态,服务端收到后同时进入。

为什么建立连接需要三次握手,两次行不行?

三次握手是为了确定客户端、服务器收发数据都正常。防止服务器接收了早已失效的连接请求,从而一直等待客户端请求,最终导致浪费资源。如果只有两次握手,就会出现,客户端发出的第一个连接请求段没有丢失,只是在某个网络节点长时间滞留,而客户端进入了超时重传,重新发送了一个连接请求,建立连接,传输完数据之后连接释放,而此时第一次发送的连接请求到达服务器,这是一个失效的连接请求,但是服务器不知道,正常确认连接,此时客户端已经关闭,服务器一直等待。所以两次不行。

  1. 第一次握手:客户端发送请求,此时服务器知道客户端发送数据正常。
  2. 第二次握手:服务器发送确认,此时客户端知道服务器接收、发送数据都正常。
  3. 第三次握手:客户端发送确认,此时服务器知道客户端接收数据正常。

如果已经建立连接,但是客户端突然出现故障怎么办?

TCP还设有一个保活计时器,Client端如果出现故障,Server端不能一直等下去,这样会浪费系统资源。每收到一次Client客户端的数据帧后,Server端都的保活计时器会复位。计时器的超时时间通常是设置为2小时,若2小时还没有收到Client端的任何数据帧,Server端就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,Server端就认为Client端出了故障,接着就关闭连接。

释放连接过程(四次挥手)

  1. 第一次挥手:客户端(服务器也可以主动发,一般是客户端)向服务器发送一个断开连接请求报文,FIN=1seq=u。发送完成后,进入FIN_WAIT-1(终止等待1)状态。这表示客户端没有业务数据要发给对方了,通知服务器自己要断开连接了。
  2. 第二次挥手:正常情况下,在收到客户端发送的FIN断开连接请求之后,服务器都会发送一个ACK响应报文,ACK=1seq=vack=u+1。该报文的意思是,我同意你的断开连接请求,服务器进入CLOSEWAIT(关闭等待)状态,此时TCP协议服务会通知高层的应用进程,对方已经没有数据要发送了,如果我方还有数据要发送,可以继续发。客户端进入FIN_WAIT-2(终止等待状态2)。
  3. 第三次握手:在发送完ACK确认报文后,服务器还能继续发送业务数据,当服务器发送完数据后,或者CLOSEWAIT(关闭等待)截止后,服务器会主动发送断开连接请求,FIN=1ACK=1seq=wack=u+1。表示服务器也没有数据要发送了,然后进入LAST_ACK(最后确认)状态。
  4. 第四次挥手:客户端在收到服务器的断开连接请求报文后,进行最后的确认,向服务器发送一个ACK确认报文,然后进入TIME_WAIT(超时等待状态),在等待2MSL的时间后,如果期间没有收到其他报文,则证明对方已正常关闭,主动断开方的连接最终关闭。

最后为什么要等待2MSL之后才关闭,为什么不收到对方关闭请求之后就关闭?

因为最后一次的确认报文有可能丢失,如果服务器超时等待没有收到确认报文,会在发一次,此时如果客户端直接断开连接,那服务器一直没得到响应,会一直发,浪费资源。2MSL对应于一次消息的来回(一个发送和一个回复)所需的最大时间。如果直到2MSL,主动断开方都没有再一次收到对方的报文(如FIN报文),则可以推断ACK已经被对方成功接收,此时,主动断开方将最终结束自己的TCP连接。

为什么关闭连接需要四次挥手,而建立连接只需要三次握手?

关闭连接时,被断开方在接收到对方的FIN断开连接请求报文时,很可能还有业务数据没发送完成,并不能马上断开连接,又不能对对方的断开连接请求置之不理,因为对方没有收到确认会认为断开连接请求在路上丢失,触发超时重传。所以只能先回复一个ACK确认报文,告诉对方,我知道你要断开连接了,但我有业务数据要发送,只能等待我所有的业务数据都发送完才能真正结束,在结束之后,我会给你发送FIN+ACK断开连接请求报文的。所以被断开方的确认报文需要分成两步,故需要四次挥手。
TCP 连接是全双工的,所以允许两个方向的数据传输被独立关闭,通信的一方可以发送结束报文段给对方,但允许继续接受来自对方的数据,直到对方也发送结束报文段,最终关闭连接。这种状态被称为半关闭状态

TIME_WAIT 状态

从上图来看,客户端连接在收到服务器的结束报文段(FIN=1. ACK=1)后 ,并没有直接进入 CLOSED 状态,而是转移到了 TIME_WAIT 状态,并等待一段长为 2MSL(Maximum Segment Life, 报文段最大生存时间),才能完全关闭。MSL 是 TCP 报文段在网络中的最大生存时间。

TIME_WAIT 状态存在的原因:

  1. 可靠地终止 TCP 连接;
  2. 保证迟到的 TCP 报文段有足够的时间被识别并丢弃。

第一个原因很好理解。假设图中用于确认服务器结束报文段的TCP报文段丢失,那么服务器将重发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误。

在 Linux 系统上,一个 TCP 端口不能被同时打开多次(两次及以上)。当一个 TCP 连接处于 TIME_WAIT状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来思考,如果不存在TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具有相同的 IP 地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身(incarnation)。新的化身可能接收到属于原来的连接的、携带应用程序数据的 TCP 报文段,这显然是不应该发生的。

另外,因为TCP报文段的最大生存时间是 2MSL,坚持 2MSL 时间的TIMB_WAIT状态能够确保网络上两个传输方向上尚未被接收到的、迟到的TCP报文段都已经消失(被中转路由器丢弃)。因此,一个连接的新的化身可以在2MSL时间之后安全地建立,而绝对不会接收到属于原来连接的应用程序数据,这就是TIME_WAIT状态要持续2MSL时间的原因。

有时候我们希望避免TIME_WAIT状态,因为当程序退出后,我们希望能够立即重启它。但由于处在TIME_WAIT状态的连接还占用着端口,程序将无法启动(直到2MSL超时时间结束)。

对客户端程序来说,我们通常不用担心上面描述的重启问题。因为客户端一般使用系统自动分配的临时端口号来建立连接,而由于随机性,临时端口号一般和程序上一次使用的口号(还处于TIME_WAIT状态的那个连接使用的端口号)不同,所以客户端程序一般可以立即重启。

但如果是服务器主动关闭连接后异常终止,则因为它总是使用同一个知名服务端口号,所以连接的TIME_WAIT状态将导致它不能立即重启。不过,我们可以通过socket选项 SO_REUSEADDR 来强制进程立即使用处于TIME_WAIT状态的连接占用的端口。

复位报文段 RST

产生复位报文段的三种情况:

1. 访问不存在的端口

当客户端程序访问一个不存在的端口时,目标主机将给它发送一个复位报文段。收到复位报文段的一端应该关闭连接或者重新连接,而不能回应这个复位报文段。

实际上,当客户端程序向服务器的某个端口发起连接,而该端口仍被处于TIME_WAIT状态的连接所占用时,客户端程序也将收到复位报文段。

2. 异常终止连接

前面讨论的连接终止方式都是正常的终止方式:数据交换完成之后,一方给另一方发送结束报文段。TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一旦发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃。

应用程序可以使用socket选项 SO_LINGER 来发送复位报文段,以异常终止一个连接。

3. 处理半打开连接

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

延迟确认

延迟确认:服务器不马上确认上次收到的数据,而是在一段延迟时间后查看本端是否有数据需要发送,如果有,则和确认信息一起发出。因为服务器对客户请求处理得很快,所以它发送确认报文段的时候总是有数据一起发送。延迟确认可以减少发送TCP报文段的数量。而由于用户的输入速度明显慢于客户端程序的处理速度,所以客户端的确认报文段总是不携带任何应用程序数据。在TCP连接的建立和断开过程中,也可能发生延迟确认。

而广域网上的交互数据流可能经受很大的延迟,并且,携带交互数据的微小TCP报文段数量一般很多(一个按键输入就导致一个TCP报文段),这些因素都可能导致拥塞发生。解决该问题的一个简单有效的方法是使用Nagle算法。

Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。这样就极大地减少了网络上的微小TCP报文段的数量。该算法的另一个优点在于其自适应性:确认到达得越快,数据也就发送得越快。

带外数据

有些传输层协议具有带外(Out of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。实际应用中,带外数据的使用很少见,已知的仅有 telnet、ftp等远程非活跃程序。

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

实现可靠传输

滑动窗口

  • 发送窗口:发送方维持的一组连续的、允许发送帧的帧序号(大致分为四组,一组是已经发送并被确认的分组,一组是已经发送但还没有被确认的分组,一组是即将要发送的分组,一组是还没轮到要发送的分组,滑动窗口控制已经发送但没被确认和马上要发送的分组,当已经发送的数据被确认,窗口开始滑动,将没轮到的分组纳入滑动窗口。滑动窗口的最大尺寸由发送方设定,接收方收到数据包之后,会发送一个确认报文给发送方,发送方收到确认报文后会将滑动窗口移动)。主要对发送方进行流量控制。

  • 接收窗口:接收方维持的一组连续的、允许接收帧的帧序号。控制可接收哪些帧数据&不可接收哪些帧数据。当收到数据帧后,将窗口向前移动一个位置,并发回确认帧,若收到的数据帧落在接收窗口之外,则一律丢弃。

重传机制

超时重传

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

快速重传

如果发送方连续收到三个重复确认应答,则认为该数据包可能丢失,不必等待超时计时器到期,立即重传该数据包。

流量控制

接收方根据自己接收缓存的大小,通过其接收通过其接收通告窗口 (RWND),动态调整发送方发送窗口(Send Window, SWND)的大小,从而控制发送方的发送速率,避免出现发送方和接收方速度不匹配的问题。

实际上,在拥塞控制中,发送端引入了拥塞窗口(Congestion Window, CWND),最终的 SWND 取自 RWND 和 CWND 中的较小值。

拥塞控制

提高网络利用率,降低丢包率,防止过多的数据注入到网络中,使得通信网络链路过载。

拥塞控制的四个部分:慢启动(slow start)、拥塞避免 (congestion avoidance)、快速重传(fast retransmit)和快速恢复(fast recovery)

慢开始

在新连接建立或网络拥塞发生后的初始阶段,通过逐步增加拥塞窗口(cwnd)的大小来探测网络容量,避免网络突然过载。

  • 初始阶段:拥塞窗口(cwnd)设置为一个较小的值(通常为1个最大报文段,MSS)。
  • 指数增长:每次收到一个ACK(确认包),cwnd增加1个MSS。这样,cwnd呈指数增长。
  • 在连接建立初期,发送方会先发送少量的数据,然后逐渐增加发送量。每收到一个确认应答(ACK),拥塞窗口(cwnd)的大小就会增加一个报文段(MSS)的大小。
  • 当拥塞窗口的到达慢启动门限(slow start threshold size, ssthresh)时,就会进入拥塞避免阶段。
拥塞避免
  • 拥塞避免算法在慢开始后期或网络已趋于稳定时,通过线性增长cwnd来防止网络拥塞。
  • 线性增长:每经过一个 RTT(往返时间),cwnd增加一个MSS。
  • 拥塞检测:如果检测到网络拥塞(通过超时或快速重传),ssthresh被设置为当前cwnd的一半,进入慢启动或快速恢复阶段。
快重传

接收方每收到一个失序的报文段后 就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到自己发送数据时才进行捎带确认。发送方只要一连收到3个重复确认就立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器到期。

  • 当接收方发现丢失了一个报文段时,会立即发送三个重复的确认应答(而不是等待超时再发送),以通知发送方尽快重传丢失的报文段。
  • 快重传可以减少不必要的等待时间,提高网络性能。
快恢复

快恢复机制在快速重传后立即调整拥塞窗口,使得网络迅速恢复到稳定状态。

  • 当发送方收到三个重复的确认应答时,就认为发生了拥塞,但情况并不严重(只丢失了一个报文段)。此时,发送方会执行快恢复算法,将慢启动门限调整为当前拥塞窗口大小的一半(ssthresh = ssthresh / 2),并将拥塞窗口设置为慢启动门限加上3个报文段的大小。(CWND = ssthresh + 3 * MSS
  • 然后,发送方会开始发送新的报文段,并继续接收确认应答。如果再次收到重复的确认应答,就增加拥塞窗口的大小;如果收到新的确认应答,就表明所有丢失的报文段都已被接收方成功接收,此时可以将拥塞窗口设置为慢启动门限的值,并回到拥塞避免状态。

拥塞控制的状态转移

TCP在传输数据时,拥塞控制状态在不同阶段之间切换,具体包括:

  • 慢启动(Slow Start):从连接建立或超时恢复后,TCP进入慢启动阶段。
  • 拥塞避免(Congestion Avoidance)cwnd 达到 ssthresh 后进入此阶段。
  • 快重传/快恢复(Fast Retransmit/Fast Recovery):检测到丢包(3个重复ACK)后进入此阶段,快速恢复窗口大小。
  • 超时重传(Timeout Retransmit):如果ACK超时,TCP重传丢失的数据包,并重新进入慢启动阶段。

参考文档

  1. TCP/IP协议详解_tcpip协议-CSDN博客
  • 38
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红茶川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值