在学习TCP控制报文协议之前,必须要先了解TCP的报文结构。在TCP连接中传输的单元都是一个个相同结构的TCP报文段,每个报文段都有首部信息,这些首部信息能告诉它从哪里来,到哪里去,同时TCP底层也通过首部来控制传输流量,网络拥塞状况,保证可靠性、准确性等。
上图是TCP报文结构,以下是首部各个字段的描述:
基于TCP的应用层要进行通讯时,发送消息的两端要先建立可靠的TCP连接,然后才能在连接基础上收发消息。这里的TCP连接只是逻辑链路,是按照TCP的连接规则在传输层上假定的一个逻辑连接。在实际网络传输中,两端数据发送过程还要往下经过网络层和数据链路层,最终经过一层层的网络节点到达目的主机,但这是另一个知识点了,本文侧重分析TCP的连接细节。
Socket是在应用和传输层之间的一个抽象层,它把传输层TCP的复杂连接操作抽象为几个步骤,方便应用层在实现点对点通讯时的连接操作。Socket分为SOCK_STREAM、SOCK_DGRAM、SOCK_RAW三种类型,SOCK_STREAM用于提供可靠的传输服务,只能读写TCP数据报,SOCK_DGRAM只能读写UDP数据报,SOCK_RAW可读写原始数据报即IP数据报。本文主要为了分析TCP协议,因此我们通过SOCK_STREAM来分析TCP的连接细节。
Socket通讯是一个客户/服务端的结构,假设客户端向服务端发起一个Socket通讯连接。如下图所示,服务端要先调用socket()函数创建一个Socket,然后通过bind()绑定端口号和IP地址,端口号是为了客户端发送数据过来时能找到对应要处理的进程,而绑定IP是由于一台主机可能有多个网卡,需要选择指定的网口去接收。当服务端有了 IP 和端口号,就可以调用 listen 函数进行监听,这个时候客户端就可以发起连接了。在服务端等待的时候,客户端可以通过 connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦连接成功,服务端的 accept 就会返回另一个 Socket。

此外,服务器要接受客户端连接,必须先创建一个用于连接客户端的Socket,该Socket绑定了端口号和IP地址在主机B运行起来,然后主机A会指定服务端的IP地址和端口号发起连接请求,客户端在发起连接时底层会自动为它创建一个本地IP地址的Socket,并为该Socket绑定一个随机端口号。而服务端的连接Socket收到连接请求并确认连接可用后会为该主机创建一个独有的通讯Socket用于收发数据报(这个过程就是上图 accept 函数的处理过程),如下图所示:

服务端的连接Socket相当于一家公司的前台服务,用来对接所有访客的到访,然后再根据客户的来访目的,为他分配一个接待员,即这里的通讯Socket,后续客户所有的需求就都由接待员来提供,即后续Socket两端的数据收发都由通讯Socket来完成。
我终于搞懂了TCP的三次握手和四次挥手(图片案例超详解)_辰兮要努力的博客-CSDN博客_tcp三次握手和四次挥手的全过程
前面介绍的Socket连接中,有提到客户端调用connect()函数后会经过TCP三次握手的连接过程。这三次握手过程就是收发双方来回发送了三个TCP数据报后就确定了连接关系,可以开始发送数据了。那三次握手的规则是怎么样的呢?接下来为你详细分解:
自此,TCP连接就建立了。

TCP的连接要经历三次握手的过程,而连接要断开同样要遵守一个规范,才能真正断开连接,这个规范就是四次挥手的过程,每一次挥手的两端都各自会有不同的状态,这些状态共同确保整个连接真正断开,下图是描述了四次挥手的状态图:

