跳到主要内容

实战:构建实时聊天应用

本章通过一个完整的实时聊天应用案例,演示如何将前面学到的 WebSocket 知识应用到实际项目中。我们将构建一个支持多房间、用户认证、消息历史的聊天系统。

需求分析

功能需求

一个完整的聊天应用需要以下核心功能:

用户管理

  • 用户登录/登出
  • 在线状态显示
  • 用户昵称设置

消息功能

  • 发送/接收文本消息
  • 消息时间戳
  • 消息历史记录(最近 100 条)
  • 消息已读状态

房间管理

  • 创建/加入/离开房间
  • 房间列表
  • 房间在线人数

系统功能

  • 系统通知(用户加入/离开)
  • 连接状态管理
  • 断线自动重连

技术选型

层级技术选择理由
服务端Node.js + ws高性能、事件驱动、易于扩展
认证JWT无状态、易于跨服务共享
消息存储Redis快速读写、支持发布订阅
持久化MySQL关系型数据、支持复杂查询
前端原生 JavaScript无需框架依赖、学习成本低

架构设计

系统架构

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Browser │ │ Browser │ │ Browser │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
│ WebSocket
┌──────▼──────┐
│ Nginx/LB │
└──────┬──────┘

┌───────────────────┼───────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ WS Server │ │ WS Server │ │ WS Server │
│ (Node.js) │ │ (Node.js) │ │ (Node.js) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘

┌──────▼──────┐
│ Redis │
│ (Pub/Sub) │
└──────┬──────┘

┌──────▼──────┐
│ MySQL │
│ (Persistent)│
└─────────────┘

消息流程

当用户发送消息时,数据流如下:

  1. 客户端通过 WebSocket 发送消息到服务器
  2. 服务器验证用户身份和房间权限
  3. 服务器将消息存储到 MySQL
  4. 服务器通过 Redis Pub/Sub 广播到其他服务器实例
  5. 所有服务器实例将消息推送给房间内的在线用户

数据结构设计

用户表 (users)

CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

房间表 (rooms)

CREATE TABLE rooms (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
created_by INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);

消息表 (messages)

CREATE TABLE messages (
id INT PRIMARY KEY AUTO_INCREMENT,
room_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES rooms(id),
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_room_created (room_id, created_at)
);

服务端实现

项目结构

chat-server/
├── src/
│ ├── server.js # 入口文件
│ ├── websocket/
│ │ ├── index.js # WebSocket 服务器
│ │ ├── connection.js # 连接管理
│ │ └── handler.js # 消息处理
│ ├── services/
│ │ ├── auth.js # 认证服务
│ │ ├── room.js # 房间服务
│ │ └── message.js # 消息服务
│ ├── models/
│ │ ├── user.js
│ │ ├── room.js
│ │ └── message.js
│ └── utils/
│ ├── jwt.js
│ └── logger.js
├── package.json
└── ecosystem.config.js # PM2 配置

核心代码实现

服务器入口 (server.js)

const http = require('http');
const express = require('express');
const WebSocket = require('ws');
const redis = require('redis');
const mysql = require('mysql2/promise');

// 创建 Express 应用
const app = express();
app.use(express.json());

// 数据库连接池
const dbPool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'chat',
waitForConnections: true,
connectionLimit: 10
});

// Redis 客户端
const redisClient = redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
const redisPublisher = redisClient.duplicate();
const redisSubscriber = redisClient.duplicate();

// WebSocket 服务器
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// 连接管理器
const connections = new Map();

// 房间管理
const rooms = new Map();

// 心跳检测
function heartbeat() {
this.isAlive = true;
}

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

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

