← Браузер/WebSocket: двунаправленная связь#187 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: async и сетьТермин: Event LoopТермин: Core Web Vitals

WebSocket: двунаправленная связь

HTTP работает по принципу «запрос-ответ»: клиент спрашивает — сервер отвечает. Но что если сервер должен сам отправлять данные клиенту? Для чатов, онлайн-игр, биржевых котировок и совместного редактирования нужна другая модель. WebSocket создаёт постоянный двунаправленный канал между клиентом и сервером.

WebSocket vs HTTP

| | HTTP | WebSocket |

|---|---|---|

| Соединение | Новое на каждый запрос | Одно постоянное |

| Инициатор | Только клиент | Клиент и сервер |

| Накладные расходы | Заголовки каждый раз | Только при открытии |

| Задержка | Высокая | Низкая |

Открытие соединения

WebSocket начинается с HTTP-запроса, который «апгрейдится» до WS-протокола:

GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...

Сервер отвечает кодом 101 Switching Protocols, и с этого момента соединение — WebSocket.

readyState

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)

    Ping/Pong

    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))
    }

    Случаи применения

  • Чаты и мессенджеры
  • Онлайн-игры и многопользовательские приложения
  • Биржевые котировки и финансовые данные в реальном времени
  • Совместное редактирование документов (Google Docs)
  • Мониторинг и дашборды
  • Примеры

    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)

    WebSocket: двунаправленная связь

    HTTP работает по принципу «запрос-ответ»: клиент спрашивает — сервер отвечает. Но что если сервер должен сам отправлять данные клиенту? Для чатов, онлайн-игр, биржевых котировок и совместного редактирования нужна другая модель. WebSocket создаёт постоянный двунаправленный канал между клиентом и сервером.

    WebSocket vs HTTP

    | | HTTP | WebSocket |

    |---|---|---|

    | Соединение | Новое на каждый запрос | Одно постоянное |

    | Инициатор | Только клиент | Клиент и сервер |

    | Накладные расходы | Заголовки каждый раз | Только при открытии |

    | Задержка | Высокая | Низкая |

    Открытие соединения

    WebSocket начинается с HTTP-запроса, который «апгрейдится» до WS-протокола:

    GET /chat HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: ...

    Сервер отвечает кодом 101 Switching Protocols, и с этого момента соединение — WebSocket.

    readyState

    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)

    Ping/Pong

    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))
    }

    Случаи применения

  • Чаты и мессенджеры
  • Онлайн-игры и многопользовательские приложения
  • Биржевые котировки и финансовые данные в реальном времени
  • Совместное редактирование документов (Google Docs)
  • Мониторинг и дашборды
  • Примеры

    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.

    Загружаем среду выполнения...
    Загружаем AI-помощника...