233 lines
7.2 KiB
Markdown
233 lines
7.2 KiB
Markdown
---
|
||
tags: [计算机网络, UDP, SCTP]
|
||
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 保留消息边界?
|
||
|
||
```go
|
||
// 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 |
|
||
|
||
```bash
|
||
$ dig example.com +trace # 默认走 UDP
|
||
$ dig example.com +tcp # 强制 TCP
|
||
$ host -t A example.com # 系统级 dig
|
||
```
|
||
|
||
## UDP 的可靠性增强
|
||
|
||
虽然 UDP 本身不提供可靠性,但许多基于 UDP 的应用自行实现了轻量级可靠传输:
|
||
|
||
```mermaid
|
||
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(前向纠错) 补包 → 无需等待确认
|
||
```
|
||
|
||
```go
|
||
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 支持
|
||
|
||
```go
|
||
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 编程示例
|
||
|
||
```go
|
||
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
|