1. TCP协议基础
大家好,我是你们的老朋友。今天咱们开始聊TCP,这个网络编程里绕不开的坎儿。
说实话,我刚开始做网络编程那会儿,对TCP也是一知半解。直到有一次线上服务莫名其妙丢数据,排查了整整两天,最后发现是对TCP的可靠性机制理解不到位。从那以后,我养成了一个习惯:写网络代码之前,先把TCP协议栈模型在脑子里过一遍。
好,咱们不废话,直接开干。
1.1 TCP/IP协议栈模型
你想想看,数据从你的应用程序发出去,到对方收到,中间经历了什么?
这就是协议栈干的事。我习惯把它理解成四层快递系统:
| 层级 | 名称 | 我眼中的角色 |
|---|---|---|
| 第4层 | 应用层 | 你写的代码,比如HTTP、FTP |
| 第3层 | 传输层 | TCP/UDP,负责端到端通信 |
| 第2层 | 网络层 | IP协议,负责寻址和路由 |
| 第1层 | 网络接口层 | 网卡、网线,物理传输 |
每一层只关心自己的事。应用层不管数据怎么打包,传输层不管数据走哪条路。这就是分层解耦的思想。
核心要点:TCP工作在传输层,它给应用层提供的是可靠的、面向连接的、基于字节流的服务。
嗯,这里要注意:TCP是面向字节流的。这句话特别重要,因为它直接导致了我们后面要讲的粘包问题。你发的是字节流,不是消息流——TCP不关心你的消息边界。
1.2 TCP报文格式:解剖麻雀
咱们来看看TCP报文长什么样。说白了,就是数据在传输时的包装盒。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口号 | 目的端口号 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 序列号(32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 确认号(32位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移 | 保留 | 标志位 | 窗口大小 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 校验和 | 紧急指针 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 选项(可选) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
这里我挑几个关键字段说说:
- 源端口/目的端口:哪个应用发的,哪个应用收的。端口号范围0-65535,0-1023是知名端口。
- 序列号:每个字节都有一个编号。我第一次看这个设计时觉得太奢侈了,后来才明白——没有序列号,TCP就没法保证有序传输。
- 确认号:告诉对方「我收到哪了」。这是TCP可靠性的基石。
- 标志位:SYN、ACK、FIN、RST、PSH、URG。咱们后面三次握手四次挥手全靠它们。
- 窗口大小:流量控制用的。告诉对方「你慢点发,我快撑不住了」。
个人经验:我在做高性能服务器时,经常要抓包分析TCP报文。Wireshark是个好工具,建议你们也学会用。有一次我发现客户端一直发RST包,排查下来是对端端口没开——这种问题看代码很难发现,抓包一眼就明白了。
1.3 三次握手与四次挥手
这是面试必考题,但很多人只背了流程,没理解为什么。
三次握手:建立连接
说白了就是双方确认彼此都在线,并且都愿意通信。
客户端 服务器
| |
|---- SYN (seq=x) ------------>| 第一次:客户端说「我要连你」
| |
|<--- SYN+ACK (seq=y, ack=x+1)-| 第二次:服务器说「收到,我也准备好了」
| |
|---- ACK (seq=x+1, ack=y+1)->| 第三次:客户端说「好,开始吧」
| |
为什么是三次,不是两次?
我举个例子你就明白了。假设只有两次握手:客户端发了SYN,服务器回了ACK,连接就建立了。但如果这个SYN在网络中延迟了,客户端等不及重发了一个,服务器收到后建立了连接。等数据传输完关闭后,那个延迟的SYN才到达服务器——服务器以为又要建立新连接,白白浪费资源。
三次握手就是为了防止「已失效的连接请求」到达服务器。第三次握手时,客户端会带上一个确认,服务器收到后才真正建立连接。如果那个延迟的SYN到达,服务器回SYN+ACK,但客户端不会理它——因为客户端没发过这个SYN。
避坑指南:我曾经遇到过一个线上问题,客户端频繁建立短连接,导致服务器TIME_WAIT状态堆积。最后把端口号都用完了,新连接建立失败。解决方案是复用连接或调整TIME_WAIT超时时间。这个后面讲粘包时还会提到。
四次挥手:断开连接
断开比建立复杂,因为双方都要说再见。
客户端 服务器
| |
|---- FIN (seq=u) ----------->| 第一次:客户端说「我不发了」
| |
|<--- ACK (ack=u+1) ----------| 第二次:服务器说「知道了」
| |
|<--- FIN (seq=v) ------------| 第三次:服务器说「我也不发了」
| |
|---- ACK (ack=v+1) --------->| 第四次:客户端说「好的,再见」
| |
注意,第二次和第三次之间可能有时间间隔。因为服务器可能还有数据没发完,它得先把数据发完,再发FIN。
这里有个关键状态:TIME_WAIT。主动关闭的一方(通常是客户端)在发送最后一个ACK后,会进入TIME_WAIT状态,持续2MSL(最大报文生存时间)。
为什么要等2MSL?
- 万一最后一个ACK丢了,服务器会重发FIN,客户端得能重新发ACK
- 确保所有旧报文在网络中消失,避免影响新连接
1.4 TCP的可靠性机制
TCP号称可靠传输,它到底是怎么做到的?我总结为四大法宝:
1. 确认与重传
发送方发了数据,必须收到接收方的ACK才算完。如果超时没收到,就重传。
这里有个细节:超时时间怎么算?TCP会动态计算RTT(往返时间),然后根据RTT设置超时时间。太短了容易误判,太长了影响效率。
我踩过的坑:有一次我写了一个自定义协议,在应用层也做了ACK确认。结果TCP层也在做同样的事,双重确认导致性能下降30%。后来我意识到:TCP已经提供了可靠传输,应用层不需要再重复造轮子。除非你需要更细粒度的控制。
2. 流量控制
接收方通过窗口大小字段告诉发送方:「我还能收多少数据」。发送方不能超过这个窗口大小。
这就像两个人倒水:你倒得太快,对方杯子满了就溢出了。TCP的流量控制就是让对方告诉你「我的杯子多大,你慢慢倒」。
3. 拥塞控制
流量控制是端到端的,拥塞控制是网络层面的。TCP通过慢启动、拥塞避免、快重传、快恢复四个算法来应对网络拥塞。
简单说:
- 慢启动:刚开始发得慢,慢慢加速
- 拥塞避免:达到阈值后,线性增长
- 快重传:收到三个重复ACK,立即重传,不等超时
- 快恢复:重传后,降低发送速度,但不回到慢启动
4. 有序传输
TCP给每个字节编号(序列号),接收方按序列号重组数据。即使数据乱序到达,也能恢复成正确的顺序。
你想想看,如果没有序列号,数据包走不同的路径到达,顺序全乱了,那还怎么用?
本章小结
好了,这一章的内容就这些。咱们把TCP的底子打好了,后面讲粘包拆包时你才能理解——为什么TCP是流式协议?为什么它不保留消息边界?
这些问题的答案,都在这一章里。
给新手的建议:别急着写代码,先把TCP协议栈模型和报文格式搞明白。我见过太多人一上来就写socket,结果遇到问题一脸懵。基础不牢,地动山摇。