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

WebTransport datagrams怎样传递低延迟不可靠消息

来源:17golang原创

时间:2026-11-05 03:51:01 331浏览 收藏

如果页面传的是实时光标、游戏状态、传感器快照或进度提示,最重要的消息往往不是“每一条都到达”,而是“尽快看到还没过时的那一条”。WebTransport datagrams 就是为这类低延迟、允许丢失且不要求按序的消息准备的通道。

本文只讨论浏览器端的 WebTransport datagrams:如何连接、发送、接收,以及怎样处理大小上限、写入背压和过期消息。官方资料入口:

官方地址:https://developer.mozilla.org/en-US/docs/Web/API/WebTransportDatagramDuplexStream

核心做法是:连接就绪后通过 transport.datagrams 取得读写流,写入前等待 writer.ready,把每条消息控制在 maxDatagramSize 内;接收端按序号或时间戳丢弃旧状态。datagram 不保证送达,也不保证顺序,必须把这个事实写进业务协议。

先判断消息是否真的适合 datagram

WebTransport 同时提供可靠的 stream 和不可靠的 datagram。stream 更适合配置、文件片段、聊天正文、订单状态这类不能缺一段的数据;datagram 更适合“新值会覆盖旧值”的状态快照。

消息特征建议通道原因
缺一条就无法还原内容双向或单向 stream需要可靠、有序的字节流
只关心最新位置、角度或进度datagram旧消息晚到时可以直接丢弃
小型实时状态,同时还要发送控制命令datagram + stream状态追求时延,命令追求可靠

“不可靠”不是“没有加密”。WebTransport 通过安全上下文连接 HTTP/3 服务端,datagram 的不可靠性指的是消息可能丢失、可能乱序,并不是让应用绕过传输层安全。

WebTransport datagrams 在浏览器与 HTTP3 服务端之间传递低延迟不可靠状态的关系说明图
图1:浏览器、WebTransport datagrams 和 HTTP/3 服务端之间的关系说明;可丢失与可能乱序属于业务需要承担的传输语义。

先把连接和读写端准备好

WebTransport 构造函数需要一个 HTTPS 地址,服务端必须提供对应的 WebTransport 会话。连接建立后再使用 transport.datagrams,可以避免把“还在连接中”和“已经能发消息”混为一谈。

async function openDatagramChannel(url) {
  // URL 必须指向 HTTPS 的 WebTransport 服务端,不能填普通 HTTP 接口。
  const transport = new WebTransport(url, {
    // 这是给浏览器的拥塞控制倾向,不等于业务层的可靠性保证。
    congestionControl: 'low-latency'
  });

  try {
    // ready 成功后才开始依赖 datagrams 发送和接收。
    await transport.ready;

    // createWritable 是较新的写入入口,不支持时回退到 writable 属性。
    const writable = typeof transport.datagrams.createWritable === 'function'
      ? transport.datagrams.createWritable()
      : transport.datagrams.writable;

    return { transport, writable };
  } catch (error) {
    // 连接失败时交给上层切换到降级通道或提示用户,不要静默丢失状态。
    await transport.close().catch(() => {});
    throw error;
  }
}

这里的 congestionControl: 'low-latency' 只是用户代理可参考的倾向,不能替代消息过期策略。某些浏览器或运行环境不支持 createWritable() 时,示例保留了旧的 writable 访问方式;实际项目应按自己的兼容范围决定是否保留回退。

发送消息时,把背压和大小上限当成协议边界

datagram 的发送端不是一个无限容量的邮箱。maxDatagramSize 给出单条 outgoing datagram 的最大尺寸;writer.ready 反映写队列是否有空间。写入失败只说明这一条没有交付,不能用重试把 datagram 伪装成可靠队列。

function createStateSender(transport, writable) {
  const writer = writable.getWriter();
  const encoder = new TextEncoder();

  return async function sendState(state) {
    // 先编码再计算长度,不能用 JavaScript 字符串长度代替字节长度。
    const bytes = encoder.encode(JSON.stringify({
      // 序号帮助接收端拒绝迟到的旧状态。
      sequence: state.sequence,
      // 时间戳只用于过期判断,不代表服务端一定使用本机时钟。
      sentAt: Date.now(),
      value: state.value
    }));

    // 超过单条上限时直接跳过;需要拆分的数据应该改用 stream。
    if (bytes.byteLength > transport.datagrams.maxDatagramSize) {
      return { sent: false, reason: 'too-large' };
    }

    try {
      // 队列满时等待可写,避免无界地把实时状态堆在内存里。
      await writer.ready;
      await writer.write(bytes);
      return { sent: true };
    } catch (error) {
      // datagram 写入被拒绝时只报告本条失败,是否补发由业务决定。
      return { sent: false, reason: 'write-failed', error };
    }
  };
}

