Bun.serve() supporta WebSocket lato server, con compressione on-the-fly, supporto TLS e un'API publish-subscribe nativa di Bun.
Note
⚡️ 7x più throughput
I WebSocket di Bun sono veloci. Per una semplice chatroom su Linux x64, Bun può gestire 7x più richieste al secondo rispetto a Node.js + "ws".
| Messaggi inviati al secondo | Runtime | Client |
|---|---|---|
| ~700.000 | (Bun.serve) Bun v0.2.1 (x64) | 16 |
| ~100.000 | (ws) Node v18.10.0 (x64) | 16 |
Internamente l'implementazione WebSocket di Bun è costruita su uWebSockets.
Avviare un server WebSocket
Qui sotto c'è un semplice server WebSocket costruito con Bun.serve, in cui tutte le richieste in ingresso sono aggiornate a connessioni WebSocket nel gestore fetch. I gestori del socket sono dichiarati nel parametro websocket.
Bun.serve({
fetch(req, server) {
// aggiorna la richiesta a un WebSocket
if (server.upgrade(req)) {
return; // non restituire una Response
}
return new Response("Upgrade fallito", { status: 500 });
},
websocket: {}, // gestori
});I seguenti gestori di eventi WebSocket sono supportati:
Bun.serve({
fetch(req, server) {}, // logica di upgrade
websocket: {
message(ws, message) {}, // viene ricevuto un messaggio
open(ws) {}, // un socket è aperto
close(ws, code, message) {}, // un socket è chiuso
drain(ws) {}, // il socket è pronto a ricevere più dati
},
});Un'API progettata per la velocità
In Bun, i gestori sono dichiarati una volta per server, invece che per socket.
ServerWebSocket si aspetta che tu passi un oggetto WebSocketHandler al metodo Bun.serve() che ha metodi per open, message, close, drain e error. Questo è diverso dalla classe WebSocket lato client che estende EventTarget (onmessage, onopen, onclose).
I client tendono a non avere molte connessioni socket aperte quindi un'API basata su eventi ha senso.
Ma i server tendono ad avere molte connessioni socket aperte, il che significa:
- Il tempo aggiunto per aggiungere/rimuovere event listener per ogni connessione si accumula
- Memoria extra spesa per memorizzare riferimenti a funzioni callback per ogni connessione
- Di solito, le persone creano nuove funzioni per ogni connessione, il che significa anche più memoria
Quindi, invece di usare un'API basata su eventi, ServerWebSocket si aspetta che tu passi un singolo oggetto con metodi per ogni evento in Bun.serve() e viene riutilizzato per ogni connessione.
Questo porta a meno utilizzo di memoria e meno tempo aggiunto per aggiungere/rimuovere event listener.
Il primo argomento di ogni gestore è l'istanza di ServerWebSocket che gestisce l'evento. La classe ServerWebSocket è un'implementazione veloce e nativa di Bun di WebSocket con alcune funzionalità aggiuntive.
Bun.serve({
fetch(req, server) {}, // logica di upgrade
websocket: {
message(ws, message) {
ws.send(message); // eco indietro del messaggio
},
},
});Inviare messaggi
Ogni istanza di ServerWebSocket ha un metodo .send() per inviare messaggi al client. Supporta una gamma di tipi di input.
Bun.serve({
fetch(req, server) {}, // logica di upgrade
websocket: {
message(ws, message) {
ws.send("Hello world"); // stringa
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
},
},
});Headers
Una volta che l'upgrade ha successo, Bun invierà una risposta 101 Switching Protocols secondo la specifica. Headers aggiuntivi headers possono essere allegati a questa Response nella chiamata a server.upgrade().
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // gestori
});Dati contestuali
Dati contestuali data possono essere allegati a un nuovo WebSocket nella chiamata .upgrade(). Questi dati sono resi disponibili sulla proprietà ws.data all'interno dei gestori WebSocket.
Per digitare fortemente ws.data, aggiungi una proprietà data all'oggetto gestore websocket. Questo digita ws.data attraverso tutti gli hook del ciclo di vita.
type WebSocketData = {
createdAt: number;
channelId: string;
authToken: string;
};
Bun.serve({
fetch(req, server) {
const cookies = new Bun.CookieMap(req.headers.get("cookie")!);
server.upgrade(req, {
// questo oggetto deve conformarsi a WebSocketData
data: {
createdAt: Date.now(),
channelId: new URL(req.url).searchParams.get("channelId"),
authToken: cookies.get("X-Token"),
},
});
return undefined;
},
websocket: {
// TypeScript: specifica il tipo di ws.data come questo
data: {} as WebSocketData,
// gestore chiamato quando viene ricevuto un messaggio
async message(ws, message) {
// ws.data è ora correttamente digitato come WebSocketData
const user = getUserFromToken(ws.data.authToken);
await saveMessageToDatabase({
channel: ws.data.channelId,
message: String(message),
userId: user.id,
});
},
},
});Note
Nota: Precedentemente, potevi specificare il tipo di ws.data usando un parametro di tipo su Bun.serve, come Bun.serve<MyData>({...}). Questo pattern è stato rimosso a causa di una limitazione in TypeScript a favore della proprietà data mostrata sopra.
Per connetterti a questo server dal browser, crea un nuovo WebSocket.
const socket = new WebSocket("ws://localhost:3000/chat");
socket.addEventListener("message", event => {
console.log(event.data);
});Note
Identificazione degli utenti
I cookie che sono attualmente impostati sulla pagina saranno inviati con la richiesta di upgrade WebSocket e disponibili su req.headers nel gestore fetch. Analizza questi cookie per determinare l'identità dell'utente che si connette e imposta il valore di data di conseguente.
Pub/Sub
L'implementazione ServerWebSocket di Bun implementa un'API publish-subscribe nativa per broadcasting basato su topic. Socket individuali possono .subscribe() a un topic (specificato con un identificatore stringa) e .publish() messaggi a tutti gli altri iscritti a quel topic (escludendo se stessi). Questa API di broadcast basata su topic è simile a MQTT e Redis Pub/Sub.
const server = Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/chat") {
console.log(`upgrade!`);
const username = getUsernameFromReq(req);
const success = server.upgrade(req, { data: { username } });
return success ? undefined : new Response("WebSocket upgrade error", { status: 400 });
}
return new Response("Hello world");
},
websocket: {
// TypeScript: specifica il tipo di ws.data come questo
data: {} as { username: string },
open(ws) {
const msg = `${ws.data.username} è entrato nella chat`;
ws.subscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
message(ws, message) {
// questa è una chat di gruppo
// quindi il server ri-broadcast il messaggio in ingresso a tutti
server.publish("the-group-chat", `${ws.data.username}: ${message}`);
// ispeziona le sottoscrizioni correnti
console.log(ws.subscriptions); // ["the-group-chat"]
},
close(ws) {
const msg = `${ws.data.username} ha lasciato la chat`;
ws.unsubscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
},
});
console.log(`In ascolto su ${server.hostname}:${server.port}`);Chiamare .publish(data) invierà il messaggio a tutti gli iscritti di un topic eccetto il socket che ha chiamato .publish(). Per inviare un messaggio a tutti gli iscritti di un topic, usa il metodo .publish() sull'istanza Server.
const server = Bun.serve({
websocket: {
// ...
},
});
// aspetta per qualche evento esterno
server.publish("the-group-chat", "Hello world");Compressione
La compressione per messaggio compression può essere abilitata con il parametro perMessageDeflate.
Bun.serve({
websocket: {
perMessageDeflate: true,
},
});La compressione può essere abilitata per messaggi individuali passando un boolean come secondo argomento a .send().
ws.send("Hello world", true);Per un controllo granulare sulle caratteristiche della compressione, consulta il Riferimento.
Backpressure
Il metodo .send(message) di ServerWebSocket restituisce un number che indica il risultato dell'operazione.
-1— Il messaggio è stato accodato ma c'è backpressure0— Il messaggio è stato scartato a causa di un problema di connessione1+— Il numero di byte inviati
Questo ti dà un migliore controllo sulla backpressure nel tuo server.
Timeout e limiti
Di default, Bun chiuderà una connessione WebSocket se è inattiva per 120 secondi. Questo può essere configurato con il parametro idleTimeout.
Bun.serve({
fetch(req, server) {}, // logica di upgrade
websocket: {
idleTimeout: 60, // 60 secondi
},
});Bun chiuderà anche una connessione WebSocket se riceve un messaggio più grande di 16 MB. Questo può essere configurato con il parametro maxPayloadLength.
Bun.serve({
fetch(req, server) {}, // logica di upgrade
websocket: {
maxPayloadLength: 1024 * 1024, // 1 MB
},
});Connettersi a un server Websocket
Bun implementa la classe WebSocket. Per creare un client WebSocket che si connette a un server ws:// o wss://, crea un'istanza di WebSocket, come faresti nel browser.
const socket = new WebSocket("ws://localhost:3000");
// Con negoziazione del subprotocollo
const socket2 = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);Nei browser, i cookie che sono attualmente impostati sulla pagina saranno inviati con la richiesta di upgrade WebSocket. Questa è una caratteristica standard dell'API WebSocket.
Per comodità, Bun ti permette di impostare header personalizzati direttamente nel costruttore. Questa è un'estensione specifica di Bun dello standard WebSocket. Questo non funzionerà nei browser.
const socket = new WebSocket("ws://localhost:3000", {
headers: {
/* header personalizzati */
},
});Per aggiungere event listener al socket:
// viene ricevuto un messaggio
socket.addEventListener("message", event => {});
// socket aperto
socket.addEventListener("open", event => {});
// socket chiuso
socket.addEventListener("close", event => {});
// gestore errori
socket.addEventListener("error", event => {});Riferimento
namespace Bun {
export function serve(params: {
fetch: (req: Request, server: Server) => Response | Promise<Response>;
websocket?: {
message: (ws: ServerWebSocket, message: string | ArrayBuffer | Uint8Array) => void;
open?: (ws: ServerWebSocket) => void;
close?: (ws: ServerWebSocket, code: number, reason: string) => void;
error?: (ws: ServerWebSocket, error: Error) => void;
drain?: (ws: ServerWebSocket) => void;
maxPayloadLength?: number; // default: 16 * 1024 * 1024 = 16 MB
idleTimeout?: number; // default: 120 (secondi)
backpressureLimit?: number; // default: 1024 * 1024 = 1 MB
closeOnBackpressureLimit?: boolean; // default: false
sendPings?: boolean; // default: true
publishToSelf?: boolean; // default: false
perMessageDeflate?:
| boolean
| {
compress?: boolean | Compressor;
decompress?: boolean | Compressor;
};
};
}): Server;
}
type Compressor =
| `"disable"`
| `"shared"`
| `"dedicated"`
| `"3KB"`
| `"4KB"`
| `"8KB"`
| `"16KB"`
| `"32KB"`
| `"64KB"`
| `"128KB"`
| `"256KB"`;
interface Server {
pendingWebSockets: number;
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer, compress?: boolean): number;
upgrade(
req: Request,
options?: {
headers?: HeadersInit;
data?: any;
},
): boolean;
}
interface ServerWebSocket {
readonly data: any;
readonly readyState: number;
readonly remoteAddress: string;
readonly subscriptions: string[];
send(message: string | ArrayBuffer | Uint8Array, compress?: boolean): number;
close(code?: number, reason?: string): void;
subscribe(topic: string): void;
unsubscribe(topic: string): void;
publish(topic: string, message: string | ArrayBuffer | Uint8Array): void;
isSubscribed(topic: string): boolean;
cork(cb: (ws: ServerWebSocket) => void): void;
}