学习TCP (Transmission Control Protocol)

88 阅读14分钟

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议

写在前面(把最特色的三次握手和四次挥手解释一下吧!)

三次握手

建立TCP连接需要三次握手,可以理解为双方在这个过程中需要确认,对方是否具有发送(SYN)和接收(ACK) 能力,只要双方都有发送和接受能力,就能建立TCP连接

记住这句话,就能很好的理解三次握手是在做什么

这是一张经典的三次握手图解

image.png

我们来解释一下到底发生了什么

首先客户端主动请求连接,发送SYN(Synchronize)报文,同时设置seq为x。从这一步可以得知:客户端具有 发送(SYN) 的能力

服务端收到请求,返回SYN+ACK(Acknowledge)报文,同时设置seq为y,ack为x+1(即客户端seq+1)。同理从这一步可以得知:服务端具有 发送(SYN) 和 接收(ACK) 的能力

客户端收到服务端返回的确认信息,返回ACK报文,同时设置seq为x+1(确保顺序,将之前的seq+1),设置ack为y+1(即服务端的seq+1),这一步,相当于最后得知了,客户端也具有接收的能力!

至此我们确认了双方都具有发送(SYN)和接收(能力),TCP连接建立完成

有了这样一个思路我们就能解释,为什么三次握手是最合理的

如果是两次握手,那么我们就无法确认客户端是否有接收的能力

如果是四次握手或更多,我们就进行了多余的操作,因为三次已经足以我们确认双方的能力


四次挥手

在了解四次挥手之前,我们要先了解TCP建立的是 全双工通信

全双工通信就是客户端和服务端之间可以同时给对方发送数据报文

所以我们想要断开连接,就要满足客户端到服务端的两条通道都正常关闭

还是一张经典的四次挥手图解

image.png

我们来解释一下整个流程

首先客户端想要结束TCP连接,向服务端发送一个FIN(Finish)报文,同时设置seq=p

服务端收到请求,并返回ACK,设置ack=p+1

在这之后服务端可能还要向客户端发送一些报文,所以不能在第二次挥手同时设置FIN和ACK

当服务端确认可以结束TCP连接时,向客户端发送一个FIN报文,同时也要携带一个ACK并设置ack的值为p+1(客户端seq+1),用于确认这期间客户端并没有向服务端发送新的报文。除此之外,还要设置一个seq=q

客户端收到服务器的请求,发送ACK报文,并设置seq=q+1

进行到这一步,会涉及到一个细节,客户端在回应服务器的请求并发送ACK之后,并不会立刻关闭,而是等待2MSL(最大报文生存时间-Maximum Segment Lifetime),并在这期间等待,看看服务器有没有重新发送FIN(重新发送是因为可能没有正常接收到客户端的ACK报文),如果有则重新发送ACK,并再等待2MSL,直到在这期间没有收到服务端的FIN,证明服务端已经成功接收到了客户端的ACK报文

至此四次挥手结束,TCP连接关闭


更系统的梳理一下 TCP 四次挥手的过程:

  1. 第一次挥手:客户端主动关闭连接,发送 FIN 报文(FIN=1, seq=p),进入 FIN_WAIT_1 状态。
    → 表示:“我已无数据可发,准备关闭。”

  2. 第二次挥手:服务端收到 FIN 后,回复 ACK 报文(ACK=1, ack=p+1),进入 CLOSE_WAIT 状态。
    → 表示:“我已收到你的关闭请求。”
    ⚠️ 此时连接并未完全关闭,因为 TCP 是全双工的,服务端可能还有数据要发送给客户端。

  3. 第三次挥手:当服务端也准备好关闭时,发送 FIN 报文(FIN=1, seq=q),同时携带 ACKACK=1, ack=p+1),进入 LAST_ACK 状态。
    → 表示:“我也要关闭了。”
    💡 注意:ack=p+1 是因为从第二次挥手以来,客户端未再发送新数据,因此确认号不变。

  4. 第四次挥手:客户端收到 FIN 后,回复 ACK 报文(ACK=1, seq=x+1, ack=q+1),进入 TIME_WAIT 状态。
    → 表示:“收到你的关闭通知。”

此时,客户端不会立即关闭,而是进入 TIME_WAIT 状态,持续等待 2MSL(Maximum Segment Lifetime,通常为 60 秒)

🔍 为什么需要 2MSL?

  • 确保最后一个 ACK 能被服务端收到:如果服务端未收到 ACK,会重发 FIN,客户端可在 TIME_WAIT 状态下重新发送 ACK
  • 防止旧连接的“延迟报文”干扰新连接:等待 2MSL 可确保网络中所有该连接的“残余报文”都消失,避免它们被误认为是新连接的数据。

2MSL 结束后,客户端进入 CLOSED 状态,连接彻底关闭。服务端在收到最后一个 ACK 后,立即进入 CLOSED 状态。


说完了连接,再讲讲TCP为什么是可靠的

TCP如何保证数据包按顺序抵达?

