传输层:TCP 与 UDP 全家桶
网络层把包裹送到了正确的机器,但一台机器上跑着几十个程序——微信、浏览器、游戏,给谁?传输层补上最后一段寻址(端口),并按需提供两种服务:快而不稳的 UDP、稳而不快的 TCP。这是 408 和面试双重核心章。
一、端到端:进程到进程
- 链路层交付到"相邻节点",网络层交付到"主机",传输层交付到"进程"
- 端口号 0~65535 就是快递柜的格子号:
一台主机 = 一栋楼(IP 地址)
端口号 = 楼里的房间号
IP + 端口 = 套接字 Socket(全网唯一的投递地址)
| 端口段 | 说明 | 例子 |
|---|---|---|
| 0~1023 | 知名端口,系统保留 | 22 SSH / 53 DNS / 80 HTTP / 443 HTTPS |
| 1024~49151 | 注册端口 | 3306 MySQL / 6379 Redis |
| 49152~65535 | 动态/临时端口 | 客户端发起连接时随机抓一个 |
💡 你手机上每个联网 App 出门都带一个临时端口——同一台机器上几十个连接不串线,全靠端口区分。
二、UDP:快而不稳的明信片
UDP 报文头只有 8 字节:源端口、目的端口、长度、校验和。没了。
| 特点 | 说明 |
|---|---|
| 无连接 | 不打招呼直接发,省一次握手 |
| 不可靠 | 发出去就不管了,丢了不补 |
| 面向报文 | 应用给多大就发多大,不拆不并 |
| 支持广播/组播 | 一对多随便发 |
| 首部开销小 | 8B vs TCP 的 20B+ |
谁在用 UDP? DNS 查询(一问一答,握手太亏)、视频通话/直播(丢一帧无所谓,重传到的旧帧反而没用)、游戏、以及 QUIC——HTTP/3 跑在 UDP 之上自己补可靠,UDP 已经从"不可靠"洗白成"可定制的底座"。
三、TCP:稳而不快的挂号信
TCP 的四张招牌:面向连接、可靠传输、字节流、全双工。首部 20 字节起步,关键字段先认识一下(后面全靠它们演戏):
| 字段 | 干什么 |
|---|---|
| 序号 seq | 字节流里每个字节的编号——乱序到达也能拼回原样 |
| 确认号 ack | "你发到我这里第 X 字节之前我都收到了,期待第 X 个" |
| 标志位 | SYN(想建连接)/ ACK(确认)/ FIN(我发完了)/ RST(重置) |
| 窗口 | 接收方还剩多少缓冲——流量控制的依据 |
🤔 "字节流"意味着:你 write 两次 100B,对方可能一次收到 200B,也可能分三次收到——TCP 只保证字节顺序对,不保证"一条消息"的边界(粘包问题的根源,应用层自己定边界,比如 HTTP 用 Content-Length)。
四、三次握手:为什么不是两次
客户端和服务器要开始通话,先对表:
客户端 服务器
| —— SYN, seq=x ————————→ | ① 我想通话(你听得见我吗)
| ←— SYN+ACK, seq=y, ack=x+1| ② 听得见!你听得见我吗
| —— ACK, ack=y+1 ——————→ | ③ 听得见!开聊
为什么两次不够? 推演一下就明白:
假设只要两次握手:
客户端 —— 旧连接的 SYN(迷路的迟信)——→ 服务器
服务器收到就当"连接成立",开始分配缓冲、单方面等数据……
但客户端早放弃这次连接了 → 服务器傻等 → 资源白占
三次的本质是双方都确认了双向链路通:
| 已确认 | ①后 | ②后 | ③后 |
|---|---|---|---|
| 客户端→服务器 | ✅ | ✅ | ✅ |
| 服务器→客户端 | ❌ | ✅ | ✅ |
两次时"服务器→客户端"这条路从未被确认。此外第三次握手的 ACK 还能捎带数据,不让服务器干等。
⚠️ SYN 洪水攻击:攻击者狂发 SYN 不回 ACK,服务器半连接队列被占满。现代内核有 syncookies 等防御,知道这个名词即可。
五、四次挥手:分手为什么这么繁琐
数据说完了,两边都要表态"我发完了"——双向各关一次:
客户端 服务器
| —— FIN, seq=u ————————→ | ① 我的数据发完了
| ←— ACK, ack=u+1 ——————— | ② 知道了(但我可能还没发完)
| ←— FIN, seq=w ————————— | ③ 我也发完了
| —— ACK, ack=w+1 ——————— | ④ 知道了,再见
为什么挥手比握手多一次? 因为 TCP 全双工:收到对方 FIN 只代表"对方不发了",我可能还有数据没发完(第②③步之间服务器还能继续发)——所以 ACK 和 FIN 分开两步。握手时 SYN 和 ACK 能合并(服务器没什么要先说的),挥手时通常合并不了。
TIME_WAIT:谁在原地等 2MSL
第④步之后,主动关闭方不立刻关门,而是等 2MSL(两倍报文最大寿命)才真正关闭:
- 若最后的 ACK 丢了,服务器会重发 FIN——在等的人才能补 ACK
- 让本连接的旧报文在网络里自然死亡,避免污染下一个同端口的新连接
💡 面试名场面:"服务器上大量 TIME_WAIT 正常吗?"——主动关闭方会有,短连接高并发服务(每次请求都由它主动关闭)会堆出几万个,属正常现象;优化方向是连接复用(长连接/连接池),不是消灭 TIME_WAIT。
六、可靠传输:TCP 把链路层三协议的作业又抄了一遍
链路层的停止-等待/回退 N/选择重传,TCP 全部继承并升级:
| 机制 | TCP 版本 |
|---|---|
| 校验 | 首部+数据一起校验 |
| 编号 | 字节级序号(不是帧号),乱序重排、去重 |
| 确认 | 累积确认:"到 X 为止全收到了" |
| 超时重传 | RFT 自适应:每次实测 RTT,超时值跟着网络抖动调 |
| 快速重传 | 收到 3 个重复 ACK,不等超时立刻重发丢失段 |
🤔 为什么链路层以太网干脆不做可靠传输、全推给 TCP?——一层做足一次就够,两层都做是浪费。这是端到端原则的经典案例。
七、滑动窗口与流量控制
发送方一口气灌太多,接收方缓冲区爆了怎么办?——让接收方定价:
接收方每次 ACK 都带上"窗口"字段:我还剩 XX 缓冲,你最多发这么多
发送方的未确认数据量 ≤ 这个窗口 —— 像给水管装了个可调阀门
接收方缓冲被读走就"开窗",塞满就"关窗"到 0;发送方收到窗口 0 后会定期发探测报文,防止"开窗通知丢了双方互相干等"的死锁。
八、拥塞控制:路堵了,大家都慢点(408 必考)
流量控制管"接收方受得了吗",拥塞控制管"网络受得了吗"——四部曲:
cwnd(拥塞窗口)从 1 开始:
慢开始 → 每过一个 RTT 翻倍(1→2→4→8…指数涨)直到 ssthresh 门限
拥塞避免 → 过了门限改线性:每 RTT 只 +1(小心试探)
快重传 → 收到 3 个重复 ACK:不等超时,立刻重发丢失段
快恢复 → 门限 ssthresh = 当前 cwnd 减半,cwnd 从新门限开始线性爬
完整走一遍(ssthresh = 8):
1, 2, 4, 8 ← 慢开始指数涨,到门限 8
9, 10, 11 ← 拥塞避免线性爬
(丢包,3 个重复 ACK)
门限 = 11/2 ≈ 5,cwnd = 5 → 6, 7, 8… ← 快重传+快恢复
(若超时丢包:门限减半,cwnd 归 1 从头慢开始)
💡 发送方真正能发多少 = min(接收窗口, 拥塞窗口)——接收方的意愿和网络的承受力,取小的那个。
九、TCP vs UDP 终极对比
| TCP | UDP | |
|---|---|---|
| 连接 | 面向连接(握手) | 无连接 |
| 可靠性 | 可靠(确认+重传+排序) | 尽力而为 |
| 形态 | 字节流(无边界,有粘包) | 面向报文(有边界) |
| 首部 | 20B 起 | 8B |
| 收发速度 | 慢(机制换稳定) | 快 |
| 一对多 | 不支持 | 支持广播/组播 |
| 场景 | 网页/文件/邮件/登录 | DNS/视频/游戏/QUIC |
一句话:要"保证送到"用 TCP,要"保证及时"用 UDP——视频会议宁可丢帧也不想要迟到三秒的"可靠"旧帧。
十、盲点自测
- 端口解决什么问题? ——主机内的进程寻址;IP+端口=套接字,全网唯一的投递地址。
- 三次握手的本质? ——双方都确认双向链路通;两次时服务器→客户端方向未被确认,且迟到的旧 SYN 会造成服务器单方面空等。
- 四次挥手为什么比握手多一次? ——全双工,ACK 和 FIN 常无法合并(收到对方 FIN 后自己可能还有数据)。
- TIME_WAIT 为什么要等 2MSL? ——给"最后的 ACK 丢失后重传 FIN"留补票机会 + 让旧报文自然死亡。
- TCP 怎么实现可靠? ——字节编号 + 累积确认 + 超时/快速重传 + 滑动窗口,乱序缓存重排。
- 流量控制和拥塞控制分别管谁? ——前者管接收方的缓冲(接收窗口),后者管网络的承载(拥塞窗口);实际发送量取两者小值。
- 慢开始"慢"在哪? ——起点慢(从 1 开始),增长并不慢(指数翻倍);过了 ssthresh 才转线性。
- 什么时候 UDP 反而是对的选择? ——实时性优先的场景:视频/语音/游戏/DNS;丢一个旧帧比等重传更划算。
- 粘包是什么? ——TCP 是字节流没有消息边界;应用层要自己定界(Content-Length / 分隔符)。
十一、动手玩一把
# 用 nc 亲手体验"服务器/客户端"
nc -l 9000 # 终端A:起一个监听 9000 端口的服务器
nc 127.0.0.1 9000 # 终端B:连上去,A/B 互打字即收(TCP 全双工)
# 看本机现有连接和端口占用
netstat -an | grep -E "ESTABLISHED|LISTEN" | head
lsof -i :443 | head # 谁占着 443
# 抓包看三次握手(Linux/macOS 通用)
# tcpdump -i any port 9000 跑着的同时用 nc 连一次,肉眼数 SYN/SYN+ACK/ACK
可靠管道铺好了,但管道里说的"话"(网页、域名、邮件)各是什么格式?最后一层 → 06-应用层
💬 评论