我们都知道TCP是一个可靠的通讯协议,它能保证数据的有序性、不丢失和无差错。那这些特性是如何保证的呢?前面我们已经有了解过序号和确认序号的使用了,就是这两个字段保证TCP的这些特性。应用层将要发送的数据不断地发送到Socket中,行程的数据流就不断地交给TCP处理,由于字节流本身是有顺序的,TCP会先将这些字节流编号,而起始编号是以ISN作为初始序号(ISN是根据系统计时器生成,防止和其他TCP连接重复)。编号后再将字节流切割成一个个MSS大小的TCP报文段,每个报文段的首字节编号作为此TCP报文首部的序号字段。

而对于客户端发送的每一个TCP报文段,服务端都要对应发送一个确认报文段,如果在一定时间内没有收到服务端的确认报文,客户端就会认为这个报文可能丢失了,就会重发,这样就保证了不丢失的特性。假设客户端发送0-999字节的报文段到服务端,服务端就会发送一个确认序号为1000的确认报文段,代表服务端接下来希望接收1000编号开始的报文段。但这样发送一个报文响应一个报文,一来一回的传送方式效率低下,真实的场景可能是客户端连续发送了多个TCP报文段,服务端只发送一个确认报文,就能确认所有客户端发来的报文,这叫做累积确认。如,客户端发送了0-999、1000-1999和2000-2999三个报文段,服务端连续收到了三个TCP报文段后,只发送一个确认序号为3000的报文段就能确认以上的三个报文。
客户端发送了多个报文后,每个报文都是独立地往底层传输,可能每个报文经过的网络路由都不一样,因此三个按照顺序发送的报文,可能会不按顺序到达服务端。那服务端是怎么解决的呢?它是通过只确认最后一个按序到达的TCP报文来解决的,比如,服务端刚发送完确认序号为3000的ACK报文段后,接着收到了一个4000-4999的报文段,这时由于还有一个3000-3999的报文段未收到,因此服务端不会发送确认序号为5000的ACK报文,而是先将4000-4999的报文段存放到接收缓存中,然后还是继续发送确认序号为3000的ACK报文段。等到失序的报文段收到后再发送一个确认序号5000的ACK来累积确认所有已收到的报文。
TCP连接建立后,每一侧的主机都为该连接建立了一个接收缓存。当连接的一端收到有序、正确的报文段后就将数据存放到接收缓存中,然后应用程序再从缓存中读取数据,被读取过数据就会从缓存中删除。但有时候应用程序处理速度较慢时,就会导致接收缓存占满空间,这时发送方继续发送报文时就会因为缓存溢出而被丢弃,因此TCP需要提供一个流量控制服务来防止缓存溢出的情况。那TCP是如何实现流量控制呢?
TCP通过在每次双方收发数据时,都给对方通告一个窗口大小,该值是接收方可用缓存的大小,用来告诉发送方我最多只能接收这么多数据。该窗口大小是通过TCP报文段结构中的16位窗口大小字段来传给对方的。由于TCP是全双工协议,两端主机都会各自维护一个接收窗口大小的变量rwnd,这个变量是发给对端的,同时自己也会维护一个发送窗口大小,这个窗口大小是控制自己发送数据的快慢。下图是关于接收窗口的计算:

不管是流量控制还是等下要分析的拥塞控制,其最终实施方案都是通过控制发送窗口的大小来实现的。TCP协议规定发送窗口的大小是取拥塞窗口和接收窗口的较小者。但现在我们先忽略拥塞情况,假设发送窗口大小只受接收端接收窗口的影响,且发TCP连接数据传输是单向的,发送端只发送数据,接收端只接收数据,如图所示是主机A的发送窗口随着主机B响应报文通报的窗口大小而动态滑动的示意图:

