Bun.serve() suporta WebSockets do lado do servidor, com compressão on-the-fly, suporte a TLS e uma API publish-subscribe nativa do Bun.
Nota
⚡️ 7x mais throughput
Os WebSockets do Bun são rápidos. Para um chatroom simples no Linux x64, o Bun pode lidar com 7x mais requisições por segundo que Node.js + "ws".
| Mensagens enviadas por segundo | Runtime | Clientes |
|---|---|---|
| ~700,000 | (Bun.serve) Bun v0.2.1 (x64) | 16 |
| ~100,000 | (ws) Node v18.10.0 (x64) | 16 |
Internamente, a implementação WebSocket do Bun é construída sobre uWebSockets.
Iniciar um servidor WebSocket
Abaixo está um servidor WebSocket simples construído com Bun.serve, no qual todas as requisições recebidas são upgraded para conexões WebSocket no handler fetch. Os handlers do socket são declarados no parâmetro websocket.
Bun.serve({
fetch(req, server) {
// upgrade da requisição para um WebSocket
if (server.upgrade(req)) {
return; // não retorna uma Response
}
return new Response("Upgrade falhou", { status: 500 });
},
websocket: {}, // handlers
});Os seguintes handlers de evento WebSocket são suportados:
Bun.serve({
fetch(req, server) {}, // lógica de upgrade
websocket: {
message(ws, message) {}, // uma mensagem é recebida
open(ws) {}, // um socket é aberto
close(ws, code, message) {}, // um socket é fechado
drain(ws) {}, // o socket está pronto para receber mais dados
},
});Uma API projetada para velocidade
No Bun, handlers são declarados uma vez por servidor, ao invés de por socket.
ServerWebSocket espera que você passe um objeto WebSocketHandler para o método Bun.serve() que tem métodos para open, message, close, drain, e error. Isto é diferente da classe WebSocket do lado do cliente que estende EventTarget (onmessage, onopen, onclose).
Clientes tendem a não ter muitas conexões de socket abertas então uma API baseada em eventos faz sentido.
Mas servidores tendem a ter muitas conexões de socket abertas, o que significa:
- Tempo gasto adicionando/removendo event listeners para cada conexão se acumula
- Memória extra gasta armazenando referências para funções callback para cada conexão
- Geralmente, pessoas criam novas funções para cada conexão, o que também significa mais memória
Então, ao invés de usar uma API baseada em eventos, ServerWebSocket espera que você passe um único objeto com métodos para cada evento em Bun.serve() e ele é reutilizado para cada conexão.
Isto leva a menos uso de memória e menos tempo gasto adicionando/removendo event listeners.
O primeiro argumento para cada handler é a instância de ServerWebSocket lidando com o evento. A classe ServerWebSocket é uma implementação rápida e nativa do Bun de WebSocket com alguns recursos adicionais.
Bun.serve({
fetch(req, server) {}, // lógica de upgrade
websocket: {
message(ws, message) {
ws.send(message); // ecoa de volta a mensagem
},
},
});Enviando mensagens
Cada instância ServerWebSocket tem um método .send() para enviar mensagens ao cliente. Suporta uma variedade de tipos de input.
Bun.serve({
fetch(req, server) {}, // lógica de upgrade
websocket: {
message(ws, message) {
ws.send("Hello world"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
},
},
});Headers
Uma vez que o upgrade é bem sucedido, o Bun enviará uma resposta 101 Switching Protocols conforme a especificação. Headers adicionais podem ser anexados a esta Response na chamada server.upgrade().
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // handlers
});Dados contextuais
data contextual pode ser anexado a um novo WebSocket na chamada .upgrade(). Estes dados são disponibilizados na propriedade ws.data dentro dos handlers WebSocket.
Para tipar fortemente ws.data, adicione uma propriedade data ao objeto handler websocket. Isto tipa ws.data em todos os hooks de lifecycle.
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, {
// este objeto deve conformar com WebSocketData
data: {
createdAt: Date.now(),
channelId: new URL(req.url).searchParams.get("channelId"),
authToken: cookies.get("X-Token"),
},
});
return undefined;
},
websocket: {
// TypeScript: especifica o tipo de ws.data assim
data: {} as WebSocketData,
// handler chamado quando uma mensagem é recebida
async message(ws, message) {
// ws.data é agora propriamente tipado como WebSocketData
const user = getUserFromToken(ws.data.authToken);
await saveMessageToDatabase({
channel: ws.data.channelId,
message: String(message),
userId: user.id,
});
},
},
});Nota
Nota: Anteriormente, você podia especificar o tipo de ws.data usando um parâmetro de tipo em Bun.serve, como Bun.serve<MyData>({...}). Este padrão foi removido devido a uma limitação no TypeScript em favor da propriedade data mostrada acima.
Para conectar a este servidor do navegador, crie um novo WebSocket.
const socket = new WebSocket("ws://localhost:3000/chat");
socket.addEventListener("message", event => {
console.log(event.data);
});Nota
Identificando usuários
Os cookies que estão atualmente definidos na página serão enviados com a requisição de upgrade do WebSocket e disponíveis em req.headers no handler fetch. Analise estes cookies para determinar a identidade do usuário conectando e defina o valor de data adequadamente.
Pub/Sub
A implementação ServerWebSocket do Bun implementa uma API publish-subscribe nativa para broadcast baseado em tópicos. Sockets individuais podem .subscribe() a um tópico (especificado com um identificador string) e .publish() mensagens para todos os outros assinantes daquele tópico (excluindo a si mesmo). Esta API de broadcast baseada em tópicos é similar ao 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("Erro de upgrade WebSocket", { status: 400 });
}
return new Response("Hello world");
},
websocket: {
// TypeScript: especifica o tipo de ws.data assim
data: {} as { username: string },
open(ws) {
const msg = `${ws.data.username} entrou no chat`;
ws.subscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
message(ws, message) {
// este é um chat em grupo
// então o servidor re-transmite mensagens recebidas para todos
server.publish("the-group-chat", `${ws.data.username}: ${message}`);
// inspeciona assinaturas atuais
console.log(ws.subscriptions); // ["the-group-chat"]
},
close(ws) {
const msg = `${ws.data.username} saiu do chat`;
ws.unsubscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
},
});
console.log(`Escutando em ${server.hostname}:${server.port}`);Chamar .publish(data) enviará a mensagem para todos os assinantes de um tópico exceto o socket que chamou .publish(). Para enviar uma mensagem para todos os assinantes de um tópico, use o método .publish() na instância Server.
const server = Bun.serve({
websocket: {
// ...
},
});
// escuta por algum evento externo
server.publish("the-group-chat", "Hello world");Compressão
Compressão por mensagem pode ser habilitada com o parâmetro perMessageDeflate.
Bun.serve({
websocket: {
perMessageDeflate: true,
},
});Compressão pode ser habilitada para mensagens individuais passando um boolean como segundo argumento para .send().
ws.send("Hello world", true);Para controle fino sobre características de compressão, consulte a Referência.
Backpressure
O método .send(message) de ServerWebSocket retorna um number indicando o resultado da operação.
-1— A mensagem foi enfileirada mas há backpressure0— A mensagem foi descartada devido a um problema de conexão1+— O número de bytes enviados
Isto lhe dá melhor controle sobre backpressure no seu servidor.
Timeouts e limites
Por padrão, o Bun fechará uma conexão WebSocket se ela estiver idle por 120 segundos. Isto pode ser configurado com o parâmetro idleTimeout.
Bun.serve({
fetch(req, server) {}, // lógica de upgrade
websocket: {
idleTimeout: 60, // 60 segundos
},
});O Bun também fechará uma conexão WebSocket se receber uma mensagem maior que 16 MB. Isto pode ser configurado com o parâmetro maxPayloadLength.
Bun.serve({
fetch(req, server) {}, // lógica de upgrade
websocket: {
maxPayloadLength: 1024 * 1024, // 1 MB
},
});Conectar a um servidor Websocket
O Bun implementa a classe WebSocket. Para criar um cliente WebSocket que conecta a um servidor ws:// ou wss://, crie uma instância de WebSocket, como você faria no navegador.
const socket = new WebSocket("ws://localhost:3000");
// Com negociação de subprotocolo
const socket2 = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);Em browsers, os cookies que estão atualmente definidos na página serão enviados com a requisição de upgrade do WebSocket. Isto é um recurso padrão da API WebSocket.
Para conveniência, o Bun permite definir headers customizados diretamente no construtor. Isto é uma extensão específica do Bun do padrão WebSocket. Isto não funcionará em browsers.
const socket = new WebSocket("ws://localhost:3000", {
headers: {
/* headers customizados */
},
});Para adicionar event listeners ao socket:
// mensagem é recebida
socket.addEventListener("message", event => {});
// socket aberto
socket.addEventListener("open", event => {});
// socket fechado
socket.addEventListener("close", event => {});
// handler de erro
socket.addEventListener("error", event => {});Referência
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; // padrão: 16 * 1024 * 1024 = 16 MB
idleTimeout?: number; // padrão: 120 (segundos)
backpressureLimit?: number; // padrão: 1024 * 1024 = 1 MB
closeOnBackpressureLimit?: boolean; // padrão: false
sendPings?: boolean; // padrão: true
publishToSelf?: boolean; // padrão: 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;
}