文章目录

TCP 核心知识精讲:从原理到实践,一篇彻底搞懂

本文是我在学习 TCP 过程中的笔记沉淀,结合了实际工程思考,用 🧩 电话类比、🏦 银行扣款、🚗 接亲车队、💧 水桶比喻、👩 妈妈叮嘱…… 带你从根儿上理解 TCP 的每一个设计。


1. 网络通信分层模型——就像寄快递 📦

网络通信的每一层都有自己的职责,我用寄快递来类比:

  • 应用层:好比你要寄的物品(比如一本书)。
  • 传输层:好比你在快递单上写的发件人和收件人的详细地址和电话,确保送到正确的人手里。
  • 网络层:好比快递公司的全国干线网络,它规划这本书从北京到上海应该走哪条干线(比如先陆运到南京,再水运到上海)。
  • 数据链路层:就像分拨中心之间的运输队,他们负责把一箱箱货物(帧)安全地从上一站送到下一站,确保路上不丢、不坏、不堵车。
  • 物理层:就是卡车、公路、仓库这些实际的硬件设施,没有它们,任何协议都是空谈。

在这里插入图片描述


2. TCP 报头——每个字段都有它的故事 📝

TCP 报文段头部长度通常为 20 字节(无选项),最大 60 字节。每个字段都有它的使命:
在这里插入图片描述

2.1 源端口 & 目的端口

  • 作用:标识发送端和接收端的应用程序。
  • 为什么需要?一台机器上可能同时运行多个网络应用(浏览器、画图板、微信),数据到达时必须知道交给哪个进程。
  • 怎么做:目的端口告诉操作系统交付给哪个进程,源端口让对方知道回复时发往哪里。

2.2 序列号(Seq) & 确认号(Ack) 🆔

  • 序列号:32 位,本报文段所携带数据的第一个字节的编号。TCP 为每个字节编号。
  • 确认号:32 位,期望收到对方下一个报文段的第一个字节序号,表示之前的数据已成功接收。
  • 作用:实现 可靠传输(有序、去重、丢包检测)。

2.3 标志位(SYN、ACK、FIN、RST、PSH、URG)

  • SYN:同步序号,建立连接。
  • ACK:确认字段有效。
  • FIN:请求终止连接。
  • RST:强制复位连接(如端口未监听)。
  • PSH:推送,要求接收方尽快交给应用层。
  • URG:紧急指针有效。

2.4 窗口大小 📏

  • 16 位,表示接收端当前可用的接收缓冲区大小。
  • 用于 流量控制:发送方根据此值限制未确认数据量。
  • 窗口大小通常由接收方的数据缓存队列决定:缓存队列剩余多少空间,返回的 ACK 中窗口大小字段就填多少。

2.5 校验和 ✅

  • 覆盖 TCP 头部和数据的校验值,用于检测传输中的 bit 错误。

2.6 数据偏移 & 选项

  • 数据偏移:4 位,表示 TCP 头部长度(以 4 字节为单位)。
  • 选项:可变长度,携带 MSS、窗口扩大因子、时间戳、SACK 等扩展功能。

2.7 保留位 🔮

在“数据偏移”和“标志位”之间有一段 保留位(Reserved),长度为 3 位或 6 位。目前必须置 0,为未来扩展预留,就像备用插槽,以后加新功能时可以直接用,不用重新设计协议。


3. 三次握手与四次挥手——连接管理的艺术 🤝

3.1 三次握手(建立连接)

三次握手是 TCP 建立连接的核心过程,也是面试中最高频的考点之一。下面我们从过程、设计原因、状态变化、功能测试四个维度来拆解。


一、三次握手的过程
步骤 方向 报文内容 状态变化
第一次 客户端 → 服务器 SYN=1, Seq=x 客户端 → SYN_SENT
第二次 服务器 → 客户端 SYN=1, ACK=1, Seq=y, Ack=x+1 服务器 → SYN_RCVD
第三次 客户端 → 服务器 ACK=1, Seq=x+1, Ack=y+1 双方 → ESTABLISHED

关键细节

  • xy 是双方各自随机选择的初始序列号(ISN),用于防止历史报文干扰和提升安全性。
  • Ack=x+1 表示“我已收到你的 SYN(它占用了序列号 x),现在期待收到序列号为 x+1 的数据”。
  • 第三次握手的 ACK 报文可以携带数据(如果应用层有数据要发),但通常不携带。