主机A和主机B建立TCP连接后,主机B通报其接收窗口大小为400字节,因此主机A将初始发送窗口设置为400,通过图左边的蓝色背景代表发送主机A的发送窗口大小。这时主机A将seq为0和100的两个报文段发送出去,随即在左边的缓存中用深黄色标注出已发送待确认的报文。之后从主机B收到响应报文,ack为200代表主机B对序号为200以前的两个报文段进行累积确认,同时接收窗口rwnd减小为300,这时有可能是主机B虽然已收到两个确认报文,都将两个报文存入接收缓存,但应用层只读取了一个,还有一个占用了缓存导致接收窗口减小了100;主机A收到响应报文后,发现0-99和100-199的报文段已经确认,于是将蓝色窗口向右移动到200-299报文段的左边缘,由于此时接收窗口减小为300了,因此右边缘向左移动一个报文段的位置。这时包含在蓝色窗口内的是200-299、300-399和400-499三个可发送未发的报文。这时主机A就可以同时将三个报文段都发送出去,并将其标注为已发送未确认的深黄色。再次收到ACK报文时,确认了400以前的200-299、300-399报文,同时rwnd减小到100,窗口还是按照规则滑动,变成只剩一个换色报文段窗口。
在主机B发送最后一个响应报文后,通报接收窗口为0,这时主机A的发送窗口也随着变为0,这时就是零窗口状态,这时主机A就不能继续发送数据了,只能等主机B将接收窗口更新为大于0后才能继续发送,但如果接收方发送更新窗口的确认报文在中途丢失了,这时双方就处于假死状态。因此当窗口为0时,客户端会启动持续计时器,在计时器触发时主动发送零窗口探测报文段,服务端收到探测报文后就会同步发送一个响应报文并更新窗口大小,但也有可能探测报文也会丢失,还是回到原来的问题了。因此零窗口探测报文也有一个重传计时器,为了避免该探测报文段也丢失的情况。
对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况称为拥塞,若出现拥塞而不进行控制,整个网络的吞吐量将随输入负载的增大而下降。因此发送端引入了一个拥塞窗口变量cwnd来动态了解网络的拥塞情况,前面滑动窗口大小rwnd是怕数据把接收窗口塞满,而拥塞窗口cwnd是怕数据把网络塞满,因此需要根据两个值来决定发送窗口的大小。
那怎么判断发送多少数据能把整个网络塞满呢?其实,相对于网络层来讲,TCP是上层协议压根就不知道底下的网络状态,而发送端在发送数据,就像往一个瓶子里倒水,不能一下子往里面灌进太多水,这样会溅出来,同样拥塞控制也是使发送的数据不出现丢包、超时重发前提下,尽量发挥带宽作用、尽快地发送数据。
因此,TCP设计了几个四个算法用来控制拥塞窗口慢开始、拥塞避免、快重传和快恢复,我们通过一个例子来说明四个算法。首先,假设接收方有足够大的接收缓存,因而发送窗口仅受拥塞窗口影响。
慢开始的算法是先设置发送窗口大小cwnd为1(1代表一个MSS大小报文段),发送完报文后,在不发生超时重发情况下每收到一个TCP报文段的确认后,cwnd就加1,即确认了几个报文就加几。刚开始发送1个报文段,当收到这个报文段的确认后,cwnd就加1变为2,这时就可以同时发送两个报文段,当收到这两个报文段的累计确认后,cwnd就加2变为4,继续同时发送四个报文出去,当收到4个报文段的累计确认后,swnd就加4变为8了。由此可知,慢开始算法是一个指数增长的关系,cwnd增长得很快,之所以叫慢开始其实是指一开始的发送窗口比较小。由于发送窗口是指数方式增长,迟早会把整个网络带宽塞满,那什么时候会结束这种指数增长呢?一般会有两种情况:
当有一个发送的报文出现超时未收到响应时,就代表出现了拥塞了。这时就将swnd重新设置为1,同时将慢开始门限变量设为出现拥塞时cwnd的一半,即ssthresh=cwnd/2,随后继续进行慢开始算法。结束慢启动算法的第二种情况是与慢开始门限ssthresh有关了,当cwnd增大到超过或等于ssthresh时,就要停止慢开始算法而改用拥塞避免算法。
当使用拥塞避免算法时,此时的拥塞窗口刚好是使用慢开始出现拥塞时的一半,这个时候可能举例出现拥塞也不远了。因此,就不能像慢开始一样快速地增加拥塞窗口大小了,而是每次收到一个数据报文的确认时,就将cwnd增加1/cwnd。假如现在的cwnd为8,于是就同时将8个TCP报文段发送出去,当收到所有8个报文段的累计确认时,就将cwnd增加8 * (1/8),即增加1个报文段为9,然后继续发送9个报文段出去,又收到9个报文段的累计确认时,又将cwnd增加1变为10,由此可知,拥塞避免算法是以线性方式增长的。那什么时候结束这种线性方式增长呢?和慢开始算法碰到拥塞情况时一样,当出现拥塞时,就将即ssthresh设为原来的一半,cwnd重新设置为1,又重新开始执行慢开始算法,然后算法流程还是和原来的一样重复执行。
在使用慢开始和拥塞避免算法时,当出现数据报超时时,就认为是出现了网络拥塞。但这并不是绝对的,当网络还是很流畅时,传输过程中有个别数据报由于各种原因导致丢失了,这时发送方就发现此数据报超时未确认,如果因此就认为是网络拥塞导致的,就会误用拥塞算法而将cwnd减小为1,这无疑就降低了传输效率了。因此,就出现了快重传算法来解决这种个别数据报丢失的情况。
快重传的原则是发送端在发现个别报文丢失时,就应该尽快重传,而不是等到超时了再重传。那发送端怎么知道是个别报文丢失呢?如下图所示,A主机和B主机已建立连接情况下,A主机先发送一个数据报文0,正常收到报文,接着发送第二个报文100,该报文在传输工程中丢失了,主机B就收不到报文100的数据,接着主机A继续发送其他报文200,该报文在主机B正常收到,但主机B发现报文100还没收到,先收到报文200,于是将报文200存放到接收缓存,继续发送确认序号为100的ACK报文,代表希望从主机A收到报文100。接着主机A继续发送报文300和400,两个报文都正常到达主机B,主机B由于设置不使用经受时延的确认,对每个收到的报文都要及时发送ACK报文,也就连续一共发送了三次确认序号为100的响应报文。当客户端连续收到三个连续的相同确认序号的报文端时,就知道这个报文可能中间丢失了,于是就要重发报文100,当服务端收到这个空缺的报文段后,就已收到的失序报文一起累计确认。

