Представь: ты строишь страницу отслеживания заказа. Покупатель должен видеть статус в реальном времени — «принят», «готовится», «в доставке», «доставлен» — без перезагрузки страницы. Как получать обновления от сервера? Есть несколько подходов с разными компромиссами.
WebSocket — двусторонний канал, но требует особой настройки сервера. Для задач, где данные идут только от сервера к клиенту (уведомления, статус заказа, live-feed), SSE проще в реализации и работает поверх обычного HTTP.
Клиент периодически отправляет запросы:
// Простой polling каждые 3 секунды — неэффективно
setInterval(async () => {
const response = await fetch('/api/notifications')
const data = await response.json()
if (data.length > 0) {
showNotifications(data)
}
}, 3000)
// Проблема: большинство запросов вернут пустой ответ,
// зря нагружая сервер и тратя трафикКлиент делает запрос и ждёт, пока сервер не ответит. Сервер держит соединение открытым до появления новых данных. Как только данные есть — сервер отвечает, клиент сразу делает новый запрос:
async function longPoll(url) {
while (true) {
try {
// Запрос может висеть долго (сервер отвечает только при новых данных)
const response = await fetch(url + '?lastEventId=' + lastId)
const data = await response.json()
processData(data) // обрабатываем данные
// Сразу делаем новый запрос — не ждём
} catch (error) {
// Ошибка сети — подождать и повторить
await sleep(5000)
}
}
}Преимущества: работает везде где есть HTTP, нет проблем с прокси.
Недостатки: высокая нагрузка на сервер при масштабировании.
SSE — это стандарт, при котором сервер отправляет поток событий через одно HTTP-соединение. Клиент использует EventSource:
const source = new EventSource('/api/events')
// Обработчик сообщений по умолчанию (тип "message")
source.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Новое событие:', data)
}
// Кастомные типы событий
source.addEventListener('order:update', (event) => {
const order = JSON.parse(event.data)
updateOrderStatus(order)
})
source.addEventListener('notification', (event) => {
showNotification(JSON.parse(event.data))
})
// Обработка ошибок — браузер автоматически переподключается
source.onerror = (error) => {
console.error('SSE ошибка, переподключение...')
}
// Закрыть соединение вручную
source.close()Сервер отправляет текст в специальном формате — каждое событие заканчивается двойным переводом строки:
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"status": "pending"}\n\n
data: {"status": "processing"}\n\n
event: order:update
data: {"id": 42, "status": "delivered"}\n\n
id: 100
retry: 5000
data: {"message": "heartbeat"}\n\n// На сервере (Node.js/Express)
app.get('/api/order-status/:id', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
function sendEvent(type, data) {
res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`)
}
sendEvent('order:update', { id: req.params.id, status: 'processing' })
const timer = setInterval(() => {
sendEvent('order:update', { id: req.params.id, status: 'ready' })
clearInterval(timer)
res.end()
}, 3000)
req.on('close', () => clearInterval(timer))
})В JavaScript можно смоделировать поток SSE через асинхронный генератор:
async function* sseStream(url) {
// В реальном коде: fetch + ReadableStream
// Для демонстрации — генерируем события
const statuses = ['pending', 'processing', 'shipped', 'delivered']
for (const status of statuses) {
await sleep(1000)
yield { type: 'order:update', data: { status } }
}
}
// Обработка потока
for await (const event of sseStream('/api/events')) {
console.log(event.type, event.data)
}| | Short Polling | Long Polling | SSE | WebSocket |
|---|---|---|---|---|
| Направление | Клиент → Сервер | Клиент → Сервер | Сервер → Клиент | Двустороннее |
| Задержка | Высокая | Низкая | Низкая | Минимальная |
| Нагрузка на сервер | Высокая | Средняя | Низкая | Низкая |
| Сложность | Минимальная | Средняя | Низкая | Высокая |
| Автопереподключение | Вручную | Вручную | Встроено | Вручную |
1. Не закрывать EventSource при уходе пользователя
// ПЛОХО — соединение висит даже после перехода на другую страницу
function initOrderTracking(orderId) {
const source = new EventSource(`/api/orders/${orderId}/stream`)
source.onmessage = (e) => updateUI(e.data)
// Утечка: source никогда не закрывается
}
// ХОРОШО — возвращаем функцию отписки
function initOrderTracking(orderId) {
const source = new EventSource(`/api/orders/${orderId}/stream`)
source.onmessage = (e) => updateUI(e.data)
return () => source.close() // вызвать при unmount компонента
}2. Бесконечный long poll без защиты от зависания
// ПЛОХО — если сервер вечно не отвечает, запрос висит бесконечно
async function longPoll(url) {
while (true) {
const res = await fetch(url) // может висеть часами
processData(await res.json())
}
}
// ХОРОШО — с таймаутом и AbortController
async function longPoll(url, timeoutMs = 30000) {
while (true) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
const res = await fetch(url, { signal: controller.signal })
processData(await res.json())
} catch (err) {
if (err.name !== 'AbortError') await sleep(3000)
} finally {
clearTimeout(timer)
}
}
}3. Игнорировать поле id в SSE — теряем позицию при переподключении
// Сервер должен отправлять id для каждого события
// data: {...}\n\n — ПЛОХО: при разрыве начнём с начала
// id: 42\ndata: {...}\n\n — ХОРОШО: браузер отправит Last-Event-ID при переподключении
// EventSource автоматически добавит заголовок: Last-Event-ID: 42Симуляция SSE потока через async generator: live-статус заказа от pending до delivered
// Симуляция Server-Sent Events через async generator
// В реальном браузере использовался бы EventSource + ReadableStream
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// Async generator — имитирует поток событий от сервера
async function* simulateOrderStream(orderId) {
const timeline = [
{ event: 'status', data: { orderId, status: 'pending', message: 'Заказ принят' } },
{ event: 'status', data: { orderId, status: 'confirmed', message: 'Оплата подтверждена' } },
{ event: 'status', data: { orderId, status: 'processing', message: 'Собирается на складе' } },
{ event: 'status', data: { orderId, status: 'shipped', message: 'Передан в доставку' } },
{ event: 'status', data: { orderId, status: 'delivered', message: 'Доставлен!' } },
]
for (const item of timeline) {
await sleep(50) // эмулируем задержку сервера (50ms вместо реальных секунд)
yield item
}
}
// Клиентский код — обработка потока (как EventSource в браузере)
async function trackOrder(orderId) {
console.log(`Отслеживаем заказ #${orderId}\n`)
const handlers = new Map()
// Регистрируем обработчики событий (как source.addEventListener)
handlers.set('status', ({ status, message }) => {
console.log(`[${status.toUpperCase()}] ${message}`)
return status === 'delivered' // true = закрыть поток
})
// Читаем поток (как цикл обработки SSE)
for await (const { event, data } of simulateOrderStream(orderId)) {
const handler = handlers.get(event)
const shouldClose = handler?.(data)
if (shouldClose) {
console.log('\nПоток закрыт — заказ доставлен!')
break
}
}
}
// Запускаем отслеживание
trackOrder(78432)
// ===
// Паттерн Long Polling (синхронная симуляция)
console.log('\n=== Long Polling паттерн ===')
function createStatusServer() {
let callCount = 0
const responses = [null, null, { status: 'shipped' }, { status: 'delivered' }]
return {
// Симулирует долгий ответ сервера (возвращает null пока нет данных)
poll() {
return responses[callCount++] || null
},
hasMore() { return callCount < responses.length }
}
}
async function simulateLongPoll(server, intervalMs = 10) {
const results = []
let attempts = 0
while (true) {
attempts++
await sleep(intervalMs) // задержка между запросами
const data = server.poll()
if (data !== null) {
results.push(data)
console.log(`Попытка ${attempts}: получен ответ → ${data.status}`)
if (data.status === 'delivered') break
} else {
console.log(`Попытка ${attempts}: сервер ещё не ответил...`)
}
if (attempts >= 10) break // защита от бесконечного цикла
}
return results
}
const mockServer = createStatusServer()
simulateLongPoll(mockServer, 10).then(results => {
console.log('\nВсе полученные статусы:', results.map(r => r.status).join(' → '))
})
// Формат SSE сообщений
console.log('\n=== Парсинг SSE сообщений ===')
function parseSSEMessage(raw) {
const result = { event: 'message', data: '', id: null, retry: null }
const lines = raw.trim().split('\n')
for (const line of lines) {
if (line.startsWith('event:')) result.event = line.slice(6).trim()
else if (line.startsWith('data:')) result.data = line.slice(5).trim()
else if (line.startsWith('id:')) result.id = line.slice(3).trim()
else if (line.startsWith('retry:')) result.retry = parseInt(line.slice(6).trim())
}
return result
}
const rawMessages = [
'data: {"status":"pending"}',
'event: order:update\ndata: {"id":42,"status":"shipped"}',
'id: 100\nevent: notification\ndata: {"text":"Ваш заказ в пути"}\nretry: 5000',
]
rawMessages.forEach(raw => {
const parsed = parseSSEMessage(raw)
console.log('Событие:', parsed.event, '| data:', parsed.data.slice(0, 40))
})Представь: ты строишь страницу отслеживания заказа. Покупатель должен видеть статус в реальном времени — «принят», «готовится», «в доставке», «доставлен» — без перезагрузки страницы. Как получать обновления от сервера? Есть несколько подходов с разными компромиссами.
WebSocket — двусторонний канал, но требует особой настройки сервера. Для задач, где данные идут только от сервера к клиенту (уведомления, статус заказа, live-feed), SSE проще в реализации и работает поверх обычного HTTP.
Клиент периодически отправляет запросы:
// Простой polling каждые 3 секунды — неэффективно
setInterval(async () => {
const response = await fetch('/api/notifications')
const data = await response.json()
if (data.length > 0) {
showNotifications(data)
}
}, 3000)
// Проблема: большинство запросов вернут пустой ответ,
// зря нагружая сервер и тратя трафикКлиент делает запрос и ждёт, пока сервер не ответит. Сервер держит соединение открытым до появления новых данных. Как только данные есть — сервер отвечает, клиент сразу делает новый запрос:
async function longPoll(url) {
while (true) {
try {
// Запрос может висеть долго (сервер отвечает только при новых данных)
const response = await fetch(url + '?lastEventId=' + lastId)
const data = await response.json()
processData(data) // обрабатываем данные
// Сразу делаем новый запрос — не ждём
} catch (error) {
// Ошибка сети — подождать и повторить
await sleep(5000)
}
}
}Преимущества: работает везде где есть HTTP, нет проблем с прокси.
Недостатки: высокая нагрузка на сервер при масштабировании.
SSE — это стандарт, при котором сервер отправляет поток событий через одно HTTP-соединение. Клиент использует EventSource:
const source = new EventSource('/api/events')
// Обработчик сообщений по умолчанию (тип "message")
source.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Новое событие:', data)
}
// Кастомные типы событий
source.addEventListener('order:update', (event) => {
const order = JSON.parse(event.data)
updateOrderStatus(order)
})
source.addEventListener('notification', (event) => {
showNotification(JSON.parse(event.data))
})
// Обработка ошибок — браузер автоматически переподключается
source.onerror = (error) => {
console.error('SSE ошибка, переподключение...')
}
// Закрыть соединение вручную
source.close()Сервер отправляет текст в специальном формате — каждое событие заканчивается двойным переводом строки:
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"status": "pending"}\n\n
data: {"status": "processing"}\n\n
event: order:update
data: {"id": 42, "status": "delivered"}\n\n
id: 100
retry: 5000
data: {"message": "heartbeat"}\n\n// На сервере (Node.js/Express)
app.get('/api/order-status/:id', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
function sendEvent(type, data) {
res.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`)
}
sendEvent('order:update', { id: req.params.id, status: 'processing' })
const timer = setInterval(() => {
sendEvent('order:update', { id: req.params.id, status: 'ready' })
clearInterval(timer)
res.end()
}, 3000)
req.on('close', () => clearInterval(timer))
})В JavaScript можно смоделировать поток SSE через асинхронный генератор:
async function* sseStream(url) {
// В реальном коде: fetch + ReadableStream
// Для демонстрации — генерируем события
const statuses = ['pending', 'processing', 'shipped', 'delivered']
for (const status of statuses) {
await sleep(1000)
yield { type: 'order:update', data: { status } }
}
}
// Обработка потока
for await (const event of sseStream('/api/events')) {
console.log(event.type, event.data)
}| | Short Polling | Long Polling | SSE | WebSocket |
|---|---|---|---|---|
| Направление | Клиент → Сервер | Клиент → Сервер | Сервер → Клиент | Двустороннее |
| Задержка | Высокая | Низкая | Низкая | Минимальная |
| Нагрузка на сервер | Высокая | Средняя | Низкая | Низкая |
| Сложность | Минимальная | Средняя | Низкая | Высокая |
| Автопереподключение | Вручную | Вручную | Встроено | Вручную |
1. Не закрывать EventSource при уходе пользователя
// ПЛОХО — соединение висит даже после перехода на другую страницу
function initOrderTracking(orderId) {
const source = new EventSource(`/api/orders/${orderId}/stream`)
source.onmessage = (e) => updateUI(e.data)
// Утечка: source никогда не закрывается
}
// ХОРОШО — возвращаем функцию отписки
function initOrderTracking(orderId) {
const source = new EventSource(`/api/orders/${orderId}/stream`)
source.onmessage = (e) => updateUI(e.data)
return () => source.close() // вызвать при unmount компонента
}2. Бесконечный long poll без защиты от зависания
// ПЛОХО — если сервер вечно не отвечает, запрос висит бесконечно
async function longPoll(url) {
while (true) {
const res = await fetch(url) // может висеть часами
processData(await res.json())
}
}
// ХОРОШО — с таймаутом и AbortController
async function longPoll(url, timeoutMs = 30000) {
while (true) {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
const res = await fetch(url, { signal: controller.signal })
processData(await res.json())
} catch (err) {
if (err.name !== 'AbortError') await sleep(3000)
} finally {
clearTimeout(timer)
}
}
}3. Игнорировать поле id в SSE — теряем позицию при переподключении
// Сервер должен отправлять id для каждого события
// data: {...}\n\n — ПЛОХО: при разрыве начнём с начала
// id: 42\ndata: {...}\n\n — ХОРОШО: браузер отправит Last-Event-ID при переподключении
// EventSource автоматически добавит заголовок: Last-Event-ID: 42Симуляция SSE потока через async generator: live-статус заказа от pending до delivered
// Симуляция Server-Sent Events через async generator
// В реальном браузере использовался бы EventSource + ReadableStream
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// Async generator — имитирует поток событий от сервера
async function* simulateOrderStream(orderId) {
const timeline = [
{ event: 'status', data: { orderId, status: 'pending', message: 'Заказ принят' } },
{ event: 'status', data: { orderId, status: 'confirmed', message: 'Оплата подтверждена' } },
{ event: 'status', data: { orderId, status: 'processing', message: 'Собирается на складе' } },
{ event: 'status', data: { orderId, status: 'shipped', message: 'Передан в доставку' } },
{ event: 'status', data: { orderId, status: 'delivered', message: 'Доставлен!' } },
]
for (const item of timeline) {
await sleep(50) // эмулируем задержку сервера (50ms вместо реальных секунд)
yield item
}
}
// Клиентский код — обработка потока (как EventSource в браузере)
async function trackOrder(orderId) {
console.log(`Отслеживаем заказ #${orderId}\n`)
const handlers = new Map()
// Регистрируем обработчики событий (как source.addEventListener)
handlers.set('status', ({ status, message }) => {
console.log(`[${status.toUpperCase()}] ${message}`)
return status === 'delivered' // true = закрыть поток
})
// Читаем поток (как цикл обработки SSE)
for await (const { event, data } of simulateOrderStream(orderId)) {
const handler = handlers.get(event)
const shouldClose = handler?.(data)
if (shouldClose) {
console.log('\nПоток закрыт — заказ доставлен!')
break
}
}
}
// Запускаем отслеживание
trackOrder(78432)
// ===
// Паттерн Long Polling (синхронная симуляция)
console.log('\n=== Long Polling паттерн ===')
function createStatusServer() {
let callCount = 0
const responses = [null, null, { status: 'shipped' }, { status: 'delivered' }]
return {
// Симулирует долгий ответ сервера (возвращает null пока нет данных)
poll() {
return responses[callCount++] || null
},
hasMore() { return callCount < responses.length }
}
}
async function simulateLongPoll(server, intervalMs = 10) {
const results = []
let attempts = 0
while (true) {
attempts++
await sleep(intervalMs) // задержка между запросами
const data = server.poll()
if (data !== null) {
results.push(data)
console.log(`Попытка ${attempts}: получен ответ → ${data.status}`)
if (data.status === 'delivered') break
} else {
console.log(`Попытка ${attempts}: сервер ещё не ответил...`)
}
if (attempts >= 10) break // защита от бесконечного цикла
}
return results
}
const mockServer = createStatusServer()
simulateLongPoll(mockServer, 10).then(results => {
console.log('\nВсе полученные статусы:', results.map(r => r.status).join(' → '))
})
// Формат SSE сообщений
console.log('\n=== Парсинг SSE сообщений ===')
function parseSSEMessage(raw) {
const result = { event: 'message', data: '', id: null, retry: null }
const lines = raw.trim().split('\n')
for (const line of lines) {
if (line.startsWith('event:')) result.event = line.slice(6).trim()
else if (line.startsWith('data:')) result.data = line.slice(5).trim()
else if (line.startsWith('id:')) result.id = line.slice(3).trim()
else if (line.startsWith('retry:')) result.retry = parseInt(line.slice(6).trim())
}
return result
}
const rawMessages = [
'data: {"status":"pending"}',
'event: order:update\ndata: {"id":42,"status":"shipped"}',
'id: 100\nevent: notification\ndata: {"text":"Ваш заказ в пути"}\nretry: 5000',
]
rawMessages.forEach(raw => {
const parsed = parseSSEMessage(raw)
console.log('Событие:', parsed.event, '| data:', parsed.data.slice(0, 40))
})Напиши async функцию simulateLongPoll(getStatus, intervalMs), которая симулирует длинный опрос. getStatus — async функция возвращающая текущий статус (строку). Функция должна опрашивать getStatus каждые intervalMs миллисекунд, собирать все статусы в массив и остановиться когда статус === "done". Вернуть массив всех полученных статусов включая "done".
await sleep(intervalMs), const status = await getStatus(), results.push(status), if (status === "done") break. Цикл while(true) с этими четырьмя шагами внутри.