Skip to content

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 可靠传输闭环

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 → close
  • socket 创建内核端点。
  • 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 → 协议解析 → 业务工作池 → write

Socket 错误如何解释

错误常见含义下一步
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。

别急,先让缓存热一下。