聊聊WebTransport


简介

WebTransport 是什么?

WebTransport是浏览器提供的一套基于QUIC协议的 API 接口,方便浏览器和服务器之间进行实时数据传输,它填补了 Web 平台中的一些空白:

  • 缺少类似 UDP 的网络 API
  • 缺少类似于 WebSocket 但不受队头阻塞影响(Head of Line Blocking)的 API

WebTransport 特性

Webtransport 基于 QUIC 协议,其底层是 UDP。虽然是 UDP 是不可靠的传输协议,但是 QUIC 在 UDP 的基础上融合了 TCP、TLS、HTTP/2 等协议的特性,使得 QUIC 成为一种低时延、安全可靠的传输协议。可以简单理解 QUIC 把 TCP+TLS 的功能基于 UDP 重新实现了一遍。

WebTransport 提供了如下功能特性:

  • 传输可靠数据流 (类似 TCP)
  • 传输不可靠数据流(类似 UDP)
  • 数据加密和拥塞控制(congestion control)
  • 基于 Origin 的安全模型(校验请求方是否在白名单内,类似于 CORS 的Access-Control-Allow-Origin
  • 支持多路复用(类似于 HTTP2 的 Stream)

WebTransport 适用场景

  • 不考虑数据传输可靠性和数据到达到顺序的场景,比如游戏中向 服务器 发送 游戏状态 数据
  • 服务器消息推送
  • 其它不考虑数据达到顺序的场景

API 使用

WebTransport 主要提供三种类型的 API

  • datagramReadabledatagramWritable,用于不可靠数据传输
  • createBidirectionalStream, 用于双向数据流可靠传输
  • createUnidirectionalStream, 用于单向数据流可靠传输

创建 WebTransport 对象

创建 WebTransport 对象,有一定的 URI 要求,格式为: quic-transport://domain:port/path,如下所示:

const transport = new WebTransport('quic-transport://localhost:4433/counter')
await transport.ready
transport.closed.then(() => {
    console.log('webtransport closed')
})

transport.ready返回一个 Promise,如果 QUIC 连接失败会报错。

transport.closed也返回一个 Promise,QUIC 连接关闭时会执行

使用 WebTransport 时需要创建一个 QUIC Server, 可以基于 Python 库aioquic来创建服务器,也可以直接使用 Google Chrome 的样例代码

不可靠数据数传

WebTransport 提供了类似于 UDP 的不可靠传输接口,分别为datagramReadabledatagramWritable。前者用于读取数据,后者用于发送数据。完整的样例如下所示:

;(async () => {
    const transport = new WebTransport(
        'quic-transport://localhost:4433/counter'
    )
    await transport.ready
    transport.closed.then(() => {
        console.log('webtransport closed')
    })

    const createUdpWriter = () => {
        const datagramWritable = transport.datagramWritable
        if (datagramWritable.locked) {
            throw new Error('previous datagram writer should be relased')
        }
        return datagramWritable.getWriter()
    }

    const createUdpReader = () => {
        const datagramReadable = transport.datagramReadable
        if (datagramReadable.locked) {
            throw new Error('previous datagram reader should be relased')
        }
        return datagramReadable.getReader()
    }

    ;(async () => {
        const udpReader = createUdpReader()

        while (true) {
            const { value, done } = await udpReader.read()

            if (done) {
                console.log('done, close datagram reader...')
                return
            }

            console.log(new TextDecoder().decode(value))
        }
    })()

    const updWriter = createUdpWriter()

    timer = setInterval(() => {
        updWriter.write(new TextEncoder('utf-8').encode(Date.now()))
    }, 1000)

    setTimeout(async () => {
        clearInterval(timer)
        // 关闭writer
        await updWriter.close()
        // 释放锁
        updWriter.releaseLock()
        // 关闭transport
        transport.close()
    }, 5 * 1000)
})()

在 上面代码中,核心代码为:transport.datagramWritable.getWriter()以及transport.datagramReadable.getReader()

transport.datagramWritable.getWriter()返回 一个 WritableStreamDefaultWriter对象,其write方法用于 发送数据,注意数据必须为TypedArray数据类型, 代码中我们用TextEncoder.encode()将字符串转为了Uint8Array

transport.datagramReadable.getReader() 返回一个ReadableStreamDefaultReader对象,其read方法用于读取数据,代码中我们用TextEncoder.decode()Uint8Array类似数据转为了字符串。

双向可靠数据流

如果需要保证可靠的数据传输并且需要返回实时结果,可以通过transport.createBidirectionalStream()来创建可靠数据传输。这个接口的功能类似于 WebSocket,但优点在于不受对头阻塞影响。

;(async () => {
    const transport = new WebTransport(
        'quic-transport://localhost:4433/counter'
    )
    await transport.ready
    transport.closed.then(() => {
        console.log('webtransport closed')
    })

    window.transport = transport

    const readerStream = async (reader) => {
        while (1) {
            const { value, done } = await reader.read()
            if (done) {
                return
            }
            console.log(value)
        }
    }

    timer = setInterval(async () => {
        try {
            const stream = await transport.createBidirectionalStream()
            let decoder = new TextDecoderStream('utf-8')
            let reader = stream.readable.pipeThrough(decoder).getReader()
            readerStream(reader)

            const writer = stream.writable.getWriter()
            writer.write(new TextEncoder('utf-8').encode(Date.now()))
            await writer.close()
        } catch (error) {
            console.error(error.toString())
        }
    }, 1000)

    setTimeout(() => {
        clearInterval(timer)
        // transport.close()
    }, 5 * 1000)
})()

注意发送可靠数据流时和 transport.datagramWritable有所不同,每次发送数据,都需要创建一个transport.createBidirectionalStream()对象,发送完毕后需要调用close()将其关闭。

这个就是就是多路复用,可以非常高效、低成本的同时创建多个 Stream,而且多个 Stream 之间相互独立,不像 HTTP2 那样受对头阻塞的影响。

transport.createBidirectionalStream()返回一个Promise<BidirectionalStream>, BidirectionalStream拥有readablewritable对象,分别用于可靠的发送数据和接受数据。

单向可靠数据流

如果想保证数据可靠到达,但是对返回结果不感兴趣,可以使用transport.createUnidirectionalStream()时来创建单向数据流,它返回一个SendStream对象,只可以发送数据。如果想获取返回的数据,可以调用transport.transport.incomingUnidirectionalStreams来获取,但是数据顺序就不保证了。一个完整的样例如下所示:

;(async () => {
    const transport = new WebTransport(
        'quic-transport://localhost:4433/counter'
    )
    await transport.ready
    transport.closed.then(() => {
        console.log('webtransport closed')
    })

    window.transport = transport

    const readIncomingStream = async () => {
        let reader = transport.incomingUnidirectionalStreams.getReader()
        while (1) {
            const { value, done } = await reader.read()
            if (done) {
                console.log('incoming stream done...')
                return
            }
            readerStream(value)
        }
    }

    const readerStream = async (stream) => {
        let reader = stream.readable.getReader()
        while (1) {
            const { value, done } = await reader.read()
            if (done) {
                return
            }
            console.log(new TextDecoder('utf-8').decode(value))
        }
    }

    readIncomingStream()

    timer = setInterval(async () => {
        try {
            // 创建单向数据流
            const stream = await transport.createUnidirectionalStream()
            const writer = stream.writable.getWriter()
            writer.write(new TextEncoder('utf-8').encode(Date.now()))
            await writer.close()
        } catch (error) {
            console.error(error.toString())
        }
    }, 1000)

    setTimeout(() => {
        clearInterval(timer)
        // transport.close()
    }, 5 * 1000)
})()

小结

WebTransport 提供的三种 API 可以根据实际情况来使用:

  1. 不需要保证数据发送先后顺序,就选择transport.datagramWritable
  2. 需要保证数据发送顺序,但是不关心返回值,可以选择transport.createUnidirectionalStream()
  3. 需要保证数据发送顺序且关心返回结果,就选择transport.createBidirectionalStream()

注意事项

WebTransport 现在只有 Google Chrome 支持,其标准也处于草案阶段,不建议使用生产环境。而且现阶段处于origin trial,也就是说,必须申请试用才可以使用。例如官方样例https://googlechrome.github.io/samples/webtransport/client.html就有类似的代码:

<!-- QuicTransport origin trial token. See https://developers.chrome.com/origintrials/#/view_trial/-6744140441987317759 -->
  <meta http-equiv="origin-trial" content="AtxDQl4geYcHaq74wzqCV5DB6zr3+aOffkteLTTLu1+S7VJPwCiUHe2qyUh8kcez+UnKg+g79wzkhdgWvtShmAgAAABdeyJvcmlnaW4iOiJodHRwczovL2dvb2dsZWNocm9tZS5naXRodWIuaW86NDQzIiwiZmVhdHVyZSI6IlF1aWNUcmFuc3BvcnQiLCJleHBpcnkiOjE2MTQxMjQ3OTl9">
</html>

浏览器检测到origin-trial才开启 WebTransport 功能。

如果向自己玩耍 WebTransport,可以通过mkcert生成下 HTTPS 证书,然后在 Google Chrome 时加上自定参数,例如 Mac 下启动 Google Chrome 需要加上如下类似代码:

open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --origin-to-force-quic-on=localhost:4433 https://googlechrome.github.io/samples/webtransport/client.html

具体代码可以参考https://github.com/GoogleChrome/samples/blob/gh-pages/quictransport/quic_transport_server.py.

以Mac为例子,在命令行依此执行如下代码,就可以启动一个 QUIC Server。

brew install mkcert
mkcert -install
mkcert localhost
python quic_transport_server.py localhost.pem localhost-key.pem

参考资料


文章作者: Asyncoder
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Asyncoder !
  目录