// WebSocket 连接处理
wss.on('connection', (ws, request) => {
ws.isAlive = true;
ws.on('pong', heartbeat);

// 从 URL 参数获取 token
const url = new URL(request.url, `http://${request.headers.host}`);
const token = url.searchParams.get('token');

// 验证 token
let user;
try {
user = verifyToken(token);
ws.userId = user.id;
ws.username = user.username;
} catch (error) {
ws.close(1008, 'Authentication failed');
return;
}

// 存储连接
connections.set(ws.userId, ws);

// 发送欢迎消息
ws.send(JSON.stringify({
type: 'connected',
data: {
userId: ws.userId,
username: ws.username,
rooms: getAvailableRooms()
}
}));

// 消息处理
ws.on('message', async (data) => {
try {
const message = JSON.parse(data);
await handleMessage(ws, message);
} catch (error) {
console.error('处理消息错误:', error);
ws.send(JSON.stringify({
type: 'error',
data: { message: error.message }
}));
}
});

// 连接关闭
ws.on('close', () => {
handleDisconnect(ws);
connections.delete(ws.userId);
});
});

// 消息路由
async function handleMessage(ws, message) {
const { type, data } = message;

switch (type) {
case 'join_room':
await handleJoinRoom(ws, data);
break;
case 'leave_room':
await handleLeaveRoom(ws, data);
break;
case 'send_message':
await handleSendMessage(ws, data);
break;
case 'get_history':
await handleGetHistory(ws, data);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
}

// 加入房间
async function handleJoinRoom(ws, { roomId }) {
// 检查房间是否存在
const room = await getRoom(roomId);
if (!room) {
throw new Error('Room not found');
}

// 添加用户到房间
if (!rooms.has(roomId)) {
rooms.set(roomId, new Set());
}
rooms.get(roomId).add(ws.userId);

// 订阅 Redis 频道
await redisSubscriber.subscribe(`room:${roomId}`);

// 广播加入消息
broadcastToRoom(roomId, {
type: 'user_joined',
data: {
roomId,
userId: ws.userId,
username: ws.username,
timestamp: Date.now()
}
}, ws.userId);

// 发送房间信息给用户
ws.send(JSON.stringify({
type: 'room_joined',
data: {
roomId,
roomName: room.name,
users: Array.from(rooms.get(roomId)).map(id => ({
userId: id,
username: connections.get(id)?.username
}))
}
}));
}

// 发送消息
async function handleSendMessage(ws, { roomId, content }) {
// 验证用户在房间中
if (!rooms.get(roomId)?.has(ws.userId)) {
throw new Error('Not in room');
}

// 存储消息
const [result] = await dbPool.execute(
'INSERT INTO messages (room_id, user_id, content) VALUES (?, ?, ?)',
[roomId, ws.userId, content]
);

const messageData = {
id: result.insertId,
roomId,
userId: ws.userId,
username: ws.username,
content,
timestamp: Date.now()
};

// 通过 Redis 广播
await redisPublisher.publish(
`room:${roomId}`,
JSON.stringify({
type: 'message',
data: messageData
})
);
}

// Redis 消息处理
redisSubscriber.on('message', (channel, message) => {
const roomId = channel.split(':')[1];
const data = JSON.parse(message);
broadcastToRoom(roomId, data);
});

// 广播到房间
function broadcastToRoom(roomId, message, excludeUserId = null) {
const userIds = rooms.get(roomId);
if (!userIds) return;

const data = JSON.stringify(message);

userIds.forEach(userId => {
if (userId !== excludeUserId) {
const ws = connections.get(userId);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
}
});
}

// 断开连接处理
function handleDisconnect(ws) {
// 从所有房间移除用户
rooms.forEach((userIds, roomId) => {
if (userIds.has(ws.userId)) {
userIds.delete(ws.userId);
broadcastToRoom(roomId, {
type: 'user_left',
data: {
roomId,
userId: ws.userId,
username: ws.username,
timestamp: Date.now()
}
});
}
});
}

// 获取可用房间列表
async function getAvailableRooms() {
const [rows] = await dbPool.execute(
'SELECT id, name FROM rooms ORDER BY created_at DESC LIMIT 50'
);
return rows;
}

// 获取房间信息
async function getRoom(roomId) {
const [rows] = await dbPool.execute(
'SELECT * FROM rooms WHERE id = ?',
[roomId]
);
return rows[0];
}

// JWT 验证
function verifyToken(token) {
const jwt = require('jsonwebtoken');
return jwt.verify(token, process.env.JWT_SECRET || 'secret');
}

// HTTP API: 登录
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;

// 验证用户
const [rows] = await dbPool.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);

