Server-Sent Events — bir tomonlama real-time
Muammodan boshlaylik
Yangiliklar sayti yasayapsiz. Sahifada aksiyalar narxi ko'rsatiladi — har 5 sekundda yangilanadi. Foydalanuvchi hech nima bosmaydi, lekin raqamlar o'zgarib turishi kerak.
Birinchi fikr: "WebSocket ishlataman!" Lekin bir zum to'xtang:
- Foydalanuvchi server ga hech narsa yubormasligi kerak — faqat qabul qiladi
- Mijoz → Server yo'nalishi umuman kerak emas
WebSocket — ikki tomonlama kanal. Ikki yo'nalish uchun ham resurs sarflaydi. Siz esa faqat bir tomoni kerak.
Aynan shu uchun SSE (Server-Sent Events) mavjud.
SSE nima?
SSE — server dan mijozga bir tomonlama uzluksiz ma'lumot oqimi. Brauzer bir marta so'rov yuboradi, server esa javobni yopmaydi — ma'lumot kelishi davomida "tomizib" yuboradi.
Mijoz Server
|--- GET /events ------->|
| |
|<-- data: yangilik 1 ---| t=0s
|<-- data: yangilik 2 ---| t=5s
|<-- data: yangilik 3 ---| t=10s
| |
| (ulanish davom etadi)
WebSocket bilan farqi:
| SSE | WebSocket | |
|---|---|---|
| Yo'nalish | Faqat Server → Mijoz | Ikki tomonlama |
| Protokol | Oddiy HTTP | WebSocket protokoli |
| Brauzer qo'llab-quvvatlashi | Har qanday zamonaviy | Har qanday zamonaviy |
| Reconnect | Avtomatik (brauzer o'zi) | Qo'lda |
| Nginx/proxy | Oson | Ba'zan maxsus sozlash kerak |
| Xabar formati | Matn | Matn yoki Binary |
Hayotiy o'xshatma
Radioeshittirish. Siz radioni yoqasiz — tinglaysiz. Radio sizga javob kutmaydi. Siz nima demaganingiz muhim emas — radio o'z ishini davom ettiradi.
SSE — aynan shu: server "radioeshittiruvchi", mijoz "tinglovchi". Bir tomonlama, uzluksiz.
WebSocket esa telefon qo'ng'irog'i — ikki tomon ham gaplashadi.
Nima uchun SSE?
Qachon SSE, qachon WebSocket tanlash kerak:
SSE tanlang:
- Foydalanuvchiga bildirish (notification) yuborish
- Yangiliklar lenti, aksiya narxlari
- Buyurtma holati yangilanishi (yetkazib berildi, yo'lda...)
- AI chatbot javobini oqim sifatida yuborish (ChatGPT shunday ishlaydi)
- Faylni yuklab olish progressi
WebSocket tanlang:
- Chat (ikki tomonlama xabar)
- Online o'yin (real-time harakat)
- Hamkorlikda tahrirlash (Google Docs kabi)
- Video/audio qo'ng'iroq signali
💡 ChatGPT javobini "bosma mashinada bosilayotgandek" ko'rasiz — bu aynan SSE. Server tokenlarni birin-ketin yuboradi, brauzer esa kelgan sayin ko'rsatadi.
Asosiy kod
Server (Express)
app.get('/events', (req, res) => {
// SSE uchun maxsus headerlar
res.setHeader('Content-Type', 'text/event-stream'); // ← SSE protokoli
res.setHeader('Cache-Control', 'no-cache'); // ← keshlanmasin
res.setHeader('Connection', 'keep-alive'); // ← ulanish saqlansin
res.setHeader('X-Accel-Buffering', 'no'); // ← Nginx buffer o'chir
res.flushHeaders(); // ← headerlarni darhol yuborish
// Har 3 sekundda ma'lumot yuborish
const interval = setInterval(() => {
const data = {
vaqt: new Date().toISOString(),
narx: Math.random() * 1000,
};
// SSE formati: "data: ...\n\n"
res.write(`data: ${JSON.stringify(data)}\n\n`); // ← ikki \n muhim!
}, 3000);
// Mijoz uzilsa — interval to'xtatilsin
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
SSE xabar formati
SSE o'ziga xos matn formati:
data: Bu oddiy xabar\n\n
data: {"ism": "Ali", "narx": 500}\n\n
event: yangilik\n
data: Maxsus event\n\n
id: 42\n
data: ID bilan xabar\n\n
data:— xabar matnievent:— maxsus event nomi (default:message)id:— xabar ID si (reconnect da kerak)\n\n— xabar oxiri (majburiy ikki qator o'tishi)
Mijoz (brauzer)
// EventSource — brauzerga o'rnatilgan, kutubxona kerak emas
const eventsource = new EventSource('/events');
// Default "message" eventi
eventsource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('Yangi ma\'lumot:', data);
};
// Maxsus event nomi
eventsource.addEventListener('yangilik', (e) => {
const data = JSON.parse(e.data);
console.log('Yangilik:', data);
});
// Xato
eventsource.onerror = (e) => {
console.log('Xato:', e);
};
// Ulashni to'xtatish
// eventsource.close();
💡
EventSource— brauzerga o'rnatilgan.npm installkerak emas. Node.js da ishlatmoqchi bo'lsangiz —eventsourcepaketini o'rnatish kerak.
Ko'p mijozlarga yuborish
Bir foydalanuvchi emas — barcha ulangan foydalanuvchilarga yuborish:
// Ulangan mijozlar ro'yxati
const clients = new Map(); // Map<socketId, res>
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const clientId = Date.now().toString();
clients.set(clientId, res);
console.log(`Ulandi: ${clientId}. Jami: ${clients.size}`);
req.on('close', () => {
clients.delete(clientId);
console.log(`Uzildi: ${clientId}. Jami: ${clients.size}`);
});
});
// Barcha ulanganlarga yuborish
const broadcastEvent = (data) => {
const message = `data: ${JSON.stringify(data)}\n\n`;
clients.forEach((res) => {
res.write(message);
});
};
// Har 5 sekundda narx yangilash
setInterval(() => {
broadcastEvent({
narx: (Math.random() * 1000).toFixed(2),
vaqt: new Date().toISOString(),
});
}, 5000);
JWT bilan autentifikatsiya
SSE HTTP so'rov bilan ochiladi — autentifikatsiyani oddiy Express middleware orqali qilish mumkin:
import jwt from 'jsonwebtoken';
// Middleware
const authSSE = (req, res, next) => {
// EventSource Authorization header ni o'zi qo'sha olmaydi
// Shuning uchun query params yoki cookie ishlatiladi
const token = req.query.token || req.cookies?.accessToken;
if (!token) {
return res.status(401).json({ xabar: 'Token kerak' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ xabar: 'Token noto\'g\'ri' });
}
};
app.get('/events', authSSE, (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
console.log('Ulandi:', req.user.email);
// Faqat shu foydalanuvchiga tegishli ma'lumot...
});
Mijozda token query params orqali:
// ❌ — EventSource Authorization header qo'shib olmaydi
const es = new EventSource('/events', {
headers: { Authorization: `Bearer ${token}` }, // ← ishlamaydi
});
// ✅ — query params
const es = new EventSource(`/events?token=${token}`);
// ✅ — httpOnly cookie (xavfsizroq)
// Cookie avtomatik yuboriladi, alohida qilish shart emas
const es = new EventSource('/events', { withCredentials: true });
⚠️ Query params da token URL da ko'rinadi — log larda, browser history da. Mumkin bo'lsa
httpOnlycookie ishlatgan ma'qul.
AI javobini oqim sifatida yuborish (ChatGPT usuli)
app.post('/ai/javob', async (req, res) => {
const { savol } = req.body;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.flushHeaders();
// OpenAI / Claude stream modda
const stream = await openai.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: savol }],
stream: true, // ← oqim rejimi
});
for await (const chunk of stream) {
const token = chunk.choices[0]?.delta?.content || '';
if (token) {
res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
}
res.write('data: [DONE]\n\n'); // ← tugadi signali
res.end();
});
Mijozda:
const javobDiv = document.getElementById('javob');
let to'liqJavob = '';
const es = new EventSource('/ai/javob'); // yoki fetch bilan
es.onmessage = (e) => {
if (e.data === '[DONE]') {
es.close();
return;
}
const { token } = JSON.parse(e.data);
to'liqJavob += token;
javobDiv.textContent = to'liqJavob; // ← har token kelganda yangilash
};
Tez-tez uchraydigan xatolar
1. \n\n o'rniga \n ishlatish
// ❌ — xabar yetib bormaydi
res.write(`data: ${JSON.stringify(data)}\n`);
// ✅ — ikki qator o'tish
res.write(`data: ${JSON.stringify(data)}\n\n`);
Sababi: SSE protokolida xabar \n\n bilan tugashi majburiy. Bitta \n — xabar davom etmoqda degan belgi.
2. res.flushHeaders() ni unutish
// ❌ — Nginx/Express ba'zan headerlarni buferlaydi
res.setHeader('Content-Type', 'text/event-stream');
// flushHeaders yo'q — birinchi ma'lumot kechi yetib boradi
// ✅
res.setHeader('Content-Type', 'text/event-stream');
res.flushHeaders(); // ← darhol yuborish
3. Mijoz uzilganda interval to'xtatmaslik
// ❌ — memory leak
const interval = setInterval(() => {
res.write(`data: ...\n\n`); // ← uzilgan ulanishga yozish xato
}, 1000);
// interval hech qachon to'xtatilmaydi!
// ✅
req.on('close', () => {
clearInterval(interval);
res.end();
});
4. Nginx da buferlash
// Header qo'shib Nginx buferni o'chirish
res.setHeader('X-Accel-Buffering', 'no');
// Nginx konfigida:
// proxy_buffering off;
Xulosa
- SSE — server dan mijozga bir tomonlama real-time: notification, lenta, AI javob
- Maxsus format:
data: ...\n\n— ikki yangi qator majburiy - Brauzerda
EventSource— kutubxona kerak emas, avtomatik qayta ulanadi - WebSocket kerak bo'lmasa — SSE soddaroq, oddiy HTTP
req.on('close')— mijoz uzilganda interval va resurslarni tozalang
Keyingi dars: 46-dars: Nodemailer — SMTP, Gmail, email yuborish