Skip to content

Bun.serve() prend en charge les WebSockets côté serveur, avec compression à la volée, prise en charge de TLS, et une API de publication-souscription native à Bun.

Note

⚡️ 7 fois plus de débit

Les WebSockets de Bun sont rapides. Pour un simple salon de discussion sur Linux x64, Bun peut gérer 7 fois plus de requêtes par seconde que Node.js + "ws".

Messages envoyés par secondeRuntimeClients
~700 000(Bun.serve) Bun v0.2.1 (x64)16
~100 000(ws) Node v18.10.0 (x64)16

En interne, l'implémentation WebSocket de Bun est basée sur uWebSockets.


Démarrer un serveur WebSocket

Ci-dessous se trouve un simple serveur WebSocket construit avec Bun.serve, dans lequel toutes les requêtes entrantes sont mises à niveau vers des connexions WebSocket dans le gestionnaire fetch. Les gestionnaires de socket sont déclarés dans le paramètre websocket.

ts
Bun.serve({
  fetch(req, server) {
    // mettre à niveau la requête vers un WebSocket
    if (server.upgrade(req)) {
      return; // ne pas retourner de Response
    }
    return new Response("Échec de la mise à niveau", { status: 500 });
  },
  websocket: {}, // gestionnaires
});

Les gestionnaires d'événements WebSocket suivants sont pris en charge :

ts
Bun.serve({
  fetch(req, server) {}, // logique de mise à niveau
  websocket: {
    message(ws, message) {}, // un message est reçu
    open(ws) {}, // une socket est ouverte
    close(ws, code, message) {}, // une socket est fermée
    drain(ws) {}, // la socket est prête à recevoir plus de données
  },
});

Une API conçue pour la vitesse

Dans Bun, les gestionnaires sont déclarés une fois par serveur, au lieu d'une fois par socket.

ServerWebSocket s'attend à ce que vous passiez un objet WebSocketHandler à la méthode Bun.serve() qui a des méthodes pour open, message, close, drain, et error. C'est différent de la classe WebSocket côté client qui étend EventTarget (onmessage, onopen, onclose).

Les clients n'ont généralement pas beaucoup de connexions socket ouvertes, donc une API basée sur les événements a du sens.

Mais les serveurs ont tendance à avoir beaucoup de connexions socket ouvertes, ce qui signifie :

  • Le temps passé à ajouter/supprimer des écouteurs d'événements pour chaque connexion s'accumule
  • Mémoire supplémentaire dépensée pour stocker des références aux fonctions de rappel pour chaque connexion
  • Généralement, les gens créent de nouvelles fonctions pour chaque connexion, ce qui signifie aussi plus de mémoire

Ainsi, au lieu d'utiliser une API basée sur les événements, ServerWebSocket s'attend à ce que vous passiez un seul objet avec des méthodes pour chaque événement dans Bun.serve() et il est réutilisé pour chaque connexion.

Cela conduit à une utilisation moindre de la mémoire et moins de temps passé à ajouter/supprimer des écouteurs d'événements.

Le premier argument de chaque gestionnaire est l'instance de ServerWebSocket gérant l'événement. La classe ServerWebSocket est une implémentation rapide et native à Bun de WebSocket avec quelques fonctionnalités supplémentaires.

ts
Bun.serve({
  fetch(req, server) {}, // logique de mise à niveau
  websocket: {
    message(ws, message) {
      ws.send(message); // renvoyer le message
    },
  },
});

Envoi de messages

Chaque instance ServerWebSocket a une méthode .send() pour envoyer des messages au client. Elle prend en charge une gamme de types d'entrée.

ts
Bun.serve({
  fetch(req, server) {}, // logique de mise à niveau
  websocket: {
    message(ws, message) {
      ws.send("Hello world"); // string
      ws.send(response.arrayBuffer()); // ArrayBuffer
      ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
    },
  },
});

En-têtes

Une fois la mise à niveau réussie, Bun enverra une réponse 101 Switching Protocols selon la spécification. Des headers supplémentaires peuvent être attachés à cette Response dans l'appel à server.upgrade().

ts
Bun.serve({
  fetch(req, server) {
    const sessionId = await generateSessionId();
    server.upgrade(req, {
      headers: { 
        "Set-Cookie": `SessionId=${sessionId}`, 
      }, 
    });
  },
  websocket: {}, // gestionnaires
});

Données contextuelles

Des data contextuelles peuvent être attachées à un nouveau WebSocket dans l'appel .upgrade(). Ces données sont mises à disposition sur la propriété ws.data à l'intérieur des gestionnaires WebSocket.

Pour typer fortement ws.data, ajoutez une propriété data à l'objet gestionnaire websocket. Cela type ws.data dans tous les hooks de cycle de vie.

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, {
      // cet objet doit être conforme à WebSocketData
      data: {
        createdAt: Date.now(),
        channelId: new URL(req.url).searchParams.get("channelId"),
        authToken: cookies.get("X-Token"),
      },
    });

    return undefined;
  },
  websocket: {
    // TypeScript: spécifier le type de ws.data comme ceci
    data: {} as WebSocketData,
    // gestionnaire appelé quand un message est reçu
    async message(ws, message) {
      // ws.data est maintenant correctement typé comme WebSocketData
      const user = getUserFromToken(ws.data.authToken);

      await saveMessageToDatabase({
        channel: ws.data.channelId,
        message: String(message),
        userId: user.id,
      });
    },
  },
});

Note

Note : Auparavant, vous pouviez spécifier le type de ws.data en utilisant un paramètre de type sur Bun.serve, comme Bun.serve<MyData>({...}). Ce modèle a été supprimé en raison d'une limitation dans TypeScript en faveur de la propriété data montrée ci-dessus.

