WebSocket 实时通信

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在实时应用中(如聊天、游戏、股票行情),WebSocket 是关键技术。本章将讲解 WebSocket 协议的基础,并在 Node.js 中使用 ws 库实现实时通信。

1. 什么是 WebSocket?

WebSocket 是一种网络传输协议,在 2011 年成为国际标准(RFC 6455)。它设计用于在客户端和服务器之间建立持久连接,双方可以随时开始发送数据。与 HTTP 不同,WebSocket 连接一旦建立,就一直保持,直到客户端或服务器主动关闭。这使得服务器可以主动向客户端推送消息,而不需要客户端频繁轮询。

WebSocket 协议使用 ws://(非加密)和 wss://(加密)作为 URI 方案。

1.1 WebSocket 与 HTTP 的对比

特性 HTTP WebSocket
通信模式 客户端主动请求,服务器响应 全双工,双方均可主动发送
连接生命周期 短连接,请求结束后断开 长连接,一直保持直到关闭
协议开销 每次请求都携带头部,开销大 建立连接后头部很小,适合高频通信
适用场景 请求-响应模式,如 REST API 实时推送、聊天、游戏、股票行情

2. WebSocket 握手

WebSocket 连接通过 HTTP 升级机制建立。客户端发送一个特殊的 HTTP 请求,带有 Upgrade: websocket 头,服务器如果支持,则返回 101 状态码(Switching Protocols),之后连接协议从 HTTP 切换到 WebSocket。

握手请求示例:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务器响应示例:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

3. Node.js 中的 WebSocket 库

Node.js 生态中有多个 WebSocket 库,最常用的是 ws,它是一个轻量级、高性能的 WebSocket 实现,兼容浏览器。其他选择包括 socket.io(封装了 WebSocket 并提供降级方案)和 websocket(更完整的实现)。

本章使用 ws,因为它简单且性能好。安装:

npm install ws

4. 创建 WebSocket 服务器

创建一个 WebSocket 服务器很简单。可以与 HTTP 服务器共享端口,也可以独立运行。

4.1 独立 WebSocket 服务器

const WebSocket = require('ws');

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

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

  ws.on('message', (message) => {
    console.log('收到消息: %s', message);
    // 发送回执
    ws.send(`服务器收到: ${message}`);
  });

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

  ws.send('欢迎连接到 WebSocket 服务器');
});

console.log('WebSocket 服务器运行在 ws://localhost:8080');

4.2 与 HTTP 服务器共享端口

许多应用需要同时提供 HTTP API 和 WebSocket,可以共享同一个端口:

const http = require('http');
const WebSocket = require('ws');
const express = require('express');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.get('/', (req, res) => {
  res.send('Hello World');
});

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    console.log('received: %s', message);
  });
  ws.send('something');
});

server.listen(3000, () => {
  console.log('HTTP + WebSocket 服务器运行在 http://localhost:3000');
});

5. WebSocket 客户端

浏览器原生支持 WebSocket,使用 WebSocket 构造函数。Node.js 中也可以使用 ws 库创建客户端。

5.1 浏览器客户端

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

socket.onopen = (event) => {
  console.log('连接已打开');
  socket.send('Hello Server!');
};

socket.onmessage = (event) => {
  console.log('收到消息: ' + event.data);
};

socket.onclose = (event) => {
  console.log('连接关闭');
};

socket.onerror = (error) => {
  console.error('WebSocket 错误: ', error);
};

5.2 Node.js 客户端

const WebSocket = require('ws');

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

ws.on('open', () => {
  console.log('连接成功');
  ws.send('Hello Server!');
});

ws.on('message', (data) => {
  console.log('收到消息: %s', data);
});

6. 广播消息

在聊天室等场景中,需要向所有连接或特定客户端发送消息。以下是一个广播示例:

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    // 广播给所有客户端
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message.toString());
      }
    });
  });
});

wss.clients 是一个 Set,包含所有已连接的客户端。需要注意检查 readyState 确保连接还开着。

