Bun.serve() 는 온더플라이 압축, TLS 지원 및 Bun 네이티브 게시 - 구독 API 를 갖춘 서버 측 WebSocket 을 지원합니다.
참고
⚡️ 7 배 더 많은 처리량
Bun 의 WebSocket 은 빠릅니다. Linux x64 에서 간단한 채팅룸 의 경우 Bun 은 Node.js + "ws" 보다 초당 7 배 더 많은 요청을 처리할 수 있습니다.
| 초당 전송된 메시지 수 | 런타임 | 클라이언트 |
|---|---|---|
| ~700,000 | (Bun.serve) Bun v0.2.1 (x64) | 16 |
| ~100,000 | (ws) Node v18.10.0 (x64) | 16 |
내부적으로 Bun 의 WebSocket 구현은 uWebSockets 를 기반으로 구축되었습니다.
WebSocket 서버 시작
아래는 Bun.serve 로 구축된 간단한 WebSocket 서버로, 모든 들어오는 요청이 fetch 핸들러에서 WebSocket 연결로 업그레이드 됩니다. 소켓 핸들러는 websocket 매개변수에 선언됩니다.
Bun.serve({
fetch(req, server) {
// 요청을 WebSocket 으로 업그레이드
if (server.upgrade(req)) {
return; // Response 를 반환하지 않음
}
return new Response("업그레이드 실패", { status: 500 });
},
websocket: {}, // 핸들러
});다음 WebSocket 이벤트 핸들러가 지원됩니다.
Bun.serve({
fetch(req, server) {}, // 업그레이드 로직
websocket: {
message(ws, message) {}, // 메시지 수신
open(ws) {}, // 소켓 열림
close(ws, code, message) {}, // 소켓 닫힘
drain(ws) {}, // 소켓이 더 많은 데이터 수신 준비됨
},
});속도를 위해 설계된 API
Bun 에서 핸들러는 소켓당 한 번이 아니라 서버당 한 번 선언됩니다.
ServerWebSocket 는 open, message, close, drain 및 error 에 대한 메서드가 있는 WebSocketHandler 객체를 Bun.serve() 메서드에 전달할 것을 기대합니다. 이는 클라이언트 측 WebSocket 클래스와 다릅니다. 이 클래스는 EventTarget(onmessage, onopen, onclose) 을 확장합니다.
클라이언트는 많은 소켓 연결이 열려 있지 않는 경향이 있으므로 이벤트 기반 API 가 의미가 있습니다.
하지만 서버는 많은 소켓 연결이 열려 있는 경향이 있습니다. 즉:
- 각 연결에 대한 이벤트 리스너 추가/제거에 소요되는 시간이 누적됩니다
- 각 연결에 대한 콜백 함수 참조를 저장하는 데 추가 메모리가 소요됩니다
- 일반적으로 각 연결에 대해 새 함수를 생성하므로 메모리가 더 많이 소요됩니다
따라서 이벤트 기반 API 를 사용하는 대신 ServerWebSocket 은 Bun.serve() 에서 각 이벤트에 대한 메서드가 있는 단일 객체를 전달할 것을 기대하며 이는 각 연결에 대해 재사용됩니다.
이는 메모리 사용량이 줄어들고 이벤트 리스너를 추가/제거하는 데 소요되는 시간이 줄어듭니다.
각 핸들러의 첫 번째 인수는 이벤트를 처리하는 ServerWebSocket 인스턴스입니다. ServerWebSocket 클래스는 몇 가지 추가 기능이 있는 WebSocket 의 빠르고 Bun 네이티브 구현입니다.
Bun.serve({
fetch(req, server) {}, // 업그레이드 로직
websocket: {
message(ws, message) {
ws.send(message); // 메시지 에코
},
},
});메시지 전송
각 ServerWebSocket 인스턴스는 클라이언트에 메시지를 보내기 위한 .send() 메서드를 가지고 있습니다. 다양한 입력 타입을 지원합니다.
Bun.serve({
fetch(req, server) {}, // 업그레이드 로직
websocket: {
message(ws, message) {
ws.send("Hello world"); // string
ws.send(response.arrayBuffer()); // ArrayBuffer
ws.send(new Uint8Array([1, 2, 3])); // TypedArray | DataView
},
},
});헤더
업그레이드가 성공하면 Bun 은 명세 에 따라 101 Switching Protocols 응답을 보냅니다. 추가 headers 는 server.upgrade() 호출에서 이 Response 에 첨부할 수 있습니다.
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // 핸들러
});컨텍스트 데이터
컨텍스트 data 는 .upgrade() 호출에서 새 WebSocket 에 첨부할 수 있습니다. 이 데이터는 WebSocket 핸들러 내부의 ws.data 속성에서 사용할 수 있습니다.
ws.data 를 강력하게 타입 지정하려면 websocket 핸들러 객체에 data 속성을 추가합니다. 이는 모든 수명 주기 후크에서 ws.data 를 타입 지정합니다.
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, {
// 이 객체는 WebSocketData 를 준수해야 함
data: {
createdAt: Date.now(),
channelId: new URL(req.url).searchParams.get("channelId"),
authToken: cookies.get("X-Token"),
},
});
return undefined;
},
websocket: {
// TypeScript: ws.data 의 타입을 이렇게 지정
data: {} as WebSocketData,
// 메시지 수신 시 호출되는 핸들러
async message(ws, message) {
// ws.data 는 이제 제대로 WebSocketData 로 타입 지정됨
const user = getUserFromToken(ws.data.authToken);
await saveMessageToDatabase({
channel: ws.data.channelId,
message: String(message),
userId: user.id,
});
},
},
});참고
참고: 이전에는 Bun.serve 에 타입 매개변수를 사용하여 ws.data 의 타입을 지정할 수 있었습니다. 예: Bun.serve<MyData>({...}). 이 패턴은 위에 표시된 data 속성을 선호하여 TypeScript 의 제한 사항 으로 인해 제거되었습니다.
브라우저에서 이 서버에 연결하려면 새 WebSocket 을 생성합니다.
const socket = new WebSocket("ws://localhost:3000/chat");
socket.addEventListener("message", event => {
console.log(event.data);
});참고
사용자 식별
현재 페이지에 설정된 쿠키는 WebSocket 업그레이드 요청과 함께 전송되며 fetch 핸들러의 req.headers 에서 사용할 수 있습니다. 이 쿠키를 구문 분석하여 연결하는 사용자의 신원을 확인하고 이에 따라 data 의 값을 설정합니다.
Pub/Sub
Bun 의 ServerWebSocket 구현은 토픽 기반 브로드캐스트를 위한 네이티브 게시 - 구독 API 를 구현합니다. 개별 소켓은 토픽 (문자열 식별자로 지정) 에 .subscribe() 하고 해당 토픽의 다른 모든 구독자에게 (자신을 제외하고) 메시지를 .publish() 할 수 있습니다. 이 토픽 기반 브로드캐스트 API 는 MQTT 및 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 업그레이드 오류", { status: 400 });
}
return new Response("Hello world");
},
websocket: {
// TypeScript: ws.data 의 타입을 이렇게 지정
data: {} as { username: string },
open(ws) {
const msg = `${ws.data.username}님이 채팅에 참여했습니다`;
ws.subscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
message(ws, message) {
// 이는 그룹 채팅입니다
// 따라서 서버는 들어오는 메시지를 모든 사람에게 재전송합니다
server.publish("the-group-chat", `${ws.data.username}: ${message}`);
// 현재 구독 검사
console.log(ws.subscriptions); // ["the-group-chat"]
},
close(ws) {
const msg = `${ws.data.username}님이 채팅을 나갔습니다`;
ws.unsubscribe("the-group-chat");
server.publish("the-group-chat", msg);
},
},
});
console.log(`리스닝 중: ${server.hostname}:${server.port}`);.publish(data) 를 호출하면 .publish() 를 호출한 소켓을 제외한 토픽의 모든 구독자에게 메시지가 전송됩니다. 토픽의 모든 구독자에게 메시지를 보내려면 Server 인스턴스의 .publish() 메서드를 사용합니다.
const server = Bun.serve({
websocket: {
// ...
},
});
// 일부 외부 이벤트 리스닝
server.publish("the-group-chat", "Hello world");압축
메시지당 압축 은 perMessageDeflate 매개변수로 활성화할 수 있습니다.
Bun.serve({
websocket: {
perMessageDeflate: true,
},
});압축은 .send() 의 두 번째 인수로 boolean 을 전달하여 개별 메시지에 대해 활성화할 수 있습니다.
ws.send("Hello world", true);압축 특성에 대한 세밀한 제어는 참조 를 참조하세요.
백프레셔
ServerWebSocket 의 .send(message) 메서드는 작업 결과를 나타내는 number 를 반환합니다.
-1— 메시지가 인큐되었지만 백프레셔가 있음0— 연결 문제로 인해 메시지가 드롭됨1+— 전송된 바이트 수
이를 통해 서버의 백프레셔를 더 잘 제어할 수 있습니다.
시간 제한 및 제한
기본적으로 Bun 은 120 초 동안 유휴 상태인 WebSocket 연결을 닫습니다. 이는 idleTimeout 매개변수로 구성할 수 있습니다.
Bun.serve({
fetch(req, server) {}, // 업그레이드 로직
websocket: {
idleTimeout: 60, // 60 초
},
});Bun 은 16 MB 보다 큰 메시지를 수신하면 WebSocket 연결을 닫습니다. 이는 maxPayloadLength 매개변수로 구성할 수 있습니다.
Bun.serve({
fetch(req, server) {}, // 업그레이드 로직
websocket: {
maxPayloadLength: 1024 * 1024, // 1 MB
},
});Websocket 서버에 연결
Bun 은 WebSocket 클래스를 구현합니다. ws:// 또는 wss:// 서버에 연결하는 WebSocket 클라이언트를 생성하려면 브라우저에서와 마찬가지로 WebSocket 인스턴스를 생성합니다.
const socket = new WebSocket("ws://localhost:3000");
// 서브프로토콜 협상 사용
const socket2 = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);브라우저에서 현재 페이지에 설정된 쿠키는 WebSocket 업그레이드 요청과 함께 전송됩니다. 이는 WebSocket API 의 표준 기능입니다.
편의상 Bun 은 생성자에서 직접 사용자 정의 헤더를 설정할 수 있습니다. 이는 WebSocket 표준의 Bun 특정 확장입니다. 이것은 브라우저에서 작동하지 않습니다.
const socket = new WebSocket("ws://localhost:3000", {
headers: {
/* 사용자 정의 헤더 */
},
});소켓에 이벤트 리스너를 추가하려면:
// 메시지 수신
socket.addEventListener("message", event => {});
// 소켓 열림
socket.addEventListener("open", event => {});
// 소켓 닫힘
socket.addEventListener("close", event => {});
// 오류 핸들러
socket.addEventListener("error", event => {});참조
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; // 기본값: 16 * 1024 * 1024 = 16 MB
idleTimeout?: number; // 기본값: 120 (초)
backpressureLimit?: number; // 기본값: 1024 * 1024 = 1 MB
closeOnBackpressureLimit?: boolean; // 기본값: false
sendPings?: boolean; // 기본값: true
publishToSelf?: boolean; // 기본값: 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;
}