• 示例工程

【免费下载链接】WebRTC-Experiment

WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!!

项目地址: https://gitcode.com/gh_mirrors/we/WebRTC-Experiment
点击查看 免费下载

socketio-over-nodejs 是 WebRTC-Experiment 仓库中一套以 Socket.io 驱动的 Node.js 信令服务器参考实现:它利用 Socket.io 的命名空间(namespace)机制,为每个 WebRTC 会话(房间)动态创建独立的信令通道,用于交换参与请求、房间信息、SDP 与 ICE 候选等信令数据。本文将以 socketio-over-nodejs/README.md 为主线,结合 server.js 源码,完整讲解服务端部署、客户端 openSignalingChannel/openSocket 接入、房间存在性检测(Presence Detection)三大部分,让你可以在一小时内把这套信令服务器跑起来并接入自己的 WebRTC 应用。

一、项目概览:为什么 WebRTC 需要 Socket.io 信令服务

WebRTC 的媒体数据(音视频流、数据通道)走 P2P 通道,但连接建立之前的协商过程(交换 SDP 描述、ICE 候选)必须依赖一个信令通道。socketio-over-nodejs 就是为这个信令环节设计的轻量级服务端实现,其核心思路是:

  • 一个 WebRTC 会话对应一个 Socket.io 命名空间:客户端通过 io.connect(URL).emit('new-channel', {channel, sender}) 请求创建新命名空间,此后该会话内所有信令消息都在这个专属命名空间内广播;
  • 服务端不做业务逻辑:只负责把消息路由、广播给同命名空间内的其他参与者,SDP/ICE 的语义处理完全由客户端(如 RTCMultiConnection、PeerConnection.js)完成;
  • 天然适配 HTTPS:WebRTC 的 getUserMedia 要求在安全上下文中运行,因此 README 与 server.js 均以 HTTPS 服务器形态给出。

从 package.json 可以看到,该 npm 包(socketio-over-nodejs,版本 3.0.x)依赖 socket.io 0.9.x,scripts.start 为 node server.js,即安装后即可直接以 server.js 作为入口运行。

二、服务端:理解 server.js 的信令核心

2.1 HTTPS 服务器与证书

server.js 使用 Node 内置 https 模块创建服务器,并加载 TLS 证书:

var options = {
    // key: fs.readFileSync('fake-keys/privatekey.pem'),
    // cert: fs.readFileSync('fake-keys/certificate.pem')
    key: fs.readFileSync('/etc/letsencrypt/live/webrtcweb.com/privkey.pem'),
    cert: fs.readFileSync('/etc/letsencrypt/live/webrtcweb.com/fullchain.pem')
};
var app = require('https').createServer(options, function(request, response) { ... });

两点实操提示:

  1. 务必替换为自己的证书。仓库在 fake-keys/ 目录下提供了 privatekey.pem 与 certificate.pem 两个占位私钥/证书文件(仅供本地测试),源码注释也明确提醒 "don't forget to use your own keys!"。正式部署时,应使用自己域名签发的证书,例如 Let's Encrypt 生成的路径(如上源码所示)。
  2. 端口通过环境变量配置:文件末尾 app.listen(process.env.PORT || 9559) 说明默认监听 9559 端口,也支持通过 PORT 环境变量覆盖,便于部署到 Heroku 等 PaaS 平台。启动后控制台会打印 Please open SSL URL: https://localhost:9559/。

2.2 Socket.io 挂载与传输配置

var io = require('socket.io').listen(app, {
    log: true,
    origins: '*:*'
});
io.set('transports', [
    // 'websocket',
    'xhr-polling',
    'jsonp-polling'
]);

这里 origins: '*:*' 表示允许任意来源跨域连接(便于各域名页面接入),传输方式被限定为 xhr-polling 与 jsonp-polling 两种轮询式传输(源码中将 websocket 注释掉)。需要说明的是:这是针对 socket.io 0.9.x 时代网络环境(如代理限制 WebSocket)的保守配置,如果你的部署环境允许,也可以放开 websocket 传输以获得更低延迟。

2.3 频道注册、Presence 与命名空间路由

服务端维护了一个内存对象 channels = {} 来登记所有频道(房间),核心逻辑在 server.js:

