目录
一、协议简介
二、TCP报文格式
1、字段解析
2、报文解析
三、TCP的重传机制
1、超时重传
2、快速重传
3、 SACK 方法
4、Duplicate SACK
四、TCP的滑动窗口机制
五、流量控制
六、拥塞机制
1、慢启动
2、拥塞避免算法
3、拥塞发生
4、快速恢复
七、TCP的三次握手
八、TCP的四次挥手
TCP(Transmission Control Protocol)传输控制协议是一种面向连接的、可靠的、基于字节流的传输层协议。TCP为了保证不发生丢包,就给每个字节一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK); 如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
首先,TCP建立连接之后,通信双方都同时可以进行数据的传输,其次,他是全双工的;在保证可靠性上,采用超时重传和捎带确认机制。
在流量控制上,采用滑动窗口协议,协议中规定,对于窗口内未经确认的分组需要重传。
在拥塞控制上,采用慢启动算法。
TCP协议的特点:
官方指导文档参考:https://datatracker.ietf.org/doc/html/rfc793
TCP报文各段说明:
TCP报文Wireshark抓包示例:
(1)源端口
从下图可以明显看出,源端口号为:02 2a,即554。
(2)目的端口
从下图可以明显看出,目的端口号为:e2 3f,即57919。
(3)序号
从下图可以明显看出,序号为:47 05 60 6a,即1191534698。
(4)确认号
从下图可以明显看出,确认号为:d0 42 0d b4,即3493989812。
(5)数据偏移/首部长度
从下图可以明显看出,数据便宜/首部长度的四位长度为:1000,即8,所以首部长度为:8*4=32字节。
(6)保留/紧急URG/确认ACK/PSH/RST/同步SYN/终止FIN
从下图可以明显看出:
SYN=1,且ACK=1,表示为同意连接的请求应答
(7)窗口
从下图可以明显看出,窗口为:ff ff,即65535。
(8)校验和
从下图可以明显看出,校验和为:4b 3b
(9)紧急指针
从下图可以明显看出,紧急指针为:00 00
(10)选项
从下图可以明显看出,MSS为65495
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。
这是正常的数据传输,但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?
超时重传机制其实就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据。
TCP 会在以下两种情况发生超时重传:
那我们的超时时间应该设置为多少呢?
讲到这里,我们先来了解一下什么是 RTT
(Round-Trip Time 往返时延),从下图我们就可以知道:
RTT
就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。
超时重传时间是以 RTO
(Retransmission Timeout 超时重传时间)表示。
根据上述图,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。当然,我们的网络是时常变化的,所以「报文往返 RTT 的值」是经常变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。
TCP 的快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。
在上图,发送方发出了 1,2,3,4,5 份数据:
所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。
比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。
根据 TCP 不同的实现,以上两种情况都是有可能的。可见,这是一把双刃剑。
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK
方法。
SACK
方法还有一种实现重传机制的方式叫:SACK
( Selective Acknowledgment 选择性确认)。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
Duplicate SACK 又称 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
(1)ACK 丢包:
D-SACK
。(2)网络延时
可见,D-SACK
有这么几个好处:
我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。
但是,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。
为解决这个问题,TCP 引入了窗口这个概念,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值,该值由接收方的TCP 头中Window字段指定,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
假设窗口大小为 3
个 TCP 段,那么发送方就可以「连续发送」 3
个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:
上图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。
我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:
在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。
在下图,当收到之前发送的数据 32~36
字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56
字节又变成了可用窗口,那么后续也就可以发送 52~56
这 5 个字节的数据了。
接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:
问题:接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
发送方不能无脑的发数据给接收方,要考虑接收方处理能力。如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
在介绍之前,先参考一下发送方的来熟悉一下,几个指针:
SND.WND
:表示发送窗口的大小(大小是由接收方指定的);
SND.UNA
:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
SND.NXT
:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
指向 #4 的第一个字节是个相对指针,它需要 SND.UNA
指针加上 SND.WND
大小的偏移量,就可以指向 #4 的第一个字节了。
下面举个正常场景的栗子,为了简单起见,假设以下场景:
200
根据上图的流量控制,说明下每个过程:
Usable
减少为 120 字节,同时 SND.NXT
指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。RCV.NXT
也就指向 321,这意味着客户端期望的下一个报文的序列号是 321,接着发送确认报文给服务端。RCV.NXT
也就指向 441,接着发送确认报文给服务端。SND.UNA
指针往右偏移后指向 321,于是可用窗口 Usable
增大到 80。SND.UNA
指针往右偏移后指向 441,于是可用窗口 Usable
增大到 200。SND.NXT
指向 601,于是可用窗口 Usable
减少到 40。RCV.NXT
也就是指向了 601,接着发送确认报文给服务端。SND.UNA
指针偏移了 160 后指向 601,可用窗口 Usable
也就增大至了 200。上面的例子是理想情况下的流量控制,但是实际上,网络是波动的,所以,可能会出现,读速度跟不上写速度的场景,例如:
360
;根据上图的流量控制,说明下每个过程:
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,通知对方,现在可以继续发送数据了。但是如果这个通告窗口的 ACK 报文在网络中丢失了,那该怎么办呢?
其实为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST
报文来中断连接。
我们知道,在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….
所以,TCP 不能忽略网络上发生的事,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
那么怎么知道当前网络是否出现了拥塞呢?其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。
为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念:拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd
和接收窗口 rwnd
是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd
变化的规则:
cwnd
就会增大;cwnd
就减少拥塞控制主要是四个算法:
TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?
慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
这里假定拥塞窗口 cwnd
和发送窗口 swnd
相等,下面举个栗子:
cwnd = 1
,表示可以传一个 MSS
大小的数据。可以看出慢启动算法,发包的个数是指数性的增长。
那慢启动涨到什么时候是个头呢?
有一个叫慢启动门限 ssthresh
(slow start threshold)状态变量。
cwnd
< ssthresh
时,使用慢启动算法。cwnd
>= ssthresh
时,就会使用「拥塞避免算法」。前面说道,当拥塞窗口 cwnd
「超过」慢启动门限 ssthresh
就会进入拥塞避免算法。
一般来说 ssthresh
的大小是 65535
字节。
那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
接上前面的慢启动的栗子,现假定 ssthresh
为 8
:
MSS
大小的数据,变成了线性增长。所以,我们可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
当触发了重传机制,也就进入了「拥塞发生算法」。
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:
这两种使用的拥塞发送算法是不同的,接下来分别来说说。
这个时候,ssthresh 和 cwnd 的值会发生变化:
ssthresh
设为 cwnd/2
,cwnd
重置为 1
拥塞发送 —— 超时重传
接着,就重新开始慢启动,慢启动是会突然减少数据流的。这真是一旦「超时重传」,马上回到解放前。但是这种方式太激进了,反应也很强烈,会造成网络卡顿。
就好像本来在秋名山高速漂移着,突然来个紧急刹车,轮胎受得了吗。。。
发生快速重传的拥塞发生算法
还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh
和 cwnd
变化如下:
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO
超时那么强烈。
正如前面所说,进入快速恢复之前,cwnd
和 ssthresh
已被更新了:
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;然后,进入快速恢复算法如下:
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了);也就是没有像「超时重传」一夜回到解放前,而是还在比较高的值,后续呈线性增长。
拥塞算法示意图
好了,以上就是拥塞控制的全部内容了,看完后,你再来看下面这张图片,每个过程我相信你都能明白:
TCP涉及连接建立和连接终止的操作可以用状态转换图(state transition diagram)来说明,我们用粗实线表示通常的客户状态转换,用粗虚线表示通常的服务器状态转换:
TCP状态及描述:
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手而进行的。
1、一开始,客户端和服务端都处于 CLOSED
状态。先是服务端主动监听某个端口,处于 LISTEN
状态
2客户端会随机初始化序号(client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN
标志位置为 1
,表示 SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT
状态。
2、服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
3、客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
4、服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
一旦完成三次握手,双方都处于 ESTABLISHED
状态,此致连接就已建立完成,客户端和服务端就可以相互发送数据了。
小结
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
1、客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态
2、服务端收到该报文后,就向客户端发送 ACK
应答报文,接着服务端进入 CLOSED_WAIT
状态
3、客户端收到服务端的 ACK
应答报文后,之后进入 FIN_WAIT_2
状态
4、等待服务端处理完数据后,也向客户端发送 FIN
报文,之后服务端进入 LAST_ACK
状态
5、客户端收到服务端的 FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态
6、服务器收到了 ACK
应答报文后,就进入了 CLOSE
状态,至此服务端已经完成连接的关闭
7、客户端在经过 2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭
为什么 TIME_WAIT 等待的时间是 2MSL?
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
关闭连接时,客户端向服务端发送 FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。
服务器收到客户端的 FIN
报文时,先回一个 ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN
报文给客户端来表示同意现在关闭连接。