4、信令服务器入门:信令的作用、WebSocket基础、搭建简易信令服务器(Node.js)、客户端连接信令服务器
好,咱们正式开始动手了。
前面几章我们把 WebRTC 的整体架构、通话流程、以及 Android 端的核心 API 都过了一遍。但有个关键问题一直悬着:两个浏览器(或 App)之间,到底怎么互相找到对方?
你想想看,WebRTC 的媒体数据走的是 P2P 通道,但建立这个通道之前,双方得先交换一些“元信息”——比如你的 IP 地址、端口、支持的编解码器、加密参数等等。这些信息不能凭空传递,必须有个中间人帮忙转发。
这个中间人,就是信令服务器。
4.1 信令到底在干什么?
说白了,信令就是“握手前的握手”。WebRTC 本身不定义信令协议,它只负责媒体传输。谁来帮双方交换 SDP(会话描述协议)和 ICE Candidate(网络候选地址)?就是信令服务器。
我在项目中遇到过不少新手,上来就写 PeerConnection,结果发现两端根本连不上。为什么?因为没走信令交换。你想想看,A 不知道 B 在哪,B 不知道 A 用什么编码,这怎么连?
信令的核心任务就三个:
- 交换 SDP:告诉对方“我能收什么格式的视频、音频,我的加密参数是什么”。
- 交换 ICE Candidate:告诉对方“我有哪些可能的网络路径可以连过来”。
- 管理房间/用户:谁加入了会议,谁离开了,谁在发言。
重要提醒:信令服务器不传输任何音视频数据。它只传控制信令。别把媒体流往信令服务器上塞,那会压垮它的。
嗯,这里要注意:信令协议你可以自己定义。WebRTC 没有规定你必须用 JSON、XML 还是二进制。我个人习惯用 JSON,简单直观,调试方便。
4.2 WebSocket 基础——为什么选它?
信令传输需要双向、实时、低延迟的通道。HTTP 是请求-响应模式,服务器不能主动推消息给客户端。轮询?太浪费资源了。
WebSocket 就完美契合这个场景。它建立在 TCP 之上,一旦连接建立,双方可以随时互发消息。而且它和 HTTP 共用 80/443 端口,不会被防火墙拦死。
我曾经在一个项目中用过 Socket.IO,后来发现它太重了。对于信令这种轻量级场景,原生 WebSocket 完全够用,而且 Node.js 对 WebSocket 的支持非常成熟。
WebSocket 的几个关键点:
- 握手阶段:客户端发 HTTP Upgrade 请求,服务器返回 101 Switching Protocols。
- 数据传输:之后就是全双工通信,消息可以是文本或二进制。
- 心跳机制:WebSocket 本身没有内置心跳,需要自己实现 ping/pong 来检测连接是否存活。
小技巧:调试 WebSocket 时,可以用 Chrome 的开发者工具 -> Network -> WS 标签,直接看到收发消息的内容。非常方便。
4.3 搭建简易信令服务器(Node.js)
好,咱们直接动手。我会用 Node.js + ws 库搭建一个最简的信令服务器。这个服务器只做一件事:转发消息。
先初始化项目:
mkdir simple-signaling
cd simple-signaling
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 clientId = Date.now() + Math.random().toString(36).substr(2, 5);
console.log(`[+] 客户端 ${clientId} 已连接`);
// 发送自己的 ID 给客户端
ws.send(JSON.stringify({ type: 'welcome', clientId }));
// 接收消息
ws.on('message', (message) => {
const data = JSON.parse(message);
console.log(`[<-] 来自 ${clientId}:`, data);
switch (data.type) {
case 'join':
// 加入房间
clients.set(clientId, { ws, room: data.room });
// 通知房间内其他用户
broadcastToRoom(data.room, {
type: 'user-joined',
clientId
}, clientId);
break;
case 'offer':
case 'answer':
case 'ice-candidate':
// 转发给目标客户端
const target = clients.get(data.target);
if (target) {
target.ws.send(JSON.stringify({
...data,
sender: clientId
}));
}
break;
case 'leave':
clients.delete(clientId);
broadcastToRoom(data.room, {
type: 'user-left',
clientId
}, clientId);
break;
}
});
ws.on('close', () => {
console.log(`[-] 客户端 ${clientId} 断开`);
// 清理
const client = clients.get(clientId);
if (client) {
broadcastToRoom(client.room, {
type: 'user-left',
clientId
}, clientId);
clients.delete(clientId);
}
});
});
function broadcastToRoom(room, message, excludeId) {
clients.forEach((client, id) => {
if (id !== excludeId && client.room === room) {
client.ws.send(JSON.stringify(message));
}
});
}
console.log('信令服务器已启动,端口: 8080');
这个服务器逻辑很简单:
- 每个客户端连接后,分配一个唯一 ID。
- 客户端可以加入某个房间(room)。
- offer、answer、ice-candidate 这三种消息会被直接转发给目标客户端。
- 用户离开时,通知同房间其他人。
注意:这个服务器没有做任何安全校验。生产环境一定要加 Token 验证、消息校验、以及连接数限制。我曾经见过一个没做限制的信令服务器,被恶意客户端刷了几万个连接,直接 OOM 了。
4.4 客户端连接信令服务器
Android 端怎么连?用 OkHttp 的 WebSocket 支持就行。我一般封装一个 SignalingClient 类:
class SignalingClient {
private var webSocket: WebSocket? = null
private var clientId: String? = null
fun connect(serverUrl: String) {
val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // WebSocket 需要长连接
.build()
val request = Request.Builder()
.url(serverUrl)
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d("Signaling", "连接成功")
}
override fun onMessage(webSocket: WebSocket, text: String) {
val json = JSONObject(text)
when (json.getString("type")) {
"welcome" -> {
clientId = json.getString("clientId")
Log.d("Signaling", "我的ID: $clientId")
}
"user-joined" -> {
// 有人加入房间,开始创建 PeerConnection
}
"offer" -> {
// 收到对方的 SDP offer
}
"answer" -> {
// 收到对方的 SDP answer
}
"ice-candidate" -> {
// 收到对方的 ICE 候选地址
}
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e("Signaling", "连接失败: ${t.message}")
// 重连逻辑
}
})
}
fun sendMessage(json: JSONObject) {
webSocket?.send(json.toString())
}
fun disconnect() {
webSocket?.close(1000, "正常关闭")
}
}
使用时,在 Activity 或 Fragment 中:
val signaling = SignalingClient()
signaling.connect("ws://192.168.1.100:8080")
// 加入房间
val joinMsg = JSONObject().apply {
put("type", "join")
put("room", "room-123")
}
signaling.sendMessage(joinMsg)
嗯,这里有个坑:Android 9 以上默认禁止明文 WebSocket(ws://),必须用 wss:// 或者在 network_security_config.xml 里放行。我建议开发阶段直接用 10.0.2.2 连接本机模拟器,或者用真机连同一局域网。
4.5 本章知识体系
下面这张图把信令的整个流程串起来了:
整个流程其实就三步:
- 连接信令服务器:双方各自建立 WebSocket 连接。
- 交换元数据:通过信令服务器转发 SDP 和 ICE Candidate。
- 建立 P2P 通道:元数据交换完成后,媒体数据直接走 P2P,信令服务器功成身退。
避坑指南:我曾经在信令服务器上犯过一个低级错误——把 SDP 和 ICE Candidate 当成普通文本直接转发,结果遇到超大 SDP(包含多个视频轨道)时,消息被截断了。后来我改用分片发送,或者用 JSON 数组打包。记住:信令消息不要超过 WebSocket 的单帧限制(通常是 64KB)。
好了,信令服务器这块咱们就讲到这里。你现在应该能自己搭一个简单的信令服务器,并且让 Android 客户端连上它了。下一章我们会把信令和 PeerConnection 结合起来,真正跑通一个视频通话。
公众号:蓝海资料掘金营,微信deep3321