এই পর্বে যা শিখবেন
- কেন request–response মডেল real-time কাজে আটকে যায়, আর polling ও long polling-এর আসল খরচ
- SSE-র
text/event-streamফরম্যাট,EventSource, named event,retryআরLast-Event-ID— Node.js-এ নিজে সার্ভার ও client বানিয়ে - WebSocket handshake, frame,
ws://বনামwss://, ব্রাউজার API, close code আরwsপ্যাকেজের সার্ভার - বার্তার প্রোটোকল নকশা, room, broadcast, presence, heartbeat, backoff-সহ reconnection আর backpressure
- Authentication, Origin যাচাই, rate limit, Redis pub/sub দিয়ে scaling, Socket.IO, আর কখন কোনটি বাছবেন
১ · সার্ভার কেন নিজে থেকে খবর দিতে পারে না — request–response-এর সীমা
পর্ব ৩৫-এ আপনি HTTP-র মূল নিয়ম দেখেছেন: client (ব্রাউজার) একটি অনুরোধ পাঠায়, server একটি উত্তর দেয়, কথা শেষ। পর্ব ৫০-এ Express দিয়ে যে REST API বানানো হয়েছে, সেটিও এই ছাঁচে চলে। নিয়মটির একটি গোপন শর্ত আছে — কথা শুরু করে সবসময় client. সার্ভারের কাছে নতুন খবর থাকলেও সে ব্রাউজারকে ডাকতে পারে না; অপেক্ষা করতে হয়, কবে ব্রাউজার আবার জিজ্ঞেস করবে।
বেশিরভাগ পাতায় এতে সমস্যা নেই। কিন্তু কিছু কাজে খবরের মূল্য তার সময়ে:
🛵 লাইভ অর্ডার ট্র্যাকিং
“রান্না হচ্ছে → রাইডার রওনা দিয়েছেন → পৌঁছে গেছে” — প্রতিটি ধাপ রেস্তোরাঁ বা রাইডারের অ্যাপে ঘটে, আর গ্রাহকের পর্দায় সাথে সাথে দেখাতে হয়।
💬 কাস্টমার-সাপোর্ট চ্যাট
গ্রাহক আর এজেন্ট দুজনেই যেকোনো সময় লেখেন — খবর দুই দিকে।
🏏 লাইভ ক্রিকেট স্কোর
বিপিএলের ফাইনালে লাখো দর্শক একই স্কোর দেখছেন। খবর শুধু সার্ভার থেকে আসে, দর্শকেরা কিছু পাঠান না।
🔔 অ্যাডমিন ড্যাশবোর্ড আর “কে এখন অনলাইন”
নতুন অর্ডার এলেই নোটিফিকেশন, আর একটি যৌথ ডকুমেন্টে কে কে এখন আছেন তার তালিকা (presence).
এই কাজগুলোকে বলা হয় real-time — খবর তৈরি হওয়া আর পর্দায় পৌঁছানোর মাঝের দেরি (latency) যতটা সম্ভব কম। ব্যবহারিক ভাষায়, সার্ভারকে খবর push করতে হয়, client-কে বারবার pull করতে নয়। ওয়েবে এর তিনটি প্রধান পথ আছে, আর সেগুলো একটি সিঁড়ির মতো — সরল থেকে শক্তিশালী।
ধরুন আপনি দর্জির দোকানে ঈদের পাঞ্জাবি বানাতে দিয়েছেন। Polling: প্রতি ঘণ্টায় দোকানে গিয়ে জিজ্ঞেস করা “হয়েছে?” — বেশিরভাগ দিন উত্তর “না”, যাতায়াতের খরচ বৃথা। Long polling: দোকানে গিয়ে বসে থাকা, দর্জি বলেছেন “হলেই বলব” — হলে নিয়ে বাড়ি ফেরেন, পরের জিনিসের জন্য আবার গিয়ে বসেন। Server-Sent Events: দর্জির নম্বরে একবার মিসড কল দিয়ে রেখেছেন, আর তিনি এসএমএসে খবর পাঠাতে থাকেন — “কাপড় কাটা হলো”, “সেলাই চলছে”, “তৈরি”। খবর একদিকে আসে। WebSocket: একটি চলমান ফোন কল — দুজনেই যেকোনো সময় কথা বলেন, “বোতামটা কালো দেবেন?” “ঠিক আছে”।
পুরো পর্বের মূল প্রশ্ন তাই একটি: আপনার কাজে খবর কোন দিকে যায়, আর কত ঘন ঘন? সিঁড়ির প্রতিটি ধাপে ক্ষমতার সাথে জটিলতাও বাড়ে।
২ · Polling আর long polling — পুরোনো পথ আর তার খরচ
পর্ব ৩৬-এ polling আর long polling-এর পরিচয় পেয়েছেন। এবার খরচের হিসাবটা করা যাক, কারণ সিদ্ধান্ত নিতে হিসাব লাগে।
একটি খাবার-ডেলিভারি অ্যাপ ধরুন। সন্ধ্যার ভিড়ে ১০,০০০ জন গ্রাহক অর্ডারের পাতা খুলে রেখেছেন, আর পাতাটি প্রতি ৫ সেকেন্ডে জিজ্ঞেস করে “অবস্থা বদলেছে?” তাহলে সার্ভারে আসে সেকেন্ডে ১০,০০০ ÷ ৫ = ২,০০০টি request. একটি অর্ডারের অবস্থা গোটা ৪০ মিনিটে হয়তো ৪–৫ বার বদলায়; অর্থাৎ ৪৮০টি প্রশ্নের মধ্যে ৪৭৫টির উত্তর “না, কিছু বদলায়নি”। প্রতিটির সাথে যায় cookie আর header, প্রতিটিতে লগইন যাচাই আর ডেটাবেস খোঁজা — তবু খবর আসে গড়ে আড়াই সেকেন্ড দেরিতে। বিরতি কমালে দেরি কমে, খরচ বাড়ে; এই টানাপোড়েনই polling-এর মূল সমস্যা।
| বিষয় | Polling (প্রতি ৫ সেকেন্ডে) | Long polling |
|---|---|---|
| খবর পৌঁছানোর দেরি | গড়ে বিরতির অর্ধেক (≈ ২.৫ সেকেন্ড) | প্রায় সাথে সাথে, তারপর নতুন request-এর একটি round-trip |
| খালি উত্তর | বেশিরভাগ | শুধু সময়সীমা পেরোলে (যেমন ২৫–৩০ সেকেন্ড পরে) |
| সার্ভারের ভার | প্রচুর ছোট request | অনেক খোলা, অপেক্ষমাণ request |
| সুবিধা | সবচেয়ে সরল; যেকোনো সার্ভার, cache, CDN-এ চলে | সাধারণ HTTP; পুরোনো proxy-র পেছনেও চলে |
| ফাঁদ | setInterval ধীর সার্ভারে request জমায় | পরপর দুটি খবরের মাঝে একটি হারাতে পারে; ক্রম রাখা কঠিন |
Long polling-এ ব্রাউজার জিজ্ঞেস করে, আর সার্ভার উত্তর আটকে রাখে যতক্ষণ না খবর আসে বা একটি সময়সীমা পেরোয়। খবর এলে উত্তর যায়, ব্রাউজার সাথে সাথে নতুন request পাঠায়। দেরি কম, কিন্তু একটি সূক্ষ্ম ফাঁক আছে: উত্তর পাঠানো আর নতুন request আসার মাঝের মুহূর্তে আরেকটি খবর তৈরি হলে সেটি কোথায় থাকবে? সমাধান হলো প্রতিটি খবরের একটি ক্রমিক id রাখা, আর client প্রতিবার বলে দেবে “আমি ১৭ পর্যন্ত পেয়েছি”। এই ধারণাটি মনে রাখুন — SSE-র Last-Event-ID ঠিক এটাই, তবে তৈরি অবস্থায়।
Polling খারাপ নয়। খবর কদাচিৎ বদলালে, ব্যবহারকারী কম হলে, বা দীর্ঘ সংযোগ রাখা যায় না এমন serverless প্ল্যাটফর্মে ৩০ সেকেন্ডের polling-ই সঠিক প্রকৌশল — পর্ব ৩৬-এর নিয়ম মেনে।
৩ · Server-Sent Events — একটি খোলা HTTP সংযোগে একমুখী ধারা
Server-Sent Events (SSE)-এর বুদ্ধিটি খুব সাদামাটা: ব্রাউজার একটি সাধারণ GET request পাঠায়, আর সার্ভার উত্তর দেয় — কিন্তু উত্তর শেষ করে না. সংযোগ খোলা রেখে সে মাঝে মাঝে কয়েক লাইন লেখা পাঠাতে থাকে। প্রতিটি লেখার টুকরো একটি event. নতুন কোনো প্রোটোকল নেই, নতুন port নেই — শুধু একটি লম্বা HTTP উত্তর, যার header-এ লেখা Content-Type: text/event-stream.
text/event-stream ফরম্যাট — চারটি field আর একটি ফাঁকা লাইন
এই event-stream ফরম্যাট UTF-8 লেখার কয়েকটি লাইন। প্রতিটি লাইন field: মান, আর একটি ফাঁকা লাইন মানে “এই event শেষ, এবার পাঠাও”। Field মোট চারটি:
retry: 3000
id: 1
event: status
data: {"step":"cooking","text":"রান্না হচ্ছে"}
: এটি একটি comment — ব্রাউজার উপেক্ষা করে, সংযোগ সজাগ রাখতে কাজে লাগে
id: 2
event: status
data: {"step":"on-the-way",
data: "text":"রাইডার রওনা দিয়েছেন"}
data: — event-এর আসল তথ্য; একাধিক data: লাইন থাকলে ব্রাউজার সেগুলো \n দিয়ে জুড়ে একটি লেখা বানায় (দ্বিতীয় event-এ তাই পুরো JSON ঠিকই জোড়া লাগে)। event: — event-এর নাম; না দিলে নাম হয় "message". id: — এই event-এর পরিচয়; ব্রাউজার শেষ পাওয়া id মনে রাখে। retry: — সংযোগ কাটলে কত মিলিসেকেন্ড পরে আবার চেষ্টা করবে। : দিয়ে শুরু হওয়া লাইন comment. Field-এর নামের পরে :, তারপর একটি ঐচ্ছিক space — সেটি বাদ দিয়ে বাকিটুকু মান। লেখা ছাড়া (binary) কিছু পাঠানো যায় না; ছবি বা ফাইল পাঠাতে হলে তার URL পাঠান।
ব্রাউজারে EventSource
ব্রাউজারের EventSource API এই ফরম্যাট পড়ে প্রতিটি event-কে একটি DOM event বানিয়ে দেয়। নামহীন event আসে onmessage-এ, আর নাম থাকলে addEventListener("নাম", …) দিয়ে ধরতে হয় — onmessage সেগুলো পায় না, নতুনদের একটি সাধারণ বিভ্রান্তি:
const es = new EventSource("/orders/42/events"); // একই origin — cookie নিজে থেকেই যায়
es.addEventListener("status", (e) => {
const { step, text } = JSON.parse(e.data);
showStep(text); // "রাইডার রওনা দিয়েছেন"
if (step === "delivered") es.close(); // কাজ শেষ — আর reconnect নয়
});
es.onmessage = (e) => console.log("নামহীন event:", e.data, e.lastEventId);
es.onerror = () => console.log("সংযোগে সমস্যা, readyState =", es.readyState);
// readyState: 0 = CONNECTING (আবার চেষ্টা চলছে), 1 = OPEN, 2 = CLOSEDএখানে সবচেয়ে মূল্যবান অংশটি কোডে দেখাই যায় না: স্বয়ংক্রিয় reconnection. নেটওয়ার্ক কাটলে বা সার্ভার উত্তর শেষ করে দিলে EventSource নিজেই retry-এর সময় (না দিলে ব্রাউজারের ঠিক করা কয়েক সেকেন্ড) অপেক্ষা করে আবার সংযোগ করে, আর নতুন request-এ একটি header যোগ করে: Last-Event-ID, যার মান শেষ পাওয়া id. সার্ভার সেটি দেখে শুধু বাকি event-গুলো পাঠায়। সার্ভার যদি চায় ব্রাউজার আর চেষ্টা না করুক, সে HTTP 204 No Content দেয়; ভুল status বা ভুল Content-Type পেলেও ব্রাউজার চেষ্টা থামিয়ে দেয়।
- সংযোগ খোলা
GET /events200, event-stream - event আসছে
id: 1, id: 2ব্রাউজার মনে রাখে - সংযোগ কাটল
onerrorreadyState 0 - আবার request
Last-Event-ID: 2retry ms পরে - বাকিটা
id: 3কিছু হারায়নি
GET /orders/42/events HTTP/1.1 Accept: text/event-stream Cookie: session=… ← HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache
retry: 3000
id: 1
event: status
data: রান্না হচ্ছে
id: 2
event: status
data: রাইডার রওনা দিয়েছেন
… (সংযোগ খোলা)
es.readyState → 0 (CONNECTING)
es.onerror → একবার ডাকা হলো
মনে রাখা lastEventId = "2"
অপেক্ষা: retry = 3000 ms
GET /orders/42/events HTTP/1.1
Accept: text/event-stream
Last-Event-ID: 2
id: 3
event: status
data: পৌঁছে গেছে
ব্রাউজার: step === "delivered" → es.close()
- সংযোগ খোলা।
new EventSource(url)একটি সাধারণ GET পাঠায়। সার্ভার200আরtext/event-streamদিয়ে উত্তর শুরু করে, কিন্তু শেষ করে না।Cache-Control: no-cacheদরকার, যাতে মাঝের কোনো cache এই চলমান উত্তর জমিয়ে না রাখে। - Event আসছে। রেস্তোরাঁ রান্না শুরু করলে সার্ভার
id: 1, রাইডার রওনা দিলেid: 2লেখে। প্রতিটি ফাঁকা লাইনে ব্রাউজার একটিstatusevent ছোড়ে আরid-টি নিজে মনে রাখে। - সংযোগ কাটল। মোবাইল টাওয়ার বদলাল, বা সার্ভার deploy-এর জন্য restart হলো।
onerrorএকবার ডাকা হয়,readyStateহয় ০ — অর্থাৎ “আবার চেষ্টা করছি”, হাল ছাড়েনি। এখানে নিজে নতুনEventSourceবানাবেন না; তাতে দুটি সংযোগ হয়ে যাবে। - Last-Event-ID নিয়ে ফেরা।
retry-এর ৩০০০ ms পরে ব্রাউজার একই URL-এ নতুন GET পাঠায়, সাথেLast-Event-ID: 2. সার্ভারকে তাই পুরোনো event কোথাও জমা রাখতে হয়। - যেখানে থেমেছিল, সেখান থেকে। সার্ভার শুধু
id: 3পাঠায়। গ্রাহক কোনো ধাপ হারাননি, আর একটিও বাড়তি কোড লেখেননি। কাজ শেষেes.close()— নইলে সার্ভার উত্তর শেষ করলেও ব্রাউজার বারবার ফিরে আসত।
নিজে চালিয়ে দেখুন — Node.js-এ SSE সার্ভার আর client একসাথে
নিচের প্রোগ্রামে একটি আসল SSE সার্ভার (node:http) আর একই প্রোগ্রামে একটি client আছে। Node 20-এ EventSource নেই, তাই client fetch দিয়ে res.body-কে ReadableStream হিসেবে পড়ে আর নিজে লাইন ধরে ফরম্যাটটি parse করে — EventSource ভেতরে ঠিক এই কাজই করে। সার্ভার প্রথম সংযোগে দুটি event পাঠিয়ে উত্তর শেষ করে দেয় (সংযোগ কাটার নকল), তারপর client Last-Event-ID নিয়ে ফিরে আসে:
const http = require("node:http");
const updates = ["রান্না হচ্ছে", "রাইডার রওনা দিয়েছেন", "পৌঁছে গেছে"];
let visits = 0;
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" });
const from = Number(req.headers["last-event-id"] ?? 0) + 1; // কোথা থেকে আবার শুরু
const to = ++visits === 1 ? 2 : updates.length; // প্রথম সংযোগ ২টির পরে কাটে
for (let id = from; id <= to; id++) res.write(`id: ${id}\nevent: status\ndata: ${updates[id - 1]}\n\n`);
res.end();
});
let lastId; // EventSource যা মনে রাখে
async function listen(url) {
const res = await fetch(url, { headers: lastId ? { "Last-Event-ID": lastId } : {} });
let buf = "", ev = {};
for await (const text of res.body.pipeThrough(new TextDecoderStream())) {
const lines = (buf + text).split("\n"); buf = lines.pop(); // অসম্পূর্ণ শেষ লাইন রেখে দিন
for (const line of lines) {
if (line) { const i = line.indexOf(":"); ev[line.slice(0, i)] = line.slice(i + 2); continue; }
console.log(`#${ev.id} ${ev.event}: ${ev.data}`); lastId = ev.id; ev = {}; // ফাঁকা লাইন
}
}
}
server.listen(0, "127.0.0.1", async () => {
const url = `http://127.0.0.1:${server.address().port}/orders/42/events`;
await listen(url);
console.log(`সংযোগ কেটে গেল — Last-Event-ID: ${lastId} দিয়ে আবার`);
await listen(url);
server.close();
});আউটপুট চার লাইন: #1 status: রান্না হচ্ছে, #2 status: রাইডার রওনা দিয়েছেন, সংযোগ কেটে গেল — Last-Event-ID: 2 দিয়ে আবার, আর #3 status: পৌঁছে গেছে. listen(0) মানে যেকোনো খালি port. নেটওয়ার্ক লেখা টুকরো টুকরো আনে — একটি টুকরো একটি লাইনের মাঝখানে শেষ হতে পারে, এমনকি একটি বাংলা অক্ষরের byte-এর মাঝখানেও। TextDecoderStream অক্ষর ভাঙা ঠেকায়, আর buf অসম্পূর্ণ শেষ লাইনটি পরের টুকরোর জন্য রেখে দেয়। এই parser ইচ্ছা করে সরল (i + 2 ধরে নিচ্ছে :-এর পরে ঠিক একটি space আছে; একাধিক data: লাইন বা comment সামলায় না)। Production-এ ব্রাউজারে EventSource, আর Node-এ একটি পরীক্ষিত লাইব্রেরি ব্যবহার করুন।
৪ · বাস্তবের SSE সার্ভার — Express, proxy আর সীমাবদ্ধতা
পর্ব ৫০-এর Express-এ একটি SSE route দেখতে প্রায় সাধারণ route-এর মতোই; পার্থক্য হলো res.json()-এর বদলে বারবার res.write(), আর ব্রাউজার চলে গেলে পরিষ্কার করা:
app.get("/orders/:id/events", requireLogin, async (req, res) => {
if (!(await canSeeOrder(req.user, req.params.id))) return res.sendStatus(403);
res.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", // nginx-কে বলুন: জমিয়ে রাখবেন না
});
res.flushHeaders(); // header এখনই পাঠান
const send = (e) => res.write(`id: ${e.seq}\nevent: status\ndata: ${JSON.stringify(e)}\n\n`);
const lastId = Number(req.get("Last-Event-ID") ?? 0);
(await orderEvents.since(req.params.id, lastId)).forEach(send); // হারানোগুলো আগে
const unsubscribe = orderEvents.subscribe(req.params.id, send); // তারপর নতুনগুলো
const ping = setInterval(() => res.write(": ping\n\n"), 25_000); // comment — সংযোগ সজাগ
req.on("close", () => { clearInterval(ping); unsubscribe(); }); // না করলে memory leak
});অনুমতি যাচাই — URL-এ অন্যের অর্ডার নম্বর বসানো খুব সহজ, তাই সংযোগের শুরুতেই দেখুন অর্ডারটি এই ব্যবহারকারীর কি না। Comment ping — অনেক proxy আর load balancer কিছুক্ষণ (প্রায়ই ৬০ সেকেন্ডের কাছাকাছি) কোনো byte না গেলে সংযোগ কেটে দেয়; ২৫ সেকেন্ড পরপর একটি comment লাইন সেটি ঠেকায়, আর ব্রাউজার comment উপেক্ষা করে। req.on("close") — ব্রাউজার tab বন্ধ করলে listener আর timer সরাতে হবে, নইলে প্রতিটি পুরোনো সংযোগ চিরকাল মেমরিতে থাকবে (পর্ব ৫৩-এ memory leak বিস্তারিত)।
Proxy আর buffering — “লোকালে চলে, সার্ভারে চলে না”
SSE-র সবচেয়ে প্রচলিত অভিযোগ: লোকালে সব ঠিক, কিন্তু deploy করার পরে event আসে একসাথে, অনেক দেরিতে, বা উত্তর শেষ হলে একবারে। কারণ প্রায় সবসময় মাঝখানের কেউ উত্তরটি জমিয়ে রাখছে। nginx reverse proxy ডিফল্টভাবে backend-এর উত্তর buffer করে — এই proxy buffering সাধারণ পাতার জন্য ভালো, stream-এর জন্য মারাত্মক। সমাধান: সেই location-এ proxy_buffering off;, অথবা উপরের মতো X-Accel-Buffering: no header; সাথে proxy_read_timeout বাড়ানো। Express-এর compression middleware-ও লেখা জমায় — stream route-এ বন্ধ রাখুন।
SSE-র সীমা
- একমুখী। সার্ভার থেকে ব্রাউজারে শুধু। ব্রাউজারের কিছু পাঠাতে হলে সাধারণ
fetchPOST — যা আসলে বেশিরভাগ অ্যাপে যথেষ্ট (চ্যাটেও বার্তা POST করে, নতুন বার্তা SSE-তে পাওয়া যায়)। - শুধু লেখা (UTF-8), শুধু GET, নিজের header নয়।
EventSource-এAuthorizationheader বসানো যায় না। তাই authentication চলে cookie দিয়ে (ভিন্ন origin হলে{ withCredentials: true }আর CORS), বা একটি স্বল্পমেয়াদি token URL-এ। POST বা header দরকার হলেfetchদিয়ে stream পড়ে নিজে parse করুন — AI চ্যাট-অ্যাপগুলো এভাবেই করে। - HTTP/1.1-এ সংযোগের সীমা। HTTP/1.1-এ ব্রাউজার একটি domain-এ একসাথে সাধারণত ৬টির বেশি সংযোগ খোলে না — সব tab মিলিয়ে। ৭টি tab-এ একই সাইট খোলা থাকলে সপ্তমটি আটকে থাকতে পারে। HTTP/2-তে একটি TCP সংযোগের ভেতরে অনেকগুলো stream চলে (সাধারণত ১০০ বা তার বেশি), তাই সমস্যাটি প্রায় মুছে যায় — production-এ HTTP/2 চালু রাখুন।
৫ · WebSocket — handshake, frame আর ব্রাউজার API
SSE-তে ব্রাউজার শুধু শোনে। সাপোর্ট চ্যাটে দুজনেই লেখেন, “টাইপ করছেন…” সেকেন্ডে কয়েকবার বদলায় — প্রতিটির জন্য আলাদা POST মানে প্রতিবার পুরো header আর লগইন যাচাই। WebSocket একটি TCP সংযোগকে খোলা রেখে দুই পক্ষকে যেকোনো সময়, একই সাথে কথা বলতে দেয় — একে বলে full-duplex. প্রতিটি বার্তার সাথে বাড়তি মাত্র কয়েক byte যায়, header-এর কয়েকশো byte নয়।
Handshake — HTTP দিয়ে শুরু, তারপর অন্য প্রোটোকল
WebSocket আলাদা port বা নতুন ধরনের সংযোগ দিয়ে শুরু হয় না। শুরু হয় একটি সাধারণ HTTP GET দিয়ে, যাতে লেখা থাকে “চলুন প্রোটোকল বদলাই” — এই protocol upgrade-এর ফলে একই port (80 বা 443), একই firewall নিয়ম আর একই সার্ভার ব্যবহার করা যায়। সার্ভার রাজি হলে উত্তর দেয় 101 Switching Protocols, আর সেই মুহূর্ত থেকে ওই TCP সংযোগে HTTP আর চলে না — চলে WebSocket-এর frame.
- Upgrade চাওয়া
GET + Upgradeসাধারণ HTTP - সার্ভার রাজি
101Accept হিসাব - frame দুই দিকে
text / binaryযেকোনো সময় - ping / pong
0x9 / 0xAবেঁচে আছ? - close
1000ভদ্র বিদায়
GET /support HTTP/1.1 Host: chat.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 Origin: https://shop.example.com Cookie: session=…
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ← এখান থেকে আর HTTP নয়
→ [text, masked] {"type":"chat","text":"আমার অর্ডার কোথায়?"}
← [text] {"type":"typing","from":"করিম"}
← [text] {"type":"chat","from":"করিম","text":"দেখছি…"}
client → server frame সবসময় masked; উল্টো দিকে নয়
← [ping] (সার্ভার, প্রতি ৩০ সেকেন্ডে)
→ [pong] (ব্রাউজার স্বয়ংক্রিয়ভাবে)
pong না এলে সার্ভার সংযোগটিকে মৃত ধরে বন্ধ করে
→ [close] code 1000, reason "চ্যাট শেষ"
← [close] code 1000
তারপর TCP সংযোগ বন্ধ; ws.onclose → e.code 1000, e.wasClean true
- Upgrade চাওয়া। ব্রাউজার একটি GET পাঠায়, সাথে
Upgrade: websocketআরConnection: Upgrade.Sec-WebSocket-Keyপ্রতিবার নতুন একটি এলোমেলো মান (base64-এ ১৬ byte). লক্ষ করুনOriginআরCookie— ব্রাউজার নিজে থেকে এগুলো পাঠায়, আর নিরাপত্তার জন্য এদের গুরুত্ব অংশ ৮-এ দেখবেন। - সার্ভার রাজি। সার্ভার key-এর সাথে RFC-তে লেখা একটি স্থির GUID জুড়ে SHA-1 hash করে base64 বানায় — এটিই
Sec-WebSocket-Accept. এটি পাসওয়ার্ড নয়, শুধু প্রমাণ যে উত্তরদাতা সত্যিই WebSocket বোঝে। Status101মানে “প্রোটোকল বদলাচ্ছি”। এই মুহূর্তে ব্রাউজারেonopenচলে। - Frame দুই দিকে। এখন দুই পক্ষ যেকোনো সময় লেখা (text) বা byte (binary) frame পাঠায়, কেউ কারও অপেক্ষায় থাকে না। Client থেকে যাওয়া প্রতিটি frame একটি এলোমেলো ৪-byte key দিয়ে masked থাকে — এটি গোপনীয়তার জন্য নয় (key frame-এই লেখা থাকে), বরং মাঝের পুরোনো proxy যাতে আক্রমণকারীর বানানো byte-কে HTTP ভেবে ভুল না করে (ভাবনার প্রশ্ন ২)।
- Ping আর pong. প্রোটোকলের নিজস্ব “control frame”: একজন ping পাঠালে অন্যজন pong দিতে বাধ্য। ব্রাউজার pong নিজেই দেয়, কিন্তু JavaScript থেকে ping পাঠানোর কোনো API নেই — তাই ব্রাউজারের দিক থেকে সজাগ থাকতে হলে নিজের
{"type":"ping"}বার্তা লাগে (অংশ ৭)। - Close. এক পক্ষ একটি close frame (code আর ঐচ্ছিক কারণ) পাঠায়, অন্যজন close frame ফেরত দেয়, তারপর TCP বন্ধ। নেটওয়ার্ক হঠাৎ কাটলে কোনো close frame আসে না — ব্রাউজার তখন code
1006দেখায়, যা কখনো তারে পাঠানো হয় না, শুধু “অস্বাভাবিকভাবে কাটা” বোঝায়।
অভিজ্ঞ পাঠকদের জন্য — Sec-WebSocket-Accept-এর হিসাব Node-এ তিন লাইনের। RFC 6455-এর নিজের উদাহরণের key দিলে নিচের মানটিই আসে (চালানো যায় না এমন উদাহরণ; লোকাল Node-এ যাচাই করা):
const { createHash } = require("node:crypto");
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // RFC 6455-এ লেখা স্থির মান
const key = "dGhlIHNhbXBsZSBub25jZQ=="; // ব্রাউজারের Sec-WebSocket-Key
console.log(createHash("sha1").update(key + GUID).digest("base64"));
// প্রত্যাশিত আউটপুট: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Frame-এর ধরন আর ws:// বনাম wss://
| Opcode | ধরন | কাজ |
|---|---|---|
0x1 | Text | UTF-8 লেখা — সাধারণত JSON |
0x2 | Binary | byte — ছবি, অডিও, MessagePack-এর মতো ঘন ফরম্যাট |
0x0 | Continuation | বড় বার্তা কয়েকটি frame-এ ভাগ হলে পরের টুকরো |
0x8 | Close | বিদায়, সাথে code আর কারণ |
0x9 / 0xA | Ping / Pong | সংযোগ বেঁচে আছে কি না যাচাই |
ws:// হলো খোলা লেখায় WebSocket (HTTP-র মতো), আর wss:// হলো TLS-এ মোড়া (HTTPS-এর মতো, port 443). Production-এ সবসময় wss://: চ্যাটের লেখা আর token পথে কেউ পড়তে পারে না, HTTPS পাতা থেকে ws:// খোলা ব্রাউজার আটকে দেয় (mixed content), আর এনক্রিপ্ট করা সংযোগ মাঝের proxy খুলে দেখতে বা ভাঙতে পারে না।
ব্রাউজারের WebSocket API
const ws = new WebSocket("wss://chat.example.com/support");
ws.binaryType = "arraybuffer"; // binary এলে Blob-এর বদলে ArrayBuffer
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", v: 1, token: getShortToken() }));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "chat") addBubble(msg.from, msg.text); // ভেতরে textContent — innerHTML নয়
};
ws.onclose = (e) => console.log("বন্ধ:", e.code, e.reason, e.wasClean);
ws.onerror = () => console.log("ভুল — কারণ ইচ্ছা করেই জানানো হয় না; onclose-এর code দেখুন");
function sendChat(text) {
if (ws.readyState !== WebSocket.OPEN) return queueForLater(text); // CONNECTING হলে send ভুল ছুড়ে
ws.send(JSON.stringify({ type: "chat", v: 1, text }));
}WebSocket একটি EventTarget, তাই addEventListener("message", …)-ও চলে। readyState-এর চারটি মান: 0 CONNECTING, 1 OPEN, 2 CLOSING, 3 CLOSED — সংযোগ খোলার আগে send() ডাকলে ব্রাউজার InvalidStateError ছোড়ে। onerror-এ কোনো বিস্তারিত থাকে না; এটি ইচ্ছাকৃত, যাতে কোনো পাতা WebSocket দিয়ে অন্যের নেটওয়ার্কের port খুঁজে বেড়াতে না পারে। আর ব্রাউজারের WebSocket constructor-এ নিজের header (যেমন Authorization) দেওয়ার কোনো উপায় নেই — শুধু URL আর ঐচ্ছিক subprotocol. তাই token যায় cookie-তে, URL-এ, বা প্রথম বার্তায়।
| Close code | অর্থ | Client-এর করণীয় |
|---|---|---|
1000 | স্বাভাবিক বন্ধ | আবার জোড়া নয় |
1001 | চলে যাচ্ছি (সার্ভার restart, পাতা বন্ধ) | কিছুক্ষণ পরে আবার জোড়া |
1006 | অস্বাভাবিকভাবে কাটা, close frame আসেনি (তারে কখনো পাঠানো হয় না) | backoff-সহ আবার জোড়া |
1008 / 1009 | নীতি ভঙ্গ / বার্তা খুব বড় | আবার একই কাজ নয় — ভুলটি ঠিক করুন |
1011 | সার্ভারের ভেতরের ভুল | backoff-সহ আবার |
4000–4999 | আপনার অ্যাপের নিজের code (যেমন 4001 = লগইন লাগবে) | আপনার প্রোটোকল যা বলে |
Node.js-এর দিক থেকে একটি খবর: Node 22 থেকে ব্রাউজারের মতো global WebSocket client আছে (Node 21-এ একটি flag-এর পেছনে ছিল), কিন্তু WebSocket সার্ভার Node-এ আজও built-in নয় — তার জন্য সাধারণত ws প্যাকেজ।
৬ · WebSocket সার্ভার, বার্তার প্রোটোকল, room আর presence
WebSocket আপনাকে শুধু একটি পাইপ দেয় — “বার্তা পাঠাও, বার্তা পাও”। কোন বার্তার অর্থ কী, কে কোন room-এ আছে, কে কাকে পাঠাতে পারে — এসব আপনার নকশা। প্রথমে ws প্যাকেজে একটি সার্ভারের কাঠামো (npm প্যাকেজ লাগে, তাই এখানে চালানো যায় না):
import { createServer } from "node:http";
import { WebSocketServer } from "ws"; // npm install ws
const server = createServer(app); // পর্ব ৫০-এর Express app-ও হতে পারে
const wss = new WebSocketServer({ noServer: true, maxPayload: 16 * 1024 }); // বার্তা ≤ 16 KB
const ALLOWED_ORIGINS = new Set(["https://shop.example.com"]);
server.on("upgrade", async (req, socket, head) => {
const user = await userFromCookie(req); // সংযোগের আগেই পরিচয়
if (!ALLOWED_ORIGINS.has(req.headers.origin) || !user) {
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
return socket.destroy();
}
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, user));
});
wss.on("connection", (ws, user) => {
ws.on("message", (data, isBinary) => {
if (isBinary) return ws.close(1003, "text only");
hub.receive(user, data.toString()); // parse, যাচাই, তারপর broadcast
});
ws.on("close", () => hub.leaveAll(user, ws));
});
server.listen(8080);noServer: true আর "upgrade" event দিয়ে 101 পাঠানোর আগেই Origin আর লগইন যাচাই করা যায়। maxPayload না দিলে ws-এর ডিফল্ট সীমা অনেক বড় (১০০ MiB).
বার্তার প্রোটোকল — প্রতিটি বার্তায় type আর version
HTTP-তে URL আর method বলে দেয় কোন কাজ। WebSocket-এ সব বার্তা একই পাইপে আসে, তাই প্রতিটি বার্তাকে নিজেই বলতে হয় সে কী। প্রচলিত নকশা হলো একটি JSON “খাম” (envelope) — এটিই আপনার message protocol:
{ "type": "chat", "v": 1, "room": "order-42", "text": "আমার অর্ডার কোথায়?", "clientId": "c-17" }
{ "type": "chat", "v": 1, "room": "order-42", "id": 981, "from": "রিনা", "text": "…", "clientId": "c-17" }
{ "type": "typing", "v": 1, "room": "order-42", "from": "করিম" }
{ "type": "presence", "v": 1, "room": "order-42", "online": ["রিনা", "করিম"] }
{ "type": "error", "v": 1, "code": "TOO_LONG", "reason": "বার্তা ২০০ অক্ষরের বেশি" }type দিয়ে সার্ভার আর client একটি switch-এ বার্তা ভাগ করে। v (version) রাখুন শুরু থেকেই — মোবাইল অ্যাপের পুরোনো সংস্করণ মাসের পর মাস চলে, নতুন সার্ভারকে পুরোনো বার্তাও বুঝতে হবে। id দেয় সার্ভার — ক্রম আর “কোথা পর্যন্ত পেয়েছি”-র জন্য। clientId দেয় client — নিজের বার্তা ফেরত এলে চিনতে (optimistic UI, পর্ব ৩৬) আর দুবার এলে বাদ দিতে।
Room, broadcast আর presence
সাপোর্ট চ্যাটে প্রতিটি অর্ডারের কথোপকথন একটি room: গ্রাহক আর সেই অর্ডারের এজেন্ট। একটি বার্তা room-এর সবার কাছে পাঠানো — এক থেকে অনেকে ছড়িয়ে দেওয়া — একে বলে fan-out বা broadcast. আর room-এ এখন কে কে আছেন, সেই তালিকা হলো presence. ধাপে ধাপে একটি বার্তার যাত্রা দেখুন:
- রিনা পাঠান। তাঁর ব্রাউজার
{"type":"chat","room":"order-42","text":"আমার অর্ডার কোথায়?"}পাঠায়। যে কেউ একটি script থেকে যা খুশি পাঠাতে পারে। - Parse আর আকার যাচাই।
JSON.parseএকটিtry/catch-এ;typeচেনা কি না,textstring কি না, ২০০ অক্ষরের মধ্যে কি না। প্রতিটি বার্তা আলাদা একটি API কলের মতো যাচাই হয় (পর্ব ১৯-এর schema validation). - অনুমতি আর সার্ভারের id. বার্তায় লেখা
roomবিশ্বাস করা যায় না — রিনা সত্যিই order-42-এর সদস্য কি না, সার্ভারের নিজের খাতায় দেখতে হবে। প্রেরকের নামও বার্তা থেকে নয়, সংযোগের লগইন থেকে নিন। তারপর সার্ভার ক্রমিকidবসায় আর ডেটাবেসে রাখে (পর্ব ৫২)। - Fan-out. Room-এর প্রতিটি সংযোগে একই বার্তা যায় — রিনার নিজের কাছেও, যাতে তাঁর পর্দার “পাঠানো হচ্ছে…” নিশ্চিত হয়। সাকিব অন্য room-এ, তাই কিছুই পান না। Room ভুলে “সবাইকে” পাঠালে এক গ্রাহকের তথ্য অন্যজন দেখবেন।
- ভুল বার্তা হলে। রিনা যদি JSON-এর বদলে
<img src=x onerror=…>পাঠাতেন, সার্ভার শুধু তাঁকেই একটিerrorবার্তা দিত — কারও কাছে ছড়াত না। আর বৈধ বার্তার লেখাতেও HTML থাকতে পারে, তাই গ্রহণকারী সবসময়textContentদিয়ে দেখায় (পর্ব ৪৩)।
এবার এই হাবের মূল যুক্তি নিজে চালিয়ে দেখুন। আসল নেটওয়ার্কের বদলে প্রতিটি “সংযোগ” এখানে একটি সাধারণ function, যা পাওয়া বার্তা ছাপে — কিন্তু room, presence, যাচাই আর fan-out-এর যুক্তি হুবহু একটি আসল সার্ভারের মতো:
class ChatHub {
#rooms = new Map(); #seq = 0; // room → Map(user → send); message id
#send(room, msg, onlyTo) { // onlyTo না থাকলে রুমের সবাইকে
for (const [user, send] of this.#rooms.get(room) ?? [])
if (!onlyTo || user === onlyTo) send(JSON.stringify(msg));
}
join(room, user, send) {
this.#rooms.set(room, (this.#rooms.get(room) ?? new Map()).set(user, send));
this.#send(room, { type: "presence", online: [...this.#rooms.get(room).keys()] });
}
receive(room, user, raw) { // client-এর বার্তা কখনো বিশ্বাস নয়
let msg = null;
try { msg = JSON.parse(raw); } catch {}
if (msg?.type !== "chat" || typeof msg.text !== "string" || msg.text.length > 200)
return this.#send(room, { type: "error", reason: "অবৈধ বার্তা" }, user);
this.#send(room, { type: "chat", id: ++this.#seq, from: user, text: msg.text });
}
}
const hub = new ChatHub();
const inbox = (name) => (data) => console.log(`${name} ← ${data}`);
hub.join("order-42", "রিনা", inbox("রিনা"));
hub.join("order-42", "করিম", inbox("করিম"));
hub.join("order-77", "সাকিব", inbox("সাকিব"));
hub.receive("order-42", "রিনা", '{"type":"chat","text":"আমার অর্ডার কোথায়?"}');
hub.receive("order-42", "রিনা", "<img src=x onerror=alert(1)>");আউটপুট সাত লাইন। প্রথম চার লাইন presence: রিনা যোগ দিলে তিনি পান {"type":"presence","online":["রিনা"]}; করিম যোগ দিলে দুজনেই পান ["রিনা","করিম"]; সাকিব অন্য room-এ, তাই শুধু নিজেকে দেখেন ["সাকিব"]. তারপর রিনার বার্তা রিনা আর করিম দুজনের কাছে যায় — {"type":"chat","id":1,"from":"রিনা","text":"আমার অর্ডার কোথায়?"} — সাকিবের কাছে নয়। শেষ লাইন রিনা ← {"type":"error","reason":"অবৈধ বার্তা"}: HTML-ভরা লেখাটি JSON নয়, তাই msg থাকে null, আর ভুলের খবর যায় শুধু প্রেরকের কাছে। লক্ষ করুন from এসেছে সংযোগের পরিচয় (user) থেকে, বার্তা থেকে নয়; আর #rooms একটি #private field (পর্ব ২৬)। আসল সার্ভারে send হবে ws.send, আর একজন ব্যবহারকারীর কয়েকটি tab মানে কয়েকটি সংযোগ — তাই Map(user → Set(ws)).
৭ · নির্ভরযোগ্যতা — heartbeat, reconnection আর backpressure
বাস্তবে মোবাইল ঘুমায়, নেটওয়ার্ক বদলায়, load balancer অলস সংযোগ কাটে, সার্ভার deploy হয়। SSE-র EventSource এসব নিজে সামলায়; WebSocket কিছুই সামলায় না। তিনটি জিনিস আপনাকে বানাতে হবে।
Heartbeat — আধমরা সংযোগ চেনা
সবচেয়ে বিপজ্জনক অবস্থা হলো “half-open” সংযোগ: ফোনের ব্যাটারি শেষ বা নেটওয়ার্ক হঠাৎ উধাও, কোনো close frame যায়নি। সার্ভারের চোখে সংযোগটি তখনো খোলা — সে বার্তা পাঠিয়েই যায়, presence-এ ব্যবহারকারীকে “অনলাইন” দেখায়, মেমরি ধরে রাখে। TCP নিজে থেকে এটি ধরতে অনেক সময় নেয়। সমাধান heartbeat: সার্ভার নির্দিষ্ট সময় পরপর ping পাঠায়; পরের ping-এর সময় পর্যন্ত pong না এলে সংযোগটি মৃত ধরে বন্ধ করে। ws প্যাকেজের প্রচলিত রূপ:
wss.on("connection", (ws) => {
ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; }); // উত্তর এল — বেঁচে আছে
});
setInterval(() => { // প্রতি ৩০ সেকেন্ডে
for (const ws of wss.clients) {
if (!ws.isAlive) { ws.terminate(); continue; } // আগের ping-এর উত্তর নেই — মৃত
ws.isAlive = false;
ws.ping();
}
}, 30_000);নিয়মিত ping-এর ফলে proxy-র idle timeout-ও সংযোগ কাটে না। ব্রাউজারের দিক থেকে সার্ভার মৃত কি না জানতে একটি অ্যাপ-স্তরের {"type":"ping"} পাঠান, উত্তর না এলে close() করে আবার জুড়ুন।
Reconnection — exponential backoff আর jitter
সংযোগ কাটলে সাথে সাথে আবার চেষ্টা করা স্বাভাবিক মনে হয়। কিন্তু সার্ভার deploy-এর সময় এক লাখ সংযোগ একসাথে কাটে — সবাই একই মুহূর্তে ফিরলে নতুন সার্ভার জন্মেই ভেঙে পড়ে। এটিই পর্ব ৩৬-এর thundering herd, আর সমাধানও সেখানেই: exponential backoff আর jitter.
function connect(url, onMessage) {
let attempt = 0, lastId = null, ws;
function open() {
ws = new WebSocket(url);
ws.onopen = () => {
attempt = 0; // সফল — অপেক্ষা আবার শুরু থেকে
ws.send(JSON.stringify({ type: "hello", v: 1, token: getShortToken(), lastId }));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.id) lastId = msg.id; // কোথা পর্যন্ত পেয়েছি
onMessage(msg);
};
ws.onclose = (e) => {
if (e.code === 1000 || e.code === 4001) return; // ইচ্ছাকৃত বন্ধ বা লগইন ব্যর্থ — আর নয়
const cap = Math.min(30_000, 500 * 2 ** attempt++);
setTimeout(open, Math.random() * cap); // full jitter: ০ থেকে cap-এর মধ্যে
};
}
open();
return { close: () => ws.close(1000) };
}সফল হলে attempt = 0 — এটি না করলে একবার অনেকবার ব্যর্থ হওয়ার পরে প্রতিটি নতুন বিচ্ছেদেও ৩০ সেকেন্ড পর্যন্ত অপেক্ষা করতে হতো। কিছু code-এ আর চেষ্টা নয় — লগইন ব্যর্থ হলে লাখবার চেষ্টাও ব্যর্থ। lastId পাঠানো — SSE-র Last-Event-ID-এর মতো, যাতে কাটা থাকার সময়ের বার্তা হারায় না। বাস্তবে online event এলে সাথে সাথে চেষ্টাও যোগ করুন।
Backpressure — পাঠানোর গতি গ্রহণের গতির চেয়ে বেশি হলে
ws.send() সাথে সাথে ফেরে, কিন্তু তার মানে বার্তা পৌঁছেছে নয় — বার্তা একটি queue-তে বসেছে মাত্র। দুর্বল 3G-র ব্যবহারকারীর কাছে সেকেন্ডে ১০০টি আপডেট গেলে queue শুধু বাড়ে। এই সমস্যার নাম backpressure. ব্রাউজারে bufferedAmount জানায় কত byte এখনো পাঠানো বাকি; সার্ভারে ws-এর সংযোগেও একই নামের property আছে। নিয়ম: সীমা পেরোলে নতুন বার্তা জমাবেন না — পুরোনো আপডেট ফেলে শুধু সর্বশেষ অবস্থা পাঠান (স্কোরের ক্ষেত্রে পুরোনো স্কোরের কোনো মূল্য নেই), বা খুব পিছিয়ে পড়া client-এর সংযোগ বন্ধ করে দিন, যাতে সে নতুন করে সর্বশেষ অবস্থা নিয়ে শুরু করে।
function sendScore(ws, score) {
if (ws.bufferedAmount > 64 * 1024) { pending.set(ws, score); return; } // শুধু সর্বশেষটি রাখুন
ws.send(JSON.stringify({ type: "score", v: 1, ...score }));
}৮ · নিরাপত্তা — লগইন, Origin আর প্রতিটি বার্তা যাচাই
HTTP API-তে প্রতিটি request middleware-এর মধ্য দিয়ে যায়; WebSocket-এ একবার handshake-এর পরে হাজার বার্তা আসে middleware ছাড়াই।
Authentication — কোথায় token রাখবেন
- Cookie: একই site-এ ব্রাউজার handshake-এ নিজে থেকেই cookie পাঠায় — সবচেয়ে সরল,
HttpOnlycookie JavaScript চুরি করতে পারে না। কিন্তু ঠিক এই কারণেই নিচের CSWSH-এর ঝুঁকি। - স্বল্পমেয়াদি ticket URL-এ: আগে একটি সাধারণ HTTPS API থেকে ৩০ সেকেন্ডের এককালীন ticket নিন, তারপর
wss://…?ticket=…. URL সার্ভার আর proxy-র log-এ লেখা থাকতে পারে, তাই দীর্ঘমেয়াদি JWT কখনো URL-এ নয়। - প্রথম বার্তায় token: সংযোগ খোলার পরে প্রথম বার্তা
{"type":"hello","token":…}; কয়েক সেকেন্ডের মধ্যে বৈধ hello না এলে সার্ভার4001দিয়ে বন্ধ করে। সংযোগের মাঝে token-এর মেয়াদ শেষ হলে কী হবে, তাও ঠিক করুন।
Origin যাচাই আর CSWSH
পর্ব ৪৩-এ দেখেছেন, same-origin policy আর CORS অন্য সাইটের JavaScript-কে আপনার API-র উত্তর পড়তে দেয় না। কিন্তু WebSocket-এ CORS প্রযোজ্য নয় — evil.example-এর পাতা new WebSocket("wss://chat.yoursite.com") খুলতে পারে, আর ব্রাউজার তাতে আপনার সাইটের cookie-ও জুড়ে দিতে পারে। সার্ভার শুধু cookie দেখে রাজি হলে দুষ্ট পাতা ব্যবহারকারীর নামে চ্যাট করতে পারে। এর নাম Cross-Site WebSocket Hijacking (CSWSH) — WebSocket-এর CSRF. প্রতিরক্ষা: handshake-এ Origin header নিজের অনুমোদিত তালিকার সাথে মেলান (অংশ ৬-এর কোডের মতো), আর cookie-তে SameSite রাখুন। মনে রাখবেন, Origin যাচাই ব্রাউজারের ব্যবহারকারীকে রক্ষা করে; ব্রাউজারের বাইরের একটি script যেকোনো Origin লিখে পাঠাতে পারে, তাই এটি লগইনের বিকল্প নয়।
প্রতিটি বার্তা একটি অবিশ্বস্ত request
| নিয়ম | কেন |
|---|---|
আকারের সীমা (maxPayload, লেখার দৈর্ঘ্য) | বিশাল বার্তায় মেমরি শেষ আর সবার জন্য সার্ভার ধীর |
| প্রতি সংযোগে বার্তার rate limit (যেমন সেকেন্ডে ৫টি চ্যাট) | একজন স্প্যামার পুরো room ভাসিয়ে দিতে পারে; সীমা পেরোলে সতর্কতা, তারপর 1008 |
Schema দিয়ে যাচাই — type, ধরন, দৈর্ঘ্য | JSON.parse-এর ভুল বা অচেনা field যেন সার্ভার ফেলে না দেয়; প্রোটোটাইপ দূষণের মতো ফাঁদও এড়ায় |
| প্রতিটি বার্তায় অনুমতি — room-এর সদস্য কি? | বার্তায় লেখা room বা from বিশ্বাস করলে যে কেউ যেকোনো room-এ লিখতে পারে |
দেখানোর সময় textContent, HTML নয় | একজনের পাঠানো <img onerror> সবার ব্রাউজারে চললে XSS — broadcast-এ একটি আক্রমণ শত জনে ছড়ায় |
Broadcast XSS-কে বহুগুণ করে। একটি সাধারণ মন্তব্যের পাতায় stored XSS যারা পাতাটি খোলেন তাঁদের আঘাত করে; একটি চ্যাট room-এ একটি দুষ্ট বার্তা সেই মুহূর্তে অনলাইন সবার ব্রাউজারে চলে — এজেন্ট আর অ্যাডমিনসহ, যাঁদের অনুমতি সবচেয়ে বেশি। সার্ভারে যাচাই আর client-এ output encoding দুটোই লাগবে; একটির ওপর ভরসা নয়।
৯ · Scaling, Socket.IO আর কখন কোনটি বাছবেন
একটি সার্ভারে সব সহজ: সব সংযোগ একই process-এর মেমরিতে, ChatHub সবাইকে চেনে। ব্যবহারকারী বাড়লে দুটি বা দশটি সার্ভার লাগে, সামনে একটি load balancer. তখন দুটি নতুন সমস্যা দেখা দেয়।
প্রথম — sticky session. Socket.IO-র মতো ব্যবস্থা শুরুতে long polling-এর কয়েকটি আলাদা request পাঠায়, যেগুলো একই সার্ভারে পৌঁছাতে হয় — load balancer-এর এই নিয়মের নাম sticky session. খাঁটি WebSocket একটিই TCP সংযোগ, তাই নিজে থেকেই এক সার্ভারে থাকে; শুধু load balancer-কে upgrade সমর্থন আর লম্বা idle timeout দিতে হয়।
দ্বিতীয় — ভিন্ন সার্ভারের মানুষ। রিনা সার্ভার ১-এ, করিম সার্ভার ২-এ। রিনার বার্তা সার্ভার ১-এর ChatHub পেল, কিন্তু করিমের সংযোগ তো সার্ভার ২-এর মেমরিতে! সমাধান একটি ভাগ করা বার্তা-পথ — পর্ব ৩৮-এ দেখা pub/sub নকশা, এবার সার্ভারগুলোর মধ্যে। সবচেয়ে প্রচলিত হাতিয়ার Redis-এর pub/sub:
- রিনা → সার্ভার ১
chatযাচাই হলো - Redis-এ publish
room:order-42নিজে পাঠায় না - Redis বিলি করে
সার্ভার ১ ও ২যারা subscribe করেছে - সার্ভার ২ → করিম
ws.sendনিজের সংযোগে
receive("order-42", "রিনা", raw)
→ parse ✓ আকার ✓ সদস্য ✓
→ msg = { type: "chat", id: 981, from: "রিনা", … }
await pub.publish("room:order-42", JSON.stringify(msg));
room:order-42 → সার্ভার ১ (রিনার সংযোগ আছে)
room:order-42 → সার্ভার ২ (করিমের সংযোগ আছে)
সার্ভার ৩-এ এই room-এর কেউ নেই — সে subscribe করেনি
await sub.subscribe("room:order-42", (raw) =>
localHub.deliver("order-42", raw)); // সার্ভার ১ → রিনা, সার্ভার ২ → করিম
- যাচাই যেখানে বার্তা আসে। রিনা যে সার্ভারে সংযুক্ত, সেটিই parse, অনুমতি আর id বসানোর কাজ করে — নিরাপত্তার নিয়ম বদলায় না।
- নিজে fan-out নয়, publish. সার্ভার ১ জানে না room-এর বাকিরা কোথায়। তাই সে বার্তাটি Redis-এর
room:order-42channel-এ প্রকাশ করে — রিনাকে ফেরত পাঠানোও এই পথেই হবে, যাতে সবার ক্রম এক থাকে। - Redis বিলি করে। যে সার্ভারগুলোতে ওই room-এর কেউ আছেন, তারা আগেই channel-টিতে subscribe করে রেখেছে। Redis pub/sub “পাঠাও আর ভুলে যাও” — কোনো subscriber সেই মুহূর্তে না থাকলে বার্তাটি তার জন্য জমা থাকে না। তাই আসল ইতিহাস ডেটাবেসে, আর পুনরায় পড়ার দরকার হলে Redis Streams-এর মতো টেকসই ব্যবস্থা।
- প্রতিটি সার্ভার নিজের সংযোগে। সার্ভার ২ তার মেমরির
ChatHubথেকে করিমের সংযোগ খুঁজে পাঠায়। সার্ভার যত খুশি বাড়ান — প্রত্যেকে শুধু নিজের ব্যবহারকারীদের চেনে, আর Redis তাদের জুড়ে রাখে।
Socket.IO — সুবিধা অনেক, কিন্তু এটি WebSocket নয়
Socket.IO Node জগতের খুব জনপ্রিয় লাইব্রেরি। এটি দেয় স্বয়ংক্রিয় reconnection, room (socket.join("order-42"), io.to("order-42").emit(…)), acknowledgement (পাঠানো বার্তার উত্তর-callback), heartbeat, আর WebSocket না চললে HTTP long polling-এ ফিরে যাওয়া; ডিফল্টভাবে long polling দিয়ে শুরু করে পরে WebSocket-এ ওঠে। একাধিক সার্ভারের জন্য আছে Redis adapter. কিন্তু মনে রাখুন: Socket.IO নিজস্ব প্রোটোকল (Engine.IO-র ওপর), যা WebSocket-কে পরিবহন হিসেবে ব্যবহার করে মাত্র। একটি সাধারণ new WebSocket() client Socket.IO সার্ভারে যুক্ত হতে পারে না, আর Socket.IO client একটি সাধারণ ws সার্ভারে পারে না — দুই পক্ষেই একই লাইব্রেরি লাগে। নিজে সার্ভার চালাতে না চাইলে আছে managed সেবা (Pusher, Ably, AWS API Gateway-এর WebSocket API, Firebase বা Supabase-এর realtime) — সংযোগ রাখার ঝামেলা তারা নেয়, বিনিময়ে মাসিক খরচ।
কখন কোনটি
| প্রযুক্তি | দিক | যা পান | মানানসই কাজ |
|---|---|---|---|
| Polling | client জিজ্ঞেস করে | সবচেয়ে সরল, cache-বান্ধব; দেরি = বিরতি | প্রতি মিনিটে বদলানো তথ্য, ছোট অ্যাপ |
| Long polling | client জিজ্ঞেস করে, সার্ভার অপেক্ষা করায় | প্রায় তাৎক্ষণিক, সাধারণ HTTP | পুরোনো পরিবেশে fallback |
| SSE | সার্ভার → client | স্বয়ংক্রিয় reconnection আর Last-Event-ID, HTTP/2-বান্ধব; শুধু লেখা | অর্ডার ট্র্যাকিং, লাইভ স্কোর, নোটিফিকেশন, AI-এর stream করা উত্তর |
| WebSocket | দুই দিকে | কম বাড়তি খরচ, text আর binary; reconnection, heartbeat নিজে | চ্যাট, “টাইপ করছেন”, মাল্টিপ্লেয়ার গেম, যৌথ সম্পাদনা |
| WebTransport | দুই দিকে, অনেক stream | HTTP/3 (QUIC)-এর ওপর; নির্ভরযোগ্য stream আর অনির্ভরযোগ্য datagram | লাইভ ভিডিও, দ্রুতগতির গেম — নতুন; সব ব্রাউজার আর সার্ভারে সমান সমর্থন নেই, ব্যবহারের আগে যাচাই করুন |
সহজ নিয়ম: খবর শুধু সার্ভার থেকে এলে আগে SSE ভাবুন; দুই দিকে ঘন ঘন কথা লাগলে তবে WebSocket; খবর কদাচিৎ বদলালে polling.
Java-তে Jakarta WebSocket API (@ServerEndpoint, @OnMessage) আর Spring-এর WebSocket ও STOMP সমর্থন একই ধারণা দেয়, তবে সেখানে shared state-এ thread-এর synchronization লাগে (Java Networking). Python-এর websockets লাইব্রেরি asyncio-র ওপর চলে, আর async for message in websocket দেখতে প্রায় JavaScript-এর for await-এর মতো (Python-এর asyncio). C++-এ Boost.Beast-এর মতো লাইব্রেরি নিচের TCP socket-এর ওপর WebSocket বানায়, যেখানে handshake আর frame প্রায় হাতে ধরতে হয় (C++ Networking).
- WebSocket আর HTTP-র পার্থক্য কী? — HTTP-তে প্রতিটি request-এর একটি উত্তর, আর কথা শুরু করে client; WebSocket একটি HTTP upgrade (101) দিয়ে শুরু হয়ে একটি স্থায়ী full-duplex সংযোগ খোলে, যেখানে দুই পক্ষই যেকোনো সময় পাঠায়।
- SSE আর WebSocket — কখন কোনটি? — শুধু সার্ভার থেকে খবর (নোটিফিকেশন, স্কোর, AI stream) হলে SSE: সরল, স্বয়ংক্রিয় reconnection; দুই দিকে ঘন ঘন বার্তা (চ্যাট, গেম) হলে WebSocket.
- Polling আর long polling-এর পার্থক্য? — Polling নির্দিষ্ট বিরতিতে জিজ্ঞেস করে, বেশিরভাগ উত্তর খালি; long polling-এ সার্ভার খবর না হওয়া পর্যন্ত উত্তর আটকে রাখে, তারপর client আবার জিজ্ঞেস করে।
- একাধিক সার্ভারে WebSocket কীভাবে scale করবেন? — প্রতিটি সার্ভার নিজের সংযোগ রাখে; বার্তা Redis pub/sub-এর মতো ভাগ করা channel-এ publish হয়, আর প্রতিটি সার্ভার নিজের client-দের পাঠায়; দরকার হলে sticky session.
১০ · AI-এর যুগে — AI-এর লেখা real-time কোড যাচাই করা
“একটি real-time চ্যাট বানিয়ে দাও” বললে AI মিনিটে একটি চলমান ডেমো দেয়। সমস্যা ধরা পড়ে প্রথম deploy, দুর্বল নেটওয়ার্ক আর দুষ্ট ব্যবহারকারীর সাথে। এই ভুলগুলো খুঁজুন:
| AI-এর সাধারণ ভুল | ফল | ঠিক রূপ |
|---|---|---|
| একমুখী নোটিফিকেশনেও WebSocket + Socket.IO | অকারণ জটিলতা, sticky session, বাড়তি সার্ভার | SSE বা এমনকি polling যথেষ্ট কি না আগে ভাবা |
| Reconnection বা heartbeat নেই | প্রথম নেটওয়ার্ক-কাটায় চ্যাট চিরতরে চুপ; “অনলাইন” তালিকায় ভূত | Backoff + jitter-সহ reconnect, lastId দিয়ে হারানো বার্তা, সার্ভারে ping/pong |
Client-এর বার্তা বিশ্বাস — from, room, isAdmin বার্তা থেকে | যে কেউ অন্যের নামে, অন্যের room-এ লেখে | পরিচয় সংযোগের লগইন থেকে; প্রতিটি বার্তায় অনুমতি আর schema যাচাই |
পাওয়া লেখা innerHTML-এ বসানো | Broadcast XSS — এক বার্তায় সবার ব্রাউজার আক্রান্ত | textContent; সার্ভারে দৈর্ঘ্য ও ধরন যাচাই |
Socket.IO client দিয়ে খাঁটি ws সার্ভারে যুক্ত হওয়া (বা উল্টো) | সংযোগই হয় না, বিভ্রান্তিকর ভুল | দুই পক্ষে একই প্রোটোকল |
Origin যাচাই নেই, maxPayload বা rate limit নেই | CSWSH, মেমরি শেষ, স্প্যাম | Handshake-এ Origin + লগইন; আকার ও হারের সীমা |
AI-কে কাজ দেওয়ার আগে প্রয়োজন আর নিয়মগুলো স্পষ্ট বলুন — বিশেষ করে “প্রোটোকল কেন” প্রশ্নটি:
AI-এর লেখা real-time কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১WebSocket আসার আগে Gmail-এর চ্যাট বা লাইভ স্কোরের সাইটগুলো কীভাবে “push” করত? ২০০৬ সালে “Comet” নামটি কেন জন্মাল?
২০০৫ সালে Jesse James Garrett “Ajax” নামটি দেন — পাতা পুরো না বদলে পেছন থেকে XMLHttpRequest-এ ডেটা আনা। Gmail আর Google Maps দেখাল ব্রাউজারে আসল অ্যাপ বানানো যায়। কিন্তু Ajax তখনো request–response: ব্রাউজার না চাইলে সার্ভার কিছু বলতে পারে না। চ্যাট আর লাইভ ডেটার জন্য প্রকৌশলীরা HTTP-কে নানা কৌশলে বাঁকাতে শুরু করলেন। ২০০৬ সালের মার্চে Dojo Toolkit-এর Alex Russell একটি ব্লগ লেখায় এই কৌশলগুলোর একটি সাধারণ নাম দেন — “Comet”. নামটি আসলে একটি রসিকতা: Ajax আর Comet দুটোই আমেরিকার পরিচিত ঘর-পরিষ্কারের পাউডারের ব্র্যান্ড।
Comet-এর কৌশলগুলো ছিল চতুর আর ভঙ্গুর। Long polling: request পাঠাও, সার্ভার খবর না হওয়া পর্যন্ত ধরে রাখে। “Forever frame”: পাতায় একটি লুকানো iframe, যার HTML উত্তর কখনো শেষ হয় না; সার্ভার মাঝে মাঝে একটি <script> ট্যাগ লিখে পাঠায়, আর ব্রাউজার আসা মাত্র সেটি চালায়। XHR streaming: একটি request-এর উত্তর ধীরে ধীরে পড়া — কিন্তু তখনকার কিছু ব্রাউজার পুরো উত্তর আসার আগে কিছুই দেখাত না। প্রতিটি ব্রাউজারের জন্য আলাদা কৌশল, proxy-র buffering আর প্রতি-domain সংযোগের সীমা মিলিয়ে Comet লাইব্রেরি ছিল ঝুঁকিপূর্ণ জাদু; পরে Socket.IO-র fallback ব্যবস্থাও এই উত্তরাধিকার বহন করে।
সার্ভারের দিকেও সমস্যা ছিল: প্রতি সংযোগে একটি thread রাখা সার্ভারে হাজার জন long polling-এ বসে থাকলে হাজার thread শুধু অপেক্ষা করে (ভাবনার প্রশ্ন ৪)। Comet-এর অভিজ্ঞতা দুটি মানদণ্ডের জন্ম দেয়: একমুখী ধারার জন্য Server-Sent Events (forever frame-এর পরিষ্কার উত্তরসূরি, সাথে reconnection), আর দুইমুখী কথার জন্য WebSocket.
মূল উপলব্ধি: মানদণ্ড প্রায়ই আসে হ্যাকের পরে। ডেভেলপাররা যখন একটি প্রয়োজন মেটাতে বছরের পর বছর পুরোনো প্রযুক্তিকে বাঁকান, সেই কষ্টই দেখিয়ে দেয় নতুন প্রযুক্তির কী থাকা দরকার — আর পুরোনো কৌশলগুলো (long polling) fallback হিসেবে টিকে থাকে।
প্র ০২WebSocket-এর মানদণ্ড হতে এত বছর লাগল কেন, আর client-এর প্রতিটি frame “mask” করার অদ্ভুত নিয়মটি কোন নিরাপত্তা-শিক্ষা থেকে এল?
HTML5-এর প্রথম দিকের খসড়ায় WHATWG-এর Ian Hickson “TCPConnection” নামে একটি ধারণা রাখেন — ব্রাউজার থেকে সরাসরি socket. ২০০৮ সালের দিকে আলোচনার মধ্য দিয়ে এর নাম হয় WebSocket, আর ২০০৯ সালের শেষে Google Chrome প্রথম প্রধান ব্রাউজার হিসেবে এটি ডিফল্টভাবে চালু করে। তারের ওপরের প্রোটোকলের দায়িত্ব যায় IETF-এর HyBi working group-এ, আর একের পর এক অসামঞ্জস্যপূর্ণ খসড়া আসতে থাকে।
২০১০ সালের শেষ দিকে নিরাপত্তা গবেষকদের একটি দল (Lin-Shung Huang, Eric Chen, Adam Barth, Eric Rescorla ও Collin Jackson) দেখান যে তখনকার খসড়া একটি বাস্তব বিপদ তৈরি করে। ইন্টারনেটে অনেক “transparent proxy” আছে — কর্পোরেট বা ISP-র যন্ত্র, যারা চুপচাপ HTTP পড়ে আর cache করে। এদের অনেকে WebSocket বোঝে না। গবেষকেরা দেখান, একটি দুষ্ট পাতা WebSocket সংযোগে এমন byte পাঠাতে পারে, যা এমন proxy-র চোখে একটি সাধারণ HTTP request-এর মতো দেখায়, আর proxy ভুল উত্তরকে একটি জনপ্রিয় ফাইলের নামে cache করে ফেলতে পারে — তারপর ওই proxy-র পেছনের সবাই সেই বিষাক্ত ফাইল পায়। একে বলে cache poisoning. প্রতিক্রিয়া ছিল দ্রুত: Firefox আর Opera তাদের আসন্ন সংস্করণে WebSocket সাময়িকভাবে বন্ধ করে দেয়।
সমাধান হলো masking. Client থেকে যাওয়া প্রতিটি frame-এর payload ব্রাউজারের বাছাই করা একটি এলোমেলো ৪-byte key দিয়ে XOR করা হয়। key frame-এই লেখা থাকে, তাই এটি গোপনীয়তা দেয় না; কিন্তু পাতার JavaScript আর জানে না শেষ পর্যন্ত তারে কোন byte যাবে — অর্থাৎ proxy-কে বোকা বানানোর মতো byte সে সাজাতে পারে না। সাথে Sec-WebSocket-Accept-এর হিসাব নিশ্চিত করে যে উত্তরদাতা সত্যিই WebSocket সার্ভার। এই নকশা নিয়ে ২০১১ সালের ডিসেম্বরে RFC 6455 প্রকাশিত হয়, আর ব্রাউজারগুলো নতুন সংস্করণে আবার WebSocket চালু করে।
মূল উপলব্ধি: ইন্টারনেট শুধু আপনার client আর সার্ভার নয় — মাঝে অসংখ্য পুরোনো, অজানা যন্ত্র আছে, যারা নিজেদের মতো করে byte পড়ে। নতুন প্রোটোকলকে শুধু নিজে কাজ করলেই চলে না, ওই যন্ত্রগুলোকে বিভ্রান্ত না করাও তার দায়িত্ব। এই একই কারণে আজ wss:// শুধু গোপনীয়তার জন্য নয়, নির্ভরযোগ্যতার জন্যও ভালো।
প্র ০৩বহু বছর ধরে প্রায় উপেক্ষিত Server-Sent Events হঠাৎ AI চ্যাটের যুগে এত গুরুত্বপূর্ণ হয়ে উঠল কেন?
SSE-র মানদণ্ডও Ian Hickson-এর হাতে, WHATWG-এর ওয়েব অ্যাপ্লিকেশন খসড়ার অংশ হিসেবে, ২০০০-এর দশকের মাঝামাঝি। Opera ২০০৬ সালে পরীক্ষামূলক একটি রূপ চালু করে, আর ২০১০–২০১১-এর মধ্যে Chrome, Safari ও Firefox EventSource সমর্থন করে। কিন্তু Internet Explorer কখনো করেনি — Microsoft-এর ব্রাউজারে এটি এল শুধু ২০২০ সালে Chromium-ভিত্তিক Edge-এর সাথে। এই এক অভাবেই বহু বছর অনেক দল SSE এড়িয়ে গেছে; ততদিনে WebSocket “real-time মানেই WebSocket” ধারণা তৈরি করে ফেলেছে, আর Socket.IO-র মতো লাইব্রেরি দুটোর পার্থক্য আড়াল করেছে।
তবু SSE-র শান্ত শক্তি ছিল: এটি সাধারণ HTTP (একই port, cookie, load balancer), HTTP/2-তে সংযোগ-সীমার দুর্বলতা নেই, আর reconnection বিনা কোডে।
তারপর এল বড় ভাষা মডেল (LLM). একটি মডেল উত্তর তৈরি করে token ধরে ধরে, আর পুরো উত্তর আসতে অনেক সেকেন্ড লাগতে পারে। ব্যবহারকারীকে ততক্ষণ ফাঁকা পর্দা দেখানো খারাপ অভিজ্ঞতা; শব্দে শব্দে দেখালে মনে হয় উত্তর সাথে সাথে আসছে। এই প্রবাহ ঠিক একমুখী — প্রশ্ন একবার যায়, উত্তর ধারায় আসে। তাই OpenAI ও Anthropic-এর মতো বড় AI API-গুলো streaming উত্তর দেয় text/event-stream ফরম্যাটে: প্রতিটি টুকরো একটি data: লাইন (কেউ কেউ event: নামও দেয়, যেমন বার্তা শুরু, লেখার টুকরো, বার্তা শেষ)। একটি মজার খুঁটিনাটি: এই API-গুলোতে প্রশ্ন পাঠাতে POST আর Authorization header লাগে, যা EventSource পারে না — তাই client-গুলো সাধারণত fetch দিয়ে stream পড়ে নিজেই ফরম্যাট parse করে, ঠিক এই পর্বের Node উদাহরণের মতো। অর্থাৎ API-টি নয়, ফরম্যাটটি আজ সবচেয়ে বেশি টিকে আছে।
মূল উপলব্ধি: প্রযুক্তির জনপ্রিয়তা আর তার উপযোগিতা এক জিনিস নয়। একটি সরল, “বিরক্তিকর” মানদণ্ড সঠিক সমস্যার দেখা পেলে দশ বছর পরেও কেন্দ্রে চলে আসতে পারে — তাই কাজের ধরন (একমুখী না দুইমুখী) দেখে বাছুন, প্রচলিত ধারণা দেখে নয়।
প্র ০৪C10K সমস্যা থেকে একটি সার্ভারে লাখো সংযোগ — WhatsApp কীভাবে একটি যন্ত্রে ২০ লাখের বেশি সংযোগ ধরে রেখেছিল, আর এর সাথে Node.js-এর সম্পর্ক কী?
১৯৯৯ সালে Dan Kegel “The C10K problem” নামে একটি লেখা প্রকাশ করেন: একটি সার্ভার কীভাবে একসাথে দশ হাজার client সামলাবে? তখনকার প্রচলিত নকশা — প্রতি সংযোগে একটি thread বা process — এখানে ভেঙে পড়ে: প্রতিটি thread-এর নিজের stack (প্রায়ই অনেক KB থেকে MB), আর হাজার thread-এর মাঝে অপারেটিং সিস্টেমের পালাবদল (context switch) নিজেই বড় খরচ। আর real-time সংযোগের বেশিরভাগ সময় কাটে কিছু না করে, শুধু অপেক্ষায়।
সমাধান এল অপারেটিং সিস্টেম থেকে: “এই হাজার socket-এর মধ্যে কোনগুলো প্রস্তুত, আমাকে জানাও” — FreeBSD-তে kqueue, Linux-এ epoll (২০০০-এর দশকের শুরুতে)। একটি thread একটি event loop-এ হাজার হাজার সংযোগ সামলাতে পারে, কারণ সে শুধু যেগুলোতে কিছু ঘটেছে সেগুলোতেই কাজ করে। nginx এই নকশায় জনপ্রিয় হয়, আর Node.js (২০০৯) পুরো ভাষার runtime-কে এর ওপর দাঁড় করায় — পর্ব ৩১-এর event loop আসলে নিচে এই epoll/kqueue ব্যবস্থাকেই ব্যবহার করে (libuv-এর মাধ্যমে)। এই কারণেই Node real-time সার্ভারের জন্য স্বাভাবিক পছন্দ হয়ে ওঠে।
২০১২ সালের শুরুতে WhatsApp তাদের প্রকৌশল ব্লগে জানায়, তারা একটি সার্ভারে একসাথে ২০ লাখের বেশি TCP সংযোগ ধরে রাখতে পেরেছে — FreeBSD আর Erlang-এ। Erlang-এর virtual machine খুব হালকা “process” দেয় (অপারেটিং সিস্টেমের thread নয়; শুরুতে মাত্র কয়েক KB মেমরি নেয়), তাই প্রতি সংযোগে একটি process রাখাও সস্তা, আর একটি ভেঙে পড়লে বাকিরা অক্ষত থাকে। পরে Elixir-এর Phoenix framework-ও একটি যন্ত্রে ২০ লাখ WebSocket সংযোগের পরীক্ষা প্রকাশ করে। আর একসময় আলোচনা শুরু হয় “C10M” — এক কোটি সংযোগ — নিয়ে, যেখানে অপারেটিং সিস্টেমের নেটওয়ার্ক স্তরও বাধা।
মূল উপলব্ধি: real-time-এর প্রকৌশল আসলে “অপেক্ষা সস্তা করার” প্রকৌশল। যে নকশায় প্রতিটি অপেক্ষমাণ সংযোগের খরচ সবচেয়ে কম, সেটিই বড় পরিসরে জেতে — thread থেকে event loop আর হালকা process পর্যন্ত যাত্রাটি সেই গল্প।
প্র ০৫WebSocket যদি এত ভালো হয়, তাহলে WebTransport আর HTTP/3 (QUIC) কেন এল? TCP-র কোন সীমাবদ্ধতা তারা সারাতে চায়?
WebSocket চলে একটি TCP সংযোগের ওপর, আর TCP একটি প্রতিশ্রুতি দেয়: সব byte ঠিক ক্রমে, একটিও না হারিয়ে পৌঁছাবে। বেশিরভাগ কাজে এটি আশীর্বাদ। কিন্তু একটি packet হারালে TCP তার পরের সব ডেটা আটকে রাখে, যতক্ষণ না হারানোটি আবার পাঠিয়ে পৌঁছায় — একে বলে head-of-line blocking. একটি মাল্টিপ্লেয়ার গেমে খেলোয়াড়ের ২০০ ms আগের অবস্থান হারালে তার জন্য অপেক্ষার কোনো মানে নেই; সর্বশেষ অবস্থানটিই দরকার। লাইভ ভিডিওতেও তাই। আর একটি সংযোগে চ্যাট, ফাইল আর অবস্থান একসাথে চললে একটির হারানো packet সবগুলোকে থামিয়ে দেয়।
Google ২০১০-এর দশকের শুরুতে QUIC নিয়ে পরীক্ষা শুরু করে — UDP-র ওপর একটি নতুন পরিবহন, যেখানে একটি সংযোগে অনেকগুলো স্বাধীন stream থাকে, একটির ক্ষতি অন্যটিকে আটকায় না; TLS 1.3 ভেতরেই বসানো, আর সংযোগ স্থাপনে কম round-trip লাগে। সংযোগের পরিচয় IP ঠিকানার সাথে বাঁধা নয়, তাই Wi-Fi থেকে মোবাইল ডেটায় গেলেও সংযোগ টিকে থাকতে পারে — IETF ২০২১ সালে QUIC (RFC 9000) আর ২০২২ সালে এর ওপরের HTTP/3 (RFC 9114) মানদণ্ড প্রকাশ করে।
WebTransport হলো এই ক্ষমতা ব্রাউজারের JavaScript-এ আনার API: একটি সংযোগে অনেকগুলো নির্ভরযোগ্য stream (ক্রমসহ, প্রতিটি আলাদা), আর অনির্ভরযোগ্য datagram — পাঠাও, পৌঁছালে ভালো, না পৌঁছালে আবার পাঠানোর চেষ্টা নেই। Chrome ২০২২ সালের শুরুতে এটি চালু করে, পরে Firefox; কিন্তু সব ব্রাউজারে সমান সমর্থন, সার্ভার লাইব্রেরি, load balancer আর কর্পোরেট firewall (অনেকে UDP আটকায়) — সবকিছু এখনো WebSocket-এর মতো পরিণত নয়। তাই আজও বেশিরভাগ চ্যাট আর নোটিফিকেশনের জন্য WebSocket বা SSE-ই বাস্তবসম্মত, আর WebTransport-এ গেলেও WebSocket fallback রাখা বুদ্ধিমানের কাজ।
মূল উপলব্ধি: প্রতিটি প্রোটোকল একটি আপস — TCP-র “সব ক্রমে, কিছু হারাবে না” প্রতিশ্রুতি কিছু কাজে বোঝা। প্রয়োজন বুঝে আপস বাছাই করাই প্রকৌশল: চ্যাটে প্রতিটি বার্তা চাই, গেমে চাই সর্বশেষটি।
অনুশীলন
প্রথম তিনটিতে কোড লিখতে হবে না — কাগজে উত্তর লিখে তারপর মিলিয়ে নিন।
-
প্রযুক্তি বাছাই: প্রতিটির জন্য polling, SSE বা WebSocket বাছুন আর এক লাইনে কারণ বলুন — (ক) বিপিএলের লাইভ স্কোর, (খ) কাস্টমার-সাপোর্ট চ্যাট, “টাইপ করছেন”-সহ, (গ) প্রতি ঘণ্টায় বদলানো ডলারের রেট, (ঘ) একটি AI সহকারীর শব্দে শব্দে উত্তর, (ঙ) দুজনের একসাথে একটি ডকুমেন্ট সম্পাদনা।
(ক) SSE — খবর শুধু সার্ভার থেকে, দর্শক কিছু পাঠান না; স্বয়ংক্রিয় reconnection আর HTTP/2-বান্ধব। (খ) WebSocket — দুই দিকে ঘন ঘন ছোট বার্তা। (গ) Polling — ঘণ্টায় একবার বদলায়। (ঘ) SSE-র ফরম্যাট, fetch দিয়ে — প্রশ্ন একবার POST, উত্তর একমুখী ধারা; POST আর header লাগে বলে
EventSourceনয়। (ঙ) WebSocket — দুজনের সম্পাদনা দুই দিকে, কম দেরিতে, আর presence (কার cursor কোথায়)। -
স্ট্রিম পড়ুন: একটি SSE সংযোগে নিচের লেখা এল।
EventSourceকয়টি event ছুড়বে, প্রতিটির নাম,dataআরlastEventIdকী?onmessageকোনগুলো পাবে?
retry: 5000/ (ফাঁকা) /id: 7/data: স্কোর 120/3/ (ফাঁকা) /: ping/ (ফাঁকা) /id: 8/event: wicket/data: সাকিব আউট/data: 120/4/ (ফাঁকা)দুটি event. প্রথম ব্লকে শুধু
retry— reconnection-এর সময় ৫ সেকেন্ড হলো, কোনোdataনেই বলে event নয়। দ্বিতীয়টি নামহীন, তাই নাম"message",data="স্কোর 120/3",lastEventId="7"— এটিইonmessageপায়।: pingcomment, উপেক্ষিত। শেষটির নাম"wicket", দুটিdataলাইন\nদিয়ে জুড়ে"সাকিব আউট\n120/4",lastEventId="8". এটিonmessageপায় না —es.addEventListener("wicket", …)লাগবে। সংযোগ এখন কাটলে ৫ সেকেন্ড পরে ব্রাউজারLast-Event-ID: 8পাঠাবে। -
বাগ খুঁজুন: একটি AI-এর লেখা চ্যাট সার্ভারের মূল অংশ:
wss.on("connection", ws => ws.on("message", d => { const m = JSON.parse(d); wss.clients.forEach(c => c.send(JSON.stringify({ from: m.from, html: m.text }))); }));আর client-এbox.innerHTML += msg.html. অন্তত পাঁচটি সমস্যা বলুন।(১)
JSON.parseকোনোtry/catch-এ নেই — একটি ভাঙা বার্তায় listener-এ exception; আর যাচাই নেই (textstring কি না, দৈর্ঘ্য)। (২)fromবার্তা থেকে নেওয়া — যে কেউ অন্যের নামে লিখতে পারে; পরিচয় আসা উচিত সংযোগের লগইন থেকে। (৩)wss.clientsমানে সব সংযুক্ত মানুষ — room নেই, এক গ্রাহকের কথা সবাই দেখে। (৪)innerHTML-এ পাওয়া লেখা — broadcast XSS;textContentদরকার, আর field-এর নামhtmlনিজেই বিপদের সংকেত। (৫) handshake-এ Origin বা লগইন যাচাই নেই (CSWSH),maxPayloadআর rate limit নেই, heartbeat নেই। বাড়তি:c.readyState === WebSocket.OPENনা দেখে পাঠানো, আর বার্তায়type/vনেই। -
কোড লিখুন — SSE-র ছোট buffer: একটি Node.js class
EventLogবানান:push(data)প্রতিটি event-কে ক্রমিক id দিয়ে রাখে (সর্বোচ্চ শেষ ১০০টি), আরsince(lastId)তার পরের event-গুলো ফেরত দেয়।lastIdযদি এত পুরোনো হয় যে buffer থেকে মুছে গেছে, তাহলে কী করবেন?একটি সম্ভাব্য সমাধান:
class EventLog { #items = []; #seq = 0; constructor(max = 100) { this.max = max; } push(data) { const ev = { id: ++this.#seq, data }; this.#items.push(ev); if (this.#items.length > this.max) this.#items.shift(); // পুরোনোটি ফেলুন return ev; } since(lastId) { const oldest = this.#items[0]?.id ?? this.#seq + 1; if (lastId < oldest - 1) return null; // ফাঁক আছে — পূর্ণ অবস্থা লাগবে return this.#items.filter((e) => e.id > lastId); } }sinceযখনnullদেয়, তখন হারানো event আর জোড়া লাগানো যাবে না — সার্ভার একটি বিশেষ event (যেমনevent: reset, সাথে অর্ডারের সম্পূর্ণ বর্তমান অবস্থা) পাঠাবে, আর client পুরো পর্দা নতুন করে আঁকবে। বাস্তবে buffer মেমরিতে না রেখে Redis বা ডেটাবেসে রাখুন, যাতে একাধিক সার্ভার আর restart-এর পরেও কাজ করে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি নিরাপদ WebSocket চ্যাট সংযোগে শুরু থেকে শেষ পর্যন্ত ঘটনাগুলো সাজান (▲▼ চেপে বা টেনে)।
- 🙋 ব্রাউজার Upgrade: websocket header-সহ একটি GET পাঠায়
- 🛡️ সার্ভার Origin আর লগইন cookie যাচাই করে
- 🤝 সার্ভার 101 Switching Protocols উত্তর দেয়, ব্রাউজারে onopen চলে
- 💬 দুই দিকে JSON বার্তার frame চলে, মাঝে মাঝে ping/pong
- 👋 এক পক্ষ code 1000-সহ close frame পাঠায়
- 🔚 অন্য পক্ষ close frame ফেরত দেয়, TCP সংযোগ বন্ধ হয়
একটি লাইভ ক্রিকেট স্কোরের পাতায় খবর শুধু সার্ভার থেকে দর্শকের দিকে যায়, আর সংযোগ কাটলে নিজে থেকেই আবার জোড়া লাগা দরকার। কোন প্রযুক্তি সবচেয়ে সরল আর মানানসই?
- প্রতি সেকেন্ডে polling
- Server-Sent Events (EventSource)
- WebSocket
- WebTransport datagram
সংযোগ কেটে যাওয়ার পরে ব্রাউজারের EventSource আবার সংযোগ করার সময় শেষ পাওয়া event-এর id কোন HTTP header-এ পাঠায়?
- If-None-Match
- Sec-WebSocket-Key
- Last-Event-ID
- Retry-After
WebSocket handshake সফল হলে সার্ভার কোন HTTP status code দিয়ে উত্তর দেয়?
- 101 Switching Protocols
- 200 OK
- 204 No Content
- 426 Upgrade Required
একটি সাধারণ ব্রাউজার new WebSocket("wss://…") client দিয়ে একটি Socket.IO সার্ভারে যুক্ত হওয়ার চেষ্টা করলে কী ঘটে?
- সব কাজ করে, room-সহ
- স্বয়ংক্রিয়ভাবে long polling-এ চলে যায়
- শুধু text বার্তা চলে, binary চলে না
- ঠিকমতো যুক্ত হয় না, কারণ Socket.IO একটি নিজস্ব প্রোটোকল
Cross-Site WebSocket Hijacking (CSWSH) ঠেকাতে WebSocket সার্ভারের handshake-এ সবচেয়ে জরুরি যাচাই কোনটি?
- Access-Control-Allow-Origin header পাঠানো
- Origin header অনুমোদিত তালিকার সাথে মেলানো
- Sec-WebSocket-Accept ঠিকমতো হিসাব করা
- শুধু wss:// ব্যবহার করা
ব্রাউজারের WebSocket অবজেক্টের bufferedAmount কী জানায়?
- এ পর্যন্ত পাওয়া বার্তার সংখ্যা
- সার্ভারের queue-তে কত বার্তা জমে আছে
- send() করা কিন্তু এখনো নেটওয়ার্কে না যাওয়া byte-এর পরিমাণ
- একটি বার্তার সর্বোচ্চ অনুমোদিত আকার
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।
মনে রাখুন
- HTTP-তে কথা শুরু করে client; real-time মানে সার্ভারের push. সিঁড়ি: polling → long polling → SSE → WebSocket — প্রয়োজনের চেয়ে উঁচুতে উঠবেন না।
- SSE = একটি না-ফুরানো HTTP উত্তর,
text/event-stream, ফাঁকা লাইনে event শেষ।EventSourceনিজে reconnect করে আরLast-Event-IDপাঠায়; proxy buffering বন্ধ, comment ping, HTTP/2. - WebSocket = HTTP upgrade (101), তারপর frame দুই দিকে। Production-এ
wss://; close code পড়ে সিদ্ধান্ত; সংযোগের আগেsendনয়। - নকশা আপনার:
typeআরv-সহ JSON বার্তা, সার্ভারের id, room আর presence, heartbeat, backoff + jitter-সহ reconnect,bufferedAmountদেখে backpressure. - প্রতিটি বার্তা অবিশ্বস্ত — Origin আর লগইন handshake-এ, schema, আকার ও হারের সীমা,
textContent. একাধিক সার্ভারে Redis pub/sub; Socket.IO আলাদা প্রোটোকল।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫২ · Node.js আর ডেটাবেসপরবর্তী পর্ববার্তা আর event স্থায়ীভাবে রাখা।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ৩৬ · AbortController, retry, debounce আর throttleফিরে দেখুনPolling, exponential backoff আর jitter — reconnection-এর ভিত্তি।
- পর্ব ৪৩ · নিরাপদ JavaScript — XSS, CSP আর CORSফিরে দেখুনOrigin, CSRF আর XSS — WebSocket নিরাপত্তার পটভূমি।
- Python-এর asyncioতুলনা করুনevent loop-এ হাজার সংযোগ — একই ধারণা Python-এ।