const user = rows[0];
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}

// 验证密码
const bcrypt = require('bcrypt');
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return res.status(401).json({ error: 'Invalid credentials' });
}

// 生成 token
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ id: user.id, username: user.username },
process.env.JWT_SECRET || 'secret',
{ expiresIn: '7d' }
);

res.json({ token, user: { id: user.id, username: user.username } });
});

// HTTP API: 注册
app.post('/api/register', async (req, res) => {
const { username, password, nickname } = req.body;

// 检查用户名是否存在
const [existing] = await dbPool.execute(
'SELECT id FROM users WHERE username = ?',
[username]
);

if (existing.length > 0) {
return res.status(400).json({ error: 'Username already exists' });
}

// 创建用户
const bcrypt = require('bcrypt');
const passwordHash = await bcrypt.hash(password, 10);

const [result] = await dbPool.execute(
'INSERT INTO users (username, password_hash, nickname) VALUES (?, ?, ?)',
[username, passwordHash, nickname || username]
);

res.status(201).json({
id: result.insertId,
username,
nickname: nickname || username
});
});

// HTTP API: 获取消息历史
app.get('/api/rooms/:roomId/messages', async (req, res) => {
const { roomId } = req.params;
const limit = parseInt(req.query.limit) || 50;
const before = req.query.before;

let query = 'SELECT m.*, u.username FROM messages m JOIN users u ON m.user_id = u.id WHERE m.room_id = ?';
const params = [roomId];

if (before) {
query += ' AND m.id < ?';
params.push(before);
}

query += ' ORDER BY m.created_at DESC LIMIT ?';
params.push(limit);

const [rows] = await dbPool.execute(query, params);
res.json(rows.reverse());
});

// 启动服务器
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
console.log(`Chat server running on port ${PORT}`);
});

客户端实现

HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>实时聊天</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
height: 100vh;
display: flex;
}

.sidebar {
width: 260px;
background: #2c3e50;
color: white;
display: flex;
flex-direction: column;
}

.sidebar-header {
padding: 20px;
border-bottom: 1px solid #34495e;
}

.room-list {
flex: 1;
overflow-y: auto;
}

.room-item {
padding: 15px 20px;
cursor: pointer;
border-bottom: 1px solid #34495e;
}

.room-item:hover {
background: #34495e;
}

.room-item.active {
background: #3498db;
}

.room-name {
font-weight: 500;
}

.room-users {
font-size: 12px;
color: #95a5a6;
margin-top: 4px;
}

.main {
flex: 1;
display: flex;
flex-direction: column;
}

.header {
padding: 15px 20px;
background: white;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}

.header-title {
font-size: 18px;
font-weight: 500;
}

.connection-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
}

.status-connected {
background: #27ae60;
color: white;
}

.status-disconnected {
background: #e74c3c;
color: white;
}

.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: white;
}

.message {
margin-bottom: 15px;
}

.message-header {
display: flex;
align-items: baseline;
margin-bottom: 4px;
}

.message-username {
font-weight: 500;
margin-right: 10px;
}

.message-time {
font-size: 12px;
color: #95a5a6;
}

.message-content {
padding: 10px 15px;
background: #f8f9fa;
border-radius: 8px;
display: inline-block;
max-width: 70%;
word-wrap: break-word;
}

.message-own .message-content {
background: #3498db;
color: white;
}

.message-system {
text-align: center;
color: #95a5a6;
font-size: 12px;
margin: 20px 0;
}

.input-area {
padding: 15px 20px;
background: white;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
}

.input-area input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
outline: none;
}

.input-area input:focus {
border-color: #3498db;
}

.input-area button {
padding: 12px 25px;
background: #3498db;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}

.input-area button:hover {
background: #2980b9;
}

.input-area button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}

/* 登录界面 */
.login-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}

.login-form {
background: white;
padding: 30px;
border-radius: 8px;
width: 350px;
}

.login-form h2 {
margin-bottom: 20px;
text-align: center;
}

