TCP 详解
1. TCP报文简介
TCP 是一种面向连接的、可靠的字节流服务
可靠性体现在:
- 应用数据被分割成 TCP 认为最适合发生的数据块。由 TCP 传递给 IP 的信息单位称为报文段。
- 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
- 当 TCP 收到发自 TCP 连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。
- TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错, TCP 将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。
- 如果必要, TCP 将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
- TCP 的接收端必须丢弃重复的数据。
- TCP 还能提供流量控制。 TCP连接的每一方都有固定大小的缓冲空间。 TCP 的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。
TCP 数据被封装在一个 IP 数据报中:
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427221518345-1771547440.png)
如下图所示,为 TCP 首部的数据格式。如果不计入任选字段,它通常是 20 个字节。
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427221639850-738735356.png)
- URG:紧急指针有效
- ACK:确认序号有效
- PSH:接收方应该尽快将这个报文段交给应用层
- RST:重新连接
- SYN:同步序号用来发起一个连接
- FIN:发端完成发生任务
2. 三次握手
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427221842592-509821616.png)
建立连接过程:
- 客户端发送一个 SYN 段指明客户端打算连接的服务器的端口,以及初始序列号(ISN,Initial Sequence Number,如图所示为 1415531521)。这个 SYN 段为报文段1。
- 服务器发回包含服务器的初始序号的 SYN 报文段(报文段2)作为应答。同时,将确认序号设置为客户的 ISN 加 1 以对客户的 SYN 报文段进行确认。一个 SYN 将占用一个序号。
- 客户必须将确认序号设置为服务器的 ISN 加 1 以对服务器的 SYN 报文段进行确认(报文段3)。
发送第一个 SYN 的一端将执行主动打开。接受这个 SYN 并发回下一个 SYN 的另一端执行被动打开。
ISN 初始序列号是否固定?
ISN 随时间而变化,因此每个连接都将具有不同的 ISN。ISN可以看作是一个 32 bit的计数器,每 4ms 加 1。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。
三次握手是否可以携带数据?
三次握手过程中仅仅可以在第三次握手中携带数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据是可以的。
简述 SYN 攻击。
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击。SYN 攻击就是客户端在短时间内伪造大量不存在的 IP 地址,并向服务器不断地发送 SYN 包,服务器则回复确认包,并等待客户端确认,由于源地址不存在,因此服务器需要不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
思考如下问题:为什么是三次握手,而非两次?
首先三次握手的目的是为了确认客户端与服务器的收包能力和发包能力如何,如下所示:
- 第一次握手是为了确认客户端的发包能力以及服务器的收包能力
- 第二次握手是为了确认客户端的收包能力以及服务器的发包能力
- 第三次握手是为了让服务器知道客户端的收包能力是正常的
倘若是两次握手,那服务器将无法得知客户端的收包能力如何,如下图所示:
(1)首先,客户端发送连接请求,但是由于连接请求报文在网络中长时间滞留而未及时到达服务器端,服务器端无法向客户端发送确认报文,于是客户端又重新发送连接请求,这次成功到达服务器端,并成功建立连接,如下图所示:
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222123345-419158436.png)
(2)客户端向服务器发送断开连接的请求,通过四次挥手,客户端与服务器断开连接,而那个在网络中滞留的数据包依然还未到达服务器端,如下图所示:
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222255403-852662880.png)
(3)客户端与服务器成功断开连接后,在网络中滞留的数据包成功到达服务器端,服务器以为客户端又要建立连接请求,故向客户端发送了确认报文,但是客户端此时处于连接关闭状态,不会对服务器发来的确认报文进行应答,同时服务器也在一直等待客户端的应答,这就造成了资源的浪费,如下图所示:
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222409375-102341429.png)
3. 四次挥手
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427221842592-509821616.png)
建立一个连接需要三次握手,而终止一个连接需要 4 次握手。这是由 TCP 的半关闭造成的。既然一个 TCP 连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN ,它必须通知应用层另一端几经终止了那个方向的数据传送。发送 FIN 通常是应用层进行关闭的结果。
断开连接过程:
- 客户端向服务器端发送一个 FIN,用来关闭从客户端到服务器的数据传输,以及 ACK 段。
- 服务器收到这个 FIN,它发回一个 ACK,确认序号为收到的序号加 1(报文段5)。和 SYN 一样,一个 FIN 将占用一个序号。同时,TCP 服务器还向应用程序传输一个文件结束符。
- 接着,这个服务器程序就关闭它的连接,导致它的 TCP 端发送一个 FIN(报文段6),客户必须发回一个确认,并将确认序号设置为收到的序号加 1(报文段7)
同样思考:为什么要四次才可以断开连接?
当客户端向服务器端发送 FIN 报文请求关闭连接,这只能表明客户端不想再发数据了,但不能表明服务器端想关闭连接,如果只用 2 次就可以断开连接,那么服务器端给客户端的数据包还未发完双方就断开了连接,就会出现资源丢失。而 4 次挥手中的后 2 次就是为了确认服务器不想再发送数据了。
4. TCP 状态迁移图
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222543157-359269024.png)
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222653915-1432189126.png)
TIME_WAIT 状态也称为 2MSL 等待状态。每个具体的 TCP 实现必须选择一个报文段最大生存时间 MSL。它是任何报文段被丢弃前在网络中生存的最长时间。那么 TIME_WAIT 存在的意义是什么?
TCP 协议在关闭连接的四次握手过程中,最终的 ACK 确认报文是由主动关闭连接的一端发出的,通常是客户端,如果这个 ACK 在网络中丢失,服务器端没有收到客户端发来的确认报文,将会重发最终的 FIN 报文,因此客户端必须维护状态信息允许它自己重发最终的 ACK 确认报文。由于一个报文段的最大生存时间是 MSL,那么第一个 MSL 是为了让客户端发出的 ACK 报文正确到达服务器端,第二个 MSL 是为了接收服务器因为没有收到 ACK 而重新发送的 FIN 报文。如果客户端在 TIME_WAIT 后没有收到重新发来的 FIN 报文,说明服务器正确接收了 ACK 报文,至此连接正式关闭。
5. 同时打开与同时关闭
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222734546-652269278.png)
一个同时打开的连接需要交换 4个报文段,比正常的三次握手多一个。
当出现同时打开的情况时,状态变迁与第 3 小节的图示不同。两端几乎在同时发送 SYN,并进入 SYN_SENT 状态。当每一端收到 SYN 时,状态变为 SYN_RCVD,同时它们都再发 SYN 并对收到的 SYN 进行确认。当双方都收到 SYN 及相应的 ACK时,状态都变迁为 ESTABLISHED。
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222804064-402493944.png)
如上图所示,当应用层发出关闭命令时,两端均从 ESTABLISHED 变为 FIN_WAIT_1。这将导致双方各发送一个 FIN,两个 FIN 经过网络传送后分别到达另一端。收到 FIN 后,状态由 FIN_WAIT_1 变迁到 CLOSING,并发送最后的 ACK。当收到最后的 ACK 时,状态变化为 TIME_WAIT。
6. 滑动窗口
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222836854-671696261.png)
在这个图中,我们将字节从 1 至 11 进行标号。接收方通告的窗口称为提供的窗口,它覆盖了从第 4 字节到第 9 字节的区域,表明接收方已经确认了包括第 3 字节在内的数据,且通告窗口大小为 6。
当接收方确认数据后,这个滑动窗口不时地向右移动。窗口两个边沿的相对运动增加或减少了窗口的大小。
- 称窗口左边沿向右边沿靠近为窗口合拢。这种现象发生在数据被发送和确认时。
- 当窗口右边沿向右移动时将允许发送更多的数据,我们称之为窗口张开。这种现象发生在另一端的接收进程读取已经确认的数据并释放了 TCP 的接收缓存时。
- 当右边沿向左移动时,我们称之为窗口收缩。
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222858613-996623.png)
因为窗口的左边沿受另一端发送的确认序号的控制,因此不可能向左边移动。如果接收到一个指示窗口左边沿向左移动的 ACK,则它被认为是一个重复 ACK,并被丢弃。如果左边沿到达右边沿,则称其为一个零窗口,此时发送方不能够发送任何数据。
如下图所示,为数据传输过程中滑动窗口的动态变化图:
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222924913-173287369.png)
从图中可以看出:
- 发送方不必发送一个全窗口大小的数据。
- 来自接收方的一个报文段确认数据并把窗口向右移动。这是因为窗口的大小是相对于确认序号的。
- 如图报文段 7 到报文段 8 中变化的那样,窗口的大小可以减小,但是窗口的右边沿却不能向左移动。
- 接收方在发送一个 ACK 前不必等待窗口被填满。
7. 超时重传
TCP 服务必须能够重传超时时间内未收到确认的 TCP 报文段。为此,TCP 模块为每个 TCP 报文段都维护一个重传定时器,该定时器在 TCP 报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP 模块将重传 TCP 报文段并重置定时器。在达到一定次数还没有成功时放弃并发送一个复位信号。
其中比较重要的概念就是重传超时时间,即RTO。
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427222955432-1717822507.png)
8. 拥塞控制
TCP 模块还有一个重要的任务,就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。这就是所谓的拥塞控制。
拥塞控制分为四个部分:
- 慢启动
- 拥塞避免
- 快速重传
- 快速恢复
拥塞控制的最终受控变量是发送端向网络一次连续写入的数据量,称为 SWND(Send Window,发送窗口)。不过,发送端最终以 TCP 报文段来发送数据,所以 SWND 限定了发送断能连续发送的 TCP 报文段数量。这些 TCP 报文段的最大长度(仅指数据部分)称为 SMSS。
发送端需要合理选择 SWND 的大小。如果 SWND 太小,会引起明显的网络延迟;反之如果太大,则容易发生网络拥堵。接收方可以通过其接受通告窗口(RWND)来控制发送端的 SWND。但其显然不够,所以发送端引入了一个称为拥塞窗口(CWND)的状态变量。如图所示:
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427223118067-36291691.png)
7.1 慢启动和拥塞避免
慢启动为发送方的 TCP 增加了另一个窗口:拥塞窗口,记为 CWND。
当与另一个网络的主机建立 TCP 连接时,拥塞窗口被初始化为 1 个报文段(即另一端通告的报文段大小)。每收到一个 ACK,拥塞窗口就增加一个报文段( CWND 以字节为单位,但是慢启动以报文段大小为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
发送方开始时发送一个报文段,然后等待 ACK。当收到该 ACK 时,拥塞窗口从1增加为 2,即可以发送两个报文段。当收到这两个报文段的 ACK 时,拥塞窗口就增加为 4。这是一种指数增加的关系。
慢启动算法的理由是:TCP 模块刚开始发送数据时并不知道网络的实际情况,需要用一种试探的方式平滑地增加 CWND 的大小。
但如果不施加其他手段,慢启动必然使得 CWND 快速膨胀,并最终导致网络拥塞。因此 TCP 拥塞控制中定义了另一个很重要的状态变量:慢启动门限(ssthresh)。当 CWND 的大小超过该值时,TCP 拥塞控制将进入拥塞避免阶段。
![](https://img2020.cnblogs.com/blog/2026333/202104/2026333-20210427223021072-691156707.png)
- 标号 1 处,TCP 初始连接进行数据交换,开始慢启动,初始 CWND = IW = 1 ,ssthresh = 16,在传输轮次 0~4 阶段进行慢启动阶段,CWND 按照 1-2-4-8-16 的顺序进行指数增长
- 标号 2 处,CWND = 16 = ssthresh,此时触发拥塞避免过程,开始线性增长,在传输轮次 4~12 阶段,CWND 按照 16-17-18-19-20-21-22-23-24 进行线性增长
- 标号 3 处,TCP 发生了 RTO 重传,认为网络发生拥塞,于是设置 ssthresh = CWND / 2 = 12,CWND = 1 重新进行慢启动过程
- 标号 4 处,TCP 从 CWND = 1 开始重新开始慢启动过程
- 标号 5 处,当 CWND = 12 时改为执行拥塞避免算法,拥塞窗口按照线性规律增长,没经过一个往返时延就增加一个 MSS 的大小
对于慢启动门限以及发生网络拥塞时为何要降为其一半的原因详见这篇文章:TCP核心概念-慢启动、拥塞避免、慢启动门限的真实含义
7.2 快速重传和快速恢复
在很多情况下,发送端都可能接收到重复的确认报文段,比如 TCP 报文段丢失,或者接收端收到乱序 TCP 报文段并重排之等。拥塞控制算法需要判断当受到重复的确认报文段时,网络是否真的发生了拥塞,或者说 TCP 报文段是否真的丢失了。
具体做法是:发送端如果连续收到 3 个重复的确认报文段,就认为是拥塞发生了。然后它启用快速重传和快速恢复算法来处理拥塞,过程如下:
- 当收到第 3 个重复的确认报文段时,按照下式计算 ssthresh,然后立即重传丢失的报文段
- 并按照下式设置 CWND
- 每次收到一个重复的确认时,设置 \(CWND = CWND + SMSS\)。此时发送端可以发送新的 TCP 报文段
- 当收到新数据的确认时,设置 \(CWND = ssthresh\),ssthresh 是新的慢启动门限值,由第一步计算得到
快速重传和快速恢复完成后,拥塞控制将恢复到拥塞避免阶段。
9. 参考文献
《TCP/IP详解》
《Linux高性能服务器编程》