在了解这个之前,我们还是躲不开去看看TCP报文是长什么样的

image.png

我们注意到,报文中有一个部分叫序列号(Sequence Number),图里叫序号我们这个统称序列号,我们之前在三次握手和四次挥手里见到的seq就是这个的缩写

序列号的值表示 该报文的第一个字节在整个数据流中的位置

现在我们来模拟一个发送报文的流程

比如客户端按照以下顺序发送报文

[seq = 100 ] => [seq = 200 ] => [seq = 300]

但是由于网络等原因,实际的到达顺序

[seq = 300 ] => [seq = 100 ] => [seq = 200] 存在乱序!

TCP是如何解决报文到达顺序和发送顺序不同的问题的呢?

我们这里简单描述,接收方在收到报文后,会将它们放入 接收缓冲区(Receive Buffer)

收到 [seq=300] → 缓冲区:______ | 300~399
收到 [seq=100] → 缓冲区:100~199 | 300~399
收到 [seq=200] → 缓冲区:100~199 | 200~299 | 300~399

只有连续的、无缺口的数据才能被上交给应用层(如浏览器、Node服务)

这里还有一个机制,如果接收方迟迟接收不到自己想要的报文来组成连续的无缺口的数据,该怎么反馈呢?

这就涉及到了 ACK的累积确认机制

也就是我们报文段里的确认序列号部分(Acknowledgement number)

比如说

[ack = 200 ] 相当于告诉发送方,我已经收到了序列号小于200(0-199)的所有数据

如果发送方迟迟没有收到这个ack,那它就知道可能自己发送的相关数据报文丢失了,触发重传,这个重传也是我们后面要讲的TCP可靠的其中一个机制

通过序列号+接收缓冲区+累积确认机制,TCP实现了数据包按序到达的功能


TCP的超时重传机制

超时重传(Retransmission Timeout, RTO)

这是一个非常基础的重传机制

在每次发送一个数据包时,启动一个计时器

在规定时间内没有收到ACK,就会触发一次超时重传

这个时间是根据一个算法动态计算的,这里我们不展开

现在假设我们有这样一个场景

发送方:发送 [seq=100, len=100]        → 定时器开始
       ↓
       网络丢包!ACK 没回来
       ↓
       ⏱️ 定时器超时(比如 500ms)
       ↓
发送方:重新发送 [seq=100, len=100]    → 定时器重置
       ↓
接收方:收到 → 回复 ack=200
       ↓
发送方:收到 ACK → 取消定时器,继续发下一个

这个场景模拟了一次超时重传并在重新发送后正常传输的过程

但是从中我们可以思考

我们该怎么判断是什么原因导致发送方没收到ACK:

是发送的数据包在到达接收方之前就丢失了,导致接收方根本不知道有数据向它发来,再导致的没有ACK返回

还是说是接收方返回了ACK,但是ACK在传输过程丢失了

二者无法区分,所以这是超时重传的一个缺点

还有一个缺点就是超时重传的等待时间基于一种算法动态计算,有时会让用户等待很长的时间


快速重传机制

为了解决刚刚说的第二个缺点,也就是超时重传的速度问题

快速重传(Fast Retransmit)出现了

它的核心思想是:如果接收方收到了乱序的数据包,它会重复的发送同一个ack,表示自己想接收的那个数据包未到达,发送方发过来的都不是接收方想要的,当发送方收到三次相同的ack之后,就会触发快速重传

这是一个示例流程

发送顺序: [100][200][300][400]
实际到达: [100][300][400][200] 丢了!

接收方行为:
  收到 [100] → 回复 ack=200
  收到 [300] → 发现 seq=300 > 期望的 200 → 乱序!
             → 仍然回复 ack=200(表示“我还在等 200”)
  收到 [400] → 再次发现乱序 → 再次回复 ack=200
             → 此时已发送 3 个 ack=200

发送方行为:
  收到第 3 个 ack=200 → 触发快速重传
             → 立即重发 [seq=200]

这样子,重传触发的时间大大缩短,用户体验会得到显著提升

快速恢复 vs 慢启动

我们上面讨论了超时重传和快速重传

它们二者对应的后续动作就是慢启动和快速恢复

举一个🌰

慢启动(超时后): cwnd: 1 → 2 → 4 → 8 → 16 → ...(指数增长,但起点低)
快速恢复(快速重传后):cwnd: 10 → 5 → 6 → 7 → 8 → ...(起点高,线性增长)

快速重传配合快速启动,可以避免性能的骤降,提升用户体验

这里我们不再深入探讨

“虽然前端不直接处理 TCP 层,但理解这些机制有助于我们分析页面加载性能。比如,关键资源如果触发了超时重传,会导致首屏加载明显变慢;而快速恢复的存在,能缓解偶发丢包的影响。”


流量控制

先来欣赏一下技术蛋老师的视频神奇的滑动窗口 | TCP流量控制

假如说有这样一个问题,发送方的主机性能极其优越,它可以快速的发送许多数据包

