12、数据传输通道:RTCDataChannel创建、文本与二进制数据传输、DataChannel事件监听

聊到WebRTC,大家第一反应往往是音视频。但说实话,真正让WebRTC变得强大的,是它附带的那条数据通道——RTCDataChannel。它就像一条隐形的“高速公路”,让你在音视频之外,还能自由地传输文本、文件、游戏状态,甚至是二进制流。

我个人习惯把DataChannel比作“WebSocket的P2P版本”。它不需要服务器中转,数据直接在两端之间跑。延迟低、效率高,而且完全加密。今天我们就把它彻底讲透。

12.1 什么是RTCDataChannel?

RTCDataChannel是WebRTC提供的一个API,允许你在两个Peer之间建立一条双向的数据通道。它基于SCTP协议(Stream Control Transmission Protocol),运行在DTLS加密层之上。

说白了,它就是一条“安全、低延迟、点对点”的数据管道。

核心特点:
  • P2P直连:数据不经过服务器,延迟极低
  • 加密传输:基于DTLS,数据全程加密
  • 可靠/不可靠模式:你可以选择是否保证数据到达
  • 有序/无序模式:你可以选择是否保证数据顺序
  • 文本与二进制:支持字符串、ArrayBuffer、Blob等

我记得在做一个在线协作白板项目时,用户画一笔,数据就要实时同步到对方。用DataChannel传JSON,延迟基本在10ms以内,体验非常顺滑。

12.2 创建RTCDataChannel

创建DataChannel有两种方式:本地创建远程接收。我们先看本地创建。

12.2.1 本地创建DataChannel

在建立RTCPeerConnection之后,你可以直接调用createDataChannel()方法。注意:必须在创建Offer之前调用

// 创建RTCPeerConnection
val peerConnection = factory.createPeerConnection(config)

// 创建DataChannel(必须在createOffer之前)
val dataChannel = peerConnection.createDataChannel("chat", DataChannel.Init().apply {
    ordered = true          // 保证顺序
    negotiated = false      // 自动协商ID
    maxRetransmits = 3      // 最多重传3次(设为null表示可靠模式)
})

// 监听DataChannel状态
dataChannel.registerObserver(object : DataChannel.Observer {
    override fun onBufferedAmountChange(previousAmount: Long) {
        // 缓冲区变化
    }

    override fun onStateChange() {
        // 状态变化:CONNECTING, OPEN, CLOSING, CLOSED
    }

    override fun onMessage(buffer: DataChannel.Buffer) {
        // 收到消息
    }
})
避坑指南:我曾经在项目中犯过一个低级错误——在createOffer之后才调用createDataChannel。结果DataChannel一直连不上。后来查文档才发现,createDataChannel必须在信令交换之前调用,否则SDP里不会包含DataChannel的m-line信息。

12.2.2 远程接收DataChannel

当远端创建了DataChannel,本地会通过RTCPeerConnection.onDataChannel回调收到通知。

peerConnection.onDataChannel = { channel ->
    Log.d("WebRTC", "收到远端DataChannel: ${channel.label()}")

    channel.registerObserver(object : DataChannel.Observer {
        override fun onMessage(buffer: DataChannel.Buffer) {
            // 处理消息
        }
    })
}

这里有个细节:label是双方约定的通道名称。你可以创建多个DataChannel,每个有不同的label,用于传输不同类型的数据。比如一个传文本,一个传文件。

12.3 文本与二进制数据传输

DataChannel支持两种数据类型:文本(String)二进制(ByteBuffer)。我们分别来看。

12.3.1 发送文本数据

发送文本最简单,直接调用send()方法,传入DataChannel.Buffer

// 发送文本消息
val text = "Hello, WebRTC!"
val buffer = DataChannel.Buffer(
    ByteBuffer.wrap(text.toByteArray(Charsets.UTF_8)),
    false  // isBinary = false,表示文本
)
dataChannel.send(buffer)

接收端判断是否是文本:

override fun onMessage(buffer: DataChannel.Buffer) {
    if (!buffer.isBinary) {
        // 文本消息
        val text = String(buffer.data.array(), Charsets.UTF_8)
        Log.d("DataChannel", "收到文本: $text")
    }
}

12.3.2 发送二进制数据

二进制数据可以是图片、文件、音频片段等。发送时把isBinary设为true。

// 发送二进制数据(比如一张图片的字节)
val imageBytes: ByteArray = loadImageBytes()
val byteBuffer = ByteBuffer.wrap(imageBytes)
val buffer = DataChannel.Buffer(byteBuffer, true)  // isBinary = true
dataChannel.send(buffer)