socket.on('new-channel', function (data) {
    if (!channels[data.channel]) {
        initiatorChannel = data.channel;   // 记录频道创建者
    }
    channels[data.channel] = data.channel;
    onNewNamespace(data.channel, data.sender);  // 动态创建命名空间
});

socket.on('presence', function (channel) {
    var isChannelPresent = !! channels[channel];
    socket.emit('presence', isChannelPresent);   // 回传频道是否存在
});

socket.on('disconnect', function (channel) {
    if (initiatorChannel) {
        delete channels[initiatorChannel];       // 创建者断开则注销频道
    }
});
  • new-channel:收到创建请求后把频道名登记进 channels,并调用 onNewNamespace 为它动态注册 /channel 命名空间;第一个创建者会被记录为 initiatorChannel;
  • presence:查询某频道是否已存在,供客户端决定是加入已有会话还是创建新会话(见本文第四节);
  • disconnect:当频道创建者断开连接时,从 channels 中删除该频道,实现房间生命周期的回收。

onNewNamespace 函数(server.js)负责在每个命名空间内广播消息与通知用户离开:

function onNewNamespace(channel, sender) {
    io.of('/' + channel).on('connection', function (socket) {
        var username;
        socket.on('message', function (data) {
            if (data.sender == sender) {
                if(!username) username = data.data.sender;
                socket.broadcast.emit('message', data.data);  // 广播给同房间其他成员
            }
        });
        socket.on('disconnect', function() {
            if(username) {
                socket.broadcast.emit('user-left', username);  // 通知其他成员有人离开
                username = null;
            }
        });
    });
}

关键机制:

  • 命名空间内仅使用 socket.broadcast.emit(向除自己外的同房间成员广播),因此无需额外实现"房间成员列表";
  • 通过记录首个发送消息者的 username 来广播 user-left 事件,让其他客户端可以移除对应的视频元素或做 UI 清理。

三、客户端接入:openSignalingChannel 与 openSocket

3.1 面向 RTCMultiConnection 的 openSignalingChannel

README 指出:在 ui.js 文件或各类库中可以找到 openSocket/openSignalingChannel 方法。以 RTCMultiConnection 为例,其 v2 API 会通过 connection.setCustomSocketHandler(openSignalingChannel) 挂载自定义信令处理器(见 RTCMultiConnection/dev/enableV2Api.js),随后调用你在 connection.openSignalingChannel 中定义的方法。

README 给出的 openSignalingChannel 完整示例:

var SIGNALING_SERVER = 'https://socketio-over-nodejs2.herokuapp.com:443/';
connection.openSignalingChannel = function(config) {
   var channel = config.channel || this.channel || 'default-namespace';
   var sender = Math.round(Math.random() * 9999999999) + 9999999999;

   io.connect(SIGNALING_SERVER).emit('new-channel', {
      channel: channel,
      sender : sender
   });

   var socket = io.connect(SIGNALING_SERVER + channel);
   socket.channel = channel;

   socket.on('connect', function () {
      if (config.callback) config.callback(socket);
   });

   socket.send = function (message) {
        socket.emit('message', {
            sender: sender,
            data  : message
        });
    };

   socket.on('message', config.onmessage);
};

对照服务端代码,这段客户端的执行流程与 server.js 一一对应:

  1. io.connect(SIGNALING_SERVER).emit('new-channel', {channel, sender}) 触发服务端 new-channel 处理器,登记频道并动态创建命名空间;
  2. io.connect(SIGNALING_SERVER + channel) 连接到刚创建的专属命名空间 /channel;
  3. 封装 socket.send(message),将其包装成 {sender, data} 后经 message 事件发出——服务端据此识别消息来源并 broadcast.emit 给同房间其他成员;
  4. 收到命名空间上的 message 事件时,回调 config.onmessage,将远端信令数据(SDP/ICE/参与请求等)交给上层连接处理。

config.channel(房间名)可来自调用方,也可从 this.channel 取得,缺省回退到 'default-namespace',保证单房间场景无需额外传参即可工作。

3.2 面向自定义库的 openSocket

openSocket 是 openSignalingChannel 的姊妹形态,使用方式几乎一致(见 README):

