搭建第一个信令服务器:为什么需要信令、WebSocket 基础、Node.js 搭建简易信令服务器、信令消息格式设计

好,咱们正式开始动手了。这一章,我们要搞定整个 WebRTC 应用的“神经中枢”——信令服务器。

你可能会有疑问:WebRTC 不是号称点对点通信吗?为什么还要搞个服务器?嗯,这个问题我当年刚接触时也困惑过。说白了,两个浏览器之间要建立连接,总得先互相认识一下吧?信令服务器就是那个“介绍人”。

为什么需要信令?

WebRTC 的媒体和数据通道确实是 P2P 的,但建立连接的过程,双方需要交换一些“元信息”。这些信息包括:

  • 会话描述(SDP):你的浏览器支持哪些编解码器?网络带宽多少?这些信息编码在 SDP 里。
  • 网络候选(ICE Candidate):你的公网 IP 是什么?用了什么端口?有没有 NAT 穿透能力?

这些信息必须通过一个双方都能访问的“中间人”来转发。这个中间人,就是信令服务器。

核心观点:信令服务器只负责“牵线搭桥”,不负责传输媒体数据。一旦连接建立,它就可以功成身退了。

我在项目中遇到过一种情况:有人试图把媒体数据也通过信令服务器转发,结果服务器瞬间崩溃。记住,信令只传控制信息,别让它干重活。

WebSocket 基础

选什么协议来实现信令?HTTP 行不行?行,但不推荐。为什么呢?

HTTP 是“请求-响应”模式,客户端问一句,服务器答一句。但信令通信往往是双向的、实时的——比如对方突然断线了,服务器得主动通知你。HTTP 做这个就很别扭,得轮询,效率低。

WebSocket 就不同了。它建立一次连接后,双方可以随时互发消息。这简直就是为信令量身定做的。

小提示:WebSocket 的 ws:// 协议和 HTTP 的 80 端口可以共用,部署起来很方便。生产环境建议用 wss://(加密版),就像 HTTPS 一样。

WebSocket 的消息格式很简单,就是字符串或二进制数据。我们通常用 JSON 字符串来封装信令消息,这样解析方便,结构清晰。

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

好,理论说完了,咱们直接上代码。我用 Node.js 加一个叫 ws 的库来搭服务器。这个库很轻量,几行代码就能跑起来。

先安装依赖:

npm init -y
npm install ws

然后写一个 server.js

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

// 存储所有连接的客户端
const clients = new Map();

wss.on('connection', (ws, req) => {
  const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
  clients.set(id, ws);
  console.log(`客户端 ${id} 已连接`);

  // 通知客户端它的 ID
  ws.send(JSON.stringify({ type: 'welcome', id }));

  ws.on('message', (data) => {
    try {
      const msg = JSON.parse(data);
      handleMessage(id, ws, msg);
    } catch (e) {
      console.error('消息解析失败:', e);
    }
  });

  ws.on('close', () => {
    clients.delete(id);
    console.log(`客户端 ${id} 已断开`);
    // 通知其他客户端
    broadcast({ type: 'peer-disconnected', id });
  });
});

function handleMessage(senderId, ws, msg) {
  switch (msg.type) {
    case 'join-room':
      // 加入房间逻辑
      break;
    case 'offer':
    case 'answer':
    case 'ice-candidate':
      // 转发给目标客户端
      const target = clients.get(msg.targetId);
      if (target) {
        target.send(JSON.stringify({ ...msg, senderId }));
      }
      break;
    default:
      console.log('未知消息类型:', msg.type);
  }
}

function broadcast(msg) {
  const str = JSON.stringify(msg);
  clients.forEach((ws) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(str);
    }
  });
}

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

这段代码做了几件事:

  • 每个客户端连接时,分配一个唯一 ID
  • 收到消息后,根据 type 字段做不同处理
  • 如果是 offeranswerice-candidate,就转发给目标客户端
  • 客户端断开时,通知其他人

注意:这个服务器没有做房间管理,所有客户端都在一个“大房间”里。实际项目中,你需要加房间逻辑,不然人一多就乱套了。

我曾经在一个 demo 里忘了加房间隔离,结果三个浏览器同时测试,信令全串了。嗯,从那以后我写信令服务器第一件事就是加房间。

信令消息格式设计

消息格式看起来简单,但设计不好后面会很难受。我建议统一用 JSON 对象,包含以下几个核心字段:

字段 类型 说明
type string 消息类型,如 offeranswerice-candidate
senderId string 发送者 ID
targetId string 目标客户端 ID(如果是点对点消息)
data object 具体内容,比如 SDP 或 ICE 候选
roomId string 房间 ID(可选,用于房间隔离)

举个例子,一个 offer 消息长这样:

{
  "type": "offer",
  "senderId": "abc123",
  "targetId": "def456",
  "roomId": "room-001",
  "data": {
    "sdp": "v=0\r\no=- ...",
    "type": "offer"
  }
}

为什么要把 senderIdtargetId 分开?因为服务器转发时需要知道谁发的、发给谁。你想想看,如果只传 targetId,服务器还得自己猜发送者是谁,容易出错。

我的习惯:所有消息都带上 roomId,哪怕当前只有一个房间。这样以后扩展多房间时,代码不用大改。

信令流程全景图

下面这张图展示了整个信令交互的流程,从客户端连接到建立 P2P 连接:

信令交互流程 客户端 A 信令服务器 客户端 B ① WebSocket 连接 ① WebSocket 连接 ② join-room ③ 通知 B 有新成员 ④ offer (含 SDP) ⑤ 转发 offer ⑥ answer (含 SDP) ⑦ 转发 answer ⑧ ice-candidate ⑨ 转发候选 ⑩ P2P 连接建立(媒体/数据通道)

从图上可以看得很清楚:信令服务器就是个“中转站”,所有信令消息都经过它转发。一旦 P2P 连接建立(第⑩步),数据就不再经过服务器了。

避坑指南:我曾经在生产环境中遇到一个问题——ICE 候选消息太多,信令服务器成了瓶颈。后来加了消息合并和限流才解决。如果你的应用有很多候选(比如有 10 个以上 ICE 候选),建议批量发送,别一条一条发。

好了,到这里你已经有了一个能跑的信令服务器。虽然简陋,但核心逻辑都在了。下一章我们会在这个基础上,真正把 WebRTC 的数据通道跑起来。


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