Skip to content

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

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

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

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

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

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

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, {
      // 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.

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

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

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

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

La compressione può essere abilitata per messaggi individuali passando un boolean come secondo argomento a .send().

ts
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'è backpressure
  • 0 — Il messaggio è stato scartato a causa di un problema di connessione
  • 1+ — 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.

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

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

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

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

Per aggiungere event listener al socket:

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

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

Bun a cura di www.bunjs.com.cn