70 行 Node 脚本跑通 WebRTC 数据透传:信令手动换,数据点对点
组成:一条 DataChannel 加两次手动信令
WebRTC 数据透传只需要三样东西:两端各一个 RTCPeerConnection、一条 DataChannel、一次 ICE 打洞。DataChannel 可以理解为一条点对点的双向字节管道,用法接近 TCP socket;一条连接上还能随时开很多条通道,各自独立,开新通道不需要重新走信令。
信令(交换连接参数的 SDP)不经过服务器,两端各打印一行 token,人工复制粘贴给对方。本文用纯 JS 实现的 werift 库,npm install werift 即装,无原生编译依赖。
完整脚本见附件 peer.min.js,全文 70 行。
信令交换与数据回传时序
人工搬运只有两次:A 的 offer token 交给 B,B 的 answer token 交回 A。连接建立后 DataChannel 全双工,任意一方发送,接收端原样回传一份副本。
分步最小实现
① 两端各建一个对等端
import { RTCPeerConnection } from "werift";
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }], // 公共 STUN 打洞
});② A:建通道,生成 offer,打印 token
// "chat" 只是给这条通道起的名字(对端能读到,用来区分通道用途),不做寻址
const dc = pc.createDataChannel("chat");
// 这行必须写在 createOffer 之前:werift 生成 offer 时如果一条通道都没有,
// 就不会启动 ICE gather,SDP 里连候选地址都没有,对端拿到的是废 token
await pc.setLocalDescription(await pc.createOffer());
// 返回时 ICE 候选收集(gathering)已完成,SDP 自带全部候选,之后无需再交换候选
console.log(encode(pc.localDescription)); // 一行 base64url,人工粘给 B③ B:收 offer,回 answer,打印 token
await pc.setRemoteDescription(decode(await ask("offer token: ")));
await pc.setLocalDescription(await pc.createAnswer());
console.log(encode(pc.localDescription)); // 一行 base64url,人工粘回 A
// 对端通道打开时的接收处理 pc.ondatachannel 见步⑤④ A:粘贴 answer token,连接自动建立
await pc.setRemoteDescription(decode(await ask("answer token: ")));
// ICE / DTLS / SCTP 自动完成,DataChannel 变为 open⑤ 收发与 loop back
B 侧从头到尾没有调用过 createDataChannel。A 建的通道会在 B 侧通过 ondatachannel 事件自动送进来,拿到它才能收发:
// B 侧:收到即原样回传。双方都回传会无限乒乓,所以只让一侧回传
function loopBack(channel) {
channel.onMessage.subscribe((msg) => channel.send(msg));
}
pc.ondatachannel = (event) => {
loopBack(event.channel);
};
// A 侧:打印对端回传的内容(A 自己建的通道直接用 dc 变量收发)
dc.onMessage.subscribe((msg) => console.log("[echo]", msg));
rl.on("line", (line) => {
if (dc && dc.readyState === "open") dc.send(line);
});token 的编解码各只有一行:
const encode = (d) =>
Buffer.from(JSON.stringify({ type: d.type, sdp: d.sdp }), "utf8").toString("base64url");
const decode = (line) =>
JSON.parse(Buffer.from(line.trim(), "base64url").toString("utf8"));运行方式
node peer.min.js answer # 终端 B:粘 offer token,打出 answer token
node peer.min.js offer # 终端 A:粘 answer token,然后直接打字两端打印 连接状态: connected 后,任意一侧输入文本回车,对端收到并原样回传。
werift 有两个专属的坑,脚本注释里有标注:setLocalDescription 的返回值里 sdp 为空,必须用 pc.localDescription;SessionDescription.type 是原型 getter,序列化 token 时必须显式取 {type, sdp},否则 answer 侧推断类型,DataChannel 建立不可靠。