【阿咘的网络编程笔记】补充TCP协议~
哈喽大家好!我是阿咘~👋
在上一篇开头里,我和大家聊到了自己在实际开发中因为 TCP 知识不扎实遇到的窘境,也立下了系统补充这块知识的 flag。今天,咱们就正式开启 TCP 知识梳理之旅,从基础概念到核心机制,再到实战排查技巧,一步步把 TCP 的 “来龙去脉” 讲清楚。
一、先搞懂:TCP 到底是什么?
在聊复杂机制前,我们得先明确 TCP 的 “身份”。TCP 全称传输控制协议(Transmission Control Protocol),是 OSI 七层模型中 “传输层” 的核心协议之一,另一个常见的是 UDP(用户数据报协议)。
TCP 最大的特点是面向连接、可靠传输—— 这和 UDP 的 “无连接、不可靠” 形成了鲜明对比。什么是 “可靠传输”?简单说,就是 TCP 能保证数据从发送端到接收端 “不丢包、不重复、按顺序到达”,这也是它被广泛用于 HTTP、FTP、SSH 等需要稳定数据传输场景的关键原因。
但可靠传输不是 “凭空实现” 的,背后依赖于 TCP 的一系列核心机制:三次握手建立连接、四次挥手断开连接、滑动窗口、流量控制、拥塞控制、确认重传等 —— 这些也是咱们今天要重点梳理的内容。
二、TCP 连接的 “开始与结束”:三次握手与四次挥手
TCP 是 “面向连接” 的协议,所以数据传输前必须先建立连接,传输完成后要断开连接,这两个过程分别对应 “三次握手” 和 “四次挥手”。
1. 三次握手:如何建立可靠连接?
“握手” 的本质是通信双方通过交换报文,确认彼此的 “发送能力” 和 “接收能力” 都正常。我们用客户端(Client)和服务器(Server)的交互来拆解这个过程:
- 第一次握手(Client → Server):客户端向服务器发送一个SYN 报文(同步报文段),报文里会包含一个初始序列号(比如 seq=100),同时把 SYN 标志位设为 1,意思是 “我想和你建立连接,我的初始序号是 100”。发送后,客户端进入SYN_SENT状态,等待服务器回复。
- 第二次握手(Server → Client):服务器收到 SYN 报文后,先确认 “自己能接收、也能发送”—— 它会回复一个SYN+ACK 报文:把 SYN 标志位设为 1(表示同意建立连接),ACK 标志位设为 1(表示确认收到客户端的报文),确认号为客户端的序列号 + 1(ack=101,意思是 “我收到了 100 及之前的所有数据,下次请发 101 开始的”),同时也带上自己的初始序列号(比如 seq=200)。发送后,服务器进入SYN_RCVD状态。
- 第三次握手(Client → Server):客户端收到 SYN+ACK 报文后,确认 “服务器的发送和接收能力都正常”,于是回复一个ACK 报文:ACK 标志位设为 1,确认号为服务器的序列号 + 1(ack=201),同时可以带上自己要发送的第一个数据(如果有的话)。发送后,客户端进入ESTABLISHED(已连接)状态;服务器收到 ACK 后,也进入 ESTABLISHED 状态 —— 至此,TCP 连接正式建立,双方可以开始传输数据了。
这里有个常见问题:为什么是 “三次” 而不是两次?因为两次握手无法确认客户端的 “接收能力”—— 比如服务器发送的 SYN+ACK 报文丢失了,客户端没收到,就不会回复;但服务器以为连接已建立,会一直等待客户端发送数据,浪费资源。三次握手通过客户端的最后一次 ACK,让双方都能确认 “彼此收发正常”,避免了这种问题。
2. 四次挥手:如何优雅断开连接?
数据传输完成后,连接需要 “优雅断开”,这就是四次挥手的作用。需要注意的是:TCP 连接是 “全双工”(双方可以同时收发数据),所以断开时要分别关闭 “发送通道” 和 “接收通道”。
假设客户端先发起断开请求,过程如下:
- 第一次挥手(Client → Server):客户端发送一个FIN 报文(结束报文段),FIN 标志位设为 1,序列号为自己最后一次发送数据的序号 + 1(比如 seq=300),意思是 “我这边没有数据要发了,准备关闭发送通道”。发送后,客户端进入FIN_WAIT_1状态。
- 第二次挥手(Server → Client):服务器收到 FIN 报文后,先回复一个ACK 报文,确认号为客户端的序列号 + 1(ack=301),表示 “我收到你要断开的请求了”。此时服务器的 “接收通道” 还没关闭 —— 它可能还有未发送完的数据,所以会继续发送。服务器进入CLOSE_WAIT状态,客户端收到 ACK 后进入FIN_WAIT_2状态,等待服务器发送完数据。
- 第三次挥手(Server → Client):当服务器把所有数据都发送完毕后,也发送一个FIN 报文,FIN 标志位设为 1,序列号为自己最后一次发送数据的序号 + 1(比如 seq=400),意思是 “我这边也没有数据要发了,准备关闭我的发送通道”。发送后,服务器进入LAST_ACK状态,等待客户端的确认。
- 第四次挥手(Client → Server):客户端收到服务器的 FIN 报文后,回复一个ACK 报文,确认号为服务器的序列号 + 1(ack=401),表示 “我收到你要断开的请求了”。发送后,客户端不会立即关闭,而是进入TIME_WAIT状态(通常等待 2 个最大报文段生存时间 MSL,约 1-4 分钟)—— 这是为了防止服务器没收到 ACK 而重发 FIN 报文,确保服务器能正常关闭。服务器收到 ACK 后,立即进入CLOSED状态;客户端等待 TIME_WAIT 结束后,也进入 CLOSED 状态 —— 至此,连接完全断开。
同样有个常见问题:为什么是 “四次” 而不是三次?因为第二次挥手时,服务器只能确认 “收到断开请求”,但可能还有数据没发完,无法同时发送 FIN;只有等数据发完后,才能发起第三次挥手,所以需要四次交互来分别关闭双方的发送通道。
三、TCP 的 “可靠传输” 秘诀:四大核心机制
三次握手和四次挥手解决了 “连接建立与断开” 的问题,但数据传输过程中的 “可靠”,还要靠以下四大机制来保障。
1. 确认重传机制:丢包了怎么办?
TCP 通过 “确认应答(ACK)” 来确认数据是否到达:发送端每发送一段数据,就会等待接收端的 ACK;如果在规定时间内(重传超时时间 RTO)没收到 ACK,就认为数据丢失了,会重新发送。
这里有两个关键细节:
- 累计确认:接收端不会对每一个字节都回复 ACK,而是会回复 “已收到的最大连续序号 + 1”—— 比如接收端收到了 1-100、101-200 的数据包,会回复 ack=201,意思是 “200 及之前的都收到了,下次发 201 开始的”。这样能减少 ACK 的数量,提高效率。
- 快速重传:如果发送端连续收到 3 个相同的 ACK(比如连续收到 3 次 ack=101),就会意识到 “101 开始的数据丢了”,不等 RTO 超时就直接重传 —— 这比等待超时更高效,能减少重传延迟。
2. 滑动窗口机制:如何提高传输效率?
如果每次只发送一个数据包、等待 ACK 后再发下一个,效率会非常低 —— 这就是 “停止 - 等待” 协议的问题。TCP 的 “滑动窗口” 机制解决了这个问题,它允许发送端 “一次发送多个数据包”,不用等待每个包的 ACK。
我们可以把窗口理解为 “允许发送但未确认的数据包范围”:
- 发送端有一个 “发送窗口”,窗口内的数据包可以连续发送;
- 接收端有一个 “接收窗口”(大小由接收端的缓存决定),它会通过 ACK 报文把接收窗口的大小告诉发送端 —— 这就引出了 “流量控制”。
3. 流量控制:避免接收端 “撑爆”
流量控制的目的是 “让发送端的发送速度匹配接收端的接收速度”,防止接收端因为缓存满了而丢弃数据。
核心逻辑是:接收端通过 ACK 报文中的 “窗口大小” 字段,告诉发送端 “我还能接收多少数据”。比如接收端缓存快满了,就把窗口大小设为 0,发送端收到后就停止发送;等接收端处理完一部分数据,缓存有空间了,再把窗口大小设为非 0,发送端继续发送。
这里要注意 “糊涂窗口综合征”—— 如果接收端每次只释放很小的缓存,就会导致发送端每次只发很小的数据包,浪费带宽。解决办法是:接收端等到缓存有足够空间(比如达到 MSS 最大报文段大小)再更新窗口大小,或者发送端等到窗口大小足够大再发送。
4. 拥塞控制:避免网络 “堵车”
如果说流量控制是 “端到端” 的控制(发送端 vs 接收端),那拥塞控制就是 “全网级” 的控制 —— 它解决的是 “网络链路拥堵” 的问题。当网络中数据包太多时,路由器会丢弃数据包,导致丢包;TCP 的拥塞控制就是通过调整发送端的发送速度,避免网络 “堵车”。
TCP 的拥塞控制主要通过四个阶段实现,以最经典的 “Reno 算法” 为例:
- 慢启动:连接刚建立时,发送端的 “拥塞窗口”(cwnd)从 1 个 MSS 开始,每收到一个 ACK 就翻倍(1→2→4→8...),快速增加发送速度,直到拥塞窗口达到 “慢启动阈值”(ssthresh,初始通常设为较大值,比如 65535 字节)。
- 拥塞避免:当拥塞窗口超过慢启动阈值后,进入拥塞避免阶段 —— 每收到一轮 ACK,拥塞窗口只增加 1 个 MSS(而不是翻倍),缓慢增加发送速度,避免网络突然拥堵。
- 拥塞发生:如果出现丢包(超时或收到 3 个重复 ACK),就认为网络发生拥塞:
- 若因超时丢包:把慢启动阈值设为当前拥塞窗口的一半,拥塞窗口重置为 1,重新进入慢启动阶段。
- 若因收到 3 个重复 ACK(快速重传触发):把慢启动阈值设为当前拥塞窗口的一半,拥塞窗口设为慢启动阈值,直接进入拥塞避免阶段(这一步叫 “快速恢复”)。
- 快速恢复:在快速恢复阶段,每收到一个重复 ACK,拥塞窗口就增加 1 个 MSS;当收到一个新的 ACK(确认了重传的数据包),就把拥塞窗口设为慢启动阈值,回到拥塞避免阶段。
四、实战必备:TCP 常见问题及排查技巧
学完理论,更重要的是能解决实际问题。阿咘结合自己的踩坑经历,整理了几个常见的 TCP 问题及排查方法。
1. 问题 1:连接建立失败,提示 “Connection refused”
可能原因:
- 服务器未启动对应端口的服务;
- 服务器防火墙禁止了该端口的访问;
- 客户端连接的端口或 IP 错误。
排查步骤:
- 用 telnet 服务器IP 端口 或 nc -zv 服务器IP 端口 测试端口是否通;
- 检查服务器服务状态(如 systemctl status 服务名);
- 查看服务器防火墙规则(如 iptables -L 或 firewall-cmd --list-ports)。
2. 问题 2:数据传输过程中频繁超时、丢包
可能原因:
- 网络链路不稳定(比如公网环境下的延迟波动);
- TCP 拥塞控制参数不合理(比如慢启动阈值设置过小);
- 服务器或客户端的 TCP 连接数达到上限。
排查步骤:
- 用 ping 或 mtr 测试网络延迟和丢包率,确认是否是链路问题;
- 查看 TCP 拥塞控制算法(sysctl net.ipv4.tcp_congestion_control),可尝试切换为更适合当前场景的算法(如 BBR 算法,适合高带宽高延迟场景);
- 查看 TCP 连接数:netstat -an | grep :端口 | wc -l,检查是否达到系统限制(sysctl net.core.somaxconn 查看最大监听队列长度)。
3. 问题 3:服务器出现大量 TIME_WAIT 连接
TIME_WAIT 是四次挥手时客户端(或主动断开方)的正常状态,但如果大量 TIME_WAIT 连接堆积,会占用系统端口资源,导致新连接无法建立。
可能原因:短连接场景下(如 HTTP1.0 无 keep-alive),连接频繁建立和断开,TIME_WAIT 未及时释放。
解决办法:
- 调整 TIME_WAIT 超时时间(sysctl net.ipv4.tcp_fin_timeout,默认 60 秒,可适当缩短,但不建议小于 30 秒);
- 开启 TIME_WAIT 快速回收(sysctl -w net.ipv4.tcp_tw_recycle=1)和复用(sysctl -w net.ipv4.tcp_tw_reuse=1),允许 TIME_WAIT 连接被复用为新连接;
- 升级到 HTTP1.1 或 HTTP2,启用 keep-alive,减少短连接数量。
五、阿咘的总结:TCP 知识的 “重点回顾”
梳理完整个 TCP 知识体系,阿咘觉得有几个核心点需要大家重点记住:
- TCP 的核心特性是面向连接、可靠传输、全双工,依赖三次握手、四次挥手、确认重传、滑动窗口、流量控制、拥塞控制六大机制实现;
- 三次握手解决 “连接建立的可靠性”,四次挥手解决 “全双工连接的优雅断开”;
- 流量控制是 “端到端” 的速度匹配,拥塞控制是 “全网级” 的拥堵避免,二者共同保障数据传输的高效与稳定;
- 实际问题排查要结合 “网络链路测试 + 系统参数查看 + 服务状态检查”,从底层到应用层逐步定位问题。
今天的 TCP 知识梳理就到这里啦~ 阿咘尽可能把每个机制的 “原理” 和 “为什么这么设计” 讲清楚,而不是单纯罗列概念。如果大家对某个机制有更深入的疑问,或者有自己的排查经验,欢迎在评论区和我交流~
更多推荐
所有评论(0)