ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 结构详解 ### IP数据报 TCP数据封装在一个IP数据报中: ![](https://img.kancloud.cn/45/fe/45febb26a66610aa8743cceb565ad89d_461x162.png) #### TCP首部 TCP报文数据格式。TCP首部如果不计选项和填充字段,它通常是**20个字节**。 ![](https://img.kancloud.cn/51/39/5139d7f46e357cef4054c2b9a5490636_428x378.png) #### 源端口和目的端口 **各占2个字节**,这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。有时一个IP地址和一个端口号也称为socket(插口)。 #### 序号(起始码) **占4个字节**,是本报文段所发送的数据项目组第一个字节的序号。在TCP传送的数据流中,每一个字节都有一个序号。例如,一报文段的序号为300,而且数据共100字节,则下一个报文段的序号就是400;序号是32bit的无符号数,序号到达2^32-1后从0开始。(注:如何防止从0开始后序号相同的问题) #### 确认序号 **占4个字节**,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个报文段的首部中的序号;**确认序号应该是上次已成功收到数据字节序号+1**。只有**ACK标志为1时,确认序号才有效**。 #### 数据偏移 占4比特,表示数据开始的地方离TCP段的起始处有多远。实际上就是**TCP段首部的长度**。由于首部长度不固定,因此数据偏移字段是必要的。数据偏移以32位为长度单位,也就是4个字节,因此TCP首部的最大长度是60个字节。即偏移最大为15个长度单位=15*32位=15*4字节。 #### 保留 6比特,供以后应用,现在置为0。 #### 6个标志位比特 * URG:当URG=1时,注解此报文应尽快传送,而不要按本来的列队次序来传送。与“紧急指针”字段共同应用,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号,使接管方可以知道紧急数据共有多长; * ACK:只有当ACK=1时,确认序号字段才有效; * PSH:当PSH=1时,接收方应该尽快将本报文段立即传送给其应用层; * RST:当RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还来拒绝一个不法的报文段或拒绝打开一个连接; * SYN:SYN=1,ACK=0时表示请求建立一个连接,携带SYN标志的TCP报文段为同步报文段; * FIN:发端完成发送任务; #### 窗口 TCP通过滑动窗口的概念来进行流量控制。设想在发送端发送数据的速度很快而接收端接收速度却很慢的情况下,为了保证数据不丢失,显然需要进行流量控制, 协调好通信双方的工作节奏。所谓滑动窗口,可以理解成接收端所能提供的缓冲区大小。TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能提供多大的缓 冲区。窗口大小为字节数,起始于确认序号字段指明的值(这个值是接收端正期望接收的字节)。窗口大小是一个16bit字段,因而窗口大小最大为65535字节。 #### 检验和 检验和覆盖了整个TCP报文段:TCP首部和数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。 **紧急指针** 只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 ## 三次握手过程 ![](https://img.kancloud.cn/30/40/304041b7c884a136f8f6e68ba782cebb_490x288.png) ①客户端向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=J ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,**SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。** ②TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=J+1,同时也要为自己初始化一个序列号 seq=K,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。**这个报文也不能携带数据**,但是同样要消耗一个序号。 ③TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=K+1,**\[自己的序列号seq=J+1\]**,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。 ④当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。 ### 为什么握手要“3”次? > 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。 在书中同时举了一个例子,如下: > 已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。” 这就很明白了,防止了服务器端的一直等待而浪费资源 ## 四次挥手过程 ![](https://img.kancloud.cn/d6/73/d67396742757ea7471825a6a623b3321_1000x642.png) ①客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,**FIN报文段即使不携带数据,也要消耗一个序号。** ②服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了**,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整**个CLOSE-WAIT状态持续的时间。 ③客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。 ④服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。 ⑤客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,**必须经过2∗∗MSL(最长报文段寿命)的时间后,**当客户端撤销相应的TCB后,才进入CLOSED状态。 ⑥服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。 ### 为什么需要四次握手 为了确保数据能够完成传输。 关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。 ### 释放连接时为什么TIME-WAIT状态必须等待2MSL时间 MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。 第一,为了保证A发送的最后一个ACK报文能够到达B。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段,重置时间等待计时器(2MSL)。如果A在TIME-WAIT状态不等待一段时间,而是在发送完ACK报文段后就立即释放连接,就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按照正常的步骤进入CLOSED状态。 第二,A在发送完ACK报文段后,再经过2MSL时间,就可以使本连接持续的时间所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求的报文段。 ### 客户端突然挂掉了怎么办?     正常连接时,客户端突然挂掉了,如果没有措施处理这种情况,那么就会出现客户端和服务器端出现长时期的空闲。解决办法是在服务器端设置保活计时器,每当服务器收到客户端的消息,就将计时器复位。超时时间通常设置为2小时。若服务器超过2小时没收到客户的信息,他就发送探测报文段。若发送了10个探测报文段,每一个相隔75秒,还没有响应就认为客户端出了故障,因而终止该连接。 ## 滑动窗口(流量控制) 滑动窗口实现了TCP流控制。首先明确滑动窗口的范畴:TCP是双工的协议,会话的双方都可以同时接收和发送数据。TCP会话的双方都各自维护一个**发送窗口**和一个**接收窗口**。各自的接收窗口大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各自的发送窗口则要求取决于对端通告的接收窗口,要求相同。 滑动窗口解决的是**流量控制**的的问题,就是**如果接收端和发送端对数据包的处理速度不同**,如何让双方达成一致。接收端的缓存传输数据给应用层,但这个过程不一定是即时的,如果发送速度太快,会出现接收端数据overflow,流量控制解决的是这个问题。 ![](https://img.kancloud.cn/29/16/2916ee64eafb6ee7dc83a7094c8b4650_679x368.png) ![](https://img.kancloud.cn/0c/73/0c73dea2e7ad7733cf17badc363e3571_1024x558.png) ![](https://img.kancloud.cn/3c/16/3c16a106495b58682d53287aa5e80bc3_672x373.png) ![](https://img.kancloud.cn/cd/69/cd69ecdf865029c6dcead8174597eeb6_666x363.png) 1. 发送方没有收到接收方发回的ACK,就不能向右滑动。假设发送方向接收方发了ABCD就滑动,只要对方没收到A,就不能滑动,那么就会出现二者不同步的局面。 2. 滑动窗口提高了信道利用率,TCP是发送报文段为单位的,假如每发一个报文就要等ACK,那么对于大数据包,等待时间就太长了。只要发送的报文在滑动窗口里面,不用等每个ACK回来就可以向右滑动。本例中,开始接收端空着AB,只有CD,此时不能滑动;之后接收到EF和H,直接向右滑动2位,不必等G到位。 3. 窗口大小不能大于序号空间大小的一半。目的是为了不让两个窗口出现交迭,比如总大小为7,窗口大小都为4,接收窗口应当滑动4,但只剩3个序号,导致两个窗口交迭。 4. 有一种情况没出现:发送方发ABCD,接收方都收到然后向右滑动,但回复的ACK包全丢了。发送方未收到任何ACK, timeout后会重发ABCD,此时的接收方按累计确认的原则,收到ABCD后只会重发D的ACK,发送方收到后向右滑动。 ### 参考资料 [解析TCP之滑动窗口(动画演示)](https://blog.csdn.net/yao5hed/article/details/81046945) ## 拥塞控制(**拥塞窗口** cwnd) TCP发送方可能因为IP网络的拥塞而被遏制,TCP拥塞控制就是为了解决这个问题(注意和TCP流量控制的区别)。 TCP拥塞控制的几种方法:**慢启动**,**拥塞避免**,**快重传**和**快恢复**。 这里引入了一个拥塞窗口的概念; 拥塞窗口:**发送方维持一个叫做拥塞窗口 cwnd的状态变量**。拥塞窗口的大小取决于网络的拥塞程度,并且动态变化。发送方的让自己的发送窗口=min(cwnd,接受端接收窗口大小)。 发送方控制拥塞窗口的原则是:**只要网络没有出现拥塞,拥塞窗口就增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数**。 ### 慢启动\\拥塞避免 **// 慢启动 指数增长:cwnd = 1 => cwnd = 2 => cwnd = 4 => cwnd = 8** **// 达到默认ssthresh 16** **// 拥塞避免 线性增长:cwnd = 17 => cwnd = 18 => cwnd = 19 => cwnd = 20** **// 发现超时(注意是这里是超时,而后面的快重传是3次失败)** **// ssthresh 设为发生超时时的拥塞窗口数值的一半** **// 设置 cwnd = 1,开始慢增长** ![](https://img.kancloud.cn/9f/c6/9fc6902cfc3608a4fd2d232619bb049c_570x306.png) **慢启动:** 当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是 先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。 ![ ](images/screenshot_1566635047914.png) > 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。慢开始门限ssthresh的用法如下: > 当 cwnd < ssthresh 时,使用上述的慢开始算法。 > 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 > 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法 **拥塞避免:**让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。 无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。 ### 快重传\\快恢复 **// 但发现3重复ACK,确定丢包了** **// 开启重传** **// ssthresh 仍然设为发生超时时的拥塞窗口数值的一半** **// 从新ssthresh 开始,使用拥堵避免算法** ![ ](images/screenshot_1566635083055.png) **快重传:**在超时重传中,重点是定时器溢出超时了才认为发送的数据包丢失,**快速重传机制,实现了另外的一种丢包评定标准,即如果我连续收到3次重复ACK**,发送方就认为这个seq的包丢失了,立刻进行重传,这样如果接收端回复及时的话,基本就是在重传定时器到期之前,提高了重传的效率。(启动快重传机制,重传数据,其他数据发送数据放入队列,待快重传结束后再正常传输。) 快恢复,与快重传配合使用的还有快恢复算法,其主要有以下两个要点: 1. 当发送方连续收到接收方发来的三个重复确认时,就执行“乘法减小”算法,把慢开始门限ssthresh减半(这个减半指的是变成发生阻塞时的阻塞窗口大小的一半),这是为了预防网络发生拥塞。(注意:接下来不执行慢开始算法) 2. 由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是把cwnd(拥塞窗口)值设置为慢开始门限减半后的值(有些版本会让cwnd=ssthresh+3),然后开始执行拥塞避免算法,使拥塞窗口缓慢的线性增大。 ## 超时重传 原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。 影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO,Retransmission TimeOut)。RTO的值被设置过大过小都会对协议造成不利影响。 1. RTO设长了,重发就慢,没有效率,性能差。 2. RTO设短了,重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。 连接往返时间(RTT,Round Trip Time),指发送端从发送TCP包开始到接收它的立即响应所消耗的时间。     RTO理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。 ## 四种定时器 ### 重传计时器 大家都知道TCP是保证数据可靠传输的。怎么保证呢?带确认的重传机制。在滑动窗口协议中,接受窗口会在连续收到的包序列中的最后一个包向接收端发送一个ACK,当网络拥堵的时候,发送端的数据包和接收端的ACK包都有可能丢失。TCP为了保证数据可靠传输,就规定在重传的“时间片”到了以后,如果还没有收到对方的ACK,就重发此包,以避免陷入无限等待中。 当TCP发送报文段时,就创建该特定报文的重传计时器。可能发生两种情况: 1. 若在计时器截止时间到之前收到了对此特定报文段的确认,则撤销此计时器。 2. 若在收到了对此特定报文段的确认之前计时器截止时间到,则重传此报文段,并将计时器复位。 ### 坚持计时器     专门对付零窗口通知而设立的,     先来考虑一下情景:发送端向接收端发送数据包知道接受窗口填满了,然后接受窗口告诉发送方接受窗口填满了停止发送数据。此时的状态称为“零窗口”状态,发送端和接收端窗口大小均为0.直到接受TCP发送确认并宣布一个非零的窗口大小。但这个确认会丢失。我们知道TCP中,对确认是不需要发送确认的。若确认丢失了,接受TCP并不知道,而是会认为他已经完成了任务,并等待着发送TCP接着会发送更多的报文段。但发送TCP由于没有收到确认,就等待对方发送确认来通知窗口大小。双方的TCP都在永远的等待着对方。     要打开这种死锁,TCP为每一个链接使用一个持久计时器。当发送TCP收到窗口大小为0的确认时,就坚持启动计时器。当坚持计时器期限到时,发送TCP就发送一个特殊的报文段,叫做探测报文。这个报文段只有一个字节的数据。他有一个序号,但他的序号永远不需要确认;甚至在计算机对其他部分的数据的确认时该序号也被忽略。探测报文段提醒接受TCP:确认已丢失,必须重传。     坚持计时器的值设置为重传时间的数值。但是,若没有收到从接收端来的响应,则需发送另一个探测报文段,并将坚持计时器的值加倍和复位。发送端继续发送探测报文段,将坚持计时器设定的值加倍和复位,直到这个值增大到门限值(通常是60秒)为止。在这以后,发送端每个60秒就发送一个探测报文,直到窗口重新打开。 ### 保活计时器         保活计时器使用在某些实现中,用来防止在两个TCP之间的连接出现长时间的空闲。假定客户打开了到服务器的连接,传送了一些数据,然后就保持静默了。也许这个客户出故障了。在这种情况下,这个连接将永远的处理打开状态。         要解决这种问题,在大多数的实现中都是使服务器设置保活计时器。**每当服务器收到客户的信息,就将计时器复位。通常设置为两小时。若服务器过了两小时还没有收到客户的信息,他就发送探测报文段。若发送了10个探测报文段(每一个像个75秒)还没有响应,就假定客户除了故障,因而就终止了该连接。** 这种连接的断开当然不会使用四次握手,而是直接硬性的中断和客户端的TCP连接。 ### 时间等待计时器 时间等待计时器是在**四次握手**的时候使用的。四次握手的简单过程是这样的:假设客户端准备中断连接,首先向服务器端发送一个FIN的请求关闭包(FIN=final),然后由established过渡到FIN-WAIT1状态。服务器收到FIN包以后会发送一个ACK,然后自己有established进入CLOSE-WAIT.此时通信进入半双工状态,即留给服务器一个机会将剩余数据传递给客户端,传递完后服务器发送一个FIN+ACK的包,表示我已经发送完数据可以断开连接了,就这便进入LAST\_ACK阶段。客户端收到以后,发送一个ACK表示收到并同意请求,接着由FIN-WAIT2进入TIME-WAIT阶段。服务器收到ACK,结束连接。此时(即客户端发送完ACK包之后),客户端还要等待2MSL(MSL=maxinum segment lifetime最长报文生存时间,2MSL就是两倍的MSL)才能真正的关闭连接。 ## 粘包/拆包问题 TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块**仅仅看成一连串无结构的字节流,**没有边界;另外从TCP的帧结构也可以看出,**在TCP的首部没有表示数据长度的字段,**基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。 ### 粘包、拆包表现形式 现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下: 第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。 ![](https://img.kancloud.cn/b3/75/b375591a741a465f90924342e48da0da_691x111.png) 第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。 ![](https://img.kancloud.cn/26/65/26651e3e7627eebb5acc35eb6d91f52d_690x113.png) 第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。 ![](https://img.kancloud.cn/bc/4a/bc4a9563c947a1687ca8150c4bad929e_690x114.png) ![](https://img.kancloud.cn/fc/a1/fca1d77a41892560cd418b96ec9e11f2_690x114.png) ### 粘包、拆包发生原因 发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充, 1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。 2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。 3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。 4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。 等等。 ### 粘包、拆包解决办法 通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个: 1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。 2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。 3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。 ## 参考资料 [TCP超详细知识点整理](https://www.jianshu.com/p/8c5ccbe51f5b)