← React/WebSockets и реальное время в React#295 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

WebSockets и реальное время в React

Что такое WebSocket

WebSocket — протокол двусторонней связи между браузером и сервером. В отличие от HTTP (запрос → ответ → закрытие), WebSocket держит соединение открытым и позволяет серверу слать данные без запроса клиента.

Когда нужен WebSocket:

  • Чат, мессенджер
  • Онлайн-игры
  • Live-обновления (курсы акций, спорт)
  • Совместное редактирование (Google Docs)
  • Уведомления в реальном времени
  • Альтернативы:

  • Long Polling — клиент постоянно делает HTTP-запросы (устаревший подход)
  • Server-Sent Events (SSE) — односторонний поток от сервера (подходит для уведомлений)
  • WebSocket — двусторонний, полный контроль
  • WebSocket API

    // Базовое использование
    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()

    useWebSocket хук

    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

    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

    При разрыве соединения важно не атаковать сервер частыми попытками:

    // Экспоненциальный 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 автоматически переподключится

    WebSockets и реальное время в React

    Что такое WebSocket

    WebSocket — протокол двусторонней связи между браузером и сервером. В отличие от HTTP (запрос → ответ → закрытие), WebSocket держит соединение открытым и позволяет серверу слать данные без запроса клиента.

    Когда нужен WebSocket:

  • Чат, мессенджер
  • Онлайн-игры
  • Live-обновления (курсы акций, спорт)
  • Совместное редактирование (Google Docs)
  • Уведомления в реальном времени
  • Альтернативы:

  • Long Polling — клиент постоянно делает HTTP-запросы (устаревший подход)
  • Server-Sent Events (SSE) — односторонний поток от сервера (подходит для уведомлений)
  • WebSocket — двусторонний, полный контроль
  • WebSocket API

    // Базовое использование
    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()

    useWebSocket хук

    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

    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

    При разрыве соединения важно не атаковать сервер частыми попытками:

    // Экспоненциальный 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"}.

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