你在浏览器敲了一个回车,数据是怎么跑到千里之外的服务器的?中间经历了什么?为什么有的快有的慢?
今天用寄快递和打电话的故事,聊聊 TCP/IP 协议栈。
原文地址
没有协议栈,网络请求会怎样?
你寄一个快递,没有快递公司行不行?
你:我要寄苹果给上海的朋友
快递员:苹果是什么?寄去哪?用什么车?
你:...我不知道怎么回答
网络请求也一样——没有协议栈,计算机之间根本"听不懂"对方说的话。
协议栈就是一套"快递公司规则":规定怎么打包、怎么运输、怎么签收。
TCP/IP四层模型
TCP/IP协议栈分为四层,从上到下:
TCP/IP 四层模型
┌───────────────────┐
│ 应用层 │ HTTP、FTP、SMTP、DNS
│ 传输层 │ TCP(可靠)、UDP(快速)
│ 网络层 │ IP 地址路由
│ 网络接口层 │ MAC 地址、网卡驱动
└───────────────────┘
比喻:
应用层 → 快递单上的寄件人/收件人
传输层 → 快递公司承诺"保证送达"
网络层 → 快递在机场按航班发货
网络接口层 → 快递小哥装车送到机场
数据封装过程
数据从上层到下层,每层都会"打包":
数据封装过程:
应用层:你好,我要访问 example.com
↓ [加上TCP头]
传输层:+ SYN标志 + 序列号
↓ [加上IP头]
网络层:+ 源IP + 目标IP
↓ [加上帧头帧尾]
网络接口层:二进制流 10101010...
如同快递打包:
你买的商品 → 商家加包装盒 → 快递公司加外包装 → 装车出发
TCP三次握手
为什么需要"握手"?
你打电话给朋友:
你:喂?(试探对方在不在)
对方:在的!(回应)
你:我要说事情了!(确认连接)
TCP三次握手就是这个道理——建立可靠连接需要双方确认对方"能收能发"。
三次握手过程
三次握手过程:
Client ──── SYN ────→ Server
ISN=100
Client ←── SYN+ACK ── Server
ISN=300, ACK=101
Client ──── ACK ────→ Server
连接建立完成
| 握手 | 方向 | 标志 | 作用 |
|---|---|---|---|
| 第一次 | Client → Server | SYN | 发起连接请求 |
| 第二次 | Server → Client | SYN + ACK | 同意连接,告知自己的序列号 |
| 第三次 | Client → Server | ACK | 确认收到,连接建立 |
序列号的作用
每个数据包都有一个序列号(Sequence Number),用于:
- 保证有序:接收方按序号重组数据
- 避免重复:如果数据包被重复发送,接收方能识别并丢弃
- 可靠性:丢包时能定位丢了哪个包
ISN(初始序列号)
ISN 不是固定的 0 或 1,而是动态生成的随机数,目的是防止历史数据包残留干扰新连接。
如果 ISN 是固定的,攻击者可能重放旧的数据包来伪造连接。动态 ISN 让攻击者难以预测每次连接的起始序列号。
UDP:更快但不保证
UDP和TCP的区别
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(丢包重传) | 不可靠(不管丢不丢) |
| 有序性 | 保证有序 | 不保证有序 |
| 传输速度 | 较慢 | 快 |
| 数据包大小 | 无限制(流式) | 有上限(一般64KB) |
| 使用场景 | 网页、邮件、文件传输 | 视频通话、直播、DNS查询 |
UDP工作方式
你(北京)→ 发送UDP数据包 → 上海服务器
│
▼
发了就不管了
丢没丢不知道
到了没有不知道
如同发短信——发出去就完事了,对方收没收不知道。
UDP适用场景
- 视频通话:偶尔卡顿比延迟好忍受
- 直播:丢几帧画面无伤大雅
- DNS查询:数据包小,丢了大不了重试一次
- 在线游戏:实时性 > 可靠性
TCP四次挥手
为什么是四次?
打电话结束:
你:我说完了(我要挂电话了)
对方:好的,等我一下...(等对方处理完)
对方:我这边也好了(对方也准备挂)
你:好的,再见(确认关闭)
TCP四次挥手就是这个道理——双方都要确认数据发完了才能关闭。
四次挥手过程
四次挥手过程:
Client ──── FIN ────→ Server
我发完了
Client ←── ACK ──── Server
收到,但等我发完
Client ←── FIN ──── Server
我也发完了
Client ──── ACK ────→ Server
再见
| 挥手 | 方向 | 标志 | 作用 |
|---|---|---|---|
| 第一次 | Client → Server | FIN | 发起关闭请求 |
| 第二次 | Server → Client | ACK | 确认收到,但可能还有数据要发 |
| 第三次 | Server → Client | FIN | Server也发完了,请求关闭 |
| 第四次 | Client → Server | ACK | 确认关闭,连接正式断开 |
为什么第二次和第三次不合并?
TCP半关闭机制:
Client:我不发数据了,但还能收
Server:好的,你先别收,等我发完
(Server发完剩余数据)
Server:我也发完了
Client:好的
第二次只回复 ACK 表示"收到了,但我还没准备好",等数据发完才发 FIN。
为什么需要TIME_WAIT?
四次挥手完成后,客户端并不会立即关闭,而是进入TIME_WAIT状态,等待2MSL(Maximum Segment Lifetime,通常为60秒)才真正关闭。
为什么要等?
- 确保对方收到最后的ACK:如果第四次挥手的 ACK 丢失,对方会重发 FIN,客户端需要处于
TIME_WAIT才能重新发送 ACK - 让旧数据包在网络中消散:确保本次连接的所有数据包都在网络中消失,避免影响新连接
TIME_WAIT 流程:
Client ──── FIN ────→ Server
Client ←── ACK ──── Server
Client ←── FIN ──── Server
Client ──── ACK ────→ Server
↓
TIME_WAIT (等待2MSL)
↓
关闭
滑动窗口:提高传输效率
原始的停等协议
最早的TCP,每发一个包都要等对方确认:
发包1 → 等待ACK → 发包2 → 等待ACK → 发包3 → ...
问题:大部分时间都在等ACK,效率很低。
滑动窗口原理
滑动窗口允许一次发送多个包,不需要等每个都确认:
滑动窗口(窗口大小=3):
发送:[包1][包2][包3] → 等待确认
收到ACK1后滑动:
发送:[包2][包3][包4] → 继续发送
收到ACK2后滑动:
发送:[包3][包4][包5] → ...
窗口大小动态调整
TCP会根据网络状况动态调整窗口大小:
| 网络状况 | 窗口大小 | 说明 |
|---|---|---|
| 网络通畅 | 大窗口 | 一次多发,提高吞吐量 |
| 网络拥塞 | 小窗口 | 少发点,别堵住 |
| 丢包严重 | 最小窗口 | 逐个确认,确保可靠 |
这就是拥塞控制的核心思想。
TCP重传机制
超时重传
发送数据包后启动计时器,超过时间没收到ACK就重发:
超时重传:发包1 → 计时器 → 超时 → 重发包1 → 收到ACK
问题是:超时时间设太长会等很久,设太短会频繁重发。
快速重传
当收到3个重复的ACK时,说明后面的包丢了,不等超时直接重传:
发送:[包1][包2][包3][包4][包5]
接收:包1收到,包2收到,包3丢了
收到包4/5/6 → 都发ACK2(还是要包2)
发送方:收到3个ACK2 → 快速重传包2
SACK(Selective Acknowledgment)
传统ACK只能告诉你"我收到了包1,包2丢了"。
SACK可以告诉你"包1收到了,包2丢了,但包3、4、5也收到了":
收到SACK信息后,发送方只需要重传丢失的包2,无需重传包3、4、5,大幅减少不必要的重传。
TCP拥塞控制
慢启动
连接刚建立时,窗口从小开始指数增长:
窗口:1 → 2 → 4 → 8 → 16 → ...
如同新手开车:先慢慢开,熟悉路况后再加速。
拥塞避免
窗口达到阈值后,改为线性增长:
窗口:16 → 17 → 18 → 19 → ...
拥塞发生
| 事件 | 处理方式 |
|---|---|
| 超时丢包 | 窗口降回1,重新慢启动 |
| 快速重传 | 窗口减半,然后线性增长 |
如同遇到堵车或事故:退回去重新规划路线。
总结:TCP核心知识点
| 概念 | 说明 |
|---|---|
| TCP/IP四层 | 应用层→传输层→网络层→网络接口层 |
| 三次握手 | SYN→SYN+ACK→ACK,建立可靠连接 |
| 四次挥手 | FIN→ACK→FIN→ACK,优雅关闭连接 |
| 序列号 | 保证有序、避免重复、定位丢包 |
| ISN | 动态生成的初始序列号,防止历史数据包干扰 |
| 滑动窗口 | 一次发送多个包,提高效率 |
| 重传机制 | 超时重传、快速重传、SACK |
| 拥塞控制 | 慢启动→拥塞避免→丢包后调整 |
| TIME_WAIT | 等待2MSL,确保对方收到最后的ACK,旧数据包消散 |
写在最后
现在应该明白了:
- TCP/IP四层 = 快递公司的组织架构
- 三次握手 = 打电话确认双方都能通话
- 四次挥手 = 双方都说完了才能挂电话
- 滑动窗口 = 同时发多个快递,不用等一个到了再发下一个
- UDP = 发短信,发出去就不管了
- ISN = 随机生成的"门牌号",防止被人伪造
- TIME_WAIT = 挂电话后等一会儿,确认对方真的听完了
下次访问网页加载变慢时,想想背后可能是 TCP 在"打电话确认"。
技术不复杂,但让"地球另一边的服务器"能和你流畅通信。