在这里插入图片描述


二、为什么必须是三次,不是两次? ❓
原因一:让双方都能确认对方的收发能力正常

用电话类比来理解:

步骤 操作 报文类型 测试了什么 结论
第一次 你拨通电话,说:“喂!” SYN 测试 你的发送功能朋友的接收功能 你知道自己能发,朋友知道自己能收
第二次 朋友回应:“我听到了,你能听见我吗?” SYN+ACK ① 确认第一次成功
② 测试 朋友的发送功能你的接收功能
你知道自己能收,朋友知道自己能发
但朋友还不知道自己的发送是否被对方听到
第三次 你回答:“我也听到了!” ACK 告诉朋友:你的发送功能我也能正常接收 朋友得知自己的发送功能正常
至此,双方确认彼此收发能力全部正常

如果只有两次握手(客户端发 SYN,服务器回 SYN+ACK 就认为连接建立),只有客户端(发送方)知道双方都正常,服务器(接收方)还不知道自己的发送功能是否被对方正常接收。这会导致服务器无法确定连接是否真的可用,后续传输可能出现问题。

原因二:防止历史连接请求占用服务器资源

假设场景:

  1. 客户端发送一个 SYN 报文,因为网络拥堵延迟了很久才到达服务器。
  2. 客户端等不到回复,以为丢包,又发了一个新的 SYN 建立新连接。
  3. 旧的 SYN 突然到达,服务器误以为客户端想建立新连接,于是回复 SYN+ACK 并进入 SYN_RCVD 状态。
  4. 但客户端早已放弃旧连接,不会回复 ACK,服务器就会一直等,浪费资源。

三次握手如何解决?
第三次握手让客户端有机会告诉服务器:“这个连接是有效的,不是历史的。”
如果客户端发现自己根本没发起过这个连接(比如序列号对不上),它会在第三次握手时发 RST 拒绝,服务器收到后立即释放资源,不会傻等。


三、为什么不用四次? ⏱️

理论上,可以设计成:SYN → ACK → SYN → ACK(四次)。但第二次和第三次可以合并,因为服务器在收到 SYN 后,既想确认客户端的 SYN,又想发送自己的 SYN,这两个动作可以放在同一个报文里。

类比:寄两个快递到同一个地址,分两次发需要两笔运费,打包成一个包裹发只需要一笔运费。


四、序列号为什么需要随机初始化? 🔑
  • 防止历史报文干扰:如果每次建立连接都从 1 开始,旧连接的延迟报文可能恰好与新连接的序列号重叠,造成数据错乱。
  • 提高安全性:避免被攻击者猜中序列号,实施会话劫持。
  • 随机范围:RFC 建议使用 32 位随机数,但实际实现中通常是基于时间戳的伪随机数。

五、三次握手中的常见面试追问

Q1:第三次握手可以携带数据吗?
可以。如果客户端在第三次握手时已经有数据要发送,可以直接把数据放在 ACK 报文中。但此时连接尚未完全建立(服务器还没收到第三次握手),如果数据丢失,客户端无法确定是握手失败还是数据丢失,所以实际很少这样做。

Q2:如果第三次握手丢失了怎么办?
服务器没有收到第三次握手的 ACK,会一直处于 SYN_RCVD 状态,等待超时后重发 SYN+ACK。客户端收到重发的 SYN+ACK 后,会重新发送 ACK。重复多次没有收到 ACK 后,服务器关闭连接。


六、小结

三次握手的本质是 用最少的交互次数,完成双方收发能力的确认和序列号的同步

  • 两次不够:无法让接收方确认自己的发送功能是否正常,也无法排除历史连接的干扰。
  • 四次多余:可以合并报文,浪费带宽。
  • 三次刚好:既保证了双方都确认对方能力,又排除了历史连接风险,兼顾了可靠性与效率。

3.2 四次挥手(关闭连接) 👋

过程

  1. 主动关闭方发送 FIN
  2. 被动方回复 ACK
  3. 被动方也发送 FIN
  4. 主动方回复 ACK

为什么是四次,不是三次? 🤔

因为被动关闭方收到 FIN 后,ACK 是由内核立即回复的,而 FIN 需要等应用层调用 close() 才能发送,这两个动作通常不同步