实时状态通常不应该把每个中间值都补发。比如拖动滑块时,可以只保留最新状态;如果业务必须记录每一条变化,那它已经更接近可靠事件流,应考虑双向 stream 或在应用层设计明确的确认、重传和去重协议。

接收端按序号和年龄过滤旧消息

接收端通过 transport.datagrams.readable 读取 Uint8Array。由于 datagram 可能乱序,不能简单地“读到什么就覆盖什么”。下面的示例把序号较小的包当作迟到状态,并用本地年龄阈值过滤已经失去意义的更新。

async function consumeStates(transport, applyState) {
  const reader = transport.datagrams.readable.getReader();
  const decoder = new TextDecoder();
  let lastSequence = -1;
  const maxAgeMs = 250;

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;

      // 每个 datagram 都要单独解析;解析失败时跳过坏包而不是终止整个会话。
      let message;
      try {
        message = JSON.parse(decoder.decode(value));
      } catch {
        continue;
      }

      // 序号不递增说明消息迟到,实时状态不值得为它回滚。
      if (!Number.isInteger(message.sequence) || message.sequence  maxAgeMs) {
        continue;
      }

      lastSequence = message.sequence;
      applyState(message.value);
    }
  } finally {
    // 组件销毁或会话关闭时释放 reader 锁,避免阻塞其他读取方。
    reader.releaseLock();
  }
}

如果更希望让浏览器在流层面淘汰过期包,可以结合 incomingMaxAge 等 datagram 设置;不过业务仍应保留序号或版本号,因为网络中的乱序和应用状态的新旧关系不是同一个概念。

WebTransport datagrams 的发送队列、writer.ready、大小上限和接收过期丢弃边界说明图
图2:datagram 从应用队列到接收队列的边界关系;大小超限、队列无空间和消息过期分别对应不同处理动作。

什么时候该换成 stream 或混合通道

可以把判断压缩成一个问题:消息晚到或缺失时,用户还能否直接使用下一条状态?如果答案是否定的,就不要只用 datagram。

  • 配置和控制命令:用可靠 stream,必要时在应用层返回确认。
  • 实时快照:用 datagram,携带单调递增的版本号,旧包直接丢弃。
  • 大对象或连续字节:用 stream,不要试图把内容拆成许多 datagram 后自行重组。
  • 同一会话的两类消息:让控制面走 stream,让状态面走 datagram,分别设置超时和关闭策略。

WebTransport 的价值不在于把 WebSocket 换一个名字,而在于让可靠流、不可靠消息和多路会话可以被放到同一个连接模型里。采用前先画出消息价值,而不是先决定所有数据都走最快的通道。

上线前保留这张简明清单

检查点要确认的事实
连接使用 HTTPS,服务端确实提供 WebTransport;连接失败有降级路径
大小发送前检查 maxDatagramSize,不要把大消息硬拆成隐式协议
背压写入前等待 writer.ready,实时状态只保留业务真正需要的最新值
顺序消息携带序号、版本或时间戳,接收端拒绝迟到旧状态
关闭释放 reader/writer,并处理 ready、closed 和单条写入错误

相关问题

WebTransport datagrams 会保证消息送达吗?

不会。它们允许丢失,也可能乱序;需要可靠送达的内容应改用 WebTransport stream,或由业务层明确设计确认和重传。

为什么要在写入前等待 writer.ready?

因为写队列可能暂时没有空间。等待 ready 可以把发送节奏和队列背压连接起来,减少实时状态在本地无界堆积。

单条 datagram 太大怎么办?

先判断消息是否应该改用 stream。只有在协议明确支持分片、丢弃和重组时才自行拆分;否则拆成多条不可靠包会把一个大小问题变成可靠性问题。

WebTransport datagrams 最适合“尽快看到新状态,旧状态没有保存价值”的消息。把大小、年龄、顺序和背压写进协议后,低延迟才不会变成难以排查的随机丢数据。

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