Skip to content

WebSocket

什么是 WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务端主动向客户端推送数据,实现真正的双向实时通信。

通俗解释

传统 HTTP 就像发短信:你发一条,对方回一条,每次都是独立的。

WebSocket 就像打电话:连接建立后,双方可以随时说话,不需要每次都重新拨号。

WebSocket vs HTTP

特性HTTPWebSocket
通信方式请求-响应(单向)全双工(双向)
连接短连接(用完即关)长连接(持续保持)
开销每次请求都有完整头部建立后数据帧很小
服务端推送不支持(需轮询)原生支持
协议http:// / https://ws:// / wss://

基本使用

客户端

javascript
// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8080')

// 连接成功
ws.onopen = () => {
  console.log('连接成功')
  ws.send('Hello Server!')
}

// 收到消息
ws.onmessage = (event) => {
  console.log('收到消息:', event.data)

  // 如果是 JSON 数据
  const data = JSON.parse(event.data)
  console.log(data)
}

// 连接关闭
ws.onclose = (event) => {
  console.log('连接关闭', event.code, event.reason)
}

// 连接错误
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error)
}

// 发送消息
ws.send('Hello')
ws.send(JSON.stringify({ type: 'chat', content: 'Hi' }))

// 主动关闭连接
ws.close()

服务端(Node.js)

javascript
const WebSocket = require('ws')

const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', (ws) => {
  console.log('客户端已连接')

  // 收到消息
  ws.on('message', (message) => {
    console.log('收到:', message.toString())

    // 回复消息
    ws.send('服务端收到: ' + message)

    // 广播给所有客户端
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message.toString())
      }
    })
  })

  ws.on('close', () => {
    console.log('客户端断开连接')
  })
})

连接状态

WebSocket 实例有 readyState 属性表示当前状态:

javascript
const ws = new WebSocket('ws://localhost:8080')

// 0: CONNECTING - 正在连接
// 1: OPEN - 已连接,可以通信
// 2: CLOSING - 正在关闭
// 3: CLOSED - 已关闭

if (ws.readyState === WebSocket.OPEN) {
  ws.send('消息')
}

心跳保活机制

长连接可能因为网络问题断开,需要心跳机制来检测和保活:

javascript
class WebSocketClient {
  constructor(url) {
    this.url = url
    this.ws = null
    this.heartbeatTimer = null
    this.reconnectTimer = null
    this.reconnectCount = 0
    this.maxReconnect = 5
  }

  connect() {
    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      console.log('连接成功')
      this.reconnectCount = 0
      this.startHeartbeat()
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'pong') {
        console.log('收到心跳响应')
        return
      }
      // 处理其他消息
      this.onMessage(data)
    }

    this.ws.onclose = () => {
      console.log('连接关闭')
      this.stopHeartbeat()
      this.reconnect()
    }

    this.ws.onerror = (error) => {
      console.error('连接错误', error)
    }
  }

  // 心跳检测
  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }))
      }
    }, 30000) // 每 30 秒发送一次心跳
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  // 断线重连
  reconnect() {
    if (this.reconnectCount >= this.maxReconnect) {
      console.log('达到最大重连次数')
      return
    }

    this.reconnectCount++
    console.log(`第 ${this.reconnectCount} 次重连...`)

    this.reconnectTimer = setTimeout(() => {
      this.connect()
    }, 3000) // 3 秒后重连
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
    }
  }

  close() {
    this.stopHeartbeat()
    clearTimeout(this.reconnectTimer)
    this.ws.close()
  }

  onMessage(data) {
    // 子类重写或外部设置
    console.log('收到消息:', data)
  }
}

// 使用
const client = new WebSocketClient('ws://localhost:8080')
client.connect()

React 中使用 WebSocket

jsx
import { useEffect, useRef, useState, useCallback } from 'react'

function useWebSocket(url) {
  const [isConnected, setIsConnected] = useState(false)
  const [lastMessage, setLastMessage] = useState(null)
  const wsRef = useRef(null)

  useEffect(() => {
    const ws = new WebSocket(url)
    wsRef.current = ws

    ws.onopen = () => {
      setIsConnected(true)
    }

    ws.onmessage = (event) => {
      setLastMessage(JSON.parse(event.data))
    }

    ws.onclose = () => {
      setIsConnected(false)
    }

    return () => {
      ws.close()
    }
  }, [url])

  const sendMessage = useCallback((data) => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify(data))
    }
  }, [])

  return { isConnected, lastMessage, sendMessage }
}

// 使用
function ChatRoom() {
  const { isConnected, lastMessage, sendMessage } = useWebSocket('ws://localhost:8080')
  const [messages, setMessages] = useState([])

  useEffect(() => {
    if (lastMessage) {
      setMessages(prev => [...prev, lastMessage])
    }
  }, [lastMessage])

  return (
    <div>
      <p>状态: {isConnected ? '已连接' : '未连接'}</p>
      <button onClick={() => sendMessage({ text: 'Hello' })}>
        发送消息
      </button>
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>{msg.text}</li>
        ))}
      </ul>
    </div>
  )
}

适用场景

WebSocket 适合以下场景:

场景说明
即时聊天多人聊天室、客服系统
实时协作在线文档协同编辑
游戏多人在线游戏
实时数据股票行情、体育比分
消息推送系统通知、订单状态更新
物联网设备状态监控

WebSocket 握手过程

WebSocket 建立连接时,先通过 HTTP 进行握手升级:

// 客户端请求
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

// 服务端响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手成功后,连接从 HTTP 协议升级为 WebSocket 协议。

常见面试题

1. WebSocket 和 HTTP 的区别?

  • HTTP 是无状态的请求-响应模式,WebSocket 是有状态的全双工通信
  • HTTP 每次通信都需要完整的请求头,WebSocket 建立连接后数据帧很小
  • HTTP 不支持服务端主动推送,WebSocket 支持双向通信

2. WebSocket 如何保持连接?

  • 心跳机制:定期发送 ping/pong 帧
  • 服务端和客户端都可以发送心跳
  • 超时未收到响应则认为连接断开,触发重连

3. WebSocket 断线重连怎么实现?

javascript
function createWebSocket(url) {
  let ws = new WebSocket(url)

  ws.onclose = () => {
    // 延迟重连,避免频繁重连
    setTimeout(() => {
      ws = createWebSocket(url)
    }, 3000)
  }

  return ws
}

4. WebSocket 和 Socket 的区别?

  • Socket 是 TCP/IP 协议的抽象,是传输层的概念
  • WebSocket 是应用层协议,基于 TCP,专为浏览器设计
  • WebSocket 需要先通过 HTTP 握手,然后升级协议

5. 如何处理 WebSocket 的安全问题?

  • 使用 wss:// 协议(WebSocket over TLS)
  • 验证 Origin 头,防止跨站 WebSocket 劫持
  • 对消息进行身份验证(如 token)
  • 限制消息大小和频率