在 mediasoup 的架构中,Router 创建 WebRtcTransport 时,根据其是否关联到已有的 WebRtcServer,会触发两种不同的底层 IPC 消息,这直接决定了传输通道(Transport)的网络端点管理方式和资源分配模式 。
一、核心差异对比
下表清晰展示了两种创建路径的核心区别:
| 特性维度 | ROUTER_CREATE_WEBRTCTRANSPORT |
ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER |
|---|---|---|
| 创建前提 | 不依赖预创建的 WebRtcServer。 |
必须 基于一个已创建并配置好的 WebRtcServer 对象。 |
| 网络端点管理 | 由 WebRtcTransport 自身创建并管理独立的 ICE、DTLS、RTP/RTCP 套接字和端口。 |
复用 其关联的 WebRtcServer 已创建的、共享的网络端点(套接字和端口)。 |
| 端口分配 | 每次创建都会从 Worker 配置的 rtcMinPort/rtcMaxPort 范围内动态分配新的端口。 |
使用 WebRtcServer 初始化时绑定的固定端口,所有关联的 Transport 共享这些端口。 |
| ICE 候选生成 | 为自身新分配的每个端口生成独立的 ICE 候选(host candidate)。 | 使用 WebRtcServer 的监听地址和端口生成 ICE 候选,所有关联 Transport 的候选相同。 |
| 资源开销 | 较高。每个 Transport 独占一组端口,增加系统端口和套接字资源消耗。 | 较低。端口和套接字在多个 Transport 间共享,显著减少资源占用。 |
| 适用场景 | 1. 未配置或不需要使用 WebRtcServer 的简单场景。 2. 需要每个 Transport 具备完全独立网络栈的特殊需求。 |
推荐的生产环境模式。适用于需要承载大量并发传输的场景,如大型会议、直播,能有效避免端口耗尽问题。 |
二、技术实现与流程解析
这两种路径的差异,源于 mediasoup 为优化资源管理和扩展性而设计的两种网络 I/O 模型。
- 独立端点模式 (
ROUTER_CREATE_WEBRTCTRANSPORT)
在此模式下,每个 WebRtcTransport 都是一个自包含的网络实体。其创建和初始化流程如下:
cpp
// 伪代码示意 Transport 内部初始化独立端点
class WebRtcTransport {
private:
std::unique_ptr<UdpSocket> iceSocket;
std::unique_ptr<TcpServer> iceTcpServer; // 可选
std::unique_ptr<DtlsTransport> dtlsTransport;
std::vector<std::unique_ptr<RtpSocket>> rtpSockets;
std::vector<std::unique_ptr<RtcpSocket>> rtcpSockets;
public:
async create() {
// 1. 动态分配端口
int udpPort = allocatePortFromWorkerRange();
int tcpPort = allocatePortFromWorkerRange();
// 2. 创建并绑定独立的套接字
this.iceSocket = new UdpSocket(udpPort);
this.dtlsTransport = new DtlsTransport(this.iceSocket);
// 3. 为 RTP/RTCP 分配额外端口(可能使用偶数/奇数端口对)
for (int i = 0; i < numStreams; ++i) {
int rtpPort = allocatePortFromWorkerRange();
int rtcpPort = rtpPort + 1; // 假设连续分配
this.rtpSockets.push_back(new RtpSocket(rtpPort));
this.rtcpSockets.push_back(new RtcpSocket(rtcpPort));
}
// 4. 基于这些新端口生成 ICE 候选信息
this.iceCandidates = generateIceCandidates(this.iceSocket, this.iceTcpServer);
// 5. 发送 ROUTER_CREATE_WEBRTCTRANSPORT 消息通知 Worker
this.channel.request('ROUTER_CREATE_WEBRTCTRANSPORT', {
transportId: this.id,
icePorts: { udp: udpPort, tcp: tcpPort },
rtpPorts: this.rtpSockets.map(s => s.port),
// ... 其他参数
});
}
}
关键点 :此模式下的每个 WebRtcTransport 在 ICE 协商时,会向客户端提供一组专属于自己 的 ICE 候选,例如 candidate:foundation 1 udp 2130706431 192.168.1.100 50000 typ host。当客户端发起连接时,数据流将直接到达这些独占的端口。
- 共享端点模式 (
ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER)
在此模式下,WebRtcTransport 作为 WebRtcServer 的客户端,共享其网络基础设施。WebRtcServer 充当了一个多路复用器(Multiplexer) 的角色。
cpp
// 伪代码示意基于 WebRtcServer 创建 Transport
class WebRtcTransport {
private:
WebRtcServer* webRtcServer; // 指向共享的 Server
std::string transportId;
public:
async createWithServer(webRtcServer) {
this.webRtcServer = webRtcServer;
// 1. 无需创建套接字或分配端口,直接关联到 Server
// 2. 从 Server 获取其监听地址和端口,作为自己的 ICE 候选
this.iceCandidates = webRtcServer.getListenInfos().map(info => ({
ip: info.announcedAddress,
port: info.port,
protocol: info.protocol,
type: 'host'
}));
// 3. 发送 ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER 消息
// 此消息包含 serverId,告知 Worker 此 Transport 属于哪个 Server
this.channel.request('ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER', {
transportId: this.id,
webRtcServerId: webRtcServer.id, // 关键关联标识
// ... 其他参数
});
}
// 当数据包到达 WebRtcServer 的共享端口时
handleIncomingPacket(packet, remoteAddress) {
// WebRtcServer 根据数据包特征(如 DTLS ClientHello 中的指纹)
// 将数据包分发到正确的 WebRtcTransport 实例进行处理
if (packet.isDtls()) {
string srtpKey = extractSrtpKeyFromDtls(packet);
WebRtcTransport* transport = findTransportBySrtpKey(srtpKey);
if (transport) {
transport.processDtlsPacket(packet);
}
} else if (packet.isRtp() || packet.isRtcp()) {
// 根据 SSRC 或 MID 等 RTP/RTCP 扩展头信息进行分发
uint32_t ssrc = packet.getSsrc();
WebRtcTransport* transport = findTransportBySsrc(ssrc);
if (transport) {
transport.processRtpPacket(packet);
}
}
}
}
关键点 :所有关联到同一 WebRtcServer 的 WebRtcTransport,其 ICE 候选中的 IP 地址和端口号是完全相同的 。例如:candidate:foundation 1 udp 2130706431 192.168.1.100 44444 typ host。区别在于每个 Transport 拥有独立的 DTLS 连接和 SRTP 密钥上下文 。WebRtcServer 在收到数据包后,需要根据 DTLS 指纹 或 RTP/RTCP 的 SSRC 等标识,在内部分发到正确的 Transport 实例。这要求 WebRtcServer 实现一个高效的 Demux(解复用) 层。
三、应用场景与选型建议
-
开发与测试环境 (
ROUTER_CREATE_WEBRTCTRANSPORT)- 优势:配置简单,无需提前规划端口。每个 Transport 独立,便于隔离和调试。
- 劣势:端口消耗快,当需要创建成百上千个传输时,可能很快耗尽可用端口范围,且大量的套接字会增加系统开销。
- 建议:适用于快速原型验证、小规模并发测试。
-
生产与高并发环境 (
ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER)- 优势 :
- 端口节约:仅需少量固定端口(如一对 UDP/TCP)即可支撑数千上万的传输连接,彻底避免端口耗尽问题。
- 连接效率:客户端可能因 ICE 候选相同而减少连通性检查的次数。
- 资源优化:减少系统调用和套接字管理开销,提升整体性能。
- 劣势 :需要前置配置
WebRtcServer,架构稍复杂。WebRtcServer成为单点,需要保证其高可用性。 - 建议 :所有中大型实时音视频应用都应采用此模式。这是 mediasoup 官方推荐的生产部署方式,能确保服务具备良好的扩展性。
- 优势 :
配置示例:
javascript
// 生产环境配置:使用 WebRtcServer
const worker = await mediasoup.createWorker({ /* ... */ });
const webRtcServer = await worker.createWebRtcServer({
listenInfos: [
{ protocol: 'udp', ip: '0.0.0.0', announcedAddress: 'public.ip.addr', port: 40000 },
{ protocol: 'tcp', ip: '0.0.0.0', announcedAddress: 'public.ip.addr', port: 40000 }
]
});
// 当创建房间Router时,后续为该Router创建Transport均基于此Server
const router = await worker.createRouter({ mediaCodecs });
// 此调用在底层会触发 ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER
const transport = await router.createWebRtcTransport({
webRtcServer, // 关键参数,指定关联的Server
// ... 其他配置如iceParameters, iceCandidates等将由Server提供
});
结论 :ROUTER_CREATE_WEBRTCTRANSPORT_WITH_SERVER 是 mediasoup 为实现高并发、高密度 WebRTC 连接而设计的优化路径。它通过 WebRtcServer 的端口共享与多路复用机制,将网络 I/O 资源与业务逻辑(Transport)解耦,是构建可扩展 SFU 服务的基石。而 ROUTER_CREATE_WEBRTCTRANSPORT 则提供了基础的、独立运行的备用方案。两者的选择本质上是在资源隔离性与系统扩展性之间权衡的结果 。