跳至正文

🔌 什么是WebSocket?

📖 定义

WebSocket是一种实现客户端和服务器之间双向实时通信的协议。与HTTP不同,一旦建立连接就会持续保持,服务器可以主动向客户端发送数据(push)。用于聊天、实时通知、游戏、协作工具等。

🎯 用比喻理解

电话 vs 邮件

HTTP (邮件)
├─ 寄信 (请求)
├─ 收到回信 (响应)
├─ 再写信 (请求)
└─ 每次都需要新信件 (开销)

WebSocket (电话)
├─ 一次接通电话
├─ 可以持续对话
├─ 双方都可以先说话
└─ 挂断前保持连接

快递 vs 直通管道

HTTP
┌─────────┐ 请求 ┌─────────┐
│ 客户端 │ ──────────→ │ 服务器 │
└─────────┘ └─────────┘
┌─────────┐ 响应 ┌─────────┐
│ 客户端 │ ←────────── │ 服务器 │
└─────────┘ └─────────┘
每次都是新连接!

WebSocket
┌─────────┐ ┌─────────┐
│ 客户端 │ ←─────────→ │ 服务器 │
└─────────┘ 持久连接 └─────────┘
↕ 双向

⚙️ 工作原理

1. WebSocket连接过程

1. HTTP握手请求
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade

2. 服务器批准
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

3. WebSocket连接建立
↕ 开始双向通信

4. 数据传输
消息 ←→ 消息 ←→ 消息

5. 关闭连接
调用 close()

2. HTTP vs WebSocket

HTTP (单向, 请求-响应)
客户端 → 服务器: 请求
客户端 ← 服务器: 响应
[连接关闭]

WebSocket (双向, 持久)
客户端 ↔ 服务器
- 服务器可以主动发送消息
- 保持连接
- 实时通信

💡 实际例子

基本WebSocket (客户端)

// 在网页浏览器中
const ws = new WebSocket('ws://localhost:8080');

// 连接成功
ws.onopen = () => {
console.log('连接完成!');
ws.send('你好!'); // 发送消息
};

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

// 错误处理
ws.onerror = (error) => {
console.error('错误:', error);
};

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

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

// 关闭连接
ws.close();

基本WebSocket服务器 (Node.js)

const WebSocket = require('ws');

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

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

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

// 欢迎消息
ws.send('欢迎!');

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

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

// 连接关闭
ws.on('close', () => {
console.log('客户端连接已关闭');
});

// 错误处理
ws.on('error', (error) => {
console.error('错误:', error);
});
});

Socket.io (实用聊天应用)

// ============ 服务器 (server.js) ============
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIO(server);

app.use(express.static('public'));

const users = new Map(); // 已连接用户

io.on('connection', (socket) => {
console.log('用户连接:', socket.id);

// 用户进入
socket.on('join', (username) => {
users.set(socket.id, username);

// 进入通知 (向所有人)
io.emit('user-joined', {
username,
userCount: users.size
});

console.log(`${username} 进入 (共 ${users.size} 人)`);
});

// 聊天消息
socket.on('chat-message', (message) => {
const username = users.get(socket.id);

// 向所有人广播
io.emit('chat-message', {
username,
message,
timestamp: new Date()
});
});

// 正在输入
socket.on('typing', () => {
const username = users.get(socket.id);
socket.broadcast.emit('user-typing', username);
});

// 连接关闭
socket.on('disconnect', () => {
const username = users.get(socket.id);
users.delete(socket.id);

// 退出通知
io.emit('user-left', {
username,
userCount: users.size
});

console.log(`${username} 退出 (剩余: ${users.size} 人)`);
});
});

server.listen(3000, () => {
console.log('服务器运行中: http://localhost:3000');
});
<!-- ============ 客户端 (public/index.html) ============ -->
<!DOCTYPE html>
<html>
<head>
<title>实时聊天</title>
<style>
#messages {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 10px;
}
.system {
color: #999;
font-style: italic;
}
</style>
</head>
<body>
<h1>实时聊天</h1>
<div id="messages"></div>
<div id="typing"></div>
<input id="message-input" type="text" placeholder="输入消息...">
<button id="send-btn">发送</button>

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const typingDiv = document.getElementById('typing');

