← JavaScript/WebSocket#108 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

WebSocket

Slack показывает «Алиса печатает...» в реальном времени без обновления страницы. Binance отображает котировки криптовалют каждую секунду. Figma позволяет нескольким дизайнерам работать над одним файлом одновременно. Все эти функции работают через WebSocket.

Какую проблему решает

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

На основе предыдущих уроков

  • Fetch — HTTP запросы, сравнение с WebSocket
  • События — паттерн обработки событий
  • Промисы — асинхронная природа соединения
  • JSON — сериализация сообщений
  • HTTP vs WebSocket

  • HTTP: Запрос -> Ответ, каждый запрос — новое соединение, клиент всегда инициирует
  • WebSocket: двунаправленный поток, одно постоянное соединение, сервер может отправлять первым
  • Создание соединения

    const ws = new WebSocket('wss://api.example.com/ws')
    // wss:// — защищённый WebSocket (как https:// для HTTP)
    // ws://  — незащищённый (только для localhost в разработке)

    События WebSocket

    ws.onopen = () => {
      console.log('Соединение установлено')
      ws.send(JSON.stringify({ type: 'auth', token: userToken }))
    }
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      handleMessage(data)
    }
    
    ws.onclose = (event) => {
      console.log('Закрыто. Код:', event.code)
      // 1000 — нормальное закрытие, 1006 — соединение оборвалось
    }
    
    ws.onerror = (error) => {
      console.error('Ошибка WebSocket:', error)
    }

    readyState

    ws.readyState
    // 0 — CONNECTING: устанавливается
    // 1 — OPEN: открыто, можно отправлять
    // 2 — CLOSING: закрывается
    // 3 — CLOSED: закрыто

    Протокол сообщений с типами

    // Всегда отправляй структурированные сообщения с полем type
    ws.send(JSON.stringify({
      type: 'chat:message',
      text: 'Привет всем!',
      timestamp: Date.now()
    }))
    
    // Обработка разных типов сообщений
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data)
      switch (msg.type) {
        case 'chat:message': appendMessage(msg.text, msg.author); break
        case 'user:join':    showNotification(msg.username + ' вошёл'); break
        case 'user:typing':  showTypingIndicator(msg.username); break
      }
    }

    Переподключение с экспоненциальной задержкой

    class WebSocketClient {
      constructor(url) {
        this.url = url
        this.ws = null
        this.reconnectDelay = 1000
        this.handlers = new Map()
      }
    
      connect() {
        this.ws = new WebSocket(this.url)
    
        this.ws.onopen = () => {
          this.reconnectDelay = 1000  // сброс при успехе
        }
    
        this.ws.onclose = () => {
          setTimeout(() => this.connect(), this.reconnectDelay)
          this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
          // Задержка: 1s -> 2s -> 4s -> 8s -> ... -> 30s
        }
    
        this.ws.onmessage = (event) => {
          const data = JSON.parse(event.data)
          this.handlers.get(data.type)?.(data)
        }
      }
    
      on(type, handler) { this.handlers.set(type, handler); return this }
      send(type, data) { this.ws.send(JSON.stringify({ type, ...data })) }
    }

    Типичные ошибки

    Ошибка 1: отправка до открытия соединения

    const ws = new WebSocket('wss://example.com/ws')
    ws.send('Привет!')  // ОШИБКА — соединение ещё не открыто!
    
    // Правильно
    ws.onopen = () => {
      ws.send('Привет!')  // теперь точно открыто
    }

    Ошибка 2: нет обработки onerror и onclose

    // При обрыве ничего не произойдёт — тихий сбой
    const ws = new WebSocket(url)
    ws.onmessage = handleMessage
    
    // Правильно — всегда обрабатывай все события
    ws.onerror = (e) => console.error('WS Error:', e)
    ws.onclose = () => reconnect()

    Ошибка 3: нет JSON.parse/stringify

    // Неправильно
    ws.send({ type: 'message', text: 'Hello' })  // '[object Object]'
    
    // Правильно
    ws.send(JSON.stringify({ type: 'message', text: 'Hello' }))
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)  // обязательно парсим строку
    }

    В реальных проектах

  • Чат: Slack, Telegram Web, Discord
  • Live-уведомления: новые заказы на Wildberries, задачи в Jira
  • Совместное редактирование: Figma, Google Docs, Notion
  • Биржа/трейдинг: котировки, стакан заявок в реальном времени
  • Примеры

    EventEmitter — паттерн Observer, лежащий в основе WebSocket-клиентов

    // EventEmitter — паттерн на котором строятся WebSocket-клиенты
    class EventEmitter {
      constructor() {
        this._events = new Map()
      }
    
      on(event, handler) {
        if (!this._events.has(event)) this._events.set(event, [])
        this._events.get(event).push(handler)
        return this
      }
    
      off(event, handler) {
        if (!this._events.has(event)) return this
        this._events.set(event, this._events.get(event).filter(h => h !== handler))
        return this
      }
    
      emit(event, data) {
        if (!this._events.has(event)) return
        this._events.get(event).forEach(h => h(data))
      }
    
      once(event, handler) {
        const wrapper = (data) => {
          handler(data)
          this.off(event, wrapper)
        }
        return this.on(event, wrapper)
      }
    }
    
    // Симуляция WebSocket-клиента чата
    const chatClient = new EventEmitter()
    
    chatClient
      .on('message',    (d) => console.log('[' + d.author + ']: ' + d.text))
      .on('user:join',  (d) => console.log('>> ' + d.name + ' вошёл в чат'))
      .on('user:leave', (d) => console.log('<< ' + d.name + ' вышел из чата'))
    
    chatClient.once('connect', () => console.log('Соединение установлено!'))
    
    // Симулируем получение событий от сервера
    chatClient.emit('connect', {})            // 'Соединение установлено!'
    chatClient.emit('connect', {})            // ничего — once удалил обработчик
    chatClient.emit('user:join',  { name: 'Алиса' })
    chatClient.emit('message',    { author: 'Алиса', text: 'Всем привет!' })
    chatClient.emit('message',    { author: 'Боб',   text: 'Привет, Алиса!' })
    chatClient.emit('user:leave', { name: 'Алиса' })

    WebSocket

    Slack показывает «Алиса печатает...» в реальном времени без обновления страницы. Binance отображает котировки криптовалют каждую секунду. Figma позволяет нескольким дизайнерам работать над одним файлом одновременно. Все эти функции работают через WebSocket.

    Какую проблему решает

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

    На основе предыдущих уроков

  • Fetch — HTTP запросы, сравнение с WebSocket
  • События — паттерн обработки событий
  • Промисы — асинхронная природа соединения
  • JSON — сериализация сообщений
  • HTTP vs WebSocket

  • HTTP: Запрос -> Ответ, каждый запрос — новое соединение, клиент всегда инициирует
  • WebSocket: двунаправленный поток, одно постоянное соединение, сервер может отправлять первым
  • Создание соединения

    const ws = new WebSocket('wss://api.example.com/ws')
    // wss:// — защищённый WebSocket (как https:// для HTTP)
    // ws://  — незащищённый (только для localhost в разработке)

    События WebSocket

    ws.onopen = () => {
      console.log('Соединение установлено')
      ws.send(JSON.stringify({ type: 'auth', token: userToken }))
    }
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      handleMessage(data)
    }
    
    ws.onclose = (event) => {
      console.log('Закрыто. Код:', event.code)
      // 1000 — нормальное закрытие, 1006 — соединение оборвалось
    }
    
    ws.onerror = (error) => {
      console.error('Ошибка WebSocket:', error)
    }

    readyState

    ws.readyState
    // 0 — CONNECTING: устанавливается
    // 1 — OPEN: открыто, можно отправлять
    // 2 — CLOSING: закрывается
    // 3 — CLOSED: закрыто

    Протокол сообщений с типами

    // Всегда отправляй структурированные сообщения с полем type
    ws.send(JSON.stringify({
      type: 'chat:message',
      text: 'Привет всем!',
      timestamp: Date.now()
    }))
    
    // Обработка разных типов сообщений
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data)
      switch (msg.type) {
        case 'chat:message': appendMessage(msg.text, msg.author); break
        case 'user:join':    showNotification(msg.username + ' вошёл'); break
        case 'user:typing':  showTypingIndicator(msg.username); break
      }
    }

    Переподключение с экспоненциальной задержкой

    class WebSocketClient {
      constructor(url) {
        this.url = url
        this.ws = null
        this.reconnectDelay = 1000
        this.handlers = new Map()
      }
    
      connect() {
        this.ws = new WebSocket(this.url)
    
        this.ws.onopen = () => {
          this.reconnectDelay = 1000  // сброс при успехе
        }
    
        this.ws.onclose = () => {
          setTimeout(() => this.connect(), this.reconnectDelay)
          this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
          // Задержка: 1s -> 2s -> 4s -> 8s -> ... -> 30s
        }
    
        this.ws.onmessage = (event) => {
          const data = JSON.parse(event.data)
          this.handlers.get(data.type)?.(data)
        }
      }
    
      on(type, handler) { this.handlers.set(type, handler); return this }
      send(type, data) { this.ws.send(JSON.stringify({ type, ...data })) }
    }

    Типичные ошибки

    Ошибка 1: отправка до открытия соединения

    const ws = new WebSocket('wss://example.com/ws')
    ws.send('Привет!')  // ОШИБКА — соединение ещё не открыто!
    
    // Правильно
    ws.onopen = () => {
      ws.send('Привет!')  // теперь точно открыто
    }

    Ошибка 2: нет обработки onerror и onclose

    // При обрыве ничего не произойдёт — тихий сбой
    const ws = new WebSocket(url)
    ws.onmessage = handleMessage
    
    // Правильно — всегда обрабатывай все события
    ws.onerror = (e) => console.error('WS Error:', e)
    ws.onclose = () => reconnect()

    Ошибка 3: нет JSON.parse/stringify

    // Неправильно
    ws.send({ type: 'message', text: 'Hello' })  // '[object Object]'
    
    // Правильно
    ws.send(JSON.stringify({ type: 'message', text: 'Hello' }))
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)  // обязательно парсим строку
    }

    В реальных проектах

  • Чат: Slack, Telegram Web, Discord
  • Live-уведомления: новые заказы на Wildberries, задачи в Jira
  • Совместное редактирование: Figma, Google Docs, Notion
  • Биржа/трейдинг: котировки, стакан заявок в реальном времени
  • Примеры

    EventEmitter — паттерн Observer, лежащий в основе WebSocket-клиентов

    // EventEmitter — паттерн на котором строятся WebSocket-клиенты
    class EventEmitter {
      constructor() {
        this._events = new Map()
      }
    
      on(event, handler) {
        if (!this._events.has(event)) this._events.set(event, [])
        this._events.get(event).push(handler)
        return this
      }
    
      off(event, handler) {
        if (!this._events.has(event)) return this
        this._events.set(event, this._events.get(event).filter(h => h !== handler))
        return this
      }
    
      emit(event, data) {
        if (!this._events.has(event)) return
        this._events.get(event).forEach(h => h(data))
      }
    
      once(event, handler) {
        const wrapper = (data) => {
          handler(data)
          this.off(event, wrapper)
        }
        return this.on(event, wrapper)
      }
    }
    
    // Симуляция WebSocket-клиента чата
    const chatClient = new EventEmitter()
    
    chatClient
      .on('message',    (d) => console.log('[' + d.author + ']: ' + d.text))
      .on('user:join',  (d) => console.log('>> ' + d.name + ' вошёл в чат'))
      .on('user:leave', (d) => console.log('<< ' + d.name + ' вышел из чата'))
    
    chatClient.once('connect', () => console.log('Соединение установлено!'))
    
    // Симулируем получение событий от сервера
    chatClient.emit('connect', {})            // 'Соединение установлено!'
    chatClient.emit('connect', {})            // ничего — once удалил обработчик
    chatClient.emit('user:join',  { name: 'Алиса' })
    chatClient.emit('message',    { author: 'Алиса', text: 'Всем привет!' })
    chatClient.emit('message',    { author: 'Боб',   text: 'Привет, Алиса!' })
    chatClient.emit('user:leave', { name: 'Алиса' })

    Задание

    Реализуй класс EventEmitter (паттерн Observer — основа WebSocket-клиентов): on(event, handler), off(event, handler), emit(event, data). Дополнительно реализуй метод once(event, handler) — обработчик вызывается только один раз, затем автоматически удаляется.

    Подсказка

    on: if (!this._events.has(event)) this._events.set(event, []); this._events.get(event).push(handler); return this. off: используй filter. once: const wrapper = (data) => { handler(data); this.off(event, wrapper) }; return this.on(event, wrapper).

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