就像你上午和下午各买了一件衣服,商家发现两个订单是同一个客户,想合并成一个包裹一起发。结果发现上午那件已经发走了,没办法,只能把下午那件再单独发一个快递了。

在 TCP 里:

  • 上午那件已发出的衣服 = 内核已经回复的 ACK
  • 下午那件单独发的快递 = 应用层后续发出的 FIN

所以 ACK 和 FIN 没法合并成一个报文,四次挥手是必然的。

在这里插入图片描述

3.3 TCP 状态 📊

状态 含义
LISTEN 服务器已启动,正在等待客户端连接请求
ESTABLISHED 连接已建立,双方可以正常传输数据
CLOSE_WAIT 被动关闭方收到 FIN 并回复 ACK,等待应用程序 close()
TIME_WAIT 主动关闭方发送最后一个 ACK 后等待 2MSL,确保 ACK 被对方收到,并让旧报文消失

4. 可靠传输的两大支柱:确认应答 & 超时重传 📬

4.1 确认应答 ✔️

接收方收到数据后,返回一个 ACK 报文,确认号 = 期望收到的下一个字节的序号。发送方根据确认号释放已确认的数据,并继续发送新数据。确认是 累积的:确认号 N 表示序号 N 之前的所有数据都已收到。

在这里插入图片描述

4.2 超时重传 ⏰

发送方为每个报文段设置一个重传计时器,超时未收到 ACK 则重传。重传超时时间(RTO)根据网络往返时间(RTT)动态调整。

如果 ACK 丢了怎么办?
不用担心,因为后续 ACK 会携带更大的确认号。比如我收到了 1~1000 的数据,ACK 返回 1001 但丢了;后来你又发来 1001~2000,我回复 ACK 2001,发送方看到 2001 就知道 1~2000 都已成功。就像大人问你“大几了?”你回答“工作三年了”,他自然知道你大学毕业了。

在这里插入图片描述


5. 滑动窗口——让传输“流水线”起来 🚰

在没有滑动窗口的 TCP 中,发送方每发送一个报文,必须等待对方确认后才能发送下一个(停等协议),效率极低。

滑动窗口的引入:允许发送方在收到确认之前连续发送多个报文(窗口大小决定数量)。接收方累积确认,发送方每收到一个确认,窗口就向前滑动,并可以发送新的报文。这种流水线式的并行发送,极大地提升了 吞吐量

  • 吞吐量 = 单位时间内实际成功传输的数据量。窗口越大,单位时间等待确认的时间越短,吞吐量越高。
  • 窗口大小由谁决定? 由接收方通告的窗口(流量控制)和网络拥塞窗口(拥塞控制)共同决定。

窗口大小实时更新与边界计算
假设发送方窗口大小为 5,已发送 1、2、3,窗口剩余 2。此时收到 ACK 确认了报文 1,并通告新窗口 = 10。那么发送方:

  • 滑动窗口:左边界移到 2
  • 右边界 = 确认号 + 新窗口大小 - 1 = 2 + 10 - 1 = 11(覆盖报文 2~11)

因为确认号本身已经占了一个位置,所以需要 -1。

在这里插入图片描述


6. 快速重传——不等超时,三个 ACK 就出手 ⚡

当发送方数据丢失时,接收方会重复发送相同的 ACK(期望的序号)。发送方连续收到三个重复 ACK,就判断该报文丢失,于是立即重传,不用等超时。

为什么不是收到一个重复 ACK 就重传?
因为网络环境有不确定性,报文可能只是延迟(还在路上),并非丢失。如果收到一个就重传,会造成资源浪费和重复数据。三个重复 ACK 是实践检验的阈值,平衡了快速恢复和避免不必要重传。

特殊情况:如果重传前原始报文刚好到达,接收方会通过序列号去重,不影响业务。

在这里插入图片描述


7. 流量控制——水桶理论,别把接收方灌满 💧

前面不是说窗口越大效率越高嘛?那为什么不定一个超级大的窗口呢?

我打个比方:我有一个水桶,底下有个洞,每秒都在往外流水。最开始我用小碗往里倒水,因为一直有水在流出去,所以水桶不会满。后来我觉得小碗太慢了,换了个大铁盆往里倒,倒得确实快,但水桶很快就满了,接着水就溢出来,浪费了。

