JS `WebTransport` (HTTP/3 over UDP) `Datagrams` 与 `Streams` 的实时性与可靠性

各位观众老爷,大家好!我是今天的主讲人,一个在代码海洋里摸爬滚打多年的老码农。今天咱们不谈风花雪月,就聊聊JS WebTransport 里的两个好兄弟:Datagrams (数据报) 和 Streams (流),看看它们在实时性和可靠性方面到底有啥门道。准备好了吗?咱们开车啦!

WebTransport:HTTP/3 的 UDP 马甲

首先,简单介绍一下WebTransport。你可以把它想象成HTTP/3穿上了UDP的马甲,专门为需要低延迟、高吞吐量的数据传输场景设计的。它允许我们在浏览器和服务器之间建立一个持久的双向连接,这对于游戏、音视频通话、实时数据推送等应用来说简直是福音。

Datagrams:风一样的男子

Datagrams,也就是数据报,就像UDP协议一样,它是一个无连接、不可靠的传输方式。你可以把它想象成一个快递小哥,他只负责把包裹送到目的地,至于包裹是否到达,是否顺序到达,他一概不负责。

  • 实时性: Datagrams 的优势在于实时性。由于没有连接建立和维护的开销,数据可以立即发送,延迟非常低。这对于实时性要求高的场景非常重要,比如游戏里的玩家位置更新,如果延迟太高,玩家就会感觉卡顿。
  • 可靠性: Datagrams 的劣势在于不可靠性。数据包可能会丢失、乱序或者重复。这意味着你需要自己在应用层处理这些问题。

Datagrams 的应用场景

  • 游戏: 玩家位置、动作等实时数据更新。可以容忍少量数据丢失,但对延迟要求极高。
  • 传感器数据: 实时采集的传感器数据,例如温度、湿度等。可以容忍少量数据丢失,但需要保证数据的实时性。
  • 心跳检测: 用于检测连接是否存活。丢失几个心跳包问题不大,重要的是快速检测到连接断开。

Datagrams 的代码示例 (Browser)

// 假设已经建立了 WebTransport 连接,并且获得了 transport 对象
// 详细的连接建立过程请参考 WebTransport 官方文档

// 发送数据报
const encoder = new TextEncoder();
const data = encoder.encode("Hello, Datagrams!");
transport.datagrams.writable.getWriter().then(writer => {
  writer.write(data);
  writer.releaseLock(); // 释放锁
});

// 接收数据报
transport.datagrams.readable.getReader().then(reader => {
  async function readLoop() {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        break;
      }
      const decoder = new TextDecoder();
      const message = decoder.decode(value);
      console.log("Received Datagram:", message);
    }
  }
  readLoop();
});

Datagrams 的代码示例 (Server – Node.js)

import { createServer } from 'node:http';
import { WebTransport } from '@failsafe/webtransport';
import { quicTransport } from '@failsafe/quictransport';

const httpServer = createServer();

const webTransport = new WebTransport({
    // 在这里配置你的证书和密钥
    // 请使用可信的证书,自签名证书可能导致连接问题
    certFile: 'cert.pem',
    keyFile: 'key.pem',
    // 可选配置:
    // maxIncomingUnidiStreams: 100, // 限制服务端接受的最大单向流数量
    // maxIncomingBidiStreams: 100, // 限制服务端接受的最大双向流数量
  });

httpServer.on('upgrade', webTransport.upgrade.bind(webTransport));

webTransport.on('session', session => {
  console.log('New WebTransport session');

  session.datagrams.addEventListener('datagramreceived', event => {
    const datagram = event.datagram;
    const message = new TextDecoder().decode(datagram);
    console.log(`Received Datagram: ${message}`);

    // Echo back the datagram
    session.datagrams.send(datagram);
  });

  session.datagrams.addEventListener('datagramsenderror', error => {
    console.error('Datagram send error:', error);
  });

  session.addEventListener('close', () => {
    console.log('WebTransport session closed');
  });

  session.addEventListener('error', error => {
    console.error('WebTransport session error:', error);
  });
});

httpServer.listen(3000, () => {
  console.log('WebTransport server listening on port 3000');
});

Streams:稳如老狗

Streams,也就是流,就像TCP协议一样,它是一个面向连接、可靠的传输方式。你可以把它想象成一个快递小哥,他不仅负责把包裹送到目的地,还保证包裹一定到达,并且按照发送的顺序到达。

  • 实时性: Streams 的实时性相对较差。由于需要建立连接、进行拥塞控制和错误重传,延迟会比 Datagrams 高。
  • 可靠性: Streams 的优势在于可靠性。数据不会丢失、乱序或者重复。这对于需要保证数据完整性的场景非常重要,比如文件传输、音视频流传输等。

Streams 的应用场景

  • 文件传输: 需要保证文件完整性,不允许任何数据丢失。
  • 音视频流传输: 虽然可以容忍少量数据丢失,但为了保证用户体验,通常会使用 Streams 来保证音视频流的稳定传输。
  • 控制指令: 需要保证控制指令的可靠传输,例如远程控制机器人。

