2、搭建信令服务器:信令的作用、WebSocket基础、Node.js搭建简易信令服务器、信令消息格式设计

好,咱们进入第二章。这一章要聊的是信令服务器——说白了,就是让两个浏览器互相找到对方、商量好怎么通话的那个“中间人”。

我记得刚接触WebRTC时,有个很大的困惑:明明WebRTC是点对点的,为什么还需要一个服务器?后来踩了坑才明白,没有信令服务器,两个Peer根本连对方在哪都不知道。就像你想打电话,但不知道对方的号码——这电话怎么打?

2.1 信令的作用

信令,本质上就是“握手前的悄悄话”。它负责传递三类关键信息:

  • 会话控制消息:谁发起通话、谁挂断、谁拒绝
  • 媒体协商信息:SDP(Session Description Protocol),包含编解码器、分辨率、IP端口等
  • 网络路径信息:ICE候选者(Candidate),告诉对方“我有哪些可能的网络入口”

你想想看,浏览器A要和浏览器B通话,A得先告诉B:“我支持H264编码,我的IP是192.168.1.100,端口3478”。这些信息怎么传?WebRTC没规定,所以需要我们自己搭信令服务器来转发。

核心要点:信令服务器只负责转发,不负责理解消息内容。它就是个“邮递员”,把A的信封原封不动递给B。

我在项目中遇到过一种情况:有人把信令服务器和媒体服务器混为一谈。信令服务器不处理音视频数据,只处理控制消息。如果它挂了,通话不会立刻断,但新用户进不来,也挂不了电话——嗯,这很尴尬。

2.2 WebSocket基础

为什么选WebSocket?因为HTTP是“请求-响应”模式,服务器不能主动给客户端发消息。而WebSocket是全双工的,服务器可以随时“推”数据给客户端。信令场景下,当B加入房间时,服务器要主动通知A——“有新伙伴来了”,这用HTTP轮询就太笨了。

WebSocket的握手很简单:客户端发一个HTTP Upgrade请求,服务器同意后,连接就升级为WebSocket。之后双方可以互相发送文本或二进制帧。

小技巧:我个人习惯用 ws:// 协议,生产环境一定要用 wss://(WebSocket Secure),否则浏览器会报安全错误。

一个最简单的WebSocket客户端示例:

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

socket.onopen = () => {
  console.log('连接已建立');
  socket.send('Hello Server!');
};

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

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

服务端呢?Node.js里用 ws 库最省事。我刚开始用 socket.io,后来发现太重了,信令场景只需要最基础的WebSocket就够了。

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

好,咱们动手搭一个。先安装依赖:

npm init -y
npm install ws

然后写一个简单的信令服务器:

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

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

server.on('connection', (ws) => {
  // 给每个客户端分配一个唯一ID
  const clientId = Date.now().toString(36);
  clients.set(clientId, ws);

  console.log(`客户端 ${clientId} 已连接`);

  // 收到消息后广播给其他客户端
  ws.on('message', (message) => {
    const data = JSON.parse(message);
    console.log('收到消息:', data);

    // 根据消息类型做不同处理
    switch (data.type) {
      case 'join':
        // 加入房间逻辑
        broadcast(clientId, {
          type: 'user-joined',
          userId: clientId
        });
        break;
      case 'offer':
        // 转发SDP offer
        sendTo(data.targetId, {
          type: 'offer',
          sdp: data.sdp,
          from: clientId
        });
        break;
      case 'candidate':
        // 转发ICE候选者
        sendTo(data.targetId, {
          type: 'candidate',
          candidate: data.candidate,
          from: clientId
        });
        break;
      default:
        console.log('未知消息类型:', data.type);
    }
  });

  ws.on('close', () => {
    console.log(`客户端 ${clientId} 已断开`);
    clients.delete(clientId);
    broadcast(clientId, {
      type: 'user-left',
      userId: clientId
    });
  });
});

function broadcast(senderId, message) {
  clients.forEach((ws, id) => {
    if (id !== senderId && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(message));
    }
  });
}

function sendTo(targetId, message) {
  const ws = clients.get(targetId);
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(message));
  }
}

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

注意:这个服务器没有做房间管理,所有消息都是全局广播。生产环境需要加房间ID,只转发给同房间的客户端。

我曾经犯过一个低级错误:忘记做 JSON.parse,直接拿字符串当对象用,结果 data.type 一直是 undefined。排查了半天才发现——嗯,这种坑踩一次就记住了。

2.4 信令消息格式设计

消息格式设计得好,后面写代码就顺畅。我建议统一用JSON,结构清晰,扩展方便。

基本的消息结构:

{
  "type": "消息类型",
  "from": "发送者ID",
  "to": "接收者ID(可选)",
  "payload": {}  // 具体数据
}

常用的消息类型:

消息类型 方向 说明
join 客户端→服务器 请求加入房间
user-joined 服务器→客户端 通知有新用户加入
offer 客户端→客户端 发送SDP offer
answer 客户端→客户端 回复SDP answer
candidate 客户端→客户端 发送ICE候选者
leave 客户端→服务器 离开房间
user-left 服务器→客户端 通知有用户离开

举个例子,A向B发送offer:

{
  "type": "offer",
  "from": "user-A",
  "to": "user-B",
  "payload": {
    "sdp": "v=0\r\no=- 123456 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1\r\n..."
  }
}

ICE候选者消息:

{
  "type": "candidate",
  "from": "user-A",
  "to": "user-B",
  "payload": {
    "candidate": "candidate:1 1 UDP 2122252543 192.168.1.100 3478 typ host",
    "sdpMid": "0",
    "sdpMLineIndex": 0
  }
}

避坑指南:我曾经把SDP和ICE候选者放在同一个消息里发,结果对方解析时顺序乱了,导致连接失败。后来老老实实分开发,先发offer/answer,再逐个发candidate。顺序很重要!

下面这张图展示了信令交互的完整流程:

信令交互流程 浏览器 A 信令服务器 浏览器 B ① join ② user-joined ③ join ④ user-joined ⑤ offer ⑥ offer ⑦ answer ⑧ answer ⑨ candidate ⑩ candidate ⑪ candidate ⑫ candidate 信令服务器只负责转发,不解析SDP和ICE内容

你看,整个流程就是“A告诉服务器,服务器告诉B,B回复,服务器再告诉A”。信令服务器就是个透明的管道,不修改消息内容。

嗯,到这里信令服务器的核心内容就讲完了。记住三点:信令只转发不处理、WebSocket全双工通信、消息格式统一用JSON。下一节咱们会把这些代码整合到实际项目中,跑通一个完整的通话流程。


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