WebSocket is not the only way
When talking about full-duplex communication on the web, most people will refer to WebSocket, as its sole purpose is to do this. But WebSocket is not the only way of duplex communication, the very fundamental HTTP itself is also a full-duplex protocol.
The traditional HTTP
When talking about the HTTP protocol, many people are familiar with this diagram:
This is the common way of using HTTP, also known as the Half Duplex Model. The client sends a request to the server, and the server processes the request and sends back a response. The connection may be closed immediately or reused multiple times afterward.
Server-sent events
But that is not the only way to use HTTP. Some people may be familiar with SSE (Server-Sent-Events), in which after the server processes the request, it doesn’t send back the response immediately and ends the communication. Instead, it keeps the session alive and continuously sends message events to the client when there are updates. The diagram is like this:
Streaming HTTP
However, SSE still doesn’t describe the full potential of the HTTP protocol. The real HTTP protocol is a streaming protocol, where both the client and the server can continuously send and receive data, as long as the connection keeps alive. So the accurate diagram is actually like this:
This model gives HTTP the ability to implement full duplex communications.
SSE is not server-side only
People who are familiar with SSE know how to use it on the server side, it is a well-designed and widely adopted format for transferring text data (sometimes JSON) frames from the server to the client.
However, such a mechanism is not server-side only, it can also be used on the client side, to continuously send data fragments to the server as well. In the following content, I’m going to demonstrate how to use this mechanism to achieve full duplex communication with the streaming HTTP protocol.
For simplicity, here we’ll just periodically send a ping
event from the client to the server, and the server responds with a pong
event to the client accordingly.
A server-side example
First, we will have a server-side example, which is written in JavaScript/TypeScript.
// server.ts
import { EventConsumer, EventEndpoint } from "@ayonli/jsext/sse";
export default {
port: 8000,
async fetch(req: Request) {
const sse = new EventEndpoint(req);
// EventConsumer takes a Response object, so we create one for it with the
// request body as its body, which is a ReadableStream.
const res = new Response(req.body!, {
headers: { "Content-Type": "text/event-stream" },
});
const client = new EventConsumer(res);
// listen the event sent by the client
client.addEventListener("ping", ev => {
console.log("ping", ev.data);
// send event to the client
sse.dispatchEvent(new MessageEvent("pong", { data: ev.data }));
});
client.addEventListener("connect", ev => {
console.log("connect", ev.data);
});
return sse.response!;
}
};
TypeScriptThen we can use one of the following commands to start the server (don’t forget to install dependencies first):
tsx --import=@ayonli/jsext/http server.ts # Node.js (with `tsx`)
deno serve server.ts # Deno
bun run server.ts # Bun
ShellScriptA client-side example
Now let’s have a client-side program to connect to the server and start sending data frames to the server, and the server will respond respectively.
// client.ts
import { EventConsumer, EventEndpoint } from "@ayonli/jsext/sse";
// EventEndpoint takes a Request object, so we create one for it for simulation.
const req = new Request("http://localhost:8000");
const sse = new EventEndpoint(req);
// We need to write the first chunk of data so the server can parse
// the header and start receiving body frames.
sse.dispatchEvent(new MessageEvent("connect", { data: "hello" }));
// @ts-ignore suppress TS error for 'duplex' option
const res = await fetch(req.url, {
method: "POST",
// Pass the response body of the EventEndpoint instance as the request body,
// so when sending events, the data will be piped to the request.
body: sse.response!.body,
headers: { "Content-Type": "text/event-stream" },
duplex: "half",
});
const client = new EventConsumer(res);
// listen the event sent by the server
client.addEventListener("pong", ev => {
console.log("pong", ev.data);
});
// periodically sending events to the server
let count = 0;
setInterval(() => {
sse.dispatchEvent(new MessageEvent("ping", { data: String(count++) }));
}, 1_000);
JavaScriptNow we can use one of the following commands to start the client.
tsx client.ts # Node.js (with `tsx`)
deno run --allow-net client.ts # Deno
bun run client.ts # Bun
ShellScriptAnd we can see in our terminal, that in the server tab, it continuously prints something like these:
connect hello
ping 0
ping 1
ping 2
ping 3
ping 4
ping 5
ping 6
ShellScriptAnd in the client tab, it continuously prints these:
pong 0
pong 1
pong 2
pong 3
pong 4
pong 5
pong 6
ShellScriptApart from using SSE, we can also use bare HTTP body chunks to transfer data between the client and the server, as long as we have a way to correctly delimit the data frames.
Browser support?
Sadly, browsers don’t support full-duplex communications over HTTP at the moment. In Chrome, If we try to run the client code above, the fetch
API will simply err with code ERR_H2_OR_QUIC_REQUIRED
. There is a discussion on the whatwg GitHub page: https://github.com/whatwg/fetch/issues/1254 talking about this issue, check it up if you’d like.
Final words
This article gives us a new perspective on using HTTP and strengthens our knowledge of the most popular transmission protocol on the web. We should always keep an open mind in learning, not only the new fancy things but those we seem to be very familiar with as well, they may still have other potentials that we don’t know about yet.
PS: JsExt is a JavaScript extension library that provides many modules and features that can be used to build strong and modern applications based on web standards, feel free to check it out and give it a star if it touches your heart.
Comments