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) {
// 收到消息
}
})
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,比如显示图片
}
}
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的核心知识结构:
12.7 避坑与最佳实践
最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。
我曾经在信令交换完成后才创建DataChannel,结果SDP里没有DataChannel的m-line,通道一直连不上。记住:createDataChannel必须在createOffer之前调用。
DataChannel单个消息最大约64KB。传大文件时,你需要自己分片。我一般用1KB一个分片,接收端根据序号拼接。别忘了在消息头里带上总片数和当前片序号。
- 使用
label区分不同通道,比如"chat"、"file"、"game" - 在
onStateChange中打印日志,方便调试 - 发送前检查
bufferedAmount,避免缓冲区溢出 - 关闭通道时,调用
dataChannel.close()释放资源
好了,DataChannel的核心内容就这些。它虽然看起来简单,但用好了能解决很多实际问题。从文本聊天到文件传输,从游戏同步到实时协作,DataChannel都是你的得力助手。
公众号:蓝海资料掘金营,微信deep3321