|
libwebsockets
Lightweight C library for HTML5 websockets
|
libwebsockets supports WebTransport (RFC 9297) and QUIC Datagrams (RFC 9221) natively over its HTTP/3 and QUIC transport layers. WebTransport is an API that provides low-latency, bidirectional, multiplexed, and secure client-server messaging.
WebTransport offers several architectural advantages over WebSocket, particularly in high-performance or lossy networking environments:
| Feature | WebSocket (ws) | WebTransport (wt) |
|---|---|---|
| Transport Protocol | TCP (HTTP/1.1 or HTTP/2) | UDP (QUIC via HTTP/3) |
| Multiplexing | Built-in for H2, none for H1. | Native QUIC streams (multiple independent streams without head-of-line blocking). |
| Delivery Guarantees | Reliable, strictly ordered. | Offers both reliable streams and unreliable Datagrams. |
| Connection Setup | Requires 1-3 RTTs (TCP + TLS + HTTP). | 0-RTT or 1-RTT (QUIC handshake). |
| Security | TLS 1.2 or 1.3 | Always TLS 1.3 (embedded in QUIC). |
Use WebSocket when you need maximum backward compatibility across older clients, infrastructure, and proxies. Use WebTransport when you require low-latency media streaming, gaming, or parallel data transfers where head-of-line blocking is unacceptable.
WebTransport in libwebsockets maps elegantly to the wsi (WebSocket Instance) abstraction. WebTransport requires an HTTP/3 virtual host.
A WebTransport connection starts with an HTTP/3 CONNECT request specifying the :protocol: webtransport pseudo-header. If accepted, libwebsockets transitions this wsi to the wt role (&role_ops_wt).
Within the WebTransport session, you can spawn multiple independent QUIC streams.
LWS_CALLBACK_WT_BIND_PROTOCOL and LWS_CALLBACK_WT_DROP_PROTOCOL are the wt role's analogues of LWS_CALLBACK_HTTP_BIND_PROTOCOL / LWS_CALLBACK_HTTP_DROP_PROTOCOL.
They are only about the logical binding of the session or stream wsi to your protocol, and say nothing about the state of the connection or the stream: they are not LWS_CALLBACK_ESTABLISHED / LWS_CALLBACK_CLOSED under another name.
A client opens a session with lws_client_connect_via_info(), with alpn "h3" and protocol "webtransport". It is sent as an extended CONNECT only once the server's SETTINGS enabled WebTransport and HTTP Datagrams, otherwise the attempt fails with LWS_CALLBACK_CLIENT_CONNECTION_ERROR.
The server's 200 is announced like any other h3 client response, with LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP; lws_wt_is_session(wsi) is true from then on. It is not the ws LWS_CALLBACK_CLIENT_ESTABLISHED, which a WebTransport client never gets. Streams and the session are then handled as on the server: LWS_CALLBACK_RECEIVE, LWS_CALLBACK_CLIENT_WRITEABLE and LWS_CALLBACK_CLOSED.
Either end ends the session by closing its session wsi, eg, with lws_set_timeout(wsi, PENDING_TIMEOUT_USER_OK, LWS_TO_KILL_ASYNC). lws finishes its side of the CONNECT stream with a FIN, which means error code 0 and no message. When the peer's FIN arrives, or the peer resets the stream, lws closes its end of the session too. Either way, both ends get LWS_CALLBACK_CLOSED for the session.
Every stream of the session is closed with it, on both sides. A stream whose sending side we had not finished is reset, discarding anything it had not sent yet, so finish a stream's last write with LWS_WRITE_H2_STREAM_END if it must arrive.
Sending or acting on a CLOSE_WEBTRANSPORT_SESSION capsule, to carry an error code or message, is not supported yet.
The wt role drives its protocol with LWS_CALLBACK_RECEIVE and LWS_CALLBACK_CLOSED, the same reasons a ws server protocol gets, but a session is not a ws connection: there is no LWS_CALLBACK_ESTABLISHED and no ws framing state. A protocol written for ws must never be given one, so a protocol only takes WebTransport sessions on a vhost where its pvo has a "webtransport" option, in the same way "default" or "raw" are given:
or in lwsws JSON config, in the vhost's "ws-protocols" section, "my-wt-protocol": { "webtransport": "1" }.
The protocol for a CONNECT is then chosen like this, and if the choice does not take WebTransport, the CONNECT is refused with a 404:
The CONNECT is subject to the same gates as a ws upgrade to that path: a mount interceptor chain like lws-login that would not let the request through refuses it with a 401, as does the mount's basic auth or a "basic-auth" pvo on the protocol, when the client did not send valid credentials. Note the browser WebTransport API sends neither cookies nor credentials, so a browser can not pass these gates; check a token in the urlargs from LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION instead, below.
Before the server answers the CONNECT with a 200 and the stream leaves the h3 role, the protocol that was selected for the session receives LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION, exactly as for a ws upgrade. in is the negotiated wt protocol name (or NULL). The request headers are still attached at that point, so the :path and any urlargs can be inspected with lws_hdr_copy() / lws_get_urlarg_by_name_safe(). Returning nonzero refuses the session: the CONNECT is answered with a 403 and the stream is closed, with no LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED.
This is the place to do things like /wt?code=... token checks, since the browser WebTransport API does not let a page set request headers.
Ensure your context and vhost are configured with HTTP/3 support and TLS:
Implement the protocol callback. Distinguish between the session and its streams using lws_wt_is_session(wsi):
Major web browsers (Chrome, Firefox, Safari) support the WebTransport JavaScript API. However, browsers strictly enforce TLS certificates for WebTransport.
If you are developing locally with self-signed certificates, the browser will instantly reject the connection. You can bypass this during development in Chromium-based browsers using:
Alternatively, you can provide the SHA-256 hash of your self-signed certificate in the JavaScript WebTransport constructor's serverCertificateHashes option.