You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中WebRTC多端对等连接数据通道问题求助

Flutter WebRTC 多用户数据通道实现方案

核心思路

每个参与者必须为其他每一位在线用户创建独立的RTCPeerConnection和RTCDataChannel实例,绝对不能复用同一个连接对象——这就是你遇到「新连接建立时旧连接断开」的核心原因。同时需要通过信令服务器(比如WebSocket)处理SDP交换、ICE候选转发,完成多端的连接协商。

关键实现步骤

1. 定义连接信息模型

用模型统一管理每个对应用户的连接资源:

class PeerConnectionInfo {
  final String userId;
  final RTCPeerConnection peerConnection;
  final RTCDataChannel? dataChannel;

  PeerConnectionInfo({
    required this.userId,
    required this.peerConnection,
    this.dataChannel,
  });
}

2. 初始化基础配置

提前准备WebRTC连接和数据通道的通用配置:

// STUN服务器配置,有需要可添加TURN服务器
final Map<String, dynamic> rtcConfig = {
  'iceServers': [
    {'urls': 'stun:stun.l.google.com:19302'},
  ]
};

// 数据通道配置:保证消息顺序,限制重传次数
final RTCDataChannelInit dataChannelConfig = RTCDataChannelInit(
  ordered: true,
  maxRetransmits: 3,
);

3. 为新用户创建独立连接

当信令服务器通知有新用户加入时,立即为该用户创建专属连接:

// 存储所有活跃连接,key为对方用户ID
final Map<String, PeerConnectionInfo> _activePeers = {};

// 处理新用户加入逻辑
Future<void> setupConnectionToUser(String remoteUserId) async {
  // 创建新的PeerConnection实例
  final pc = await createPeerConnection(rtcConfig);

  // 监听ICE候选,通过信令转发给对方
  pc.onIceCandidate = (candidate) {
    if (candidate != null) {
      _sendSignalingMsg({
        'type': 'ice_candidate',
        'target': remoteUserId,
        'data': candidate.toMap(),
      });
    }
  };

  // 创建数据通道并绑定监听
  final dataChannel = await pc.createDataChannel('group_chat', dataChannelConfig);
  _bindDataChannelEvents(dataChannel, remoteUserId);

  // 生成Offer并发送给对方
  final offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  _sendSignalingMsg({
    'type': 'offer',
    'target': remoteUserId,
    'data': offer.toMap(),
  });

  // 保存连接信息
  _activePeers[remoteUserId] = PeerConnectionInfo(
    userId: remoteUserId,
    peerConnection: pc,
    dataChannel: dataChannel,
  );
}

4. 处理信令消息(Offer/Answer/ICE)

对接信令服务器,处理来自其他用户的协商请求:

void handleSignalingMsg(Map<String, dynamic> msg) {
  final senderId = msg['sender'];
  switch (msg['type']) {
    case 'offer':
      _processOffer(senderId, msg['data']);
      break;
    case 'answer':
      _processAnswer(senderId, msg['data']);
      break;
    case 'ice_candidate':
      _processIceCandidate(senderId, msg['data']);
      break;
  }
}

// 处理收到的Offer
Future<void> _processOffer(String senderId, Map<String, dynamic> sdp) async {
  final pc = await createPeerConnection(rtcConfig);

  pc.onIceCandidate = (candidate) {
    if (candidate != null) {
      _sendSignalingMsg({
        'type': 'ice_candidate',
        'target': senderId,
        'data': candidate.toMap(),
      });
    }
  };

  // 监听对方创建的数据通道
  pc.onDataChannel = (channel) {
    _bindDataChannelEvents(channel, senderId);
    _activePeers[senderId] = PeerConnectionInfo(
      userId: senderId,
      peerConnection: pc,
      dataChannel: channel,
    );
  };

  // 设置远程SDP并生成Answer
  await pc.setRemoteDescription(RTCSessionDescription(sdp['sdp'], sdp['type']));
  final answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);
  _sendSignalingMsg({
    'type': 'answer',
    'target': senderId,
    'data': answer.toMap(),
  });
}

// 处理收到的Answer
Future<void> _processAnswer(String senderId, Map<String, dynamic> sdp) async {
  final peerInfo = _activePeers[senderId];
  if (peerInfo != null) {
    await peerInfo.peerConnection.setRemoteDescription(
      RTCSessionDescription(sdp['sdp'], sdp['type'])
    );
  }
}

// 处理收到的ICE候选
Future<void> _processIceCandidate(String senderId, Map<String, dynamic> candidate) async {
  final peerInfo = _activePeers[senderId];
  if (peerInfo != null) {
    await peerInfo.peerConnection.addIceCandidate(
      RTCIceCandidate(candidate['candidate'], candidate['sdpMid'], candidate['sdpMLineIndex'])
    );
  }
}

5. 绑定数据通道事件

统一处理消息接收、通道状态变更:

void _bindDataChannelEvents(RTCDataChannel channel, String senderId) {
  // 接收消息
  channel.onMessage = (msg) {
    final content = String.fromCharCodes(msg.data);
    // 这里可以添加UI更新逻辑,比如展示消息
    print('收到来自$senderId的消息:$content');
  };

  // 通道打开通知
  channel.onOpen = () {
    print('与$senderId的数据通道已建立');
  };

  // 通道关闭时清理连接
  channel.onClose = () {
    print('与$senderId的数据通道已断开');
    _activePeers.remove(senderId);
  };
}

6. 发送消息给所有用户

遍历所有活跃数据通道,实现全员消息推送:

void broadcastMessage(String content) {
  _activePeers.values.forEach((peerInfo) {
    final channel = peerInfo.dataChannel;
    if (channel != null && channel.readyState == RTCDataChannelState.RTCDataChannelOpen) {
      channel.send(RTCDataChannelMessage(content));
    }
  });
}

常见问题排查

  • 旧连接被断开:检查是否复用了RTCPeerConnection实例,必须为每个用户单独创建。
  • 消息无法接收:确认数据通道的onMessage监听已绑定,且通道状态为Open。
  • 连接协商失败:检查STUN/TURN服务器配置是否有效,信令服务器是否正确转发ICE候选。

内容的提问来源于stack exchange,提问作者Ravi Taak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 09:08:19