.login-form input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}

.login-form button {
width: 100%;
padding: 12px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<!-- 登录界面 -->
<div class="login-container" id="loginContainer">
<div class="login-form">
<h2>登录聊天室</h2>
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button onclick="login()">登录</button>
<p style="margin-top: 15px; text-align: center; font-size: 12px; color: #95a5a6;">
没有账号?请通过 API 注册
</p>
</div>
</div>

<!-- 主界面 -->
<div class="sidebar">
<div class="sidebar-header">
<h2>聊天室</h2>
</div>
<div class="room-list" id="roomList">
<!-- 房间列表 -->
</div>
</div>

<div class="main">
<div class="header">
<span class="header-title" id="currentRoom">选择一个房间</span>
<span class="connection-status status-connected" id="connectionStatus">已连接</span>
</div>
<div class="messages" id="messages">
<!-- 消息列表 -->
</div>
<div class="input-area">
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
<button id="sendButton" onclick="sendMessage()" disabled>发送</button>
</div>
</div>

<script src="app.js"></script>
</body>
</html>

JavaScript 客户端

// 应用状态
const state = {
token: localStorage.getItem('token'),
user: null,
socket: null,
currentRoom: null,
reconnectAttempts: 0,
maxReconnectAttempts: 5
};

// DOM 元素
const elements = {
loginContainer: document.getElementById('loginContainer'),
roomList: document.getElementById('roomList'),
messages: document.getElementById('messages'),
messageInput: document.getElementById('messageInput'),
sendButton: document.getElementById('sendButton'),
connectionStatus: document.getElementById('connectionStatus'),
currentRoom: document.getElementById('currentRoom')
};

// 初始化
function init() {
if (state.token) {
connectWebSocket();
} else {
elements.loginContainer.style.display = 'flex';
}
}

// 登录
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;

if (!username || !password) {
alert('请输入用户名和密码');
return;
}

try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error);
}

state.token = data.token;
state.user = data.user;
localStorage.setItem('token', data.token);

elements.loginContainer.style.display = 'none';
connectWebSocket();
} catch (error) {
alert('登录失败: ' + error.message);
}
}

// 连接 WebSocket
function connectWebSocket() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}/ws?token=${state.token}`;

state.socket = new WebSocket(wsUrl);

state.socket.onopen = () => {
console.log('WebSocket 连接成功');
state.reconnectAttempts = 0;
updateConnectionStatus(true);
startHeartbeat();
};

state.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};

state.socket.onclose = (event) => {
console.log('WebSocket 关闭:', event.code, event.reason);
updateConnectionStatus(false);

// 非正常关闭时尝试重连
if (event.code !== 1000 && state.reconnectAttempts < state.maxReconnectAttempts) {
scheduleReconnect();
}
};

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

// 心跳机制
let heartbeatTimer;

function startHeartbeat() {
heartbeatTimer = setInterval(() => {
if (state.socket && state.socket.readyState === WebSocket.OPEN) {
state.socket.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
}

function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
}
}

// 重连机制
function scheduleReconnect() {
state.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);

console.log(`${delay / 1000} 秒后第 ${state.reconnectAttempts} 次重连`);

setTimeout(() => {
connectWebSocket();
}, delay);
}

// 更新连接状态显示
function updateConnectionStatus(connected) {
const status = elements.connectionStatus;
if (connected) {
status.textContent = '已连接';
status.className = 'connection-status status-connected';
} else {
status.textContent = '已断开';
status.className = 'connection-status status-disconnected';
}
}

// 处理接收的消息
function handleMessage(message) {
switch (message.type) {
case 'connected':
state.user = message.data;
renderRoomList(message.data.rooms);
break;

case 'room_joined':
state.currentRoom = message.data.roomId;
elements.currentRoom.textContent = message.data.roomName;
elements.messageInput.disabled = false;
elements.sendButton.disabled = false;
elements.messages.innerHTML = '';
break;

case 'message':
appendMessage(message.data);
break;

case 'user_joined':
appendSystemMessage(`${message.data.username} 加入了房间`);
break;

case 'user_left':
appendSystemMessage(`${message.data.username} 离开了房间`);
break;

case 'error':
console.error('服务器错误:', message.data.message);
break;
}
}

// 渲染房间列表
function renderRoomList(rooms) {
elements.roomList.innerHTML = rooms.map(room => `
<div class="room-item" onclick="joinRoom(${room.id})">
<div class="room-name">${room.name}</div>
<div class="room-users">在线: ${room.userCount || 0} 人</div>
</div>
`).join('');
}

// 加入房间
function joinRoom(roomId) {
if (!state.socket || state.socket.readyState !== WebSocket.OPEN) {
alert('未连接到服务器');
return;
}

// 离开当前房间
if (state.currentRoom) {
state.socket.send(JSON.stringify({
type: 'leave_room',
data: { roomId: state.currentRoom }
}));
}

// 加入新房间
state.socket.send(JSON.stringify({
type: 'join_room',
data: { roomId }
}));
}

// 发送消息
function sendMessage() {
const content = elements.messageInput.value.trim();
if (!content || !state.currentRoom) return;

state.socket.send(JSON.stringify({
type: 'send_message',
data: {
roomId: state.currentRoom,
content
}
}));

elements.messageInput.value = '';
}

// 添加消息到列表
function appendMessage(data) {
const isOwn = data.userId === state.user?.id;
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isOwn ? 'message-own' : ''}`;