Pour se connecter à ce serveur depuis le navigateur, créez un nouveau WebSocket.

js
const socket = new WebSocket("ws://localhost:3000/chat");

socket.addEventListener("message", event => {
  console.log(event.data);
});

Note

Identifier les utilisateurs

Les cookies actuellement définis sur la page seront envoyés avec la requête de mise à niveau WebSocket et disponibles sur req.headers dans le gestionnaire fetch. Analysez ces cookies pour déterminer l'identité de l'utilisateur qui se connecte et définissez la valeur de data en conséquence.

Publication/Souscription (Pub/Sub)

L'implémentation ServerWebSocket de Bun implémente une API native de publication-souscription pour la diffusion basée sur des sujets. Les sockets individuels peuvent .subscribe() à un sujet (spécifié avec un identifiant de chaîne) et .publish() des messages à tous les autres abonnés à ce sujet (en s'excluant eux-mêmes). Cette API de diffusion basée sur des sujets est similaire à MQTT et Redis Pub/Sub.

ts
const server = Bun.serve({
  fetch(req, server) {
    const url = new URL(req.url);
    if (url.pathname === "/chat") {
      console.log(`mise à niveau !`);
      const username = getUsernameFromReq(req);
      const success = server.upgrade(req, { data: { username } });
      return success ? undefined : new Response("Erreur de mise à niveau WebSocket", { status: 400 });
    }

    return new Response("Hello world");
  },
  websocket: {
    // TypeScript: spécifier le type de ws.data comme ceci
    data: {} as { username: string },
    open(ws) {
      const msg = `${ws.data.username} a rejoint le chat`;
      ws.subscribe("le-salon-de-discussion");
      server.publish("le-salon-de-discussion", msg);
    },
    message(ws, message) {
      // c'est un chat de groupe
      // donc le serveur rediffuse les messages entrants à tout le monde
      server.publish("le-salon-de-discussion", `${ws.data.username}: ${message}`);

      // inspecter les abonnements actuels
      console.log(ws.subscriptions); // ["le-salon-de-discussion"]
    },
    close(ws) {
      const msg = `${ws.data.username} a quitté le chat`;
      ws.unsubscribe("le-salon-de-discussion");
      server.publish("le-salon-de-discussion", msg);
    },
  },
});

console.log(`Écoute sur ${server.hostname}:${server.port}`);

L'appel de .publish(data) enverra le message à tous les abonnés d'un sujet sauf la socket qui a appelé .publish(). Pour envoyer un message à tous les abonnés d'un sujet, utilisez la méthode .publish() sur l'instance Server.

ts
const server = Bun.serve({
  websocket: {
    // ...
  },
});

// écouter un événement externe
server.publish("le-salon-de-discussion", "Hello world");

Compression

La compression par message peut être activée avec le paramètre perMessageDeflate.

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

La compression peut être activée pour des messages individuels en passant un boolean comme deuxième argument à .send().

ts
ws.send("Hello world", true);

Pour un contrôle précis des caractéristiques de compression, consultez la Référence.

Contre-pression (Backpressure)

La méthode .send(message) de ServerWebSocket retourne un number indiquant le résultat de l'opération.

  • -1 — Le message a été mis en file d'attente mais il y a une contre-pression
  • 0 — Le message a été abandonné en raison d'un problème de connexion
  • 1+ — Le nombre d'octets envoyés

Cela vous donne un meilleur contrôle sur la contre-pression dans votre serveur.

Délais d'attente et limites

Par défaut, Bun fermera une connexion WebSocket si elle est inactive pendant 120 secondes. Cela peut être configuré avec le paramètre idleTimeout.

ts
Bun.serve({
  fetch(req, server) {}, // logique de mise à niveau
  websocket: {
    idleTimeout: 60, // 60 secondes
  },
});

Bun fermera également une connexion WebSocket si elle reçoit un message plus grand que 16 Mo. Cela peut être configuré avec le paramètre maxPayloadLength.

ts
Bun.serve({
  fetch(req, server) {}, // logique de mise à niveau
  websocket: {
    maxPayloadLength: 1024 * 1024, // 1 Mo
  },
});

Se connecter à un serveur WebSocket

Bun implémente la classe WebSocket. Pour créer un client WebSocket qui se connecte à un serveur ws:// ou wss://, créez une instance de WebSocket, comme vous le feriez dans le navigateur.

ts
const socket = new WebSocket("ws://localhost:3000");

// Avec négociation de sous-protocole
const socket2 = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);

Dans les navigateurs, les cookies actuellement définis sur la page seront envoyés avec la requête de mise à niveau WebSocket. C'est une fonctionnalité standard de l'API WebSocket.

Pour plus de commodité, Bun vous permet de définir des en-têtes personnalisés directement dans le constructeur. C'est une extension spécifique à Bun de la norme WebSocket. Cela ne fonctionnera pas dans les navigateurs.

ts
const socket = new WebSocket("ws://localhost:3000", {
  headers: {
    /* en-têtes personnalisés */
  }, 
});

Pour ajouter des écouteurs d'événements à la socket :

ts
// un message est reçu
socket.addEventListener("message", event => {});

// socket ouverte
socket.addEventListener("open", event => {});

// socket fermée
socket.addEventListener("close", event => {});

// gestionnaire d'erreur
socket.addEventListener("error", event => {});

Référence

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; // par défaut: 16 * 1024 * 1024 = 16 Mo
      idleTimeout?: number; // par défaut: 120 (secondes)
      backpressureLimit?: number; // par défaut: 1024 * 1024 = 1 Mo
      closeOnBackpressureLimit?: boolean; // par défaut: false
      sendPings?: boolean; // par défaut: true
      publishToSelf?: boolean; // par défaut: 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 édité par www.bunjs.com.cn