一、STUN协议核心介绍
STUN(Session Traversal Utilities for NAT)是一种用于NAT(网络地址转换)穿透的标准化网络协议,其核心功能在于帮助终端设备(如浏览器客户端)在复杂的网络地址转换环境中,通过查询公网服务器来获取自身在互联网上的真实公网IP地址与端口映射,并进一步验证网络连接的连通性。
二、协议的核心作用与应用场景
根据RFC 5389标准定义,STUN协议的主要作用包含以下两个层面:
| 作用层级 | 具体功能 | 应用场景 |
|---|---|---|
| 地址发现 | 客户端向部署在公网的STUN服务器发送请求,服务器在响应中告知客户端其公网IP和端口。 | 这是WebRTC建立P2P连接前的关键步骤,用于确定候选地址。 |
| 连通性检查 | 通过STUN请求-响应交互,验证从客户端到服务器的网络路径是否通畅,即"打洞"是否成功。 | 用于ICE(交互式连接建立)框架中的连接检查,确认候选地址对的有效性。 |
在具体的实现中,如Chrome浏览器等WebRTC客户端,会主动向配置的STUN服务器发送STUN绑定请求(Binding Request),以获取其NAT映射后的公网地址。而像mediasoup、SRS这类实现了"ICE-Lite"模式的WebRTC服务器,它们自身并不需要依赖外部STUN服务,而是能够直接响应客户端发来的STUN请求,通过这种响应本身来向客户端证明网络路径是可达的,从而简化了服务端的部署架构。
三、STUN协议消息结构分析
STUN协议消息采用基于TLV(Type-Length-Value)结构的二进制格式在UDP(也可运行于TCP/TLS)上传输。一个完整的STUN消息由消息头和若干属性构成。
1. 消息头(固定20字节)
消息头定义了协议的基本信息,其结构如下表所示:
| 字段名 | 位宽 | 描述 |
|---|---|---|
| 消息类型 | 16 bits | 标识STUN消息的类别,例如 ` |
0x0001 代表绑定请求(Binding Request), |
||
| 0x0101` 代表绑定成功响应(Binding Success Response)。 | ||
| 消息长度 | 16 bits | 指示消息中所有属性的总长度(以字节为单位),不包括20字节的头部。 |
| 魔术字 | 32 bits | 固定值 ` |
| 0x2112A442`,用于区分STUN数据包与其他协议的数据包(如RTP)。 | ||
| 事务ID | 96 bits | 一个随机生成的96位标识符,用于唯一匹配请求与响应,确保通信的安全性。 |
2. 关键属性(Attributes)
属性是STUN消息的载荷,携带具体的控制或数据信息。每个属性同样由类型(Type)、长度(Length)和值(Value)组成。以下是几个在地址发现和连通性检查中至关重要的属性:
- MAPPED-ADDRESS(0x0001)与 XOR-MAPPED-ADDRESS(0x0020):这两个属性都用于在STUN服务器的响应中携带客户端的公网映射地址。XOR-MAPPED-ADDRESS是增强安全性的版本,其地址和端口信息与魔术字和事务ID进行了异或(XOR)编码,以防止中间件(如ALG)错误地修改STUN报文。
- SOFTWARE(0x8022):一个可选的字符串属性,用于标识STUN客户端或服务器的软件版本信息。
- FINGERPRINT(0x8028):一个可选的32位CRC32校验和属性,附加在消息末尾,用于在与其他协议(如RTP)共享同一端口时,进一步校验报文是否为合法的STUN消息。
四、典型交互流程代码示例
以下伪代码模拟了WebRTC客户端发起STUN绑定请求以获取公网地址的核心逻辑:
javascript
// 伪代码:STUN客户端发起地址发现请求
class StunClient {
constructor(serverAddr, serverPort) {
this.server = { addr: serverAddr, port: serverPort };
this.transactionId = this.generateTransactionId(); // 生成96位随机事务ID
}
async discoverPublicAddress() {
// 1. 构建STUN绑定请求消息头
const stunHeader = this.buildHeader(0x0001); // 0x0001 = Binding Request
// 2. (可选)添加SOFTWARE等属性
const attributes = this.buildSoftwareAttribute('MyWebRTCClient/1.0');
// 3. 组装完整STUN报文
const stunMessage = Buffer.concat([stunHeader, attributes]);
// 4. 通过UDP Socket发送至STUN服务器
const socket = createUdpSocket();
socket.send(stunMessage, this.server.port, this.server.addr);
// 5. 等待并解析响应
const response = await this.waitForResponse(socket, this.transactionId);
const publicAddress = this.parseXorMappedAddress(response); // 解析XOR-MAPPED-ADDRESS
return publicAddress;
}
buildHeader(messageType) {
// 构建20字节头部:类型、长度、魔术字、事务ID
const buffer = Buffer.alloc(20);
buffer.writeUInt16BE(messageType, 0);
// ... 写入其他字段
buffer.writeUInt32BE(0x2112A442, 4); // 魔术字
this.transactionId.copy(buffer, 8); // 事务ID
return buffer;
}
}
在上述流程中,服务器收到请求后,会从收到的UDP数据包的源IP和源端口识别出客户端的公网映射地址,将其编码在 XOR-MAPPED-ADDRESS 属性中返回。客户端解码后即可获得用于P2P通信的候选地址。