You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

求基于TURN服务器的无信令纯WebRTC客户端-服务器数据通道示例

无信令(极简SDP交换)WebRTC Data Channel 实现(基于coturn)

首先得跟你说清楚:严格来说,WebRTC没办法完全跳过SDP和ICE候选的交换(这其实就是"信令"的核心),但我们可以把信令简化到极致——不用专门搞WebSocket或Socket.io这类实时信令服务器,只用简单的HTTP请求来交换必要的SDP数据,同时用coturn作为TURN中继解决NAT穿透问题,刚好能满足你每个客户端和服务器建立专属Data Channel的需求。


1. 配置coturn服务器

先修改coturn的配置文件turnserver.conf,关键配置项如下:

  • listening-port=3478 # 监听的TURN端口
  • external-ip=你的公网IP/你的内网IP # 如果服务器在NAT后,需要指定公网IP和内网IP的映射
  • relay-ip=你的内网IP # 中继服务绑定的内网IP
  • user=webrtc:secret123 # 预共享的用户名密码,客户端和服务器都要用这个
  • no-tls no-dtls # 测试阶段禁用TLS/DTLS,简化配置(生产环境务必开启)
  • verbose # 开启日志,方便调试

启动coturn服务:

turnserver -c /etc/turnserver.conf

2. 服务器端实现(Node.js)

我们用wrtc库在Node.js中实现WebRTC对等端,同时用Express提供简单的HTTP接口来交换SDP(这就是极简的"信令",没有实时通信)。

首先安装依赖:

npm install wrtc express cors

服务器代码示例:

const express = require('express');
const cors = require('cors');
const wrtc = require('wrtc');
const { RTCPeerConnection } = wrtc;

const app = express();
app.use(cors());
app.use(express.json());

// coturn ICE配置,和客户端保持一致
const iceConfig = {
  iceServers: [
    {
      urls: 'turn:你的TURN服务器IP:3478',
      username: 'webrtc',
      credential: 'secret123'
    }
  ]
};

// 存储每个客户端的PeerConnection和数据通道,用客户端ID作为键
const clientConnections = new Map();

// 生成唯一客户端ID的工具函数
function generateClientId() {
  return Math.random().toString(36).substring(2, 10);
}

// 服务器创建PeerConnection并生成Offer
app.get('/offer', (req, res) => {
  const clientId = generateClientId();
  const pc = new RTCPeerConnection(iceConfig);

  // 创建专属Data Channel
  const dataChannel = pc.createDataChannel(`client-${clientId}`);

  // 监听Data Channel事件
  dataChannel.onopen = () => {
    console.log(`客户端 ${clientId} 的数据通道已打开`);
    dataChannel.send(`欢迎连接服务器,你的专属ID是 ${clientId}`);
  };

  dataChannel.onmessage = (event) => {
    console.log(`收到客户端 ${clientId} 的消息: ${event.data}`);
    // 回复客户端
    dataChannel.send(`服务器收到: ${event.data}`);
  };

  dataChannel.onclose = () => {
    console.log(`客户端 ${clientId} 的数据通道已关闭`);
    clientConnections.delete(clientId);
    pc.close();
  };

  // 监听ICE候选(关闭Trickle ICE,一次性收集所有候选)
  pc.onicecandidate = (event) => {
    if (!event.candidate) {
      // 所有ICE候选收集完成,发送Offer给客户端
      res.json({
        clientId,
        sdp: pc.localDescription.sdp
      });
    }
  };

  // 创建Offer,关闭音频/视频流(我们只需要数据通道)
  pc.createOffer({ iceRestart: false, offerToReceiveAudio: false, offerToReceiveVideo: false })
    .then(offer => pc.setLocalDescription(offer))
    .catch(err => {
      console.error('生成Offer失败:', err);
      res.status(500).send('生成Offer失败');
    });

  // 存储连接
  clientConnections.set(clientId, { pc, dataChannel });
});

// 接收客户端的Answer,完成连接建立
app.post('/answer', (req, res) => {
  const { clientId, sdp } = req.body;
  const connection = clientConnections.get(clientId);

  if (!connection) {
    return res.status(404).send('客户端ID不存在');
  }

  const { pc } = connection;
  pc.setRemoteDescription(new wrtc.RTCSessionDescription({ type: 'answer', sdp }))
    .then(() => {
      res.send('连接已建立');
    })
    .catch(err => {
      console.error('设置Answer失败:', err);
      res.status(500).send('设置Answer失败');
    });
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

3. 客户端实现(浏览器端)

客户端代码直接在HTML中编写,通过HTTP请求获取服务器的Offer,生成Answer后发送给服务器,即可建立专属Data Channel。

<!DOCTYPE html>
<html>
<head>
  <title>WebRTC Data Channel 客户端</title>
</head>
<body>
  <h1>WebRTC Data Channel 测试</h1>
  <input type="text" id="messageInput" placeholder="输入消息">
  <button onclick="sendMessage()">发送消息</button>
  <div id="messages"></div>

  <script>
    // coturn ICE配置,和服务器保持一致
    const iceConfig = {
      iceServers: [
        {
          urls: 'turn:你的TURN服务器IP:3478',
          username: 'webrtc',
          credential: 'secret123'
        }
      ]
    };

    let pc;
    let dataChannel;
    let clientId;

    // 初始化连接
    async function initConnection() {
      // 获取服务器的Offer
      const response = await fetch('http://你的服务器IP:3000/offer');
      const { clientId: id, sdp } = await response.json();
      clientId = id;

      pc = new RTCPeerConnection(iceConfig);

      // 监听服务器创建的专属Data Channel
      pc.ondatachannel = (event) => {
        dataChannel = event.channel;
        dataChannel.onopen = () => {
          addMessage('数据通道已打开');
        };
        dataChannel.onmessage = (event) => {
          addMessage(`服务器: ${event.data}`);
        };
        dataChannel.onclose = () => {
          addMessage('数据通道已关闭');
        };
      };

      // 设置服务器的Offer
      await pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp }));

      // 生成Answer
      const answer = await pc.createAnswer();
      await pc.setLocalDescription(answer);

      // 发送Answer给服务器
      await fetch('http://你的服务器IP:3000/answer', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          clientId,
          sdp: pc.localDescription.sdp
        })
      });
    }

    // 发送消息
    function sendMessage() {
      const input = document.getElementById('messageInput');
      const message = input.value.trim();
      if (message && dataChannel && dataChannel.readyState === 'open') {
        dataChannel.send(message);
        addMessage(`你: ${message}`);
        input.value = '';
      }
    }

    // 添加消息到页面
    function addMessage(text) {
      const messagesDiv = document.getElementById('messages');
      const p = document.createElement('p');
      p.textContent = text;
      messagesDiv.appendChild(p);
    }

    // 页面加载完成后初始化连接
    window.onload = initConnection;
  </script>
</body>
</html>

关键说明

  • 这里的"无信令"是指没有使用专门的实时信令服务器,只用简单的HTTP请求交换必要的SDP数据,这是WebRTC能正常工作的最小化信令方式。
  • 每个客户端请求/offer时,服务器会生成唯一的客户端ID,并创建专属的RTCPeerConnectionData Channel,完全满足你"每个客户端与服务器建立唯一连接"的需求。
  • coturn作为TURN中继,确保即使客户端在严格NAT环境下,也能成功建立连接。
  • 生产环境中,建议开启coturn的TLS/DTLS加密,同时对HTTP接口做身份验证,避免未授权连接。

内容的提问来源于stack exchange,提问作者avi dahan

火山引擎 最新活动