Skip to content

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 SekundeLaufzeitumgebungClients
~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.

ts
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:

ts
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.

ts
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.

ts
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
    },
  },
});

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.

ts
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.

ts
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.

js
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.

ts
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.

ts
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.

ts
Bun.serve({
  websocket: {
    perMessageDeflate: true, 
  },
});

Die Komprimierung kann für einzelne Nachrichten aktiviert werden, indem ein boolean als zweites Argument an .send() übergeben wird.

ts
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 Gegendruck
  • 0 — Die Nachricht wurde aufgrund eines Verbindungsproblems verworfen
  • 1+ — 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.

ts
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.

ts
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.

ts
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.

ts
const socket = new WebSocket("ws://localhost:3000", {
  headers: {
    /* benutzerdefinierte Header */
  }, 
});

Um Event-Listener zum Socket hinzuzufügen:

ts
// 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

ts
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;
}

Bun von www.bunjs.com.cn bearbeitet