接收端处理二进制:

override fun onMessage(buffer: DataChannel.Buffer) {
    if (buffer.isBinary) {
        // 二进制数据
        val bytes = ByteArray(buffer.data.remaining())
        buffer.data.get(bytes)
        // 处理bytes,比如显示图片
    }
}
注意:DataChannel的单个消息大小有限制。Android平台上,SCTP消息最大约为64KB。如果你要传大文件,需要自己分片发送。我曾在传一个10MB的日志文件时踩过这个坑,后来用1KB一个分片,接收端再拼接,才搞定。

12.4 DataChannel事件监听

DataChannel有四个关键事件,你需要全部监听:

事件 说明 触发时机
onStateChange 通道状态变化 CONNECTING → OPEN → CLOSING → CLOSED
onMessage 收到数据 每次收到消息时
onBufferedAmountChange 缓冲区大小变化 发送数据导致缓冲区变化时

完整的事件监听示例:

dataChannel.registerObserver(object : DataChannel.Observer {
    override fun onStateChange() {
        when (dataChannel.state()) {
            DataChannel.State.CONNECTING -> {
                Log.d("DataChannel", "通道连接中...")
            }
            DataChannel.State.OPEN -> {
                Log.d("DataChannel", "通道已打开,可以发送数据")
                // 可以在这里发送初始消息
            }
            DataChannel.State.CLOSING -> {
                Log.d("DataChannel", "通道关闭中...")
            }
            DataChannel.State.CLOSED -> {
                Log.d("DataChannel", "通道已关闭")
                // 清理资源
            }
        }
    }

    override fun onMessage(buffer: DataChannel.Buffer) {
        if (buffer.isBinary) {
            // 处理二进制
        } else {
            // 处理文本
        }
    }

    override fun onBufferedAmountChange(previousAmount: Long) {
        val currentAmount = dataChannel.bufferedAmount()
        Log.d("DataChannel", "缓冲区变化: $previousAmount → $currentAmount")
        // 如果缓冲区太大,可以暂停发送
        if (currentAmount > 1024 * 1024) {  // 超过1MB
            // 暂停发送,等待缓冲区减少
        }
    }
})
个人经验:我建议你在onStateChange中打印日志,方便调试。尤其是在CONNECTING状态停留太久时,很可能是网络问题或SDP协商出了问题。另外,onBufferedAmountChange这个回调很多人忽略,但它在做流控时非常有用。

12.5 可靠模式 vs 不可靠模式

创建DataChannel时,你可以选择可靠或不可靠模式。这取决于你的业务场景。

模式 配置 适用场景
可靠有序 ordered=true, maxRetransmits=null 文本聊天、文件传输、状态同步
不可靠无序 ordered=false, maxRetransmits=0 实时游戏位置、音视频同步辅助数据
部分可靠 ordered=true, maxRetransmits=3 心跳、日志上报

举个例子:做实时游戏时,玩家的位置信息每秒更新30次。如果丢了一帧,直接发下一帧就好,不需要重传。这时候用不可靠模式,延迟更低。

但如果是文件传输,丢一个字节文件就坏了,必须用可靠模式。

12.6 知识体系图

下面这张图总结了DataChannel的核心知识结构:

RTCDataChannel 知识体系 RTCDataChannel 创建方式 本地创建 远程接收 数据类型 文本 二进制 传输模式 可靠有序 不可靠无序 事件监听 onStateChange onMessage onBufferedAmountChange 应用场景 文本聊天 文件传输 游戏同步 基于SCTP协议,运行在DTLS加密层之上 支持P2P直连,延迟低至10ms以内

12.7 避坑与最佳实践

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。

坑1:不要在createOffer之后创建DataChannel

我曾经在信令交换完成后才创建DataChannel,结果SDP里没有DataChannel的m-line,通道一直连不上。记住:createDataChannel必须在createOffer之前调用

坑2:大文件传输需要分片

DataChannel单个消息最大约64KB。传大文件时,你需要自己分片。我一般用1KB一个分片,接收端根据序号拼接。别忘了在消息头里带上总片数和当前片序号。

最佳实践:
  • 使用label区分不同通道,比如"chat"、"file"、"game"
  • onStateChange中打印日志,方便调试
  • 发送前检查bufferedAmount,避免缓冲区溢出
  • 关闭通道时,调用dataChannel.close()释放资源

好了,DataChannel的核心内容就这些。它虽然看起来简单,但用好了能解决很多实际问题。从文本聊天到文件传输,从游戏同步到实时协作,DataChannel都是你的得力助手。


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