Streams 的类型

WebTransport 支持两种类型的 Streams:

  • Unidirectional Streams (单向流): 只能由一方发送数据,另一方接收数据。就像一个单行道,只能单向行驶。
  • Bidirectional Streams (双向流): 双方都可以发送和接收数据。就像一个双向车道,可以双向行驶。

Streams 的代码示例 (Browser – Unidirectional)

// 创建一个单向流
transport.createUnidirectionalStream().then(stream => {
  const writer = stream.getWriter();
  const encoder = new TextEncoder();
  const data = encoder.encode("Hello, Unidirectional Stream!");
  writer.write(data);
  writer.close(); // 关闭写入流
});

// 接收单向流
transport.incomingUnidirectionalStreams.getReader().then(reader => {
  async function readLoop() {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        break;
      }
      const stream = value;
      const streamReader = stream.getReader();
      let result = await streamReader.read();
      let decoder = new TextDecoder();
      let message = decoder.decode(result.value);
      console.log("Received Unidirectional Stream:", message);
    }
  }
  readLoop();
});

Streams 的代码示例 (Server – Unidirectional)

//Server 端接收单向流

webTransport.on('session', session => {

    session.incomingUnidirectionalStreams.addEventListener('datagramreceived', async event => {
        const reader = event.stream.getReader();
        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    break;
                }
                console.log('Received data from unidirectional stream:', new TextDecoder().decode(value));
            }
        } catch (error) {
            console.error('Error reading from unidirectional stream:', error);
        } finally {
            reader.releaseLock(); // 释放 reader
        }
    });
});

Streams 的代码示例 (Browser – Bidirectional)

// 创建一个双向流
transport.createBidirectionalStream().then(stream => {
  const writer = stream.writable.getWriter();
  const encoder = new TextEncoder();
  const data = encoder.encode("Hello, Bidirectional Stream!");
  writer.write(data);

  // 接收数据
  const reader = stream.readable.getReader();
  async function readLoop() {
    while (true) {
      const { value, done } = await reader.read();
      if (done) {
        break;
      }
      const decoder = new TextDecoder();
      const message = decoder.decode(value);
      console.log("Received Bidirectional Stream:", message);
    }
  }
  readLoop();

  // 关闭写入流
  writer.close();
});

Streams 的代码示例 (Server – Bidirectional)

//Server 端接收双向流

webTransport.on('session', async session => {
    session.addEventListener('stream', async event => {
        const stream = event.stream;
        const reader = stream.readable.getReader();
        const writer = stream.writable.getWriter();

        try {
            while (true) {
                const { done, value } = await reader.read();
                if (done) {
                    break;
                }
                const message = new TextDecoder().decode(value);
                console.log('Received data from bidirectional stream:', message);

                // Echo back the message
                await writer.write(new TextEncoder().encode(`Server received: ${message}`));
            }
        } catch (error) {
            console.error('Error reading/writing to bidirectional stream:', error);
        } finally {
            reader.releaseLock();
            await writer.close();
        }
    });
});

Datagrams vs. Streams:选择困难症?

那么,在实际应用中,我们应该选择 Datagrams 还是 Streams 呢?这取决于你的具体需求。

特性 Datagrams Streams
连接 无连接 面向连接
可靠性 不可靠 (可能丢失、乱序、重复) 可靠 (保证数据完整性和顺序)
实时性 相对较低
开销
适用场景 实时性要求高,可以容忍少量数据丢失的场景 可靠性要求高,需要保证数据完整性的场景
例子 游戏、传感器数据、心跳检测 文件传输、音视频流传输、控制指令

总结

  • Datagrams: 适合对实时性要求极高,但可以容忍少量数据丢失的场景。
  • Streams: 适合对可靠性要求极高,需要保证数据完整性的场景。

一些额外的思考

  • 可靠性与实时性的权衡: 在实际应用中,我们往往需要在可靠性和实时性之间进行权衡。例如,在音视频通话中,我们可以使用 Streams 来保证音视频流的稳定传输,但同时也可以使用 Datagrams 来传输一些控制指令,以提高响应速度。
  • 应用层协议: 无论选择 Datagrams 还是 Streams,都需要设计合适的应用层协议来处理数据。例如,在使用 Datagrams 时,我们需要自己处理数据包的丢失、乱序和重复问题。
  • 拥塞控制: WebTransport 使用 QUIC 协议作为底层传输协议,QUIC 协议自带拥塞控制机制,可以有效地避免网络拥塞。

最后的提醒

WebTransport 还在发展中,API 可能会发生变化。在使用 WebTransport 时,请务必参考最新的官方文档。

好了,今天的讲座就到这里。希望大家对 WebTransport 的 Datagrams 和 Streams 有了更深入的了解。记住,没有最好的技术,只有最适合的技术。根据你的实际需求,选择合适的传输方式,才能构建出高性能、高可靠的应用。

感谢大家的观看!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注