WebSocket — протокол двусторонней связи между браузером и сервером. В отличие от HTTP (запрос → ответ → закрытие), WebSocket держит соединение открытым и позволяет серверу слать данные без запроса клиента.
Когда нужен WebSocket:
Альтернативы:
// Базовое использование
const ws = new WebSocket('wss://api.example.com/ws')
ws.onopen = () => {
console.log('Соединение установлено')
ws.send(JSON.stringify({ type: 'auth', token: 'abc123' }))
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Получено:', data)
}
ws.onerror = (error) => {
console.error('Ошибка WebSocket:', error)
}
ws.onclose = (event) => {
console.log('Соединение закрыто:', event.code, event.reason)
// Коды: 1000 (норма), 1001 (уходит), 1006 (аварийно), 1011 (ошибка сервера)
}
// Отправка
ws.send(JSON.stringify({ type: 'message', text: 'Привет!' }))
// Закрытие
ws.close()import { useState, useEffect, useRef, useCallback } from 'react'
function useWebSocket(url) {
const ws = useRef(null)
const [status, setStatus] = useState('disconnected') // connecting | connected | disconnected
const [messages, setMessages] = useState([])
const reconnectTimer = useRef(null)
const reconnectAttempts = useRef(0)
const connect = useCallback(() => {
if (ws.current?.readyState === WebSocket.OPEN) return
setStatus('connecting')
ws.current = new WebSocket(url)
ws.current.onopen = () => {
setStatus('connected')
reconnectAttempts.current = 0
console.log('WebSocket подключён')
}
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data)
setMessages(prev => [...prev, data])
}
ws.current.onclose = () => {
setStatus('disconnected')
// Экспоненциальный backoff: 1с, 2с, 4с, 8с...
const delay = Math.min(1000 * 2 ** reconnectAttempts.current, 30000)
reconnectAttempts.current++
console.log('Переподключение через ' + delay + 'мс...')
reconnectTimer.current = setTimeout(connect, delay)
}
ws.current.onerror = () => {
ws.current.close()
}
}, [url])
useEffect(() => {
connect()
return () => {
clearTimeout(reconnectTimer.current)
ws.current?.close(1000, 'Компонент размонтирован')
}
}, [connect])
const send = useCallback((data) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(data))
}
}, [])
return { status, messages, send }
}function ChatRoom({ roomId }) {
const { status, messages, send } = useWebSocket(
`wss://api.example.com/chat/${roomId}`
)
const [text, setText] = useState('')
const handleSend = () => {
if (!text.trim()) return
send({ type: 'message', text, timestamp: Date.now() })
setText('')
}
return (
<div>
<div>Статус: {status}</div>
<div className="messages">
{messages.map((msg, i) => (
<div key={i}>{msg.author}: {msg.text}</div>
))}
</div>
<input
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend} disabled={status !== 'connected'}>
Отправить
</button>
</div>
)
}Socket.io — библиотека поверх WebSocket с автоматическим переподключением, комнатами и fallback на polling:
// Сервер (Node.js)
import { Server } from 'socket.io'
const io = new Server(httpServer)
io.on('connection', (socket) => {
socket.join('general') // комнаты
socket.on('message', (data) => {
// Отправить всем в комнате кроме себя
socket.to('general').emit('message', data)
})
socket.on('disconnect', () => {
console.log('Пользователь отключился')
})
})
// Клиент (React)
import { io } from 'socket.io-client'
function useChatSocket(room) {
const socket = useRef(null)
useEffect(() => {
socket.current = io('https://api.example.com', {
query: { room },
reconnectionDelay: 1000,
reconnectionAttempts: 5,
})
socket.current.on('message', handleMessage)
return () => socket.current.disconnect()
}, [room])
const sendMessage = (text) => {
socket.current.emit('message', { text })
}
return { sendMessage }
}При разрыве соединения важно не атаковать сервер частыми попытками:
// Экспоненциальный backoff с jitter
function getReconnectDelay(attempt, baseDelay = 1000, maxDelay = 30000) {
const exponential = baseDelay * Math.pow(2, attempt)
const capped = Math.min(exponential, maxDelay)
// Jitter: добавляем случайность, чтобы клиенты не подключались одновременно
const jitter = Math.random() * 0.3 * capped
return capped + jitter
}
// attempt=0: ~1000мс, attempt=1: ~2000мс, attempt=2: ~4000мс...EventEmitter-симуляция WebSocket в ванильном JS: подключение, обработка сообщений, переподключение с экспоненциальным backoff
// Симулируем WebSocket с переподключением.
// Используем EventEmitter-паттерн без реального сокета.
// --- Симулятор сервера ---
class FakeServer {
constructor() {
this.clients = []
this.messageLog = []
}
// Принять клиента
accept(client) {
this.clients.push(client)
setTimeout(() => client._receive({ type: 'welcome', msg: 'Привет от сервера!' }), 50)
// Слать обновления каждые 800мс (первые 3 раза)
let count = 0
const interval = setInterval(() => {
if (!this.clients.includes(client)) { clearInterval(interval); return }
client._receive({ type: 'update', value: Math.round(Math.random() * 100), seq: ++count })
if (count >= 3) clearInterval(interval)
}, 800)
}
// Разорвать соединение с клиентом
dropClient(client) {
this.clients = this.clients.filter(c => c !== client)
client._simulateClose()
}
broadcast(message) {
this.clients.forEach(c => c._receive(message))
}
}
// --- WebSocket менеджер с reconnect ---
function createWebSocketManager(server) {
let connection = null
let status = 'disconnected'
let reconnectAttempts = 0
let reconnectTimer = null
const messageHandlers = []
const statusHandlers = []
function setStatus(newStatus) {
status = newStatus
statusHandlers.forEach(h => h(newStatus))
}
function createConnection() {
// Симулируем объект соединения
const conn = {
_receive(data) {
messageHandlers.forEach(h => h(data))
},
_simulateClose() {
console.log('[WS] Соединение разорвано сервером')
connection = null
setStatus('disconnected')
scheduleReconnect()
},
send(data) {
console.log('[WS] Отправлено на сервер:', JSON.stringify(data))
server.messageLog.push(data)
},
close() {
server.clients = server.clients.filter(c => c !== conn)
connection = null
setStatus('disconnected')
}
}
return conn
}
function getReconnectDelay(attempt) {
return Math.min(500 * Math.pow(2, attempt), 8000)
}
function scheduleReconnect() {
const delay = getReconnectDelay(reconnectAttempts)
reconnectAttempts++
console.log('[WS] Переподключение через ' + delay + 'мс (попытка ' + reconnectAttempts + ')')
reconnectTimer = setTimeout(connect, delay)
}
function connect() {
if (status === 'connected') return
clearTimeout(reconnectTimer)
setStatus('connecting')
console.log('[WS] Подключение...')
// Симулируем установку соединения (50мс задержка)
setTimeout(() => {
connection = createConnection()
setStatus('connected')
reconnectAttempts = 0
console.log('[WS] Подключено!')
server.accept(connection)
}, 50)
}
return {
connect,
disconnect() {
clearTimeout(reconnectTimer)
if (connection) connection.close()
console.log('[WS] Отключено')
},
send(data) {
if (connection && status === 'connected') {
connection.send(data)
} else {
console.log('[WS] Нельзя отправить — статус:', status)
}
},
onMessage(handler) { messageHandlers.push(handler) },
onStatus(handler) { statusHandlers.push(handler) },
getStatus() { return status },
}
}
// --- Запуск ---
const server = new FakeServer()
const ws = createWebSocketManager(server)
ws.onStatus(s => console.log('[STATUS]', s))
ws.onMessage(msg => console.log('[MESSAGE]', JSON.stringify(msg)))
ws.connect()
// Через 1с отправляем сообщение
setTimeout(() => ws.send({ type: 'ping' }), 1000)
// Через 1.5с сервер разрывает соединение
setTimeout(() => {
console.log('
--- Сервер разрывает соединение ---')
server.dropClient(server.clients[0])
}, 1500)
// WS автоматически переподключитсяWebSocket — протокол двусторонней связи между браузером и сервером. В отличие от HTTP (запрос → ответ → закрытие), WebSocket держит соединение открытым и позволяет серверу слать данные без запроса клиента.
Когда нужен WebSocket:
Альтернативы:
// Базовое использование
const ws = new WebSocket('wss://api.example.com/ws')
ws.onopen = () => {
console.log('Соединение установлено')
ws.send(JSON.stringify({ type: 'auth', token: 'abc123' }))
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
console.log('Получено:', data)
}
ws.onerror = (error) => {
console.error('Ошибка WebSocket:', error)
}
ws.onclose = (event) => {
console.log('Соединение закрыто:', event.code, event.reason)
// Коды: 1000 (норма), 1001 (уходит), 1006 (аварийно), 1011 (ошибка сервера)
}
// Отправка
ws.send(JSON.stringify({ type: 'message', text: 'Привет!' }))
// Закрытие
ws.close()import { useState, useEffect, useRef, useCallback } from 'react'
function useWebSocket(url) {
const ws = useRef(null)
const [status, setStatus] = useState('disconnected') // connecting | connected | disconnected
const [messages, setMessages] = useState([])
const reconnectTimer = useRef(null)
const reconnectAttempts = useRef(0)
const connect = useCallback(() => {
if (ws.current?.readyState === WebSocket.OPEN) return
setStatus('connecting')
ws.current = new WebSocket(url)
ws.current.onopen = () => {
setStatus('connected')
reconnectAttempts.current = 0
console.log('WebSocket подключён')
}
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data)
setMessages(prev => [...prev, data])
}
ws.current.onclose = () => {
setStatus('disconnected')
// Экспоненциальный backoff: 1с, 2с, 4с, 8с...
const delay = Math.min(1000 * 2 ** reconnectAttempts.current, 30000)
reconnectAttempts.current++
console.log('Переподключение через ' + delay + 'мс...')
reconnectTimer.current = setTimeout(connect, delay)
}
ws.current.onerror = () => {
ws.current.close()
}
}, [url])
useEffect(() => {
connect()
return () => {
clearTimeout(reconnectTimer.current)
ws.current?.close(1000, 'Компонент размонтирован')
}
}, [connect])
const send = useCallback((data) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(data))
}
}, [])
return { status, messages, send }
}function ChatRoom({ roomId }) {
const { status, messages, send } = useWebSocket(
`wss://api.example.com/chat/${roomId}`
)
const [text, setText] = useState('')
const handleSend = () => {
if (!text.trim()) return
send({ type: 'message', text, timestamp: Date.now() })
setText('')
}
return (
<div>
<div>Статус: {status}</div>
<div className="messages">
{messages.map((msg, i) => (
<div key={i}>{msg.author}: {msg.text}</div>
))}
</div>
<input
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend} disabled={status !== 'connected'}>
Отправить
</button>
</div>
)
}Socket.io — библиотека поверх WebSocket с автоматическим переподключением, комнатами и fallback на polling:
// Сервер (Node.js)
import { Server } from 'socket.io'
const io = new Server(httpServer)
io.on('connection', (socket) => {
socket.join('general') // комнаты
socket.on('message', (data) => {
// Отправить всем в комнате кроме себя
socket.to('general').emit('message', data)
})
socket.on('disconnect', () => {
console.log('Пользователь отключился')
})
})
// Клиент (React)
import { io } from 'socket.io-client'
function useChatSocket(room) {
const socket = useRef(null)
useEffect(() => {
socket.current = io('https://api.example.com', {
query: { room },
reconnectionDelay: 1000,
reconnectionAttempts: 5,
})
socket.current.on('message', handleMessage)
return () => socket.current.disconnect()
}, [room])
const sendMessage = (text) => {
socket.current.emit('message', { text })
}
return { sendMessage }
}При разрыве соединения важно не атаковать сервер частыми попытками:
// Экспоненциальный backoff с jitter
function getReconnectDelay(attempt, baseDelay = 1000, maxDelay = 30000) {
const exponential = baseDelay * Math.pow(2, attempt)
const capped = Math.min(exponential, maxDelay)
// Jitter: добавляем случайность, чтобы клиенты не подключались одновременно
const jitter = Math.random() * 0.3 * capped
return capped + jitter
}
// attempt=0: ~1000мс, attempt=1: ~2000мс, attempt=2: ~4000мс...EventEmitter-симуляция WebSocket в ванильном JS: подключение, обработка сообщений, переподключение с экспоненциальным backoff
// Симулируем WebSocket с переподключением.
// Используем EventEmitter-паттерн без реального сокета.
// --- Симулятор сервера ---
class FakeServer {
constructor() {
this.clients = []
this.messageLog = []
}
// Принять клиента
accept(client) {
this.clients.push(client)
setTimeout(() => client._receive({ type: 'welcome', msg: 'Привет от сервера!' }), 50)
// Слать обновления каждые 800мс (первые 3 раза)
let count = 0
const interval = setInterval(() => {
if (!this.clients.includes(client)) { clearInterval(interval); return }
client._receive({ type: 'update', value: Math.round(Math.random() * 100), seq: ++count })
if (count >= 3) clearInterval(interval)
}, 800)
}
// Разорвать соединение с клиентом
dropClient(client) {
this.clients = this.clients.filter(c => c !== client)
client._simulateClose()
}
broadcast(message) {
this.clients.forEach(c => c._receive(message))
}
}
// --- WebSocket менеджер с reconnect ---
function createWebSocketManager(server) {
let connection = null
let status = 'disconnected'
let reconnectAttempts = 0
let reconnectTimer = null
const messageHandlers = []
const statusHandlers = []
function setStatus(newStatus) {
status = newStatus
statusHandlers.forEach(h => h(newStatus))
}
function createConnection() {
// Симулируем объект соединения
const conn = {
_receive(data) {
messageHandlers.forEach(h => h(data))
},
_simulateClose() {
console.log('[WS] Соединение разорвано сервером')
connection = null
setStatus('disconnected')
scheduleReconnect()
},
send(data) {
console.log('[WS] Отправлено на сервер:', JSON.stringify(data))
server.messageLog.push(data)
},
close() {
server.clients = server.clients.filter(c => c !== conn)
connection = null
setStatus('disconnected')
}
}
return conn
}
function getReconnectDelay(attempt) {
return Math.min(500 * Math.pow(2, attempt), 8000)
}
function scheduleReconnect() {
const delay = getReconnectDelay(reconnectAttempts)
reconnectAttempts++
console.log('[WS] Переподключение через ' + delay + 'мс (попытка ' + reconnectAttempts + ')')
reconnectTimer = setTimeout(connect, delay)
}
function connect() {
if (status === 'connected') return
clearTimeout(reconnectTimer)
setStatus('connecting')
console.log('[WS] Подключение...')
// Симулируем установку соединения (50мс задержка)
setTimeout(() => {
connection = createConnection()
setStatus('connected')
reconnectAttempts = 0
console.log('[WS] Подключено!')
server.accept(connection)
}, 50)
}
return {
connect,
disconnect() {
clearTimeout(reconnectTimer)
if (connection) connection.close()
console.log('[WS] Отключено')
},
send(data) {
if (connection && status === 'connected') {
connection.send(data)
} else {
console.log('[WS] Нельзя отправить — статус:', status)
}
},
onMessage(handler) { messageHandlers.push(handler) },
onStatus(handler) { statusHandlers.push(handler) },
getStatus() { return status },
}
}
// --- Запуск ---
const server = new FakeServer()
const ws = createWebSocketManager(server)
ws.onStatus(s => console.log('[STATUS]', s))
ws.onMessage(msg => console.log('[MESSAGE]', JSON.stringify(msg)))
ws.connect()
// Через 1с отправляем сообщение
setTimeout(() => ws.send({ type: 'ping' }), 1000)
// Через 1.5с сервер разрывает соединение
setTimeout(() => {
console.log('
--- Сервер разрывает соединение ---')
server.dropClient(server.clients[0])
}, 1500)
// WS автоматически переподключитсяСоздай React-приложение с имитацией WebSocket чата. Компонент должен показывать статус соединения (connecting/connected/disconnected), список сообщений и поле ввода. Используй кастомный хук useWebSocket для управления соединением.
В useWebSocket: setStatus("connecting"), затем setStatus("connected"). При disconnect: setStatus("disconnected"). В send добавляй myMessage. StatusIndicator использует colors[status]. Chat передаёт status={status}, onClick={connect}, onClick={disconnect}, send(input), disabled={status !== "connected"}.