对于TCP的了解,首先想到的就是三次握手和四次挥手
- 三次握手:指的是,在建立TCP连接时,客户端与服务端需要经过三次的通信,才能建立连接
- 四次挥手:指的是,客户端和服务端断开连接时,需要经过四次通信,连接才算断开
那么问题就来了,为什么需要三次握手才能建立连接呢?
- 首先说一下三次握手的过程
- 第一次,客户端向服务端发送
SYN[SYN标志位为1,序号为 x]包 - 第二次,服务端收到请求后,向客户端发送
ACK[ACK标志位为1,确认号为 x+1]包和SYN[SYN标志位为1,序号为 y]包 - 第三次,客户端再次向服务端发送
ACK[ACK标志位为1,确认号为 y+1]包 - 大致流程参考下图
sequenceDiagram participant Client participant Server Client ->> Server : SYN标志为1,序号为x Server ->> Client : SYN标志为1, 序号为y,ACK标志为1,确认号为x+1 Client ->> Server : ACK标志为1, - 第一次,客户端向服务端发送
- 假如第三次这个包不发,会有什么问题?
- 看上图的话,也就是说服务端这边没有收到客户端的序号y的确认,那么也就无法确定客户端是否处于可通信状态了。TCP是一个双向都能通信的协议,也就是说,客户端和服务端双方都有发送数据和接收数据的能力,而这,则是建立在双方都获得对方的确认号的基础上才能实现,如果只有只有两次握手的话,那么,最多只有客户端这边知道服务端准备就绪了,可以进入 ESTABLISHED 状态,但是服务端这边不知道客户端是否准备就绪,所以无法进入 ESTABLISHED 状态
- 另外一点,如果服务端没收到客户端的确认号,那么这个连接,就会在半连接队列里呆着,如果说客户端是故意这么做的,而且还伪装了假IP,并且大量发送,那么这就是有名的
SYN flood攻击了。一般来说,可以开启tcp_syncookies来防御一部分的攻击,但大规模防御的,可能还得要用防火墙,也就是做多一层代理的方式来防御。然后关于这种情况的介绍和防御则可以参考 关于tcp你应该知道的 和 tcp的半连接与完全连接队列 。
建立连接三次就可以了,为什么关闭连接需要四次
- 照旧,还是先说下四次挥手的过程
- 第一次,客户端发送
FIN包给服务端,此时要关闭的是客户端到服务端的这条通路,客户端进入FIN_WAIT 1阶段 - 第二次,服务器收到这个包后,返回一个确认包
ACK,服务端进入到CLOSE_WAIT状态, 客户端收到这个包后,进入FIN_WAIT 2阶段 - 第三次,服务器在数据全部发完后,向客户端发送
FIN包,此时要关闭的是服务端到客户端的这条通路,服务端进入LAST_ACK状态 - 第四次,客户端收到这个包后,返回一个确认包
ACK,客户端进入TIME_WAIT状态,等待2MSL时间后,才进入关闭状态 - 服务端收到
ACK包后,就进入了关闭状态 - 大致流程参考下图,图片来自 blog.csdn.net
- 第一次,客户端发送
- 说到底,还是因为TCP是一个双向都能通信的协议,客户端和服务端各自负责自己的通路,服务端收到客户端发的
FIN包只代表客户端想要关闭客户端负责的通路,服务端自己的通路还没关闭的,所以需要服务端发送FIN包给客户端,并收到客户端的ACK包回复,才能关闭服务端负责的通路 - 所以,需要经过四次的挥手,才能使客户端和服务端双方各自关闭自己负责的那条通路。如果只进行两次或三次,那么最多就只有客户端负责的那条通路关闭了,服务端负责的那条无法关闭,或者是客户端卡在了
FIN_WAIT 2阶段,这都会导致这个连接失效 - 然后看到第三次的挥手前,此时虽然客户端请求要关闭连接,但服务端这边有可能还有数据还没发送完的,所以这个时候,需要等服务端把所需的数据下发完了,才给客户端发送
FIN包,请求关闭
如果客户端在第四次挥手后不进入 TIME_WAIT 状态,而是直接关闭,会有什么问题
- 也就是说在第四次挥手后,客户端这边就进入关闭状态,那么假如第四次挥手发的
ACK包丢失了,那么服务端这边就会认为说,我刚才发的FIN包没有被回复,需要重发 - 然后客户端这边就会再次收到
FIN包,但是因为客户端已经进入了关闭状态,对这个包的回复就会是RST包了,而不是ACK包 - 服务端这边收到这个
RST包后,就会认为是连接错误,并上报给上层(应用层),这将会导致TCP协议变得不可靠 - 而如果客户端在
2MSL时间内收到了FIN包,则会重新发送ACK包,并重置定时器,如果定时2MSL内没有收到服务器发来的FIN包,则认为关闭成功,客户端进入CLOSED状态 - 另外一个原因,就是要让网络中的旧数据包过期消失,
MSL指的是一个数据库在网络中的存活时间,如果没有TIME_WAIT阶段,那么假如现在客户端又再次连接了服务端,而且端口跟刚才的一样,建立上后发的数据包,会在旧的数据包之后,而且全部都会发送到服务端这边,此时服务端就会收到两次连接的包,这是不符合规范的,所以需要等上2MSL的时间,让旧的数据包都消失了,才进入关闭状态,并让出占用的端口
说的什么 FIN ACK 的到底指的是什么
-
其实这几个是TCP报文头里的标志位,先看下TCP报文的组成
-
图片引用自 blog.csdn.net
-
图片引用自 blog.csdn.net
-
下面的说明引用自 理解TCP报文段的头部结构
- 首先TCP报文头部前20个字节是固定的,后面的选项是
4N,根据需要加上的 - 16位端口号:标示该段报文来自哪里(源端口)以及要传给哪个上层协议或应用程序(目的端口)。进行tcp通信时,一般client是通过系统自动选择的临时端口号,而服务器一般是使用知名服务端口号或者自己指定的端口号。
- 32位序号:表示一次tcp通信过程(从建立连接到断开)过程中某一次传输方向上的字节流的每个字节的编号。假定主机A和B进行tcp通信,A传送给B一个tcp报文段中,序号值被系统初始化为某一个随机值ISN,那么在该传输方向上(从A到B),后续的所有tcp报文断中的序号值都会被设定为ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025。
- 32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到对方的tcp报文段的序号值+1。假定主机A和B进行tcp通信,那么A发出的tcp报文段不但带有自己的序号,也包含了对B发送来的tcp报文段的确认号。反之也一样。
- 4位头部长度:表示tcp头部有多少个32bit字(4字节),因为4位最大值是15,所以最多有15个32bit,也就是60个字节是最大的tcp头部长度。
- 6位标志位:
URG:紧急指针是否有效ACK:表示确认好是否有效,携带ack标志的报文段也称确认报文段PSH:提示接收端应用程序应该立即从tcp接受缓冲区中读走数据,为后续接收的数据让出空间RST:表示要求对方重建连接。带RST标志的tcp报文段也叫复位报文段SYN:表示建立一个连接,携带SYN的tcp报文段为同步报文段FIN:表示告知对方本端要关闭连接了
- 16位窗口:是TCP流量控制的一个手段,这里说的窗口是指接收通告窗口,它告诉对方本端的tcp接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
- 16位校验和:由发送端填充,接收端对tcp报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意这个校验不仅包括tcp头部,也包括数据部分。这也是tcp可靠传输的一个重要保障。
- 16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此这个字段是紧急指针相对当前序号的偏移量。不妨称之为紧急便宜,发送紧急数据时会用到这个。
- TCP头部选项:最后一个选项字段是可变长的可选信息,最多包含40字节的数据。
- 首先TCP报文头部前20个字节是固定的,后面的选项是
-
看了上面的解释,大概就知道,说的
FIN包,就是指FIN标志位为1的包
上面说的是TCP的组成以及连接的建立和断开,那么TCP协议,还有哪些手段能使其成为一个稳定可靠的协议?
- 说明引用自 TCP超详细知识点整理
- 超时重传
- 原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的ACK报文,那么就重新发送数据,直到发送成功为止。
- 影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO,Retransmission TimeOut)。RTO的值被设置过大过小都会对协议造成不利影响。
- RTO设长了,重发就慢,没有效率,性能差。
- RTO设短了,重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
- 连接往返时间(RTT,Round Trip Time),指发送端从发送TCP包开始到接收它的立即响应所消耗的时间。
- RTO理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。
- 流量控制
- TCP流量控制主要是针对接收端的处理速度不如发送端发送速度快的问题,消除发送方使接收方缓存溢出的可能性。
- TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。
- 接收端通过TCP首部的窗口大小字段反馈当前可接收的字节数。
- 下图引用自 www.jianshu.com
- 过程说明
- 发送方接收到了对方发来的报文 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发送方可能因为IP网络的拥塞而被遏制,TCP拥塞控制就是为了解决这个问题 (注意和TCP流量控制的区别)。
- TCP拥塞控制的几种方法:慢启动,拥塞避免,快重传和快恢复。
- 这里引入了一个拥塞窗口的概念
- 拥塞窗口:发送方维持一个叫做拥塞窗口 cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态变化。发送方的让自己的发送窗口=min(cwnd,接受端接收窗口大小)。
- 发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。
- 下面将讨论拥塞窗口cwnd的大小是怎么变化的。
- 慢启动
- 当主机开始发送数据时,如果立即所大量数据字节注入到网络,那么就有可能引起网络拥塞,因为现在并不清楚网络的负荷情况。因此,较好的方法是 先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。通常在刚刚开始发送报文段时,先把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。用这样的方法逐步增大发送方的拥塞窗口 cwnd ,可以使分组注入到网络的速率更加合理。
- 图片引用自 www.jianshu.com
- 每经过一个传输轮次,拥塞窗口 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,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。
- 图片引用自 www.jianshu.com
- 当TCP连接进行初始化时,把拥塞窗口cwnd置为1。前面已说过,为了便于理解,图中的窗口单位不使用字节而使用报文段的个数。慢开始门限的初始值设置为16个报文段,即 cwnd = 16 。
- 在执行慢开始算法时,拥塞窗口 cwnd 的初始值为1。以后发送方每收到一个对新报文段的确认ACK,就把拥塞窗口值加1,然后开始下一轮的传输(图中横坐标为传输轮次)。因此拥塞窗口cwnd 随着传输轮次按指数规律增长。当拥塞窗口cwnd增长到慢开始门限值ssthresh时(即当cwnd=16时),就改为执行拥塞控制算法,拥塞窗口按线 性规律增长。
- 假定拥塞窗口的数值增长到24时,网络出现超时(这很可能就是网络发生拥塞了)。更新后的ssthresh值变为12(即变为出现超时时的拥塞窗口数值 24的一半),拥塞窗口再重新设置为1,并执行慢开始算法。当cwnd=ssthresh=12时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过 一个往返时间增加一个MSS的大小。
- 强调:“拥塞避免”并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免”是说在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
- 快重传
- 在超时重传中,重点是定时器溢出超时了才认为发送的数据包丢失,快速重传机制,实现了另外的一种丢包评定标准,即如果我连续收到3次重复ACK,发送方就认为这个seq的包丢失了,立刻进行重传,这样如果接收端回复及时的话,基本就是在重传定时器到期之前,提高了重传的效率。
- 图片引用自 www.jianshu.com
- 例如:M1,M2,M3 -----> M1,M3,缺失M2,则向发送方发送M2重复确认,当发送方收到M2的三次重复确认,则认为M2报文丢失,启动快重传机制,重传数据,其他数据发送数据放入队列,待快重传结束后再正常传输。
- 快恢复
- 与快重传配合使用的还有快恢复算法,其主要有以下两个要点
- 当发送方连续收到接收方发来的三个重复确认时,就执行“乘法减小”算法,把慢开始门限ssthresh减半(这个减半指的是变成发生阻塞时的阻塞窗口大小的一半),这是为了预防网络发生拥塞。(注意:接下来不执行慢开始算法)
- 由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,而是把cwnd(拥塞窗口)值设置为慢开始门限减半后的值(有些版本会让cwnd=ssthresh+3),然后开始执行拥塞避免算法,使拥塞窗口缓慢的线性增大。
- 图片引用自 www.jianshu.com
- 与快重传配合使用的还有快恢复算法,其主要有以下两个要点
- 慢启动
四种定时器
- 重传计时器
- 大家都知道TCP是保证数据可靠传输的。怎么保证呢?带确认的重传机制。在滑动窗口协议中,接受窗口会在连续收到的包序列中的最后一个包向接收端发送一个ACK,当网络拥堵的时候,发送端的数据包和接收端的ACK包都有可能丢失。TCP为了保证数据可靠传输,就规定在重传的“时间片”到了以后,如果还没有收到对方的ACK,就重发此包,以避免陷入无限等待中。
- 当TCP发送报文段时,就创建该特定报文的重传计时器。可能发生两种情况
- 若在计时器截止时间到之前收到了对此特定报文段的确认,则撤销此计时器。
- 若在收到了对此特定报文段的确认之前计时器截止时间到,则重传此报文段,并将计时器复位。
- 坚持计时器
- 专门对付零窗口通知而设立的,
- 先来考虑一下情景:发送端向接收端发送数据包知道接受窗口填满了,然后接受窗口告诉发送方接受窗口填满了停止发送数据。此时的状态称为“零窗口”状态,发送端和接收端窗口大小均为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)才能真正的关闭连接。