HTTP работает по принципу «запрос-ответ»: клиент спрашивает — сервер отвечает. Но что если сервер должен сам отправлять данные клиенту? Для чатов, онлайн-игр, биржевых котировок и совместного редактирования нужна другая модель. WebSocket создаёт постоянный двунаправленный канал между клиентом и сервером.
| | HTTP | WebSocket |
|---|---|---|
| Соединение | Новое на каждый запрос | Одно постоянное |
| Инициатор | Только клиент | Клиент и сервер |
| Накладные расходы | Заголовки каждый раз | Только при открытии |
| Задержка | Высокая | Низкая |
WebSocket начинается с HTTP-запроса, который «апгрейдится» до WS-протокола:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...Сервер отвечает кодом 101 Switching Protocols, и с этого момента соединение — WebSocket.
WebSocket имеет четыре состояния:
0 — CONNECTING: соединение устанавливается1 — OPEN: соединение открыто, можно передавать данные2 — CLOSING: соединение закрывается3 — CLOSED: соединение закрытоconst ws = new WebSocket('wss://echo.websocket.org')
ws.onopen = () => {
console.log('Соединение открыто')
ws.send('Привет, сервер!')
}
ws.onmessage = (event) => {
console.log('Получено:', event.data)
}
ws.onclose = (event) => {
console.log('Закрыто:', event.code, event.reason)
// 1000 — нормальное закрытие, 1006 — аномальное
}
ws.onerror = (error) => {
console.error('Ошибка WebSocket')
}// Текст
ws.send('Привет')
// JSON
ws.send(JSON.stringify({ type: 'message', text: 'Привет' }))
// Бинарные данные
const buffer = new ArrayBuffer(4)
ws.send(buffer)WebSocket имеет встроенный механизм проверки соединения: сервер периодически отправляет ping-фрейм, клиент автоматически отвечает pong. Если pong не пришёл — соединение считается разорванным. В браузерном JS нельзя отправить ping вручную — это делает сам браузер.
WebSocket не переподключается сам. Нужно реализовать это вручную:
function createReconnectingWS(url) {
let ws, reconnectTimer
function connect() {
ws = new WebSocket(url)
ws.onclose = () => {
console.log('Разорвано, переподключаемся через 3с...')
reconnectTimer = setTimeout(connect, 3000)
}
}
connect()
return { send: (data) => ws.send(data), close: () => ws.close() }
}По умолчанию WS получает бинарные данные как Blob. Можно изменить:
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) => {
const view = new DataView(e.data)
console.log(view.getInt32(0))
}EventEmitter-based WebSocket мок с машиной состояний, очередью сообщений и автопереподключением
// Полноценный WebSocket клиент с очередью и автопереподключением
const STATES = { CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 }
class MockWebSocketServer {
constructor() { this._clients = new Set() }
accept(client) {
this._clients.add(client)
// Симулируем установку соединения с задержкой 50мс
setTimeout(() => client._handleOpen(), 50)
}
broadcast(message) {
this._clients.forEach(c => {
if (c.readyState === STATES.OPEN) c._handleMessage(message)
})
}
echo(client, message) {
setTimeout(() => client._handleMessage(`Echo: ${message}`), 30)
}
}
class MockWebSocket {
constructor(url, server) {
this.url = url
this.readyState = STATES.CONNECTING
this._server = server
this._messageQueue = []
this._handlers = { open: [], message: [], close: [], error: [] }
console.log(`[WS] Подключение к ${url}...`)
server.accept(this)
}
on(event, handler) {
this._handlers[event].push(handler)
return this
}
send(data) {
if (this.readyState === STATES.OPEN) {
console.log(`[WS → Server] ${data}`)
this._server.echo(this, data)
} else {
// Буферизуем сообщения пока соединение не открылось
this._messageQueue.push(data)
console.log(`[WS] Добавлено в очередь (readyState=${this.readyState}): ${data}`)
}
}
close(code = 1000, reason = '') {
this.readyState = STATES.CLOSING
console.log('[WS] Закрытие соединения...')
setTimeout(() => this._handleClose(code, reason), 20)
}
_handleOpen() {
this.readyState = STATES.OPEN
console.log('[WS] Соединение открыто')
this._handlers.open.forEach(fn => fn())
// Отправляем буферизованные сообщения
if (this._messageQueue.length > 0) {
console.log(`[WS] Отправка ${this._messageQueue.length} буферизованных сообщений`)
this._messageQueue.forEach(msg => this.send(msg))
this._messageQueue = []
}
}
_handleMessage(data) {
console.log(`[WS ← Server] ${data}`)
this._handlers.message.forEach(fn => fn({ data }))
}
_handleClose(code, reason) {
this.readyState = STATES.CLOSED
console.log(`[WS] Закрыто. Код: ${code}, причина: ${reason || 'нет'}`)
this._handlers.close.forEach(fn => fn({ code, reason }))
}
}
// Демо
const server = new MockWebSocketServer()
const ws = new MockWebSocket('wss://example.com/chat', server)
ws.on('open', () => {
console.log(`\nСостояние: OPEN (${ws.readyState})`)
ws.send('Привет!')
ws.send(JSON.stringify({ type: 'join', room: 'general' }))
})
ws.on('message', (event) => {
console.log('Получено приложением:', event.data)
})
ws.on('close', (event) => {
console.log(`Соединение закрыто с кодом ${event.code}`)
})
// Отправляем до открытия — попадёт в очередь
ws.send('Это сообщение в очереди')
// Закрываем через 200мс
setTimeout(() => ws.close(1000, 'Выход'), 200)HTTP работает по принципу «запрос-ответ»: клиент спрашивает — сервер отвечает. Но что если сервер должен сам отправлять данные клиенту? Для чатов, онлайн-игр, биржевых котировок и совместного редактирования нужна другая модель. WebSocket создаёт постоянный двунаправленный канал между клиентом и сервером.
| | HTTP | WebSocket |
|---|---|---|
| Соединение | Новое на каждый запрос | Одно постоянное |
| Инициатор | Только клиент | Клиент и сервер |
| Накладные расходы | Заголовки каждый раз | Только при открытии |
| Задержка | Высокая | Низкая |
WebSocket начинается с HTTP-запроса, который «апгрейдится» до WS-протокола:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...Сервер отвечает кодом 101 Switching Protocols, и с этого момента соединение — WebSocket.
WebSocket имеет четыре состояния:
0 — CONNECTING: соединение устанавливается1 — OPEN: соединение открыто, можно передавать данные2 — CLOSING: соединение закрывается3 — CLOSED: соединение закрытоconst ws = new WebSocket('wss://echo.websocket.org')
ws.onopen = () => {
console.log('Соединение открыто')
ws.send('Привет, сервер!')
}
ws.onmessage = (event) => {
console.log('Получено:', event.data)
}
ws.onclose = (event) => {
console.log('Закрыто:', event.code, event.reason)
// 1000 — нормальное закрытие, 1006 — аномальное
}
ws.onerror = (error) => {
console.error('Ошибка WebSocket')
}// Текст
ws.send('Привет')
// JSON
ws.send(JSON.stringify({ type: 'message', text: 'Привет' }))
// Бинарные данные
const buffer = new ArrayBuffer(4)
ws.send(buffer)WebSocket имеет встроенный механизм проверки соединения: сервер периодически отправляет ping-фрейм, клиент автоматически отвечает pong. Если pong не пришёл — соединение считается разорванным. В браузерном JS нельзя отправить ping вручную — это делает сам браузер.
WebSocket не переподключается сам. Нужно реализовать это вручную:
function createReconnectingWS(url) {
let ws, reconnectTimer
function connect() {
ws = new WebSocket(url)
ws.onclose = () => {
console.log('Разорвано, переподключаемся через 3с...')
reconnectTimer = setTimeout(connect, 3000)
}
}
connect()
return { send: (data) => ws.send(data), close: () => ws.close() }
}По умолчанию WS получает бинарные данные как Blob. Можно изменить:
ws.binaryType = 'arraybuffer'
ws.onmessage = (e) => {
const view = new DataView(e.data)
console.log(view.getInt32(0))
}EventEmitter-based WebSocket мок с машиной состояний, очередью сообщений и автопереподключением
// Полноценный WebSocket клиент с очередью и автопереподключением
const STATES = { CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3 }
class MockWebSocketServer {
constructor() { this._clients = new Set() }
accept(client) {
this._clients.add(client)
// Симулируем установку соединения с задержкой 50мс
setTimeout(() => client._handleOpen(), 50)
}
broadcast(message) {
this._clients.forEach(c => {
if (c.readyState === STATES.OPEN) c._handleMessage(message)
})
}
echo(client, message) {
setTimeout(() => client._handleMessage(`Echo: ${message}`), 30)
}
}
class MockWebSocket {
constructor(url, server) {
this.url = url
this.readyState = STATES.CONNECTING
this._server = server
this._messageQueue = []
this._handlers = { open: [], message: [], close: [], error: [] }
console.log(`[WS] Подключение к ${url}...`)
server.accept(this)
}
on(event, handler) {
this._handlers[event].push(handler)
return this
}
send(data) {
if (this.readyState === STATES.OPEN) {
console.log(`[WS → Server] ${data}`)
this._server.echo(this, data)
} else {
// Буферизуем сообщения пока соединение не открылось
this._messageQueue.push(data)
console.log(`[WS] Добавлено в очередь (readyState=${this.readyState}): ${data}`)
}
}
close(code = 1000, reason = '') {
this.readyState = STATES.CLOSING
console.log('[WS] Закрытие соединения...')
setTimeout(() => this._handleClose(code, reason), 20)
}
_handleOpen() {
this.readyState = STATES.OPEN
console.log('[WS] Соединение открыто')
this._handlers.open.forEach(fn => fn())
// Отправляем буферизованные сообщения
if (this._messageQueue.length > 0) {
console.log(`[WS] Отправка ${this._messageQueue.length} буферизованных сообщений`)
this._messageQueue.forEach(msg => this.send(msg))
this._messageQueue = []
}
}
_handleMessage(data) {
console.log(`[WS ← Server] ${data}`)
this._handlers.message.forEach(fn => fn({ data }))
}
_handleClose(code, reason) {
this.readyState = STATES.CLOSED
console.log(`[WS] Закрыто. Код: ${code}, причина: ${reason || 'нет'}`)
this._handlers.close.forEach(fn => fn({ code, reason }))
}
}
// Демо
const server = new MockWebSocketServer()
const ws = new MockWebSocket('wss://example.com/chat', server)
ws.on('open', () => {
console.log(`\nСостояние: OPEN (${ws.readyState})`)
ws.send('Привет!')
ws.send(JSON.stringify({ type: 'join', room: 'general' }))
})
ws.on('message', (event) => {
console.log('Получено приложением:', event.data)
})
ws.on('close', (event) => {
console.log(`Соединение закрыто с кодом ${event.code}`)
})
// Отправляем до открытия — попадёт в очередь
ws.send('Это сообщение в очереди')
// Закрываем через 200мс
setTimeout(() => ws.close(1000, 'Выход'), 200)Реализуй createWebSocketClient(url) — объект с методами: connect() устанавливает соединение (симулируй setTimeout 100мс), send(data) отправляет данные (если не подключён — добавляет в очередь и отправляет при подключении), onMessage(cb) регистрирует обработчик входящих сообщений, disconnect() закрывает соединение, getState() возвращает строку "connecting" | "open" | "closed".
Используй три строковых состояния: "connecting", "open", "closed". В connect() установи state = "connecting", через setTimeout — state = "open" и вызови flushQueue(). В send() проверяй state === "open", иначе messageQueue.push(data). getState() просто возвращает переменную state.