但是接收方是一个老年机,它处理的速度非常的慢,这样子的话,发送方发来的数据包就会疯狂的丢失并频繁的触发重传

TCP是如何解决这样的情况的呢?

这就涉及到了流量控制

流量控制的核心就在于:使用滑动窗口机制 ,让接收方在每次返回ACK的时候在首部设置window的大小,告诉发送方我还能接受多少的数据,从而防止发送方发送太快导致接收方缓冲区溢出的问题

这里再贴一张截图,展示发送窗口和接受窗口的图示,也就是说发送方和接收方都有滑动窗口,而且长度可变

image.png

拥塞控制

流量控制一直在关注发送方和接收方的事,但是并没有考虑它们之间的网络相关的问题,如果它们之间的网络出问题了,比如说路由器缓存区满了,带宽被别的应用占走了等问题,这些情况就叫做网络拥塞

TCP是如何感知网络拥塞的,又是如何解决的呢?

TCP使用一个叫做拥塞窗口的机制来解决这个问题

拥塞窗口(Congestion Window - cwnd) 由发送方维护

因此发送方的发送窗口此时不只是由原先流量控制时 接收方返回的window大小决定了,也就是不单纯由接收窗口大小决定了

发送窗口的实际大小为接收方返回的接收窗口的大小和发送方拥塞窗口的大小的最小值 来决定

这样说有些绕,可以这样来写

发送窗口大小 = Min(接收窗口rwnd,拥塞窗口cwnd)

这样也就相当于,哪怕我此时接收方还能接受很多数据,但是我的拥塞窗口很小,发送方也别想发送很多数据

指导这些概念后,我们要了解拥塞控制的主要流程,或者说四个阶段

这里上一张图

image.png

这张图描述了这样一件事请

  1. 慢启动

我现在开始网络传输,但是一开始我不知道网络的情况,我先用慢启动试试水

设置初始的cwnd为1MSS(最大报文长度- Maximum Segment Size)

之后每接收到一次ACK,cwnd 都呈现指数级增长

所以这里的慢启动,它只是启动慢,后面增长速度可不慢

但是当指数增长到一定程度时,就会进入下一个阶段,避免突然拥塞出现

  1. 拥塞避免 每接收到一个ACK,cwnd线性增长

  2. 快速重传 当我问的发送方接收到3个重复的ACK,触发快速重传

重新发送丢失的数据包,这里我们在上面已经阐述过

然后执行快速恢复

  1. 快速恢复 说是快速恢复,就是咱们不从头来,让cwnd = 1/2 cwnd

然后继续线性增长

图中没有涉及到超时重传,因为超时重传就相当于直接重新从慢启动开始了

这就是一个完整的拥塞控制流程

TCP 的拥塞控制,是一套无需网络设备反馈的自适应系统:

  1. 通过 慢启动 快速探测带宽;
  2. 通过 拥塞避免 稳步逼近极限;
  3. 通过 快速重传 + 快速恢复 快速应对轻度拥塞;
  4. 通过 超时重传 → 慢启动 应对严重拥塞。

这套“加法增大,乘法减小”(AIMD)的策略

  • 加法增大(Additive Increase):网络还好 → 慢慢提速(每次加一点)
  • 乘法减小(Multiplicative Decrease):网络堵了 → 赶紧降速(直接砍一半) 让 TCP 在复杂网络中既能高效利用带宽,又能避免拥塞崩溃,堪称互联网的“交通规则”。

TCP和UDP的区别

上面我们讲了很多TCP的知识

我们现在来讲一下UDP的知识

UDP(User Datagram Protocol - 用户数据报协议)

它是无连接、不可靠的传输层协议

我们按照之前讲解TCP的顺序,来对比着理解UDP,在这个过程中,就能搞清楚它们之间的区别

  1. 连接方式上

首先TCP需要三次握手建立连接后才能发送数据,四次挥手断开连接

但是UDP不需要,它可以直接发送数据,无需建立连接

  1. 可靠性上

TCP是可靠的,它可以保证数据报按序到达,可靠性高,有重传机制,有流量控制和拥塞控制

UDP是不可靠的,不保证数据报到达顺序,也不保证数据报能到达,反正TCP为了保证自己可靠的机制,UDP都没有

  1. 应用场景

既然UDP这么不可靠,那要它干嘛?

UDP用的地方可多了,正因为它无需连接,不需要考虑数据是否正常传输,这让UDP在实时音视频比如打视频电话,开直播,打游戏,DNS查询等对速度要求高但允许丢包的场景下就很实用

TCP一般用于需要可靠传输,需要数据报按序抵达的场景,比如文件传输,网页加载等。

举个反例,如果用TCP传输实时音视频,如果丢包了,它还要重传,那现在你就能看到过去的事情,这和现实明显割裂了,不如用UDP,也就是卡一会的事情


参考资料

作为前端的你了解多少tcp的内容

(建议收藏)TCP协议灵魂之问,巩固你的网路底层基础

神奇的滑动窗口 | TCP流量控制