引言
HTTP、MySQL、Redis 集群间通信、RPC 框架,底层大量场景都建立在 TCP 之上。学网络如果跳过 TCP,后面的很多问题都只能停留在表面。
这一篇重点解决四类高频问题:
- 三次握手为什么不能变成两次
- 四次挥手到底在关闭什么
- 粘包和拆包是怎么来的
- TCP 和 UDP 到底该怎么区分
TCP 三次握手
TCP 是面向连接的协议,正式传输数据前,需要先建立连接。
第一次握手:SYN
客户端向服务端发送 SYN 报文,表示要建立连接,并带上自己的初始序列号。
第二次握手:SYN + ACK
服务端收到后,返回 SYN + ACK,表示:
- 你的请求我收到了
- 我也愿意建立连接
第三次握手:ACK
客户端再回一个 ACK,表示自己收到了服务端的响应。
到这里,连接建立成功。
为什么不是两次握手
这个问题的关键不是"能不能少一次",而是"能不能确认双方都具备正常收发能力"。
如果只有两次握手,会出现一个问题:
- 客户端发出 SYN
- 服务端回应 SYN + ACK
- 但客户端是否收到了这个响应,服务端并不知道
也就是说,两次握手只能保证:
- 客户端能发
- 服务端能收、能发
但不能保证客户端能收,也不能保证双方对连接状态达成一致。
三次握手多出来的最后一次 ACK,本质上就是客户端告诉服务端:
"你的回复我收到了,连接状态可以正式成立。"
此外,它还能帮助避免历史失效连接请求引发的误连接问题。
客户端网络抖动,多发了几次 SYN,为什么不会建立多个连接
这类问题通常由 TCP 的状态机和重传机制保证。
可以从几个角度理解:
超时重传不会凭空创建新连接
如果服务端发出 SYN + ACK 后没有收到客户端的 ACK,它可能会重传 SYN + ACK,但这仍然属于同一次建连尝试。
序列号帮助识别连接尝试
每次新的连接尝试会带上新的初始序列号。服务端会结合四元组和当前状态识别这是不是同一个连接过程。
最终要以 ACK 完成建连
没有最后那一步确认,服务端不会把连接视为真正建立完成。
所以重复 SYN 不等于重复建立连接。
TCP 四次挥手
TCP 是全双工协议,两个方向都可以独立发送数据。因此关闭连接时,通常需要分两边分别关闭。
第一次挥手:FIN
客户端发送 FIN,表示"我这边没有数据要发了"。
第二次挥手:ACK
服务端回复 ACK,表示"我知道你这边发完了"。
第三次挥手:FIN
服务端等自己也发送完剩余数据后,再发 FIN,表示"我这边也结束了"。
第四次挥手:ACK
客户端回复 ACK,连接关闭流程完成。
所以四次挥手不是为了复杂,而是因为:
"一端不再发送数据"和"另一端也不再发送数据"通常不是同一时刻发生的。
TCP 粘包和拆包的原因
很多人第一次接触 TCP 编程时,都会遇到"明明发了三条消息,为什么对面一次收到了两条半"这类问题。
原因在于:
TCP 是面向字节流的协议,它只保证字节流有序、可靠到达,但不保证应用层消息边界。
于是就会出现两种现象。
粘包
发送方连续发送多个小包,接收方可能一次性读到了合并后的数据。
拆包
发送方发出一个大包,接收方可能分多次才读完整。
所以粘包和拆包本质上都不是 TCP 出错,而是应用层自己必须定义消息边界。
粘包和拆包的解决办法
固定长度消息
每条消息长度固定,不够的部分补齐。
优点是实现简单,缺点是浪费空间,也不够灵活。
使用分隔符
例如每条消息末尾加特定分隔符,接收方读到分隔符就认为一条消息结束。
这种方式实现成本低,但要注意分隔符不能和消息内容冲突。
自定义协议头
最常见的做法是消息由"消息头 + 消息体"组成,消息头里写明消息体长度。接收方先读长度,再按长度读取完整消息。
这是工程上最稳妥、最常见的做法。
TCP 和 UDP 的区别
TCP
TCP 的特点:
- 面向连接
- 可靠传输
- 有序到达
- 有重传、流量控制、拥塞控制
适合对可靠性要求高的场景,比如:
- Web 请求
- 数据库连接
- 文件传输
UDP
UDP 的特点:
- 无连接
- 开销小
- 传输速度快
- 不保证可靠到达和顺序
适合对实时性要求高、可容忍少量丢包的场景,比如:
- 语音通话
- 视频直播
- 实时游戏
如何回答面试里的 TCP 和 UDP 区别
不要只背"可靠"和"不可靠",建议从下面四个维度回答:
- 是否需要建立连接
- 是否保证可靠和有序
- 协议开销大小
- 适合的业务场景
这样回答会更完整。
总结
TCP 最核心的几件事可以总结为:
- 通过三次握手建立可靠连接
- 通过序列号和确认机制保证可靠传输
- 通过四次挥手关闭双向连接
- 只提供字节流,不负责应用层消息边界
如果你能把这些点讲清楚,TCP 这部分基础就已经比较扎实了。
如果这篇文章对你有帮助,欢迎继续阅读本系列后续内容。若文中有不准确或需要补充的地方,也欢迎指出。