21、SFU服务器搭建:Mediasoup简介、Mediasoup架构、Mediasoup安装与配置、创建Router与Transport
好,咱们今天聊点硬核的——SFU服务器搭建。
前面几章我们一直在讲WebRTC的客户端逻辑,信令、媒体协商、P2P连接。但说实话,真正在生产环境里,P2P模式用得并不多。为什么?因为一旦超过4个人开会,每个客户端都要跟其他人建立连接,带宽和CPU都扛不住。这时候就需要SFU(Selective Forwarding Unit)出场了。
我个人最常用的SFU方案就是Mediasoup。它性能好、灵活度高,而且是用C++写的底层,Node.js做上层控制,非常适合二次开发。今天我们就把它拆开揉碎了讲清楚。
核心要点:Mediasoup是一个基于WebRTC的SFU服务器,它不处理信令,只负责媒体流的转发。你想想看,这其实是个很聪明的设计——把信令交给业务层,Mediasoup只做它最擅长的事:高效转发音视频数据。
Mediasoup简介
Mediasoup是什么?简单说,它是一个运行在服务端的WebRTC协议栈。你可以把它理解成一个媒体路由器——客户端把音视频流推上来,Mediasoup负责把这些流转发给其他客户端。
我记得第一次接触Mediasoup时,最让我惊讶的是它的性能。单台服务器可以轻松支撑几百路并发流。这在WebRTC的世界里,已经相当能打了。
Mediasoup的几个关键特性:
- 纯C++核心:媒体处理层用C++编写,性能极高
- Node.js控制层:API层用Node.js,开发效率高
- 支持Simulcast:同一路流可以发送多个分辨率,接收端按需选择
- 支持SVC:可伸缩视频编码,适应不同网络条件
- 灵活的Pipe模式:支持服务器间级联,实现大规模分发
我的经验:如果你要做视频会议、在线教育、直播连麦这类场景,Mediasoup基本是首选。我曾经在一个项目中用Mediasoup支撑了200人同时在线会议,服务器CPU占用率不到40%。
Mediasoup架构
Mediasoup的架构其实不复杂,但有几个核心概念你必须搞懂。我画了一张图,帮你理清它们之间的关系。
这张图其实已经把Mediasoup的核心逻辑讲清楚了。从上往下看:
- Worker:最底层,一个Worker就是一个C++进程。我建议一台机器上跑多个Worker,充分利用多核CPU。
- Router:Worker下面可以创建多个Router。每个Router相当于一个独立的"房间"或"会议"。不同Router之间的媒体流是隔离的。
- Transport:Router下面创建Transport。每个Transport对应一个客户端的WebRTC连接。说白了,Transport就是客户端和服务器之间的管道。
- Producer/Consumer:Transport上可以创建Producer(推流)和Consumer(拉流)。Producer把客户端的音视频数据推上来,Consumer从服务器拉取数据。
注意:Mediasoup的架构是分层的,你不能跨Worker操作Router,也不能跨Router操作Transport。我曾经见过有人试图在一个Worker里创建100个Router,结果性能反而下降了。合理的做法是:根据CPU核心数创建Worker,每个Worker下再按需创建Router。
Mediasoup安装与配置
好,理论讲完了,咱们动手装一个。Mediasoup的安装其实不复杂,但有几个坑需要注意。
环境要求
| 组件 | 版本要求 | 备注 |
|---|---|---|
| Node.js | >= 18.0.0 | 推荐使用LTS版本 |
| Python | >= 3.6 | 编译依赖 |
| GCC / Clang | C++17支持 | Linux用GCC,macOS用Clang |
| 操作系统 | Linux / macOS | Windows需要WSL2 |
安装步骤
安装Mediasoup其实就一行命令:
npm install mediasoup
但这里有个坑——Mediasoup需要编译原生模块。如果你的机器上没有C++编译环境,安装会失败。我个人习惯在安装前先确认一下:
# 检查C++编译器
g++ --version
# 检查Python
python3 --version
# 如果缺少,Ubuntu上可以这样装
sudo apt-get install build-essential python3
避坑指南:我曾经在一台CentOS服务器上装Mediasoup,折腾了半天都编译不过。后来发现是GCC版本太旧,不支持C++17。升级GCC后一次通过。所以,装之前一定先确认编译器版本。
基础配置
安装完成后,我们需要配置Mediasoup。配置项比较多,但核心的就几个:
const mediasoup = require('mediasoup');
// 创建Worker的配置
const workerSettings = {
logLevel: 'warn', // 日志级别:debug, warn, error
logTags: ['info'], // 日志标签
rtcMinPort: 40000, // RTC端口范围(最小值)
rtcMaxPort: 49999 // RTC端口范围(最大值)
};
// 创建Worker
const worker = await mediasoup.createWorker(workerSettings);
console.log('Worker创建成功,PID:', worker.pid);
这里要注意端口范围。Mediasoup需要一大段UDP端口用于媒体传输。我建议至少分配10000个端口。如果端口不够,客户端连接会失败。
重要:生产环境中,一定要确保防火墙开放了这些UDP端口。我曾经排查过一个线上问题,客户端一直连不上服务器,最后发现是防火墙把UDP端口全封了。嗯,这种低级错误,犯过一次就记住了。
创建Router与Transport
Worker创建好了,接下来就是创建Router和Transport。这部分是Mediasoup的核心操作,也是你写业务逻辑时最常打交道的地方。
创建Router
Router的创建很简单:
// 在Worker下创建一个Router
const router = await worker.createRouter({
mediaCodecs: [
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2
},
{
kind: 'video',
mimeType: 'video/VP8',
clockRate: 90000,
parameters: {
'x-google-start-bitrate': 1000
}
},
{
kind: 'video',
mimeType: 'video/H264',
clockRate: 90000,
parameters: {
'packetization-mode': 1,
'profile-level-id': '42e01f',
'level-asymmetry-allowed': 1
}
}
]
});
console.log('Router创建成功,ID:', router.id);
这里有个关键点——mediaCodecs配置。它告诉Mediasoup这个Router支持哪些编解码器。我建议至少配置opus(音频)和VP8/H264(视频)。为什么?因为大部分浏览器都支持这些编码。
创建Transport
Transport是客户端和服务器之间的桥梁。创建Transport需要客户端先发来一些信息:
// 服务端创建WebRTC Transport
const transport = await router.createWebRtcTransport({
listenIps: [
{
ip: '0.0.0.0', // 监听所有网卡
announcedIp: '你的公网IP' // 公网IP,客户端用这个IP连接
}
],
enableUdp: true,
enableTcp: true,
preferUdp: true,
initialAvailableOutgoingBitrate: 1000000 // 初始发送码率 1Mbps
});
// 返回给客户端的信息
const clientTransportInfo = {
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters
};
// 客户端用这些信息创建本地的RTCPeerConnection
// 然后与服务端建立连接
关键点:announcedIp这个配置很容易被忽略。如果你的服务器在内网,客户端在外网,必须配置公网IP。否则客户端拿到的ICE Candidate是内网IP,连不上。我刚开始做的时候就在这里栽过跟头。
建立连接后的操作
Transport创建完成后,客户端会发来DTLS握手信息。服务端需要处理:
// 客户端发来DTLS参数后,服务端调用
await transport.connect({ dtlsParameters: clientDtlsParameters });
// 连接建立后,客户端可以创建Producer推流
const producer = await transport.produce({
kind: 'video',
rtpParameters: clientRtpParameters // 客户端的RTP参数
});
// 其他客户端可以创建Consumer拉流
const consumer = await transport.consume({
producerId: producer.id,
rtpCapabilities: router.rtpCapabilities
});
// 把Consumer的RTP参数发给拉流客户端
const consumerRtpParams = {
id: consumer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters
};
到这里,一个基本的SFU通信链路就建立起来了。客户端A推流到Mediasoup,客户端B从Mediasoup拉流,实现了媒体转发。
我的建议:在实际项目中,不要把所有的Transport创建逻辑都写在一个文件里。我习惯把Worker管理、Router管理、Transport管理分别封装成独立的模块。这样代码清晰,也方便后期维护。
嗯,Mediasoup的核心内容基本就这些。你想想看,其实它的设计思路很清晰——Worker管进程,Router管房间,Transport管连接,Producer/Consumer管媒体流。每一层各司其职,互不干扰。
这种分层设计的好处是,你可以根据业务需求灵活扩展。比如,一个会议一个Router,每个Router下管理多个Transport。如果需要支持大规模并发,就多开几个Worker,用负载均衡把客户端分散到不同的Worker上。
总结一下今天的内容:
- Mediasoup是一个高性能的SFU服务器,C++核心+Node.js控制层
- 架构分层:Worker → Router → Transport → Producer/Consumer
- 安装时注意C++编译环境和端口配置
- 创建Router时要配置支持的编解码器
- 创建Transport时要注意公网IP的配置
好了,这一章的内容就到这里。代码部分你可以直接拿去用,但建议根据你的实际场景调整配置参数。下一章我们会聊Mediasoup的高级特性,比如Simulcast和SVC的处理,以及如何做服务器级联。
公众号:蓝海资料掘金营,微信deep3321