这个问题的本质是:接收方有一个数据缓存队列,一边在收数据,一边在处理数据。如果发送方传输太快,超过接收方的处理速度,缓存队列满了,多余的数据就只能丢掉。数据一丢包,发送方就要重传,重传又加重负担,陷入恶性循环,时间浪费了,资源也浪费了。

所以就有了流量控制:接收方在返回的报文里,把头部的一个字段——窗口大小,设置成当前缓存队列的剩余空间。发送方收到后就知道:“哦,你还能装这么多啊”,于是就把自己的发送窗口控制在对方能承受的范围内。这样一来,就不会出现水满溢出的情况了。

📷 图片预留位置:水桶比喻示意图


8. 拥塞控制——马路上车多了要限流 🚦

拥塞控制:恋爱故事 💑

拥塞控制的核心思想是:在“试探”和“退让”之间找到平衡。用恋爱来比喻,整个过程分为四个阶段:

第一阶段:慢启动 —— 热恋期 💞
刚在一起时,爱意指数级飙升。窗口从 1 开始,每收到一个 ACK,窗口就翻倍。这叫 慢启动,虽然叫“慢”,实际是指数增长。热恋中的人恨不得天天黏在一起,迅速升温。

第二阶段:拥塞避免 —— 稳定期 🌸
当爱意增长到某个阈值(慢启动阈值 ssthresh),再加速会让双方疲惫。于是进入 拥塞避免,每个来回(RTT)只增加 1 点爱意。线性增长,细水长流,感情平稳发展。

第三阶段:丢包 —— 吵架 😠

  • 快速重传(小摩擦):连续三个重复 ACK 触发。窗口减半,阈值设为当前窗口的一半,进入快速恢复。这就像情侣之间的小争执,冷静一下,各退一步,然后继续好好相处。
  • 超时丢包(严重吵架):如果对方长时间不回复,确实会触发超时,窗口会直接降到初始值(通常是 1),并重新慢启动。但这种“严重吵架”在现代网络中已经很少见,因为快速重传和 SACK 能提前修复大部分丢包。只有在网络真的断了很久,或者设备宕机时,才会走到这一步。

第四阶段:快速恢复 —— 冷静期 🤝
窗口减半后,不重新慢启动,而是直接进入拥塞避免。这样既避免了过度退让,又给感情留了缓冲空间。就像吵架后,两人决定继续一起往前走,而不是回到热恋期重新来过。

在这里插入图片描述


9. 延时应答 & 捎带应答——让 ACK 搭上顺风车 🚌

9.1 延时应答:等一小会儿,让窗口更大 ⏳

延时应答的核心就是:收到数据后,我不立刻返回 ACK,而是等一小会儿。

为什么等?

  • 让窗口变大:我的缓存队列可能剩余空间不大,比如只有 10 个字节。但我稍微等一下,应用程序就能多处理一些数据,把空间腾出来。等我发 ACK 时,通告的窗口可能就变成 30 个字节了。发送方收到更大的窗口,就能扩大自己的发送窗口,提高吞吐量。
  • 节省网络资源:如果不延时应答,收到一个包就回一个 ACK,一问一答,包的数量翻倍。多个数据包可以共用一个 ACK,减少网络里的包数。

妈妈叮嘱比喻 👩:
妈妈说“天冷多穿衣服,饿了记得吃饭,路上注意安全”,我不需要每句都回“知道了”,等妈妈说完最后一句,回一句“知道了”就够了。延时应答就是让多个数据包共用一次 ACK。

水桶比喻 💧:
延时应答就像在桶边等了一会儿,让水多流出去一些,再告诉倒水的人“我还能装更多”。这样倒水的人就可以一次多倒点,效率更高。

时间阈值 ⏱️:
延时应答的等待时间是有上限的,一般 40~200 毫秒。如果在这个阈值内没有要回传的数据,超时后就会单独发 ACK。这样既保证了不会无限等待,又给了捎带应答足够的机会。


9.2 捎带应答:让 ACK 搭上数据的顺风车 🚗

捎带应答是延时应答的“好搭档”。在延时应答的等待时间内,如果应用层正好处理完数据,需要给发送方返回一些数据,那就可以把这些数据和原本要发的 ACK 合并成一个包发回去——两个包裹一次发送,省了一次网络交互。

