参考资料:
https://blog.csdn.net/qzcsu/article/details/72861891
https://www.cnblogs.com/duan2/p/9180861.html
《计算机网络 第六版》谢希仁编著
难免有遗漏,希望大佬们在评论中指出还有那些需要添加的点。感谢!感谢!感谢!
本文内容如下:
- TCP头部结构和字段介绍
- TCP三次握手过程
- TCP三次握手问题补充
- TCP四次挥手过程
- TCP四次挥手问题补充
- TCP流量控制
滑动窗口 - TCP拥塞控制
慢启动、拥塞避免、快重传、快恢复 - TCP超时重传
- TCP的四种定时器
- TCP粘包/拆包问题
一、TCP首部结构详解
图片来自《TCP/IP详解卷1》
TCP数据封装在一个IP数据报中:
下图是TCP报文数据格式。TCP首部如果不计选项和填充字段,它通常是20个字节。
下面分别对其中的字段进行介绍:
源端口和目的端口:各占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个长度单位=1532位=154字节。
保留: 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时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。
二、TCP三次握手过程
百度图片,侵删。
①客户端向服务器发出连接请求报文,这时报文首部中的同部位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状态,此后双方就可以开始通信了。
三、TCP三次握手问题补充
2.1为什么需要三次握手
首先我们要知道信道是不可靠的,但是我们要建立可靠的连接发送可靠的数据,也就是数据传输是需要可靠的。在这个时候三次握手是一个理论上的最小值,并不是说是tcp协议要求的,而是为了满足在不可靠的信道上传输可靠的数据所要求的。
在《计算机网络》一书中其中有提到,三次握手的目的是“为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误”,这种情况是:一端(client)A发出去的第一个连接请求报文并没有丢失,而是因为某些未知的原因在某个网络节点上发生滞留,导致延迟到连接释放以后的某个时间才到达另一端(server)B。本来这是一个早已失效的报文段,但是B收到此失效的报文之后,会误认为是A再次发出的一个新的连接请求,于是B端就向A又发出确认报文,表示同意建立连接。如果不采用“三次握手”,那么只要B端发出确认报文就会认为新的连接已经建立了,但是A端并没有发出建立连接的请求,因此不会去向B端发送数据,B端没有收到数据就会一直等待,这样B端就会白白浪费掉很多资源。如果采用“三次握手”的话就不会出现这种情况,B端收到一个过时失效的报文段之后,向A端发出确认,此时A并没有要求建立连接,所以就不会向B端发送确认,这个时候B端也能够知道连接没有建立。
四、TCP四次挥手过程
①客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,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连接的时间要比客户端早一些。
五、TCP四次挥手问题补充
4.1 为什么需要四次握手?
为了确保数据能够完成传输。
关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
4.2 释放连接时为什么TIME-WAIT状态必须等待2MSL时间?
首先说下什么是MSL:
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时间,就可以使本连接持续的时间所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求的报文段。
4.3 客户端突然挂掉了怎么办?
正常连接时,客户端突然挂掉了,如果没有措施处理这种情况,那么就会出现客户端和服务器端出现长时期的空闲。解决办法是在服务器端设置保活计时器,每当服务器收到客户端的消息,就将计时器复位。超时时间通常设置为2小时。若服务器超过2小时没收到客户的信息,他就发送探测报文段。若发送了10个探测报文段,每一个相隔75秒,还没有响应就认为客户端出了故障,因而终止该连接。
TCP超时重传
原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO,Retransmission TimeOut)。RTO的值被设置过大过小都会对协议造成不利影响。
(1)RTO设长了,重发就慢,没有效率,性能差。
(2)RTO设短了,重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
连接往返时间(RTT,Round Trip Time),指发送端从发送TCP包开始到接收它的立即响应所消耗的时间。
RTO理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。
六、TCP流量控制
TCP流量控制主要是针对接收端的处理速度不如发送端发送速度快的问题,消除发送方使接收方缓存溢出的可能性。
TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。
接收端通过TCP首部的窗口大小字段反馈当前可接收的字节数。
- 发送方接收到了对方发来的报文 ack = 33, win = 10,知道对方收到了 33 号前的数据,现在期望接收 [33, 43) 号数据。发送方连续发送了 4 个报文段假设为 A, B, C, D, 分别携带 [33, 35), [35, 36), [36, 38), [38, 41) 号数据。
- 接收方接收到了报文段 A, C,但是没收到 B 和 D,也就是只收到了 [33, 35) 和 [36, 38) 号数据。接收方发送回对报文段 A 的确认:ack = 35, win = 10。
- 发送方收到了 ack = 35, win = 10,对方期望接收 [35, 45) 号数据。接着发送了一个报文段 E,它携带了 [41, 44) 号数据。
- 接收方接收到了报文段 B: [35, 36), D:[38, 41),接收方发送对 D 的确认:ack = 41, win = 10. (这是一个累积确认)
- 发送方收到了 ack = 41, win = 10,对方期望接收 [41, 51) 号数据。
- ……
需要注意的是,接收方接收 tcp 报文的顺序是不确定的,并非是一定先收到 35 再收到 36,也可能是先收到 36,37,再收到 35.
对于发送端的数据缓冲区有这些量:LastByteSent是目前发送的最后1字节的数据编号;LastByteAckd是目前接收到确认的最后1字节的数据编号;Rcvwin是窗口大小。鉴于每次发送方都是收到ACK之后滑动窗口继续发送,发送到LastByteSent这个位置,LastByteSent-LastByteAckd也就是这次发送数据的多少,那么只要满足:LastByteSent–LastByteAckd<=RcvWin(接收端空闲窗口大小) 就会保证不会溢出了。
那么接收端RcvWin怎么算呢?假设接收端缓冲区大小为RcvBuffer。LastByteRead:上层应用程序接收的最后一个字节序号,LastByteRcvd:接收端从网络接收的最后一个字节序号,那么LastByteRcvd–LastByteRead就是已经接受但是还没有传递给上层的数据。所以空闲区域RcvWin= RcvBuffer-(LastByteRcvd–LastByteRead).
TCP拥塞控制
TCP发送方可能因为IP网络的拥塞而被遏制,TCP拥塞控制就是为了解决这个问题(注意和TCP流量控制的区别)。
TCP拥塞控制的几种方法:慢启动,拥塞避免,快重传和快恢复。
这里引入了一个拥塞窗口的概念;
拥塞窗口:发送方维持一个叫做拥塞窗口 cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态变化。发送方的让自己的发送窗口=min(cwnd,接受端接收窗口大小)。
发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。
下面将讨论拥塞窗口cwnd的大小是怎么变化的。
慢启动
当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是 先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。
每经过一个传输轮次,拥塞窗口 cwnd 就加倍。一个传输轮次所经历的时间其实就是往返时间RTT。不过“传输轮次”更加强调:把拥塞窗口cwnd所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。
另外,慢开始的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大cwnd。
为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。慢开始门限ssthresh的用法如下:
当 cwnd < ssthresh 时,使用上述的慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。
拥塞避免
让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。
无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。
如下图,用具体数值说明了上述拥塞控制的过程。现在发送窗口的大小和拥塞窗口一样大。
<1>当TCP连接进行初始化时,把拥塞窗口cwnd置为1。前面已说过,为了便于理解,图中的窗口单位不使用字节而使用报文段的个数。慢开始门限的初始值设置为16个报文段,即 cwnd = 16 。
<2>在执行慢开始算法时,拥塞窗口 cwnd 的初始值为1。以后发送方每收到一个对新报文段的确认ACK,就把拥塞窗口值加1,然后开始下一轮的传输(图中横坐标为传输轮次)。因此拥塞窗口cwnd 随着传输轮次按指数规律增长。当拥塞窗口cwnd增长到慢开始门限值ssthresh时(即当cwnd=16时),就改为执行拥塞控制算法,拥塞窗口按线 性规律增长。
<3>假定拥塞窗口的数值增长到24时,网络出现超时(这很可能就是网络发生拥塞了)。更新后的ssthresh值变为12(即变为出现超时时的拥塞窗口数值 24的一半),拥塞窗口再重新设置为1,并执行慢开始算法。当cwnd=ssthresh=12时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过 一个往返时间增加一个MSS的大小。
强调:“拥塞避免”并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免”是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
快重传
在超时重传中,重点是定时器溢出超时了才认为发送的数据包丢失,快速重传机制,实现了另外的一种丢包评定标准,即如果我连续收到3次重复ACK,发送方就认为这个seq的包丢失了,立刻进行重传,这样如果接收端回复及时的话,基本就是在重传定时器到期之前,提高了重传的效率。
例如:M1,M2,M3 -----> M1,M3,缺失M2,则会向发送方发送M2重复确认,当发送方收到M2的三次重复确认,则认为M2报文丢失,启动快重传机制,重传数据,其他数据发送数据放入队列,待快重传结束后再正常传输。
快恢复
与快重传配合使用的还有快恢复算法,其主要有以下两个要点:
1)当发送方连续收到接收方发来的三个重复确认时,就执行“乘法减小”算法,把慢开始门限ssthresh减半(这个减半指的是变成发生阻塞时的阻塞窗口大小的一半),这是为了预防网络发生拥塞。(注意:接下来不执行慢开始算法)
2)由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是把cwnd(拥塞窗口)值设置为慢开始门限减半后的值(有些版本会让cwnd=ssthresh+3),然后开始执行拥塞避免算法,使拥塞窗口缓慢的线性增大。
TCP的四种定时器
TCP中有四种计时器(Timer),分别为:
1.重传计时器:Retransmission Timer
2.坚持计时器:Persistent Timer
3.保活计时器:Keeplive Timer
4.时间等待计时器:Timer_Wait Timer
(1)重传计时器
大家都知道TCP是保证数据可靠传输的。怎么保证呢?带确认的重传机制。在滑动窗口协议中,接受窗口会在连续收到的包序列中的最后一个包向接收端发送一个ACK,当网络拥堵的时候,发送端的数据包和接收端的ACK包都有可能丢失。TCP为了保证数据可靠传输,就规定在重传的“时间片”到了以后,如果还没有收到对方的ACK,就重发此包,以避免陷入无限等待中。
当TCP发送报文段时,就创建该特定报文的重传计时器。可能发生两种情况:
1.若在计时器截止时间到之前收到了对此特定报文段的确认,则撤销此计时器。
2.若在收到了对此特定报文段的确认之前计时器截止时间到,则重传此报文段,并将计时器复位。
(2)坚持计时器
专门对付零窗口通知而设立的,
先来考虑一下情景:发送端向接收端发送数据包知道接受窗口填满了,然后接受窗口告诉发送方接受窗口填满了停止发送数据。此时的状态称为“零窗口”状态,发送端和接收端窗口大小均为0.直到接受TCP发送确认并宣布一个非零的窗口大小。但这个确认会丢失。我们知道TCP中,对确认是不需要发送确认的。若确认丢失了,接受TCP并不知道,而是会认为他已经完成了任务,并等待着发送TCP接着会发送更多的报文段。但发送TCP由于没有收到确认,就等待对方发送确认来通知窗口大小。双方的TCP都在永远的等待着对方。
要打开这种死锁,TCP为每一个链接使用一个持久计时器。当发送TCP收到窗口大小为0的确认时,就坚持启动计时器。当坚持计时器期限到时,发送TCP就发送一个特殊的报文段,叫做探测报文。这个报文段只有一个字节的数据。他有一个序号,但他的序号永远不需要确认;甚至在计算机对其他部分的数据的确认时该序号也被忽略。探测报文段提醒接受TCP:确认已丢失,必须重传。
坚持计时器的值设置为重传时间的数值。但是,若没有收到从接收端来的响应,则需发送另一个探测报文段,并将坚持计时器的值加倍和复位。发送端继续发送探测报文段,将坚持计时器设定的值加倍和复位,直到这个值增大到门限值(通常是60秒)为止。在这以后,发送端每个60秒就发送一个探测报文,直到窗口重新打开。
(3)保活计时器
保活计时器使用在某些实现中,用来防止在两个TCP之间的连接出现长时间的空闲。假定客户打开了到服务器的连接,传送了一些数据,然后就保持静默了。也许这个客户出故障了。在这种情况下,这个连接将永远的处理打开状态。
要解决这种问题,在大多数的实现中都是使服务器设置保活计时器。每当服务器收到客户的信息,就将计时器复位。通常设置为两小时。若服务器过了两小时还没有收到客户的信息,他就发送探测报文段。若发送了10个探测报文段(每一个像个75秒)还没有响应,就假定客户除了故障,因而就终止了该连接。
这种连接的断开当然不会使用四次握手,而是直接硬性的中断和客户端的TCP连接。
(4)时间等待计时器
时间等待计时器是在四次握手的时候使用的。四次握手的简单过程是这样的:假设客户端准备中断连接,首先向服务器端发送一个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)才能真正的关闭连接。