Server-Sent Events — bir tomonlama real-time

~15 daq · Express · 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 matni
  • event: — 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 install kerak emas. Node.js da ishlatmoqchi bo'lsangiz — eventsource paketini 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 httpOnly cookie 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