Bun.serve() unterstützt serverseitige WebSockets mit On-the-Fly-Komprimierung, TLS-Unterstützung und einer Bun-nativen Publish-Subscribe-API.
Hinweis
⚡️ 7x höherer Durchsatz
Buns WebSockets sind schnell. Für einen einfachen Chatroom unter Linux x64 kann Bun 7x mehr Anfragen pro Sekunde verarbeiten als Node.js + "ws".
| Nachrichten pro Sekunde | Laufzeitumgebung | Clients |
|---|---|---|
| ~700.000 | (Bun.serve) Bun v0.2.1 (x64) | 16 |
| ~100.000 | (ws) Node v18.10.0 (x64) | 16 |
Internally basiert Buns WebSocket-Implementierung auf uWebSockets.
WebSocket-Server starten
Unten ist ein einfacher WebSocket-Server mit Bun.serve aufgebaut, bei dem alle eingehenden Anfragen im fetch-Handler zu WebSocket-Verbindungen upgegradet werden. Die Socket-Handler werden im websocket-Parameter deklariert.
Bun.serve({
fetch(req, server) {
// Request zu einem WebSocket upgraden
if (server.upgrade(req)) {
return; // keine Response zurückgeben
}
return new Response("Upgrade fehlgeschlagen", { status: 500 });
},
websocket: {}, // Handler
});Die folgenden WebSocket-Ereignis-Handler werden unterstützt:
Bun.serve({
fetch(req, server) {}, // Upgrade-Logik
websocket: {
message(ws, message) {}, // eine Nachricht wird empfangen
open(ws) {}, // ein Socket wird geöffnet
close(ws, code, message) {}, // ein Socket wird geschlossen
drain(ws) {}, // der Socket ist bereit, mehr Daten zu empfangen
},
});Eine API für Geschwindigkeit
In Bun werden Handler einmal pro Server deklariert, nicht pro Socket.
ServerWebSocket erwartet, dass Sie ein WebSocketHandler-Objekt an die Bun.serve()-Methode übergeben, das Methoden für open, message, close, drain und error hat. Dies unterscheidet sich von der clientseitigen WebSocket-Klasse, die EventTarget erweitert (onmessage, onopen, onclose).
Clients haben tendenziell nicht viele Socket-Verbindungen offen, daher macht eine ereignisbasierte API Sinn.
Aber Server haben tendenziell viele Socket-Verbindungen offen, was bedeutet:
- Die Zeit für das Hinzufügen/Entfernen von Event-Listenern für jede Verbindung summiert sich
- Zusätzlicher Speicher für die Speicherung von Referenzen auf Callback-Funktionen für jede Verbindung
- Normalerweise erstellen Leute neue Funktionen für jede Verbindung, was auch mehr Speicher bedeutet
Anstatt eine ereignisbasierte API zu verwenden, erwartet ServerWebSocket, dass Sie ein einzelnes Objekt mit Methoden für jedes Ereignis in Bun.serve() übergeben, das für jede Verbindung wiederverwendet wird.
Dies führt zu weniger Speichernutzung und weniger Zeit für das Hinzufügen/Entfernen von Event-Listenern.
Das erste Argument jedes Handlers ist die Instanz von ServerWebSocket, die das Ereignis verarbeitet. Die ServerWebSocket-Klasse ist eine schnelle, Bun-native Implementierung von WebSocket mit einigen zusätzlichen Funktionen.
Bun.serve({
fetch(req, server) {}, // Upgrade-Logik
websocket: {
message(ws, message) {
ws.send(message); // Nachricht zurücksenden
},
},
});Nachrichten senden
Jede ServerWebSocket-Instanz hat eine .send()-Methode zum Senden von Nachrichten an den Client. Sie unterstützt eine Reihe von Eingabetypen.
Bun.serve({
fetch(req, server) {}, // Upgrade-Logik
websocket: {
message(ws, message) {
ws.send("Hallo Welt"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
},
},
});Header
Sobald das Upgrade erfolgreich ist, sendet Bun eine 101 Switching Protocols-Response gemäß der Spezifikation. Zusätzliche headers können dieser Response im Aufruf von server.upgrade() hinzugefügt werden.
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // Handler
});Kontextdaten
Kontextuelle data können einem neuen WebSocket im .upgrade()-Aufruf angehängt werden. Diese Daten sind über die ws.data-Eigenschaft in den WebSocket-Handlern verfügbar.
Um ws.data stark zu typisieren, fügen Sie eine data-Eigenschaft zum websocket-Handler-Objekt hinzu. Dies typisiert ws.data über alle Lifecycle-Hooks hinweg.
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, {
// dieses Objekt muss WebSocketData entsprechen
data: {
createdAt: Date.now(),
channelId: new URL(req.url).searchParams.get("channelId"),
authToken: cookies.get("X-Token"),
},
});
return undefined;
},
websocket: {
// TypeScript: Typ von ws.data so angeben
data: {} as WebSocketData,
// Handler wird aufgerufen, wenn eine Nachricht empfangen wird
async message(ws, message) {
// ws.data ist jetzt korrekt als WebSocketData typisiert
const user = getUserFromToken(ws.data.authToken);
await saveMessageToDatabase({
channel: ws.data.channelId,
message: String(message),
userId: user.id,
});
},
},
});Hinweis
Hinweis: Früher konnten Sie den Typ von ws.data mit einem Typparameter bei Bun.serve angeben, wie Bun.serve<MyData>({...}). Dieses Muster wurde aufgrund einer Einschränkung in TypeScript zugunsten der oben gezeigten data-Eigenschaft entfernt.
Um von diesem Server im Browser zu verbinden, erstellen Sie einen neuen WebSocket.
const socket = new WebSocket("ws://localhost:3000/chat");
socket.addEventListener("message", event => {
console.log(event.data);
});Hinweis
Benutzer identifizieren
Die Cookies, die aktuell auf der Seite gesetzt sind, werden mit der WebSocket-Upgrade-Anfrage gesendet und sind im fetch-Handler unter req.headers verfügbar. Parsen Sie diese Cookies, um die Identität des verbindenden Benutzers zu bestimmen und den Wert von data entsprechend zu setzen.
Pub/Sub
Buns ServerWebSocket-Implementierung bietet eine native Publish-Subscribe-API für themenbasierte Übertragungen. Einzelne Sockets können sich zu einem Thema (spezifiziert mit einem String-Identifier) .subscribe() und Nachrichten an alle anderen Abonnenten dieses Themas .publish() (außer sich selbst). Diese themenbasierte Broadcast-API ist ähnlich wie MQTT und 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-Fehler", { status: 400 });
}
return new Response("Hallo Welt");
},
websocket: {
// TypeScript: Typ von ws.data so angeben
data: {} as { username: string },
open(ws) {
const msg = `${ws.data.username} hat den Chat betreten`;
ws.subscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
message(ws, message) {
// dies ist ein Gruppenchat
// also sendet der Server eingehende Nachrichten an alle weiter
server.publish("the-group-chat", `${ws.data.username}: ${message}`);
// aktuelle Abonnements inspizieren
console.log(ws.subscriptions); // ["the-group-chat"]
},
close(ws) {
const msg = `${ws.data.username} hat den Chat verlassen`;
ws.unsubscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
},
});
console.log(`Lauscht auf ${server.hostname}:${server.port}`);Der Aufruf von .publish(data) sendet die Nachricht an alle Abonnenten eines Themas außer dem Socket, der .publish() aufgerufen hat. Um eine Nachricht an alle Abonnenten eines Themas zu senden, verwenden Sie die .publish()-Methode auf der Server-Instanz.
const server = Bun.serve({
websocket: {
// ...
},
});
// auf ein externes Ereignis lauschen
server.publish("the-group-chat", "Hallo Welt");Komprimierung
Pro-Nachrichten-Komprimierung kann mit dem perMessageDeflate-Parameter aktiviert werden.
Bun.serve({
websocket: {
perMessageDeflate: true,
},
});Die Komprimierung kann für einzelne Nachrichten aktiviert werden, indem ein boolean als zweites Argument an .send() übergeben wird.
ws.send("Hallo Welt", true);Für feinkörnige Kontrolle über Komprimierungseigenschaften siehe die Referenz.
Gegendruck
Die .send(message)-Methode von ServerWebSocket gibt eine number zurück, die das Ergebnis der Operation anzeigt.
-1— Die Nachricht wurde in die Warteschlange gestellt, aber es gibt Gegendruck0— Die Nachricht wurde aufgrund eines Verbindungsproblems verworfen1+— Die Anzahl der gesendeten Bytes
Dies gibt Ihnen bessere Kontrolle über Gegendruck in Ihrem Server.
Timeouts und Limits
Standardmäßig schließt Bun eine WebSocket-Verbindung, wenn sie 120 Sekunden inaktiv ist. Dies kann mit dem idleTimeout-Parameter konfiguriert werden.
Bun.serve({
fetch(req, server) {}, // Upgrade-Logik
websocket: {
idleTimeout: 60, // 60 Sekunden
},
});Bun schließt auch eine WebSocket-Verbindung, wenn sie eine Nachricht empfängt, die größer als 16 MB ist. Dies kann mit dem maxPayloadLength-Parameter konfiguriert werden.
Bun.serve({
fetch(req, server) {}, // Upgrade-Logik
websocket: {
maxPayloadLength: 1024 * 1024, // 1 MB
},
});Verbindung zu einem WebSocket-Server herstellen
Bun implementiert die WebSocket-Klasse. Um einen WebSocket-Client zu erstellen, der eine Verbindung zu einem ws://- oder wss://-Server herstellt, erstellen Sie eine Instanz von WebSocket, wie Sie es im Browser tun würden.
const socket = new WebSocket("ws://localhost:3000");
// Mit Subprotokoll-Verhandlung
const socket2 = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);In Browsern werden die Cookies, die aktuell auf der Seite gesetzt sind, mit der WebSocket-Upgrade-Anfrage gesendet. Dies ist ein Standardfeature der WebSocket-API.
Der Einfachheit halber ermöglicht Bun das Setzen benutzerdefinierter Header direkt im Konstruktor. Dies ist eine Bun-spezifische Erweiterung des WebSocket-Standards. Dies funktioniert nicht in Browsern.
const socket = new WebSocket("ws://localhost:3000", {
headers: {
/* benutzerdefinierte Header */
},
});Um Event-Listener zum Socket hinzuzufügen:
// Nachricht wird empfangen
socket.addEventListener("message", event => {});
// Socket geöffnet
socket.addEventListener("open", event => {});
// Socket geschlossen
socket.addEventListener("close", event => {});
// Fehlerhandler
socket.addEventListener("error", event => {});Referenz
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; // Standard: 16 * 1024 * 1024 = 16 MB
idleTimeout?: number; // Standard: 120 (Sekunden)
backpressureLimit?: number; // Standard: 1024 * 1024 = 1 MB
closeOnBackpressureLimit?: boolean; // Standard: false
sendPings?: boolean; // Standard: true
publishToSelf?: boolean; // Standard: 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;
}