Files
cs-note/hhs/NETWORK/04-传输层/06-UDP协议与SCTP.md
T
2026-05-24 11:42:38 +08:00

7.2 KiB
Raw Blame History

tags, create time
tags create time
计算机网络
UDP
SCTP
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", "&lt;local&gt;:3868", "&lt;peer&gt;: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"))
}

关联笔记