var config = {
    openSocket: function (config) {
        var SIGNALING_SERVER = 'https://socketio-over-nodejs2.herokuapp.com:443/';

        config.channel = config.channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, '');
        var sender = Math.round(Math.random() * 999999999) + 999999999;

        io.connect(SIGNALING_SERVER).emit('new-channel', {
            channel: config.channel,
            sender: sender
        });

        var socket = io.connect(SIGNALING_SERVER + config.channel);
        socket.channel = config.channel;
        socket.on('connect', function () {
            if (config.callback) config.callback(socket);
        });

        socket.send = function (message) {
            socket.emit('message', {
                sender: sender,
                data: message
            });
        };

        socket.on('message', config.onmessage);
    }
};

与上一节的区别仅在于默认频道名的生成方式:openSocket 用当前页面 URL 去掉 / : # % . [ ] 等特殊字符后作为默认 channel,这样同一页面地址天然对应同一信令房间,适合"打开同一页面即可互相连接"的演示型应用。

3.3 消息流总结

把上述两端拼起来,一条完整信令链路的形态是:

客户端A                        服务端(server.js)                  客户端B
  |-- new-channel{channel,sender} -->|
  |                                  |-- 登记channels、创建/channel命名空间
  |-- connect(/channel) ----------->|                            |
  |                                  |<-- connect(/channel) -----|
  |-- send({sender,data}) --------->|                            |
  |                                  |-- broadcast.emit('message',data) -->|
  |<-- message事件(远端数据) --------|                            |

README 中明确说明:io.connect(URL).emit('new-channel') 会启动一个全新的命名空间,它被"私有或公开地"用于传输/交换房间详情、参与请求、SDP、ICE 等一切协商数据——这正是该方案"一房间一命名空间"的核心价值。

四、Presence Detection:检测房间是否已存在

在实际应用中,发起方需要判断"目标房间是否已有人创建",决定自己是创建新会话还是加入既有会话。README 给出的完整实现:

var SIGNALING_SERVER = 'https://socketio-over-nodejs2.herokuapp.com:443/';
function testChannelPresence(channel) {
    var socket = io.connect(SIGNALING_SERVER);
    socket.on('presence', function (isChannelPresent) {
        console.log('is channel present', isChannelPresent);
        if (!isChannelPresent) startNewSession();
    });
    socket.emit('presence', channel);
}

// test whether default channel already created or not!
testChannelPresence('default-channel');

对应服务端 server.js 的实现:socket.on('presence', function (channel) { var isChannelPresent = !! channels[channel]; socket.emit('presence', isChannelPresent); })——服务端在内存 channels 中查表并回传布尔值。

使用注意:

  • 服务端以 channels 内存对象为准,若服务器重启,所有房间记录清空,客户端会收到 false 并重新创建会话;
  • 该机制依赖"创建者在线"(见 disconnect 时删除 initiatorChannel),因此它是轻量的"会话存活性"探测,而非持久化房间注册表。

五、部署与接入步骤

5.1 本地运行

npm install socketio-over-nodejs
node server.js
# 输出:Please open SSL URL: https://localhost:9559/

也可以从本仓库直接运行:socketio-over-nodejs/package.json 定义了 npm start 即 node server.js。注意默认代码加载的是 /etc/letsencrypt/live/webrtcweb.com/ 下的正式证书,本地测试请按 server.js 注释提示改用 fake-keys/ 下的 privatekey.pem 与 certificate.pem。

5.2 接入自有 WebRTC 应用

  1. 页面引入 socket.io 客户端脚本(仓库内配套示例见 socket.io/PeerConnection.js 与 demos/client-side-socket-io.html,后者展示了如何用 io.connect 连接信令通道并配合 RTCPeerConnection 交换分段 SDP 与 ICE 候选);
  2. 将 SIGNALING_SERVER 替换为你部署的信令服务器地址;
  3. 按第三节示例实现 connection.openSignalingChannel 或 config.openSocket;
  4. 需要判断房间存活性时,按第四节实现 presence 检测。

六、协议与许可

socketio-over-nodejs 采用 MIT 许可协议,版权归 Muaz Khan(详见 README 末节)。这意味着你可以自由地将这套信令方案集成进商业或开源项目中,只需保留版权声明即可。

  • 示例工程

【免费下载链接】WebRTC-Experiment

WebRTC, WebRTC and WebRTC. Everything here is all about WebRTC!!

项目地址: https://gitcode.com/gh_mirrors/we/WebRTC-Experiment
点击查看 免费下载
Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