6、信令服务搭建(上):信令的作用与设计原则、WebSocket基础回顾、Node.js搭建简易信令服务器

好,咱们进入第六章。前面几章我们把 WebRTC 的底层原理、媒体协商、ICE 流程都捋了一遍。你可能会想:“道理我都懂,但代码怎么写?”

别急,这一章开始,我们就要动手搭一个真正的信令服务。我个人的习惯是,先搞清楚“为什么需要它”,再动手写代码。这样踩坑的时候,你才知道坑在哪。

6.1 信令到底在干什么?

先问一个问题:WebRTC 能不能自己找到对方?

答案是不能。WebRTC 只负责媒体数据的传输,它不负责“发现对方”。就像两个人打电话,你得先知道对方的号码,才能拨号。信令就是那个“拨号”的过程。

信令的核心职责:

  • 交换 SDP(会话描述协议)——告诉对方“我能收什么格式的视频/音频”
  • 交换 ICE 候选地址——告诉对方“我有哪些 IP 和端口可以连”
  • 管理房间/用户状态——谁进来了、谁出去了

我在项目中遇到过最典型的坑:有人把媒体数据也塞进信令通道里。结果信令服务器直接崩了。记住,信令只传控制信息,不传音视频流。这是设计红线。

6.2 信令服务的设计原则

说白了,信令服务就是一个“中间人”。它不需要多聪明,但必须可靠。我总结了三条原则,你写代码时照着来就行。

原则 说明 我的经验
轻量 只做消息转发,不做复杂业务逻辑 曾经把用户认证也塞进去,后来拆出来了
可靠 消息必须按顺序到达,不能丢 WebSocket 天然有序,但要注意重连机制
无状态 服务器不保存会话状态,挂了重启也不怕 用 Redis 存房间信息,服务器本身无状态

你想想看,如果信令服务器挂了,正在通话的两个人其实不受影响——因为媒体流是 P2P 的。但新用户就进不来了。所以信令服务的高可用,主要影响“接入”阶段。

6.3 WebSocket 基础回顾

WebRTC 的信令通道,最常用的就是 WebSocket。为什么不用 HTTP?因为 HTTP 是“请求-响应”模式,服务器没法主动推消息。而信令需要双向实时通信。

WebSocket 的核心就四个字:全双工。连接建立后,两端随时可以发消息。我刚开始用的时候,总觉得它跟 HTTP 差不多,其实完全不是一回事。

小提示:WebSocket 的 URL 以 ws:// 或 wss:// 开头。wss 是加密版本,生产环境必须用。我见过有人把 ws 直接部署到公网,结果被中间人攻击了。

来看一个最简单的 WebSocket 客户端代码:

// 客户端连接
const socket = new WebSocket('ws://localhost:8080');

socket.onopen = () => {
  console.log('连接成功');
  socket.send('你好,服务器');
};

socket.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

socket.onclose = () => {
  console.log('连接关闭');
};

嗯,这里要注意:onmessage 收到的是字符串。如果你要传 JSON,记得 JSON.parse。我曾经犯过一个错,直接拿 event.data 当对象用,结果报了一堆 undefined

6.4 Node.js 搭建简易信令服务器

好,动手时间。我们用 Node.js 的 ws 库,搭一个最简信令服务器。这个服务器只做一件事:收到消息后,广播给房间里的其他人。

先安装依赖:

npm init -y
npm install ws

然后写服务器代码:

const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

// 存储所有连接,按房间分组
const rooms = new Map();

server.on('connection', (ws) => {
  console.log('新用户连接');

  ws.on('message', (message) => {
    const data = JSON.parse(message);
    const { type, room, payload } = data;

    switch (type) {
      case 'join':
        // 用户加入房间
        if (!rooms.has(room)) {
          rooms.set(room, new Set());
        }
        rooms.get(room).add(ws);
        ws.room = room;
        console.log(`用户加入房间: ${room}`);
        break;

      case 'message':
        // 转发消息给同房间其他人
        const peers = rooms.get(ws.room);
        if (peers) {
          peers.forEach((peer) => {
            if (peer !== ws && peer.readyState === WebSocket.OPEN) {
              peer.send(JSON.stringify(payload));
            }
          });
        }
        break;

      case 'leave':
        // 用户离开房间
        if (ws.room && rooms.has(ws.room)) {
          rooms.get(ws.room).delete(ws);
          if (rooms.get(ws.room).size === 0) {
            rooms.delete(ws.room);
          }
        }
        break;
    }
  });

  ws.on('close', () => {
    // 断开时自动离开房间
    if (ws.room && rooms.has(ws.room)) {
      rooms.get(ws.room).delete(ws);
      if (rooms.get(ws.room).size === 0) {
        rooms.delete(ws.room);
      }
    }
    console.log('用户断开');
  });
});

console.log('信令服务器已启动,端口: 8080');

注意:这段代码只是演示原理。生产环境你还需要处理:

  • 心跳检测(防止僵尸连接)
  • 消息格式校验(防止恶意数据)
  • 房间密码/权限控制

我曾经在生产环境忘了加心跳,结果一堆死连接占着房间名额,新用户进不来。

6.5 信令流程可视化

为了让你更直观地理解信令的流转过程,我画了一张图。这张图展示了两个浏览器通过信令服务器交换 SDP 和 ICE 候选的完整流程。

WebRTC 信令交换流程 浏览器 A 信令服务器 浏览器 B ① 创建 Offer ② 转发 Offer ③ 返回 Answer ④ 转发 Answer ⑤ ICE 候选 ⑥ ICE 候选 ⑦ ICE 候选 ⑧ ICE 候选 P2P 连接建立,开始通话 音视频数据流(不经过服务器)

这张图你看懂了吗?核心就两点:

  • SDP 交换(①-④)是“我能提供什么、我需要什么”的协商
  • ICE 候选交换(⑤-⑧)是“我在哪、怎么连我”的地址交换

等这两步走完,浏览器之间就直接建立 P2P 连接了。信令服务器功成身退,后面的事它就不管了。

6.6 避坑指南

最后分享几个我踩过的坑,你写代码时注意避开:

  1. 消息顺序问题:WebSocket 虽然有序,但如果你同时发多条消息,接收端可能乱序处理。建议每条消息带一个序列号。
  2. 房间清理:用户断线时一定要清理房间。我曾经忘了做,结果房间里堆满了死连接,新用户加不进来。
  3. 消息大小限制:SDP 有时候会很大(尤其是带多个视频轨),WebSocket 默认限制是 1MB。如果超过,需要分片发送。
  4. 不要信任客户端:所有消息格式都要做校验。我见过有人故意发畸形数据,导致服务器崩溃。

好了,这一章我们搞清楚了信令的作用、设计原则,还用 Node.js 搭了一个能跑起来的信令服务器。下一章我们会在这个基础上,加入房间管理和更完善的消息路由。你先把这段代码跑起来,感受一下信令的流转过程。

练习建议:打开两个浏览器窗口,都连上你的信令服务器。在控制台手动发送 joinmessage 消息,看看能不能互相收到。这是理解信令最好的方式。


公众号:蓝海资料掘金营,微信deep3321