Node.js WebSocket 通信
章节概述
在传统的 Web 应用中,客户端和服务器之间的通信主要基于 HTTP 请求-响应模式,这种模式在需要实时数据更新的场景下存在局限性。WebSocket 协议的出现解决了这个问题,它提供了一种全双工的通信通道,允许服务器主动向客户端推送数据。本集将介绍如何在 Node.js 中实现 WebSocket 通信,以及如何使用 socket.io 库简化 WebSocket 应用的开发。
核心知识点讲解
1. WebSocket 协议概述
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它的主要特点包括:
- 全双工通信:客户端和服务器可以同时发送和接收数据
- 持久性连接:连接建立后保持打开状态,避免了 HTTP 的重复连接开销
- 低延迟:数据传输延迟低,适合实时应用
- 基于 TCP:使用 TCP 作为传输层协议,保证了数据传输的可靠性
- 与 HTTP 兼容:WebSocket 连接的建立基于 HTTP 握手
2. WebSocket 连接的建立
WebSocket 连接的建立过程如下:
- 客户端发送一个 HTTP 请求,包含特殊的头部信息,表明想要升级到 WebSocket 协议
- 服务器验证请求,如果支持 WebSocket,则返回 101 Switching Protocols 响应
- 连接从 HTTP 协议升级到 WebSocket 协议,此后客户端和服务器可以通过这个连接自由通信
3. 使用 ws 模块实现 WebSocket 服务器
在 Node.js 中,可以使用 ws 模块来实现 WebSocket 服务器。首先需要安装 ws 模块:
npm install ws3.1 创建 WebSocket 服务器
const WebSocket = require('ws');
// 创建 WebSocket 服务器,监听端口 8080
const wss = new WebSocket.Server({ port: 8080 });
// 处理新连接
wss.on('connection', (ws) => {
console.log('新客户端已连接');
// 向客户端发送欢迎消息
ws.send('欢迎连接到 WebSocket 服务器!');
// 处理客户端发送的消息
ws.on('message', (message) => {
console.log(`接收到客户端消息: ${message}`);
// 向客户端发送响应
ws.send(`服务器已收到: ${message}`);
});
// 处理连接关闭
ws.on('close', () => {
console.log('客户端已断开连接');
});
// 处理错误
ws.on('error', (err) => {
console.error('连接错误:', err);
});
});
console.log('WebSocket 服务器已启动,监听端口 8080');3.2 创建 WebSocket 客户端
const WebSocket = require('ws');
// 创建 WebSocket 客户端,连接到服务器
const ws = new WebSocket('ws://localhost:8080');
// 处理连接建立
ws.on('open', () => {
console.log('已连接到服务器');
// 向服务器发送消息
ws.send('Hello, WebSocket Server!');
});
// 处理服务器发送的消息
ws.on('message', (message) => {
console.log(`接收到服务器消息: ${message}`);
});
// 处理连接关闭
ws.on('close', () => {
console.log('已断开与服务器的连接');
});
// 处理错误
ws.on('error', (err) => {
console.error('连接错误:', err);
});4. 使用 socket.io 库
socket.io 是一个流行的库,它基于 WebSocket 协议,提供了更多的功能和更好的兼容性。它的主要特点包括:
- 自动降级:当 WebSocket 不可用时,自动降级到其他传输方式(如长轮询)
- 房间支持:可以将客户端分组到不同的房间,实现定向消息广播
- 事件系统:基于事件的通信模型,使代码更加清晰
- 命名空间:支持多个独立的通信通道
首先需要安装 socket.io 模块:
npm install socket.io4.1 创建 socket.io 服务器
const http = require('http');
const socketIO = require('socket.io');
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket 服务器运行中\n');
});
// 将 HTTP 服务器包装为 socket.io 服务器
const io = socketIO(server);
// 处理新连接
io.on('connection', (socket) => {
console.log('新客户端已连接,ID:', socket.id);
// 向客户端发送欢迎消息
socket.emit('message', '欢迎连接到 socket.io 服务器!');
// 处理客户端发送的消息
socket.on('chat message', (msg) => {
console.log(`接收到客户端消息: ${msg}`);
// 向所有客户端广播消息
io.emit('chat message', msg);
});
// 处理连接关闭
socket.on('disconnect', () => {
console.log('客户端已断开连接,ID:', socket.id);
});
});
// 启动服务器,监听端口 3000
server.listen(3000, () => {
console.log('服务器已启动,监听端口 3000');
});4.2 创建 socket.io 客户端
<!DOCTYPE html>
<html>
<head>
<title>Socket.io 客户端</title>
<!-- 引入 socket.io 客户端库 -->
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
</head>
<body>
<h1>Socket.io 聊天客户端</h1>
<div id="messages"></div>
<form id="chat-form">
<input id="message-input" type="text" placeholder="输入消息...">
<button type="submit">发送</button>
</form>
<script>
// 连接到 socket.io 服务器
const socket = io('http://localhost:3000');
// 处理连接建立
socket.on('connect', () => {
console.log('已连接到服务器');
});
// 处理服务器发送的消息
socket.on('message', (msg) => {
console.log('接收到服务器消息:', msg);
addMessage('服务器', msg);
});
// 处理广播消息
socket.on('chat message', (msg) => {
console.log('接收到广播消息:', msg);
addMessage('其他用户', msg);
});
// 处理连接关闭
socket.on('disconnect', () => {
console.log('已断开与服务器的连接');
});
// 发送消息
document.getElementById('chat-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('message-input');
const message = input.value;
if (message) {
socket.emit('chat message', message);
addMessage('我', message);
input.value = '';
}
});
// 添加消息到页面
function addMessage(sender, message) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.innerHTML = `<strong>${sender}:</strong> ${message}`;
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>5. socket.io 的高级特性
5.1 房间(Rooms)
房间是 socket.io 中的一个重要概念,它允许将客户端分组,实现定向消息广播:
// 加入房间
io.on('connection', (socket) => {
// 加入名为 'room1' 的房间
socket.join('room1');
console.log('客户端已加入 room1');
// 向房间内的所有客户端发送消息(不包括发送者)
socket.to('room1').emit('message', '新用户加入了房间');
// 向房间内的所有客户端发送消息(包括发送者)
io.to('room1').emit('message', '房间内的所有用户请注意');
// 离开房间
socket.leave('room1');
});5.2 命名空间(Namespaces)
命名空间允许在同一个服务器上创建多个独立的通信通道:
// 创建命名空间
const nsp = io.of('/chat');
// 处理命名空间中的连接
nsp.on('connection', (socket) => {
console.log('客户端已连接到 /chat 命名空间');
// 处理消息
socket.on('message', (msg) => {
console.log('接收到消息:', msg);
nsp.emit('message', msg);
});
});
// 客户端连接到命名空间
// const socket = io('http://localhost:3000/chat');实用案例分析
案例:实时聊天应用
下面我们将使用 socket.io 实现一个完整的实时聊天应用,支持多个用户同时聊天。
服务器端代码
const http = require('http');
const express = require('express');
const socketIO = require('socket.io');
// 创建 Express 应用
const app = express();
// 提供静态文件
app.use(express.static('public'));
// 创建 HTTP 服务器
const server = http.createServer(app);
// 将 HTTP 服务器包装为 socket.io 服务器
const io = socketIO(server);
// 存储在线用户
const users = [];
// 处理新连接
io.on('connection', (socket) => {
console.log('新客户端已连接,ID:', socket.id);
// 处理用户登录
socket.on('login', (username) => {
// 存储用户信息
users.push({ id: socket.id, username });
console.log(`${username} 已登录`);
// 向所有客户端广播用户登录消息
io.emit('user joined', username);
// 向所有客户端广播在线用户列表
io.emit('users list', users.map(user => user.username));
// 向当前客户端发送欢迎消息
socket.emit('message', { sender: '系统', text: `欢迎 ${username} 加入聊天室!` });
});
// 处理客户端发送的消息
socket.on('chat message', (message) => {
// 查找发送者
const user = users.find(u => u.id === socket.id);
if (user) {
console.log(`${user.username}: ${message}`);
// 向所有客户端广播消息
io.emit('chat message', { sender: user.username, text: message });
}
});
// 处理连接关闭
socket.on('disconnect', () => {
// 查找并移除用户
const index = users.findIndex(u => u.id === socket.id);
if (index !== -1) {
const username = users[index].username;
users.splice(index, 1);
console.log(`${username} 已断开连接`);
// 向所有客户端广播用户离开消息
io.emit('user left', username);
// 向所有客户端广播更新后的在线用户列表
io.emit('users list', users.map(user => user.username));
}
});
});
// 启动服务器,监听端口 3000
server.listen(3000, () => {
console.log('服务器已启动,监听端口 3000');
console.log('访问 http://localhost:3000 开始聊天');
});客户端代码(public/index.html)
<!DOCTYPE html>
<html>
<head>
<title>实时聊天应用</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f0f0f0;
}
.container {
max-width: 800px;
margin: 0 auto;
background-color: white;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background-color: #4CAF50;
color: white;
padding: 15px;
text-align: center;
}
.users-list {
background-color: #f9f9f9;
padding: 10px;
border-bottom: 1px solid #ddd;
}
.messages {
padding: 20px;
height: 400px;
overflow-y: auto;
border-bottom: 1px solid #ddd;
}
.message {
margin-bottom: 15px;
padding: 10px;
border-radius: 8px;
max-width: 70%;
}
.message.own {
background-color: #e3f2fd;
margin-left: auto;
}
.message.other {
background-color: #f1f1f1;
}
.message.system {
background-color: #fff3cd;
max-width: 100%;
text-align: center;
}
.message .sender {
font-weight: bold;
margin-bottom: 5px;
font-size: 12px;
}
.message .text {
word-break: break-word;
}
.chat-form {
padding: 15px;
display: flex;
}
.chat-form input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 10px;
}
.chat-form button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.chat-form button:hover {
background-color: #45a049;
}
.login-form {
padding: 40px;
text-align: center;
}
.login-form input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 10px;
}
.login-form button {
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>实时聊天应用</h1>
</div>
<div class="users-list">
<strong>在线用户:</strong> <span id="users-list">加载中...</span>
</div>
<div id="login-form" class="login-form">
<h2>请输入用户名登录</h2>
<input type="text" id="username" placeholder="用户名">
<button id="login-btn">登录</button>
</div>
<div id="chat-container" style="display: none;">
<div class="messages" id="messages"></div>
<form class="chat-form" id="chat-form">
<input type="text" id="message-input" placeholder="输入消息...">
<button type="submit">发送</button>
</form>
</div>
</div>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<script>
// 连接到 socket.io 服务器
const socket = io();
let username;
// 处理登录
document.getElementById('login-btn').addEventListener('click', () => {
username = document.getElementById('username').value.trim();
if (username) {
// 发送登录事件
socket.emit('login', username);
// 隐藏登录表单,显示聊天界面
document.getElementById('login-form').style.display = 'none';
document.getElementById('chat-container').style.display = 'block';
}
});
// 处理按 Enter 键登录
document.getElementById('username').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('login-btn').click();
}
});
// 发送消息
document.getElementById('chat-form').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('message-input');
const message = input.value.trim();
if (message) {
socket.emit('chat message', message);
input.value = '';
}
});
// 处理服务器发送的消息
socket.on('message', (msg) => {
addMessage(msg.sender, msg.text, msg.sender === '系统' ? 'system' : msg.sender === username ? 'own' : 'other');
});
// 处理聊天消息
socket.on('chat message', (msg) => {
addMessage(msg.sender, msg.text, msg.sender === username ? 'own' : 'other');
});
// 处理用户加入
socket.on('user joined', (user) => {
addMessage('系统', `${user} 加入了聊天室`, 'system');
});
// 处理用户离开
socket.on('user left', (user) => {
addMessage('系统', `${user} 离开了聊天室`, 'system');
});
// 处理在线用户列表更新
socket.on('users list', (users) => {
document.getElementById('users-list').textContent = users.join(', ');
});
// 添加消息到页面
function addMessage(sender, text, type) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.className = `message ${type}`;
if (type !== 'system') {
messageElement.innerHTML = `
<div class="sender">${sender}</div>
<div class="text">${text}</div>
`;
} else {
messageElement.innerHTML = `<div class="text">${text}</div>`;
}
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>代码解析:
服务器端:
- 使用 Express 提供静态文件服务
- 使用 socket.io 创建 WebSocket 服务器
- 存储在线用户信息
- 处理用户登录、消息发送和断开连接等事件
- 向客户端广播用户状态变化和消息
客户端:
- 提供登录界面,让用户输入用户名
- 连接到 socket.io 服务器
- 处理服务器发送的消息和事件
- 提供聊天界面,显示消息和在线用户列表
- 允许用户发送消息
运行方法:
- 安装依赖:
npm install express socket.io - 创建
public目录,将客户端代码保存为public/index.html - 将服务器端代码保存为
server.js - 启动服务器:
node server.js - 打开多个浏览器窗口,访问
http://localhost:3000 - 输入不同的用户名登录,开始聊天
WebSocket 与 HTTP 的比较
| 特性 | HTTP | WebSocket |
|---|---|---|
| 连接方式 | 无状态,每次请求都需要建立连接 | 有状态,连接建立后保持打开 |
| 通信方向 | 单向,客户端请求,服务器响应 | 双向,客户端和服务器可以自由通信 |
| 数据格式 | 文本(JSON、XML 等) | 文本或二进制数据 |
| 适用场景 | 传统 Web 应用,请求-响应模式 | 实时应用,如聊天、游戏、实时数据更新 |
| 开销 | 每次请求都有 HTTP 头部开销 | 只有建立连接时有开销,后续通信开销小 |
学习目标
通过本集的学习,你应该能够:
- 理解 WebSocket 协议的基本概念和工作原理
- 掌握使用 ws 模块创建 WebSocket 服务器的方法
- 掌握使用 socket.io 库实现实时通信的方法
- 理解 socket.io 中的房间和命名空间概念
- 实现一个完整的实时聊天应用
- 了解 WebSocket 与 HTTP 的区别和适用场景
小结
WebSocket 协议为 Web 应用提供了实时双向通信的能力,弥补了 HTTP 请求-响应模式的不足。在 Node.js 中,可以使用 ws 模块或更高级的 socket.io 库来实现 WebSocket 服务器。socket.io 库不仅简化了 WebSocket 应用的开发,还提供了房间、命名空间等高级特性,以及在 WebSocket 不可用时的自动降级机制。
通过本集的学习,你已经掌握了 WebSocket 通信的核心概念和实现方法,并通过一个完整的实时聊天应用案例展示了 WebSocket 的实际应用。WebSocket 在需要实时数据更新的场景中非常有用,如聊天应用、在线游戏、实时协作工具等。
在下一集中,我们将学习 Node.js MongoDB 连接,了解如何在 Node.js 应用中使用 MongoDB 数据库。