7.2 KiB
7.2 KiB
tags, create time
| tags | create time | |||
|---|---|---|---|---|
|
2026-05-18 02:20 |
UDP 协议与 SCTP
概述
UDP 是 TCP 的对立面:没有连接、不可靠、无拥塞控制——但正因如此,它在延迟敏感场景和无差错检测场景中不可替代。SCTP 则是被低估的"第三条路",结合了 TCP 和 UDP 的优势。
UDP 首部格式
┌─────────────┬─────────────┐
│ Source Port │ Destination Port │ 各 2 bytes
├─────────────┼─────────────┤
│ Length │ Checksum │ 各 2 bytes
└─────────────┴─────────────┘
总长: 8 bytes(固定!)
| 字段 | 说明 |
|---|---|
| Source/Dest Port | 同 TCP |
| Length | UDP 包总长(含首部),最小 8 字节 |
| Checksum | 可选!(IPv4 中如果为 0 表示无校验;IPv6 中强制启用) |
极简首部的代价
TCP 20B 首部 vs UDP 8B 首部
一个 IP 包 = IP(20) + TCP(20) + Data = 40B overhead + N bytes payload
IP(20) + UDP(8) + Data = 28B overhead + N bytes payload
UDP 节省: 12 bytes / packet × 每秒数据包数 = 显著的带宽节省
UDP 核心特性
| 特性 | UDP | TCP |
|---|---|---|
| 连接 | ❌ 无连接 | ✅ 三次握手 |
| 可靠性 | ❌ 不保证送达 | ✅ ACK + 重传 |
| 顺序保证 | ❌ 可能乱序到达 | ✅ 有序 |
| 流量控制 | ❌ | ✅ 滑动窗口 |
| 拥塞控制 | ❌ | ✅ 慢启动/快恢复等 |
| 头部开销 | 8 bytes | 20-60 bytes |
| 半双工/全双工 | N/A(无方向概念) | 全双工 |
| 流模式/报文模式 | 数据报模式(保留边界) | 字节流模式 |
为什么 UDP 保留消息边界?
// UDP: send("HELLO") → recv() = "HELLO"
// 一条 UDP datagram 就是一个完整消息
// TCP: write("H"); write("E"); write("L"); write("L"); write("O")
// read() 可能是 "HE", "LLO", "HELL", "LLO"...
UDP 适用场景
典型应用
| 应用 | 为什么用 UDP? |
|---|---|
| DNS | 查询很短(通常 < 512 bytes),等待重传得不偿失 |
| NTP | 最新时间最重要,旧值无意义 |
| VoIP / WebRTC | 丢一帧比延迟卡顿更可接受 |
| 在线游戏 | 每秒几十次状态同步,延迟比可靠更重要 |
| DHCP | 客户端无 IP,无法使用 TCP(需要三次握手) |
| SNMP | 监控轮询,丢一次下轮重来即可 |
| Redis | 多数操作幂等或客户端自行重试 |
DNS 用 UDP 还是 TCP?
| 情况 | 协议 | 原因 |
|---|---|---|
| 标准查询 (< 512B) | UDP port 53 | 快,RTT + 少量开销 |
| Zone Transfer (AXFR) | TCP port 53 | 传输大量记录,需可靠 |
| Response > 512B (RFC 5102) | EDNS0 + UDP | 扩展至 4096B |
| EDNS0 不支持,response 大 | TCP fallback | 设置 TC (Truncation) 位,客户端自动切 TCP |
$ dig example.com +trace # 默认走 UDP
$ dig example.com +tcp # 强制 TCP
$ host -t A example.com # 系统级 dig
UDP 的可靠性增强
虽然 UDP 本身不提供可靠性,但许多基于 UDP 的应用自行实现了轻量级可靠传输:
flowchart LR
App["应用层"] --> U["UDP Datagram"]
subgraph "用户态实现(自加可靠性)"
U --> QUIC["QUIC<br/>内置 ACK+重传+流控"]
U --> DTLS["DTLS<br/>TLS over UDP"]
U --> RTP["RTP/RTCP<br/>媒体流+反馈"]
U --> Custom["自定义可靠协议<br/>如 uTP, KCP, Nakadi"]
end
QUIC -.->|"最终都到"| IPv4["IP"]
DTLS -.-> IPv4
RTP -.-> IPv4
Custom -.-> IPv4
KCP 协议(国内广泛使用)
由俄罗斯开发者 Stan 设计,专为网游低延迟优化:
KCP vs TCP in game scenario:
──────────────────────────
TCP: 丢包 → RTO ≥ 1s → 卡顿
KCP: 丢包 → ARQ 立即重传 → < 100ms 恢复
FEC(前向纠错) 补包 → 无需等待确认
import "github.com/xtaci/kcp-go"
// Go 中使用 KCP
sess, _ := kcp.Listen(":8080")
conn, _ := sess.AcceptKCP()
conn.SetNoDelay(1, 10, 2, 1) // 无Nagle, 10ms interval, 2 rounds, 1 SSThresh
conn.SetWindowSize(1024, 1024)
conn.SetMtu(1200)
SCTP(Stream Control Transmission Protocol)
为何存在?
SCTP (RFC 4960) 试图结合 TCP 和 UDP 的优点:
| 特性 | TCP | UDP | SCTP |
|---|---|---|---|
| 可靠性 | ✅ | ❌ | ✅ (可选) |
| 顺序保证 | ✅ 全局有序 | ❌ | ✅ 多流独立有序 |
| 多宿支持 | ❌ 单 IP | ❌ | ✅ 多 IP 冗余 |
| 消息边界 | ❌ 字节流 | ✅ 报文 | ✅ 报文 |
| 拥塞控制 | ✅ | ❌ | ✅ |
| 适用场景 | HTTP/Web | DNS/VoIP | 信令(Diameter/SIP) |
Multi-streaming(多流)— SCTP 的王牌特性
传统 TCP 的一个问题:head-of-line blocking。如果一个包丢了,所有后续消息都要等它重传完。SCTP 通过多流解决这个问题:
SCTP 连接拥有 3 个独立 stream:
Stream 0: [Data_0_A][Data_0_B][Data_0_C] ← 信令通道 (有序)
Stream 1: [Data_1_A] [Data_1_C] ← 聊天消息 (无序 OK)
Stream 2: [Data_2_A][Data_2_B] ← 文件传输
即使 Stream 0 的 B 丢失了 → Stream 1 和 2 继续前进!
四元组 vs 六元组:
TCP: src_ip, src_port, dst_ip, dst_port → 一个连接
SCTP: src_ip, src_port, dst_ip, dst_port → association (关联)
+ stream_id → stream within association
SCTP 的使用场景
| 场景 | 说明 |
|---|---|
| Diameter / SIP 信令 | 3GPP/ITU-T 标准强制使用 SCTP over IP |
| SS7 over IP (SIGTRAN) | 电信信令迁移到 IP 网络 |
| 数据库复制 | Oracle Data Guard 可配置 SCTP 连接 |
| 游戏服务器 | 多玩家流独立,互不影响 |
Go 中的 SCTP 支持
import "golang.org/x/net/sctp"
// 创建 SCTP 关联
conn, _ := sctp.DialSCTP("ip4:sctp", "<local>:3868", "<peer>:3868")
// 设置多流
conn.Set SCTPNodeAddresses(sctp.SCTP_PEER_ADDR_CHANGE, ...)
// 发送指定 stream 的消息
conn.WriteToSCTP([]byte("hello"), &sctp.SCTPAddr{
PeerAddr: net.SCTPAddr{IPAddrs: peerIPs},
Stream: 0, // 选择 stream 0
})
[!tip] SCTP 的生态局限 虽然技术层面很优秀,但因为运营商部署迟缓和中间设备(NAT/防火墙)对 SCTP 的不兼容,其普及度远不及 TCP。但在电信行业仍是事实标准。
UDP 编程示例
package main
import (
"net"
"fmt"
)
func main() {
// 服务端
addr, _ := net.ResolveUDPAddr("udp4", ":8080")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()
buf := make([]byte, 4096)
n, remote, _ := conn.ReadFromUDP(buf)
fmt.Printf("Received %d bytes from %v\n", n, remote)
// 客户端 — 注意每条 Write 是一个独立的 UDP 数据报
client, _ := net.DialUDP("udp", nil, addr)
client.Write([]byte("Hello UDP"))
}
关联笔记
- hhs/NETWORK/TCP段结构与状态机 — UDP 缺少的连接管理功能
- hhs/NETWORK/DNS原理与优化 — DNS 主要使用 UDP port 53
- hhs/NETWORK/HTTPS与TLS握手 — TLS 也有 over UDP 的版本: DTLS