Appearance
TCP、UDP 与 Socket
传输层把网络层的“主机到主机”交付扩展为“进程到进程”通信。TCP 提供可靠有序的字节流,UDP 提供保留消息边界的数据报,Socket 则是应用操作这些能力的系统接口。
UDP:最小传输语义
UDP Header 只有源端口、目的端口、长度和校验和。它不建立连接,也不承诺到达、顺序、去重和拥塞控制。
一次 UDP send 对应一个 Datagram。接收缓冲区过小,超出部分通常被截断,不会留到下一次读取。报文超过路径 MTU 还可能发生 IP 分片,任一分片丢失都会让整个数据报无法重组。
UDP 适合以下情况:
- 数据过期后不值得重传,例如实时位置和媒体帧。
- 应用有自己的序号、确认、超时或纠错机制。
- 需要广播、多播或低连接状态开销。
- 作为 QUIC、WebRTC、DNS 等更高层协议的底座。
“UDP 更快”不是普遍结论。若应用重新实现可靠、拥塞控制与加密,复杂度和成本并不会消失。
TCP 三次握手
TCP 连接由源 IP、源端口、目的 IP、目的端口标识。握手要确认双向路径,并同步双方初始序列号。
text
Client Server
| -------- SYN, seq=x --------------> |
| <--- SYN-ACK, seq=y, ack=x+1 ------- |
| -------- ACK, ack=y+1 -------------> |三次而不是两次的关键,是客户端必须确认自己收到了服务器的序列号和响应。握手还会协商 MSS、窗口缩放、SACK、时间戳等能力。
常见状态:
| 状态 | 含义 |
|---|---|
| LISTEN | 服务端等待新连接 |
| SYN-SENT | 客户端已发 SYN,等待响应 |
| SYN-RECEIVED | 服务端收到 SYN,等待最终 ACK |
| ESTABLISHED | 双向连接建立 |
bash
ss -tan state syn-sent
ss -tan state syn-recv
sudo tcpdump -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'序列号、ACK 与重传
TCP 序列号标识的是字节位置,不是报文编号。若 Segment 从 seq=1001 开始并携带 500 字节,它覆盖 1001 到 1500。接收端返回 ack=1501,表示 1501 之前的字节已经连续收到。
TCP 主要通过以下机制发现丢失:
- 重传超时:根据 RTT 与波动计算 RTO,超时后重发。
- 重复 ACK:连续收到相同累计确认,提示中间出现缺口。
- SACK:接收端报告已收到的离散区间,发送端只补缺失部分。
重传不一定证明物理链路丢包,也可能来自抓包点遗漏、接收端压力、重排序或 MTU 问题。应核对序列范围、ACK、SACK 与时间间隔。
流量控制
接收端通过 Window 告诉发送端还能容纳多少字节。发送端允许的在途数据同时受接收窗口 rwnd 和拥塞窗口 cwnd 限制:
text
send allowance = min(rwnd, cwnd)应用读取太慢时,接收缓冲逐渐填满,窗口可能降到零。发送端停止普通数据并周期发送 Window Probe。此时网络可能完全正常,瓶颈在接收应用的消费速度。
窗口缩放选项在握手中协商,使高速高延迟网络可以使用大于 65535 字节的有效窗口。
拥塞控制
流量控制保护接收端,拥塞控制保护网络。发送端根据 ACK、丢包、时延或显式拥塞信号调整 cwnd。
慢启动
连接开始时缺少路径容量信息,cwnd 从较小值快速增长。它不是发送速度很慢,而是避免刚建立连接就注入过多数据。
拥塞避免
达到阈值后增长变得平缓。出现丢包或拥塞信号时降低窗口,再逐步恢复。
CUBIC 与 BBR
CUBIC 主要依据丢包和时间函数调整窗口,是 Linux 常见默认算法。BBR 建模瓶颈带宽和最小 RTT,试图让在途数据接近带宽时延积。算法效果依赖网络、队列、内核版本和流量模型,不能脱离环境只比较名称。
bash
sysctl net.ipv4.tcp_congestion_control
ss -ti dst 203.0.113.20四次挥手与 TIME_WAIT
TCP 是全双工连接,两个方向要分别关闭:
text
Active closer Passive closer
| -------- FIN ----------------------> |
| <------- ACK ------------------------ |
| <------- FIN ------------------------ |
| -------- ACK ----------------------> |ACK 与 FIN 可以合并,因此抓包不一定恰好看到四个 Segment。所谓四次挥手描述的是两个方向分别结束的逻辑过程。
| 状态 | 工程含义 |
|---|---|
| CLOSE-WAIT | 对端已关闭写方向,本地应用尚未 close |
| FIN-WAIT-2 | 本地 FIN 已确认,等待对端 FIN |
| TIME-WAIT | 主动关闭方暂存状态,处理迟到报文并确保最终 ACK 可重发 |
大量长期 CLOSE-WAIT 常表示应用泄漏连接。TIME-WAIT 则是协议正常状态,是否过多要结合连接建立速率、端口范围和连接复用分析。
RST 表示连接被立即复位,常见于目标端口无人监听、应用异常关闭、对失效连接继续发送,或中间设备主动拒绝。
Socket API 的服务端路径
text
socket → bind → listen → accept → recv/send → closesocket创建内核端点。bind绑定本地地址和端口。listen进入监听状态并维护握手相关队列。accept从完成队列取出一条已连接 Socket。recv/send在 Socket 缓冲区与应用之间搬运数据。
监听 Socket 与已连接 Socket 是不同对象。一个监听端口可以接受大量客户端连接,因为每条连接拥有不同四元组。
客户端通常执行:
text
socket → connect → send/recv → close非阻塞 connect 返回 EINPROGRESS 后,应用等待可写事件,再读取 SO_ERROR 判断是否真正成功。
阻塞、非阻塞与事件循环
阻塞 Socket 会让线程等待 I/O。连接数量较少时简单可靠;大量长连接中,大部分时间都在等待,为每条连接固定操作系统线程会增加线程栈和调度成本。
I/O 多路复用让少量线程等待大量 fd:
| API | 工作方式 | 主要边界 |
|---|---|---|
| select | 每轮传入 fd 位图并扫描 | 集合大小与 maxfd 扫描成本 |
| poll | 每轮传入 pollfd 数组 | 仍需线性扫描全部监控项 |
| epoll | 内核维护关注集合并返回就绪队列 | Linux 专有,需要正确处理触发模式 |
水平触发只要状态仍可读就继续通知,更容易写正确。边缘触发强调状态变化,收到事件后必须使用非阻塞调用处理到 EAGAIN,否则连接可能停住。
事件循环线程不应执行不可控的数据库查询或重计算。正确结构通常是:
text
epoll_wait → accept/read → 协议解析 → 业务工作池 → writeSocket 错误如何解释
| 错误 | 常见含义 | 下一步 |
|---|---|---|
ECONNREFUSED | 目标端口未监听或主动拒绝 | 检查服务监听与防火墙 REJECT |
ETIMEDOUT | 路径静默丢弃或对端无响应 | 双端抓包并检查回程路由 |
EADDRINUSE | 本地地址端口已占用 | 查看监听与绑定策略 |
EMFILE | 进程 fd 达到上限 | 检查 ulimit -n 和连接泄漏 |
EAGAIN | 非阻塞操作暂时无法完成 | 等待下一次就绪事件 |
ECONNRESET | 对端或中间设备发送 RST | 关联关闭原因与应用日志 |
可复现实验
bash
# 启动监听端
nc -l 9000
# 建立客户端连接
nc 127.0.0.1 9000
# 查看 Socket、队列和进程
ss -ltnp 'sport = :9000'
ss -tnpi 'dport = :9000'
# 观察系统调用和报文
strace -e socket,bind,listen,accept,connect,sendto,recvfrom -p PID
sudo tcpdump -i lo -nn 'tcp port 9000'下一步进入 现代应用与实时协议,把传输层能力映射为 TLS、HTTP、WebSocket、gRPC 和 WebRTC。