当发送方连续收到三个重复的ACK报文时,就知道现在只是个别报文端丢失了,于是不启用慢开始算法,而改用快恢复算法。快恢复算法一般有两种实现方案:
TCP为了减少TCP数据单元的交互,增强效率。在收到TCP连接远程一端的数据报时,不会马上发送确认报文,而是等待系统的定时器,在定时器一个200ms周期内,如果本地端有要发送的数据,则连同确认报文一起发送出去,如果没有的话在200ms的触发时机才会发送单独的确认报文。这种现象也被称为数据捎带确认,它相当于TCP的一个功能,可开启也可被关闭,如前文介绍的快重传算法就没有启动该功能。
Nagle算法为了避免发送小的数据包,规定同一个时间段内只能有一个未被确认的数据分组发送出去,在一个数据包发送出去还没收到确认报文之前,所有待发送的数据报都要在缓存区等待,等上一个数据报收到确认后,再将缓冲区内积累的所有数据当做一个数据分组发送出去(前提是不能超过MSS)。这样能提高端对端的数据传输效率,假设发送端每次发送的数据只有一个字节,如果没有启用Nagle算法的话,每次发送的一个字节就作为一个数据包发送出去,每个数据包就会有41字节(20字节的IP首部和20字节的TCP首部,加上一个字节的业务数据),这样负荷数据就增加了40倍。
Nagle算法虽然能提高发送效率,因为其他数据包都要等到确认到达时才能发送出去,这对于经常发送细小数据包的应用层业务很有好处的,比如Rlogin这种命令式交互的应用,而对于数据实时性要求比较高的业务,如射击类游戏这显示是不合适的,因为Nagle算法也增加了延迟,这时就需要禁用Nagle算法来提供传输速率。Nagle算法在大部分系统是默认开启的,但也是可以关闭的,如Linux提供了TCP_NODELAY的选项来禁用Nagle算法。
在拥塞控制的介绍中,已经了解到一个TCP报文发送出去后,如果在一定时间内没有收到确认报文段,就会导致发送端超时重传。那这个超时时间是怎么定的呢?其实计算TCP超时重传时间是个很复杂的过程,我们先通过一个图了解一下RTT往返时间和超时时间的关系:

这里,RTT是指TCP数据报文的从发送数据报到接收响应报文的往返时间,RTO是指TCP报文超时重传时间。如上图所示,当网络不出现拥塞时,TCP报文的往返时间为RTT,假如超时重传时间RTO定得远小于RTT,就会出现不必要的重传,而如果将RTO定得远大于RTT,当出现拥塞时就会等待过长时间才重发报文,降低效率。因此,RTO就应该设置得比RTT大一点才更合理。但是,网络环境很复杂,在一个TCP连接中,可能上一个报文段的RTT小一些,下一个报文段就会因为网络问题而变得很大,那怎么通过RTT来计算RTO的值?
通过上面的分析,显然不能直接使用某一次的RTT来计算超时重传时间RTO。因此,TCP利用每次测量得到的RTT样本,来计算加权平均往返时间RTTs(又称平滑往返时间),再RTTs来计算RTO,它们计算公式如下:
RTTs1 = RTT1 (第一个报文段的RTT作为RTTs)
RTTs = (1 - α) × 旧的RTTs + α × 新的RTT样本 (RFC6298标准推荐α值为0.125)
RTO = RTTs + 4 × RTTd
这里我们看到了一个新的变量RTTd,这个叫做RTT偏差的加权平均,计算公式如下;
RTTd1 = RTT1 ÷ 2 (第一个报文段的RTTd值)
新的RTTd = (1 - β) × 旧的RTTd + β × (RTTs - 新的RTT样本) (RFC6298标准推荐β值为0.25)
由于网络环境的复杂性,当主机A在时间T1将TCP报文段发送出去后由于网络拥塞导致超时,因此在时间T2重发,之后在时间T3收到了响应报文,那这时RTT的值T3-T2还是T3-T1呢?这时会有两种情况,如果第一次报文段在发送过程中丢失了,而第二次重发后正常到达主机B并收到响应报文,因此RTT=T3-T2,第二种情况是第一次报文段正常到达主机B了,并发回了响应报文,但响应报文发回主机A时发生延迟了,等第二次重发报文后发送端才收到此确认报文,这时RTT就应该为T3-T1。但客户端主机A这种情况是无法得知的。
因此,针对出现超时重传时无法推测往返时间RTT的问题,Karn提出了一个算法:在计算加权平均往返时间RTTs时,只要报文段重传了,就不采用其往返时间RTT样本。也就是出现重传时,不重新计算RTTs,进行RTO也不会重新计算。
但这又引发了另一个问题,假设网络突然拥塞,一个报文段的往返时间RTT突然增大很多,这就导致了该报文段超时重传,之后的很多报文段都是保持这样大的RTT,于是都超时重传了。但根据Karn算法,超时重传的报文不计入更新RTO,就会导致RTO比实际情况偏小,大量的报文段都反复被重发。因此,Karn算法有了更新,方法是:报文段每重传一次,就把超时重传时间RTO增大一些,普遍的做法就将RTO取值为旧RTO的2倍。
为什么TCP连接的时候是3次?2次不可以吗?
因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。
为什么TCP连接的时候是3次,关闭的时候却是4次?
因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。
为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?
这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。 作者:C语言编程__Plus https://www.bilibili.com/read/cv16389896 出处:bilibili
原文来自