关键点:如果触发了捎带应答,就不需要等待时间阈值了。因为已经有数据要发,ACK 可以立即跟着数据一起走,不用再等那个“最后期限”。这就像等公交的时候,本来要等 5 分钟,结果 1 分钟就来了辆顺路车,那就直接上车走人,不用再傻等 5 分钟了。

例子
四次挥手在某些情况下可以变成三次。如果被动关闭方在延时应答的等待窗口内正好调用了 close(),它就可以把 ACK 和 FIN 合并成一个包发出去,少一次挥手。


9.3 小结

延时应答不是单纯地“拖时间”,而是在收益大于成本的前提下,用几十毫秒的延迟换取更大的窗口和更少的网络包数。捎带应答则进一步把 ACK 和数据“拼车”发送,让网络利用率更高。两者配合的逻辑是:有数据就立即捎带,没数据就等到阈值再发。这样既保证了效率,又避免了无限等待。

在这里插入图片描述


10. 面向字节流——粘包问题的根源 🌊

TCP 是面向字节流的协议,它将数据视为连续的字节流,没有消息边界。这就带来了粘包问题:字节和字节之间边界模糊,接收方不知道一条数据的结束点在哪里。

解决方案:应用层需要自己设计消息边界,如固定长度、特殊分隔符、长度字段等。


11. 通信异常处理——当连接突然断掉 💥

11.1 进程崩溃

崩溃的一方会通过操作系统内核发送 FIN,正常进行四次挥手。因为 TCP 独立于进程,进程崩溃不会阻止内核发送 FIN。

11.2 主机关机(手动)

系统会逐个关闭进程,每个进程关闭时都会触发四次挥手。如果关机太快,四次挥手没完成,接收方收不到 ACK 就会超时重传 FIN。重传几次后无回应,就认为对方已断开,自己也会关闭连接,并发送一个 RST 包通知对方(即使对方收不到也无所谓)。

11.3 主机关机(断电)⚡

  • 发送方断开:接收方长期收不到数据,就会尝试发送心跳包。如果多次心跳无回应,接收方主动断开。
  • 接收方断开:发送方发数据后收不到 ACK,会超时重传。重传多次无果,主动断开。

11.4 网线断开 🔌

TCP 本身无法立即感知,需要依赖:

  • 应用程序主动检测(如发送数据失败)
  • 心跳包机制
  • TCP 的保活机制(默认几小时才触发)

小结

四次挥手的主要作用,是让双方都知道对方已经把连接释放了,彼此确认,干净收尾。但在异常情况下(断电、网线断开、主机突然崩溃),对方可能永远收不到最后的 ACK 或 FIN,这时候就没法“互相通知”了。此时 TCP 的策略是:能挥就挥,不能挥就自己先断开,不再死等对方。毕竟,当一方已经彻底失联,再等下去只会浪费资源。

在这里插入图片描述


12. 心跳包——让空闲连接“活”起来 💓

心跳包:连接建立后,如果长时间没有数据传输,想要维持连接的一方会定期发送一个小包,确认对方是否还在。

  • 周期性:针对的是空闲状态,双方都有数据传时不需要心跳。
  • 丢包处理:单次丢包不致命,连续多次失败才判断对方已死。就像朋友发“在吗”你没及时回,他不会立刻拉黑你,但连续三次不回,他才觉得你可能真不在了。
  • 方向:通常客户端发,服务器回;如果双方都需要感知,也可以互发。

在这里插入图片描述


13. 总结与展望 🌟

TCP 的每一个设计都在回答三个问题:遇到了什么问题?为什么重要?怎么解决? 从三次握手到四次挥手,从滑动窗口到流量控制,从快速重传到延时应答,背后都是对效率、可靠性和资源消耗的精妙权衡。

通过生活中的类比(电话、水桶、快递、妈妈叮嘱、接亲车队、银行扣款……),我们可以更轻松地理解这些机制。希望这篇博客能帮你建立起 TCP 的知识体系。如果你觉得某个部分还不够清楚,欢迎留言讨论!

在这里插入图片描述


参考资料

  • RFC 793 – Transmission Control Protocol
  • 《TCP/IP 详解 卷1》
  • 小林 coding – TCP 图解系列
  • 自己踩过的坑和总结的笔记
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