يدعم Bun.serve() WebSockets من جانب الخادم، مع ضغط فوري، ودعم TLS، وواجهة برمجة تطبيقات نشر-اشتراك Bun-native.
ملاحظة
⚡️ إنتاجية أعلى بـ 7 مرات
WebSockets في Bun سريعة. بالنسبة لـ غرفة دردشة بسيطة على Linux x64، يمكن لـ Bun التعامل مع طلبات أكثر بـ 7 مرات في الثانية من Node.js + "ws".
| الرسائل المرسلة في الثانية | وقت التشغيل | العملاء |
|---|---|---|
| ~700,000 | (Bun.serve) Bun v0.2.1 (x64) | 16 |
| ~100,000 | (ws) Node v18.10.0 (x64) | 16 |
داخليًا، تم بناء تنفيذ WebSocket في Bun على uWebSockets.
بدء خادم WebSocket
فيما يلي خادم WebSocket بسيط تم إنشاؤه باستخدام Bun.serve، حيث يتم ترقية جميع الطلبات الواردة إلى اتصالات WebSocket في معالج fetch. يتم الإعلان عن معالجات المقبس في معلمة 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) {}, // المقبس جاهز لاستقبال المزيد من البيانات
},
});واجهة برمجة تطبيقات مصممة للسرعة
في Bun، يتم الإعلان عن المعالجات مرة واحدة لكل خادم، بدلاً من لكل مقبس.
يتوقع ServerWebSocket أن تمرر كائن WebSocketHandler إلى طريقة Bun.serve() الذي يحتوي على طرق لـ open و message و close و drain و error. هذا يختلف عن فئة WebSocket من جانب العميل التي تمتد EventTarget (onmessage، onopen، onclose).
العملاء لا يميلون إلى فتح العديد من اتصالات المقابس لذا فإن واجهة برمجة التطبيقات القائمة على الأحداث منطقية.
لكن الخوادم تميل إلى فتح العديد من اتصالات المقابس، مما يعني:
- الوقت المستغرق في إضافة/إزالة مستمعي الأحداث لكل اتصال يتراكم
- ذاكرة إضافية تُنفق على تخزين مراجع لدوال الاستدعاء لكل اتصال
- عادةً، ينشئ الأشخاص دوالاً جديدة لكل اتصال، مما يعني أيضًا المزيد من الذاكرة
لذلك، بدلاً من استخدام واجهة برمجة تطبيقات قائمة على الأحداث، يتوقع ServerWebSocket أن تمرر كائنًا واحدًا يحتوي على طرق لكل حدث في Bun.serve() ويتم إعادة استخدامه لكل اتصال.
هذا يؤدي إلى استخدام أقل للذاكرة ووقت أقل يُنفق في إضافة/إزالة مستمعي الأحداث.
الوسيطة الأولى لكل معالج هي مثيل ServerWebSocket الذي يتعامل مع الحدث. فئة ServerWebSocket هي تنفيذ Bun-native سريع لـ WebSocket مع بعض الميزات الإضافية.
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 وفقًا للمواصفات spec. يمكن إرفاق headers إضافية بهذه الاستجابة في استدعاء server.upgrade().
Bun.serve({
fetch(req, server) {
const sessionId = await generateSessionId();
server.upgrade(req, {
headers: {
"Set-Cookie": `SessionId=${sessionId}`,
},
});
},
websocket: {}, // المعالجات
});البيانات السياقية
يمكن إرفاق data سياقية بـ WebSocket جديد في استدعاء .upgrade(). هذه البيانات متاحة على خاصية ws.data داخل معالجات WebSocket.
لتحديد نوع ws.data بقوة، أضف خاصية data إلى كائن معالج websocket. هذا يحدد نوع 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,
});
},
},
});ملاحظة
ملاحظة: سابقًا، يمكنك تحديد نوع ws.data باستخدام معلمة نوع على Bun.serve، مثل Bun.serve<MyData>({...}). تم إزالة هذا النمط بسبب قيود في TypeScript لصالح خاصية data الموضحة أعلاه.
للاتصال بهذا الخادم من المتصفح، أنشئ WebSocket جديدًا.
const socket = new WebSocket("ws://localhost:3000/chat");
socket.addEventListener("message", event => {
console.log(event.data);
});ملاحظة
تحديد هوية المستخدمين
سيتم إرسال ملفات تعريف الارتباط المحددة حاليًا في الصفحة مع طلب ترقية WebSocket ومتاحة على req.headers في معالج fetch. حلل ملفات تعريف الارتباط هذه لتحديد هوية المستخدم المتصل واضبط قيمة data وفقًا لذلك.
نشر/اشتراك
ينفذ تنفيذ ServerWebSocket في Bun واجهة برمجة تطبيقات نشر-اشتراك أصلية للبث القائم على الموضوعات. يمكن للمقابس الفردية .subscribe() لموضوع (محدد بمعرف سلسلة) و .publish() رسائل إلى جميع المشتركين الآخرين في ذلك الموضوع (باستثناء نفسها). واجهة برمجة التطبيقات للبث القائمة على الموضوعات هذه مشابهة لـ 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(`Listening on ${server.hostname}:${server.port}`);استدعاء .publish(data) سيرسل الرسالة إلى جميع المشتركين في موضوع باستثناء المقبس الذي استدعى .publish(). لإرسال رسالة إلى جميع المشتركين في موضوع، استخدم طريقة .publish() على مثيل Server.
const server = Bun.serve({
websocket: {
// ...
},
});
// الاستماع لحدث خارجي ما
server.publish("the-group-chat", "Hello world");الضغط
يمكن تفعيل الضغط لكل رسالة مع معلمة perMessageDeflate.
Bun.serve({
websocket: {
perMessageDeflate: true,
},
});يمكن تفعيل الضغط للرسائل الفردية بتمرير boolean كوسيطة ثانية لـ .send().
ws.send("Hello world", true);للتحكم الدقيق في خصائص الضغط، راجع المرجع.
Backpressure
ترجع طريقة .send(message) لـ ServerWebSocket number يشير إلى نتيجة العملية.
-1— تم وضع الرسالة في قائمة الانتظار لكن هناك backpressure0— تم إسقاط الرسالة بسبب مشكلة في الاتصال1+— عدد البايتات المرسلة
هذا يمنحك تحكمًا أفضل في backpressure في خادمك.
المهل والحدود
افتراضيًا، سيغلق Bun اتصال WebSocket إذا كان خاملاً لمدة 120 ثانية. يمكن تكوين هذا مع معلمة idleTimeout.
Bun.serve({
fetch(req, server) {}, // منطق الترقية
websocket: {
idleTimeout: 60, // 60 ثانية
},
});سيغلق Bun أيضًا اتصال WebSocket إذا استلم رسالة أكبر من 16 ميجابايت. يمكن تكوين هذا مع معلمة maxPayloadLength.
Bun.serve({
fetch(req, server) {}, // منطق الترقية
websocket: {
maxPayloadLength: 1024 * 1024, // 1 ميجابايت
},
});الاتصال بخادم Websocket
ينفذ Bun فئة WebSocket. لإنشاء عميل WebSocket يتصل بخادم ws:// أو wss://، أنشئ مثيل WebSocket، كما تفعل في المتصفح.
const socket = new WebSocket("ws://localhost:3000");
// مع مفاوضات البروتوكول الفرعي
const socket2 = new WebSocket("ws://localhost:3000", ["soap", "wamp"]);في المتصفحات، سيتم إرسال ملفات تعريف الارتباط المحددة حاليًا في الصفحة مع طلب ترقية WebSocket. هذه ميزة قياسية من واجهة برمجة تطبيقات WebSocket.
للراحة، يسمح Bun بتعيين رؤوس مخصصة مباشرة في المنشئ. هذا امتداد خاص بـ Bun لمعيار WebSocket. هذا لن يعمل في المتصفحات.
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; // default: 16 * 1024 * 1024 = 16 MB
idleTimeout?: number; // default: 120 (seconds)
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;
}