// 输入用户名
const username = prompt('请输入用户名:') || '匿名';
socket.emit('join', username);

// 用户进入通知
socket.on('user-joined', (data) => {
addMessage(`${data.username} 进入了。 (${data.userCount} 人在线)`, 'system');
});

// 用户退出通知
socket.on('user-left', (data) => {
addMessage(`${data.username} 退出了。 (${data.userCount} 人在线)`, 'system');
});

// 接收聊天消息
socket.on('chat-message', (data) => {
const time = new Date(data.timestamp).toLocaleTimeString();
addMessage(`[${time}] ${data.username}: ${data.message}`);
});

// 显示正在输入
socket.on('user-typing', (username) => {
typingDiv.textContent = `${username} 正在输入...`;
setTimeout(() => {
typingDiv.textContent = '';
}, 1000);
});

// 发送消息
function sendMessage() {
const message = messageInput.value.trim();
if (message) {
socket.emit('chat-message', message);
messageInput.value = '';
}
}

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});

// 输入事件
let typingTimeout;
messageInput.addEventListener('input', () => {
clearTimeout(typingTimeout);
socket.emit('typing');
typingTimeout = setTimeout(() => {
// 停止输入
}, 500);
});

// 添加消息
function addMessage(text, className = '') {
const messageEl = document.createElement('div');
messageEl.className = `message ${className}`;
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>

Room功能 (群聊)

// 服务器
io.on('connection', (socket) => {
// 进入房间
socket.on('join-room', (roomId) => {
socket.join(roomId);
console.log(`${socket.id} 进入了房间 ${roomId}`);

// 只向同房间的人发送
socket.to(roomId).emit('user-joined-room', {
userId: socket.id,
roomId
});
});

// 向特定房间发送消息
socket.on('room-message', ({ roomId, message }) => {
// 只向该房间发送
io.to(roomId).emit('room-message', {
userId: socket.id,
message
});
});

// 退出房间
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left-room', socket.id);
});
});

实时通知系统

// 服务器
const notifications = io.of('/notifications');

notifications.on('connection', (socket) => {
console.log('通知连接:', socket.id);

// 用户认证
const userId = socket.handshake.query.userId;

// 进入个人房间
socket.join(`user-${userId}`);

// 向特定用户发送通知
socket.on('send-notification', ({ targetUserId, notification }) => {
notifications.to(`user-${targetUserId}`).emit('notification', notification);
});
});

// 从任何地方发送通知
function sendNotification(userId, notification) {
notifications.to(`user-${userId}`).emit('notification', {
type: notification.type,
message: notification.message,
timestamp: new Date()
});
}

// 例: 新订单通知
app.post('/api/orders', async (req, res) => {
const order = await createOrder(req.body);

// 向卖家发送实时通知
sendNotification(order.sellerId, {
type: 'new-order',
message: `收到新订单!`,
orderId: order.id
});

res.json(order);
});

实时协作 (协同编辑)

// 服务器
io.on('connection', (socket) => {
socket.on('join-document', (docId) => {
socket.join(docId);

// 发送当前文档内容
const document = getDocument(docId);
socket.emit('document-loaded', document);
});

// 文档修改
socket.on('document-change', ({ docId, changes }) => {
// 更新文档
updateDocument(docId, changes);

// 向其他用户发送修改内容
socket.to(docId).emit('document-updated', {
userId: socket.id,
changes
});
});

// 共享光标位置
socket.on('cursor-move', ({ docId, position }) => {
socket.to(docId).emit('cursor-update', {
userId: socket.id,
position
});
});
});

🤔 常见问题

Q1. WebSocket vs HTTP轮询?

A:

// HTTP轮询 (低效)
// 客户端定期向服务器发送请求

setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(messages => {
// 检查新消息
});
}, 1000); // 每秒请求一次

// 问题:
// - 不必要的请求 (即使没有新消息也请求)
// - 延迟时间 (最多1秒)
// - 服务器负载

// WebSocket (高效)
// 实时双向通信

const socket = io();

socket.on('new-message', (message) => {
// 立即收到新消息!
});

// 优点:
// - 立即接收 (无延迟)
// - 服务器推送
// - 高效

Q2. WebSocket总是更好吗?

A:

// ✅ WebSocket适合的情况
1. 实时聊天
2. 实时通知
3. 协作工具 (协同编辑)
4. 实时游戏
5. 股票信息 (实时数据)
6. IoT设备控制

// ❌ WebSocket不需要的情况
1. 一般网站 (博客、新闻)
2. REST API (CRUD)
3. 静态内容
4. 不需要实时更新

// HTTP更好的情况:
// - 简单的请求-响应
// - 需要缓存
// - RESTful设计

Q3. Socket.io vs 原生WebSocket?

A:

// 原生WebSocket
const ws = new WebSocket('ws://localhost:8080');
ws.send('Hello');

// 优点: 轻量、快速
// 缺点: 没有额外功能、没有降级方案

// Socket.io
const socket = io();
socket.emit('message', 'Hello');

// 优点:
// 1. 自动重连
// 2. Room/Namespace 支持
// 3. 降级方案 (WebSocket不可用时使用Polling)
// 4. 基于事件
// 5. 二进制支持

// 缺点:
// 1. 体积大 (库大小)
// 2. 与原生WebSocket不兼容

// 选择标准:
// 简单项目 → 原生WebSocket
// 复杂项目 → Socket.io

Q4. WebSocket安全性?

A:

// 1. 使用 wss:// (WebSocket Secure)
// 就像 HTTP → HTTPS
// ws:// → wss://

const socket = new WebSocket('wss://example.com'); // ✅ 加密
const socket = new WebSocket('ws://example.com'); // ❌ 明文

// 2. 认证
// Socket.io
io.use((socket, next) => {
const token = socket.handshake.auth.token;

if (isValidToken(token)) {
next();
} else {
next(new Error('认证失败'));
}
});

// 客户端
const socket = io({
auth: {
token: 'user-token'
}
});

// 3. 权限验证
socket.on('delete-message', (messageId) => {
const userId = socket.userId;

if (canDeleteMessage(userId, messageId)) {
deleteMessage(messageId);
} else {
socket.emit('error', '没有权限');
}
});

// 4. 速率限制
const rateLimit = require('socket.io-rate-limit');

io.use(rateLimit({
max: 10, // 最多10个
interval: 1000 // 每秒
}));

// 5. 输入验证
socket.on('message', (message) => {
// 防止XSS
const sanitized = sanitizeHtml(message);

if (sanitized.length > 1000) {
return socket.emit('error', '消息太长');
}

broadcast(sanitized);
});

Q5. WebSocket连接管理?

A:

// 1. 自动重连 (Socket.io)
const socket = io({
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});

socket.on('disconnect', () => {
console.log('连接断开');
});

socket.on('reconnect', () => {
console.log('重连成功');
});

// 2. 心跳 (连接检查)
// 服务器
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 无响应时关闭
}

ws.isAlive = false;
ws.ping(); // 发送ping
});
}, 30000);

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

// 3. 内存管理
const MAX_CONNECTIONS = 1000;

wss.on('connection', (ws) => {
if (wss.clients.size > MAX_CONNECTIONS) {
ws.close(1008, '服务器已满');
return;
}

// 处理连接...
});

// 4. 优雅关闭
process.on('SIGTERM', () => {
console.log('服务器正在关闭...');

// 向所有连接发送关闭通知
wss.clients.forEach((ws) => {
ws.send(JSON.stringify({ type: 'server-shutdown' }));
ws.close();
});

wss.close(() => {
console.log('WebSocket服务器已关闭');
process.exit(0);
});
});

🎓 下一步

如果你理解了WebSocket,接下来可以学习:

  1. 什么是Node.js? - 构建WebSocket服务器
  2. 什么是React? - 实现实时UI
  3. 什么是Docker? (待创建文档) - 部署WebSocket服务器

实践练习

# 创建Socket.io聊天应用

# 1. 项目初始化
mkdir chat-app
cd chat-app
npm init -y

# 2. 安装包
npm install express socket.io

# 3. 创建服务器 (参考上面的例子)
# server.js

# 4. 运行
node server.js

# 5. 访问 http://localhost:3000

🎬 总结

WebSocket是实时Web的核心技术:

  • 双向通信: 服务器 ↔ 客户端
  • 持久连接: 一次连接,持续使用
  • 实时: 即时数据传输
  • 高效: 比轮询高效得多

如果需要实时功能,就使用WebSocket! 🔌✨