Prevent your application from drowning in WebSocket messages or flooding a WebSocket server with messages by applying back pressure.
Background
The WebSocket API
the WebSocket API
provides a JavaScript interface for WebSocket protocol, which allows opening a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-based responses without polling the server for a response.
The Streams API
the Streams API
Allows JavaScript to programmatically access data streams received over the network and process them as desired. An important concept in the context of streams is
back pressure. This is the process by which a single stream or chain of pipes regulates the speed of reading or writing. When the transmission itself or a subsequent transmission in the pipeline is still busy and not yet ready to accept more fragments, it sends a signal back through the chain to slow delivery accordingly.
The problem with the current WebSocket API
Applying back pressure to received messages is impossible
With the current WebSocket API, the reaction to a message happens in
WebSocket.onmessage
, a EventHandler
called when a message is received from the server.
Suppose you have an application that needs to perform extensive data processing operations each time a new message is received. I would probably set up the flow similar to the code below, and since await
the result of the process ()
call, you should be fine right?
const process = async (data) => {
return new Promise((resolve) => {
window.setTimeout(() => {
console.log('WebSocket message processed:', data);
return resolve('done');
}, 1000);
});
};webSocket.onmessage = async (event) => {
const data = event.data;
await process(data);
};
Incorrect! The problem with the current WebSocket API is that there is no way to apply back pressure. When messages come faster than the process ()
The method can handle them, the rendering process will fill the memory by buffering those messages, it will stop responding due to CPU usage at 100%, or both.
Applying back pressure to sent messages is not ergonomic
It is possible to apply back pressure to sent messages, but it does involve polling the
WebSocket.bufferedAmount
property, which is inefficient and not ergonomic. This read-only property returns the number of bytes of data that have been queued by calls to
WebSocket.send ()
, but not yet transmitted to the network. This value is reset to zero after all the queued data has been sent, but if you keep calling WebSocket.send ()
, it will continue to rise.
What is the WebSocketStream API?
The WebSocketStream API addresses the problem of non-existent or non-ergonomic back pressure by integrating streams with the WebSocket API. This means that back pressure can be applied "free", at no additional cost.
Suggested Use Cases for the WebSocketStream API
Examples of sites that can use this API include:
- High bandwidth WebSocket applications that need to maintain interactivity, in particular video and screen sharing.
- Similarly, video capture and other applications that generate a large amount of data in the browser that must be uploaded to the server. With back pressure, the client can stop producing data instead of accumulating data in memory.
Actual state
He passed | Condition |
---|---|
1. Create an explainer | To complete |
2. Create initial draft specification | In progress |
3. Collect feedback and repeat the design | In progress |
4. Proof of origin | To complete |
5. Launch | Not started |
How to use the WebSocketStream API
Introductory example
The WebSocketStream API is based on promise, which makes dealing with it feel natural in a modern JavaScript world. Start by building a new WebSocketStream
and passing it the URL of the WebSocket server. Then wait for the connection
established, resulting in a
ReadableStream
and / or a
WritableStream
.
Calling the
ReadableStream.getReader ()
method, you finally get a
ReadableStreamDefaultReader
, that later you can read ()
data from until the sequence ends, that is, until it returns an object of the form
{value: undefined, done: true}
.
Consequently, calling the
WritableStream.getWriter ()
method, you finally get a
WritableStreamDefaultWriter
, that later you can write ()
data a.
const wss = new WebSocketStream(WSS_URL);
const {readable, writable} = await wss.connection;
const reader = readable.getReader();
const writer = writable.getWriter();while (true) {
const {value, done} = await reader.read();
if (done) {
break;
}
const result = await process(value);
await writer.write(result);
}
Back pressure
What about the promised back pressure function? As I wrote earlier, you get it "free", no extra steps required. Yes process ()
takes longer, the next message will only be consumed once the pipeline is ready. Also, the WritableStreamDefaultWriter.write ()
The step will only continue if it is safe to do so.
Advanced examples
The second argument to WebSocketStream is a bag of options to allow future extension. Currently the only option is protocols
, which behaves the same as the
second argument to the WebSocket constructor:
const chatWSS = new WebSocketStream(CHAT_URL, {protocols: ['chat', 'chatv2']});
const {protocol} = await chatWSS.connection;
The selected protocol
as well as potential extensions
are part of the dictionary available through the WebSocketStream.connection
promise. All information about the live connection is provided by this promise, as it is not relevant if the connection fails.
const {readable, writable, protocol, extensions} = await chatWSS.connection;
Information about the closed WebSocketStream connection
The information that was available in the
WebSocket.onclose
and
WebSocket.onerror
events in the WebSocket API is now available through the WebSocketStream.closed
promise. The promise is rejected in case of an unclean close, otherwise it is resolved with the code and reason sent by the server.
All possible status codes and their meaning are explained in the
list of CloseEvent
status codes.
const {code, reason} = await chatWSS.closed;
Close a WebSocketStream connection
A WebSocketStream can be closed with a
AbortController
. Therefore, spend a AbortSignal
to the WebSocketStream
builder.
const controller = new AbortController();
const wss = new WebSocketStream(Url, {signal: controller.signal});
setTimeout(() => controller.abort(), 1000);
Alternatively, you can also use the WebSocketStream.close ()
method, but its main purpose is to allow you to specify the
code
and reason why it is sent to the server.
wss.close({code: 4000, reason: 'Game over'});
Interoperability and progressive improvement
Currently, Chrome is the only browser that implements the WebSocketStream API. For interoperability with the classic WebSocket API, it is not possible to apply back pressure to received messages. It is possible to apply back pressure to sent messages, but it does involve polling the
WebSocket.bufferedAmount
property, which is inefficient and not ergonomic.
Feature detection
To check if the WebSocketStream API is supported, use:
if ('WebSocketStream' in window) {
}
Manifestation
In supported browsers, you can see the WebSocketStream API in action in the embedded iframe, or directly in Glitch.
Feedback
The Chrome team wants to hear about your experiences with the WebSocketStream API.
Tell us about the API design
Is there something in the API that is not working as you expected? Or are you missing any methods or properties you need to implement your idea? Have a question or comment about the security model? File a spec issue in the corresponding GitHub repositoryor add your thoughts to an existing problem.
Report a problem with the deployment
Found a bug with the Chrome implementation? Or is the implementation different from the specification? File a bug in new.crbug.com. Be sure to include as much detail as you can, simple instructions to reproduce, and enter Blink> Network> WebSockets
at Components box.
Failure works great for quick and easy replay case sharing.
Show API support
Thinking of using the WebSocketStream API? Your public support helps the Chrome team prioritize features and shows other browser vendors how important it is to support them.
Send a tweet to @Cromodev with the #WebSocketStream
hashtag and let us know where and how you are using it.
Helpful Links
Thanks
The WebSocketStream API was implemented by Adam Rice and
Yutaka hirano. Hero image of Daan mooij in
Unsplash.