7. 简单聊天室示例

下面构建一个简单的聊天室,服务器广播所有消息,客户端在浏览器中显示。

7.1 服务器端(server.js)

const WebSocket = require('ws');
const http = require('http');
const express = require('express');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// 提供静态页面(可选)
app.use(express.static('public'));

wss.on('connection', (ws) => {
  console.log('新用户加入');

  // 广播加入消息
  broadcast(JSON.stringify({ type: 'system', message: '新用户加入聊天室' }));

  ws.on('message', (message) => {
    // 将收到的消息广播给所有人
    broadcast(JSON.stringify({ type: 'chat', message: message.toString() }), ws);
  });

  ws.on('close', () => {
    console.log('用户离开');
    broadcast(JSON.stringify({ type: 'system', message: '用户离开聊天室' }));
  });
});

function broadcast(data, sender) {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(data);
    }
  });
}

server.listen(3000, () => {
  console.log('服务器启动在 http://localhost:3000');
});

7.2 客户端页面(public/index.html)

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket 聊天室</title>
</head>
<body>
    <h1>聊天室</h1>
    <div id="messages"></div>
    <input type="text" id="messageInput" placeholder="输入消息">
    <button onclick="sendMessage()">发送</button>

    <script>
        const ws = new WebSocket('ws://localhost:3000');
        const messagesDiv = document.getElementById('messages');
        const input = document.getElementById('messageInput');

        ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            const p = document.createElement('p');
            if (data.type === 'system') {
                p.style.color = 'gray';
                p.textContent = data.message;
            } else {
                p.textContent = data.message;
            }
            messagesDiv.appendChild(p);
        };

        function sendMessage() {
            const msg = input.value.trim();
            if (msg) {
                ws.send(msg);
                input.value = '';
            }
        }

        input.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') sendMessage();
        });
    </script>
</body>
</html>

8. 处理二进制数据

WebSocket 不仅可以发送文本,还可以发送二进制数据(如 ArrayBufferBuffer)。在 ws 中,如果接收到二进制数据,默认是 Buffer 对象。

ws.on('message', (data) => {
  if (data instanceof Buffer) {
    console.log('收到二进制数据,长度: %d', data.length);
  } else {
    console.log('收到文本: %s', data);
  }
});

发送二进制:

const buffer = Buffer.from('hello', 'utf8');
ws.send(buffer);

9. 心跳机制

由于网络不稳定,WebSocket 连接可能意外断开。为了及时发现死连接,通常需要实现心跳(ping-pong)机制。一些 WebSocket 服务器会自动处理,但也可以手动实现:

const WebSocket = require('ws');

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

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', heartbeat);
});

const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

wss.on('close', () => {
  clearInterval(interval);
});

10. 安全性考虑

  • 使用 wss://:在生产环境必须使用加密的 WebSocket(wss://),避免数据泄露。
  • 身份验证:可以在握手阶段携带 token(如通过查询参数或自定义头),并在 connection 事件中验证。例如:
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws, req) => {
  const token = new URL(req.url, 'http://localhost').searchParams.get('token');
  if (!isValidToken(token)) {
    ws.close(1008, 'unauthorized');
    return;
  }
  // 继续处理
});
  • 限制消息大小:防止恶意发送超大数据包导致内存耗尽。
  • 源验证:检查 Origin 头,只允许特定源连接。

11. 与 socket.io 的区别

socket.io 是一个封装了 WebSocket 的库,提供了自动重连、命名空间、房间、降级方案(轮询)等高级功能。如果你需要更丰富的特性,可以使用 socket.io。而 ws 更轻量,适合对性能和简单性有要求的场景。

小结

WebSocket 是实现实时通信的强大工具。本章介绍了 WebSocket 协议基础、在 Node.js 中使用 ws 库创建服务器和客户端、广播消息、构建聊天室示例,以及安全考虑。掌握这些知识后,你可以构建实时应用如聊天系统、游戏、股票行情推送等。下一章我们将讨论 Node.js 的性能优化策略。