传输层: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(两倍报文最大寿命)才真正关闭:

  1. 若最后的 ACK 丢了,服务器会重发 FIN——在等的人才能补 ACK
  2. 让本连接的旧报文在网络里自然死亡,避免污染下一个同端口的新连接

💡 面试名场面:"服务器上大量 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——视频会议宁可丢帧也不想要迟到三秒的"可靠"旧帧。

十、盲点自测

  1. 端口解决什么问题? ——主机内的进程寻址;IP+端口=套接字,全网唯一的投递地址。
  2. 三次握手的本质? ——双方都确认双向链路通;两次时服务器→客户端方向未被确认,且迟到的旧 SYN 会造成服务器单方面空等。
  3. 四次挥手为什么比握手多一次? ——全双工,ACK 和 FIN 常无法合并(收到对方 FIN 后自己可能还有数据)。
  4. TIME_WAIT 为什么要等 2MSL? ——给"最后的 ACK 丢失后重传 FIN"留补票机会 + 让旧报文自然死亡。
  5. TCP 怎么实现可靠? ——字节编号 + 累积确认 + 超时/快速重传 + 滑动窗口,乱序缓存重排。
  6. 流量控制和拥塞控制分别管谁? ——前者管接收方的缓冲(接收窗口),后者管网络的承载(拥塞窗口);实际发送量取两者小值。
  7. 慢开始"慢"在哪? ——起点慢(从 1 开始),增长并不慢(指数翻倍);过了 ssthresh 才转线性。
  8. 什么时候 UDP 反而是对的选择? ——实时性优先的场景:视频/语音/游戏/DNS;丢一个旧帧比等重传更划算。
  9. 粘包是什么? ——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-应用层

⬅️ 网络层 🏠 00-基础与理论 ➡️ 应用层