登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

WebTransport 怎样选择双向流和 Datagram

来源:17golang原创

时间:2026-10-14 15:31:53 440浏览 收藏

我第一次把实时面板从单一 WebSocket 连接拆成 WebTransport 时,最容易混淆的不是连接 API,而是“所有消息都走同一条通道”。告警确认、配置变更和实时坐标对可靠性的要求并不一样。

判断方法可以先记成一句话:必须到达、必须保持顺序的数据,用双向流;允许丢失、下一次状态可以覆盖上一次的数据,用 Datagram。双向流也不是现成的消息队列,它只负责可靠的有序字节,应用层仍要处理消息边界。

官方文档:https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API

选择 WebTransport 通道时,先看业务数据能不能丢,再看它是否需要顺序;不要先被“低延迟”三个字带着走。

我先按数据性质拆开这两个通道

同一个 WebTransport 会话可以创建可靠的双向流,也可以通过 transport.datagrams 发送不可靠数据。双向流提供可读和可写两端,适合聊天文本、订单状态、配置变更这类必须完整接收的消息;Datagram 更适合“只要最新值”的数据,例如游戏位置、鼠标轨迹或实时仪表盘采样。

判断维度双向流Datagram
到达保证可靠传输允许丢失
顺序保证按字节顺序交付不保证顺序
适合数据命令、文本、结果、配置瞬时状态、采样、可覆盖更新
应用层工作需要 framing 区分消息需要接受丢包和乱序
WebTransport session 中双向流和 Datagram 的可靠性与业务数据边界静态说明图
图1:WebTransport 两类传输通道的静态结构说明图,展示可靠双向流与 Datagram 的职责边界,不是运行截图。

我在项目里会先给消息写一个“能不能重放”的判断:用户点击保存后返回的结果不能丢,所以走双向流;播放器已经产生了新位置,旧位置再送达也没有意义,所以可以走 Datagram。这个判断比单纯比较 API 名称更稳定。

我在可靠消息里使用双向流时,额外补一层消息边界

流的可靠性只说明字节不会随意丢失或乱序,不代表发送端的一次 write() 就会对应接收端的一次 read()。多个写入可能合并,一次写入也可能被拆开,所以聊天消息、JSON 命令这类内容要在字节流之上定义 framing。

function encodeMessage(text) {
  // 统一使用 UTF-8,并把消息长度放在前四个字节
  const body = new TextEncoder().encode(text)
  const frame = new Uint8Array(4 + body.length)
  new DataView(frame.buffer).setUint32(0, body.length)
  frame.set(body, 4)
  return frame
}

async function sendMessage(transport, text) {
  // 双向流保证字节可靠有序,但不替应用层定义消息边界
  const stream = await transport.createBidirectionalStream()
  const writer = stream.writable.getWriter()
  try {
    await writer.write(encodeMessage(text))
  } finally {
    // 释放 writer,避免长时间占用这个流的写入端
    writer.releaseLock()
  }
}

示例只展示发送侧的协议形状:长度前缀是四字节无符号整数,接收侧需要持续把 ReadableStream 的字节放入缓冲区,至少积累到完整长度后再交给 JSON 解析器或业务处理器。不要把一次 read() 当作一条完整消息。

WebTransport 双向流通过长度前缀和接收缓冲区还原完整业务消息的静态说明图
图2:双向流消息 framing 的静态关系说明图,展示字节序列、长度前缀、缓冲区与消息解析器的关系,不是运行截图。

Datagram 适合会被新状态覆盖的实时数据

Datagram 的优势是不用为过期状态排长队,但代价是应用必须接受数据可能没有到达,也可能顺序变化。我通常只把它用于“晚到就没有价值”的数据:例如每 50 毫秒发送一次光标位置,客户端收到新位置后就可以覆盖旧位置。

async function sendCursor(transport, x, y) {
  // 坐标是瞬时状态,旧值晚到时不值得阻塞新值
  const payload = JSON.stringify({ x, y, sentAt: Date.now() })
  const writer = transport.datagrams.writable.getWriter()
  try {
    await writer.write(new TextEncoder().encode(payload))
  } finally {
    // 每次发送完成后释放 writer,避免把无关状态长期锁住
    writer.releaseLock()
  }
}

如果消息代表扣款、权限变更、聊天正文或服务端确认,就不要因为它“看起来很小”而使用 Datagram。丢一条坐标通常只会让画面短暂不精确,丢一条确认结果却可能让两端状态永久分叉。

上手前我会检查这三个条件

  1. 安全上下文:浏览器接口要求 HTTPS 等安全上下文,开发环境要准备可信的本地 HTTPS 方案。
  2. 服务端能力:WebTransport 依赖 HTTP/3 服务端和对应的会话处理,不能只把 WebSocket URL 原样替换成新构造器。
  3. 浏览器支持:当前 MDN 将 WebTransport 标为 Baseline 2026,但旧设备和部分浏览器仍可能不完整支持,生产页面应保留 WebSocket 或 HTTP 长轮询的降级路径。
async function chooseChannel(url, message) {
  // 连接建立后再读取 reliability,避免把 pending 当成最终能力
  const transport = new WebTransport(url)
  await transport.ready

  if (transport.reliability === 'reliable-only') {
    // 只能使用可靠传输时,状态更新也要走流或退回旧方案
    return { transport, channel: 'stream' }
  }

  // 只有允许不可靠传输时,才把可覆盖状态交给 Datagram
  return { transport, channel: message.replaceable ? 'datagram' : 'stream' }
}

我的落地清单很短:先把消息分成“不可丢”和“可覆盖”两类;不可丢的数据定义 framing 并处理断线重建;可覆盖的数据记录序号或时间戳,收到旧包时直接丢弃;最后为不支持 WebTransport 的环境保留备用传输。

常见问题

双向流比 Datagram 更快吗?

不能只用“快慢”概括。Datagram 不为可靠性和顺序等待,适合低延迟状态;双向流提供可靠有序字节,适合不能丢的业务消息。选择应由数据价值决定。

双向流为什么还需要长度前缀?

因为流提供的是连续字节序列,不保留应用层消息边界。长度前缀、分隔符或固定长度协议都可以完成 framing,关键是发送和接收两端使用同一套规则。

Datagram 丢包后要不要自动重发?

如果每个数据包都是可替换状态,通常直接等待下一次采样更合理;如果必须重发才能保证业务完成,这条消息本质上已经需要可靠语义,应改用双向流或在应用层明确设计确认与重试。

总结一下:双向流负责可靠、有序的字节,Datagram 负责允许丢失的低延迟状态;双向流上面还要自己定义消息边界。把“能不能丢”和“是否需要顺序”写进协议设计,WebTransport 的两种通道就不会因为名称相近而被混用。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>