messageDiv.innerHTML = `
<div class="message-header">
<span class="message-username">${data.username}</span>
<span class="message-time">${formatTime(data.timestamp)}</span>
</div>
<div class="message-content">${escapeHtml(data.content)}</div>
`;

elements.messages.appendChild(messageDiv);
elements.messages.scrollTop = elements.messages.scrollHeight;
}

// 添加系统消息
function appendSystemMessage(text) {
const div = document.createElement('div');
div.className = 'message-system';
div.textContent = text;
elements.messages.appendChild(div);
elements.messages.scrollTop = elements.messages.scrollHeight;
}

// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}

// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

// 回车发送
elements.messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});

// 启动应用
init();

部署建议

PM2 集群部署

// ecosystem.config.js
module.exports = {
apps: [{
name: 'chat-server',
script: 'src/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 8080,
DB_HOST: 'localhost',
REDIS_URL: 'redis://localhost:6379'
},
error_file: './logs/error.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss'
}]
};

Nginx 配置

upstream chat_servers {
least_conn;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}

server {
listen 80;
server_name chat.example.com;

# HTTP API
location /api {
proxy_pass http://chat_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

# WebSocket
location /ws {
proxy_pass http://chat_servers;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}

# 静态文件
location / {
root /var/www/chat;
try_files $uri $uri/ /index.html;
}
}

Docker 部署

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY src ./src

EXPOSE 8080

CMD ["node", "src/server.js"]
# docker-compose.yml
version: '3.8'

services:
chat:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=mysql
- REDIS_URL=redis://redis:6379
- JWT_SECRET=your-secret-key
depends_on:
- mysql
- redis

mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=chat
volumes:
- mysql_data:/var/lib/mysql

redis:
image: redis:7-alpine
volumes:
- redis_data:/data

volumes:
mysql_data:
redis_data:

小结

本章实现了一个完整的实时聊天应用,涵盖了 WebSocket 开发的核心要点:

架构设计

  • 使用 Redis Pub/Sub 实现跨服务器消息同步
  • 分离 HTTP API 和 WebSocket 连接
  • 合理的数据模型设计

核心功能

  • 用户认证与授权
  • 房间管理与消息路由
  • 心跳检测与断线重连

生产部署

  • PM2 集群模式
  • Nginx 负载均衡
  • Docker 容器化

这个案例展示了如何将 WebSocket 技术应用到实际项目中。在实际开发中,还需要考虑:

  • 消息加密与安全验证
  • 文件/图片传输支持
  • 消息撤回与编辑
  • 私聊功能
  • 表情与富文本支持
  • 移动端适配

通过这个实战案例,你应该能够理解如何将前面学到的 WebSocket 知识应用到实际项目中。