目錄
1. 前言:為什么要學習WebSocket主從架構
第一章:基礎知識準備
2.1 什么是WebSocket
生活中的例子
技術特點
2.2 WebSocket vs HTTP
什么時候用WebSocket?
2.3 什么是主從架構
生活中的例子
技術架構圖
2.4 環境準備
需要的軟件
項目結構
第二章:WebSocket基礎入門
3.1 第一個WebSocket服務器
3.2 第一個WebSocket客戶端
3.3 消息通信基礎
消息格式設計
常見消息類型
完整的消息處理示例
第三章:主從架構設計原理
4.1 架構設計思路
為什么需要主從架構?
架構演進過程
4.2 核心組件說明
1. 主服務器(Master Server)
2. 從服務器(Slave Server)
3. 客戶端(Client)
4.3 通信協議設計
通信流程詳解
消息協議規范
第四章:主服務器詳細實現
5.1 主服務器架構
核心類設計
5.2 從服務器管理
從服務器信息結構
從服務器注冊流程
5.3 負載均衡實現
多種負載均衡策略
5.4 完整代碼實現
第五章:從服務器詳細實現
6.1 從服務器架構
核心類設計
6.2 與主服務器通信
連接管理
6.3 客戶端管理
客戶端連接處理
6.4 完整代碼實現
第六章:客戶端詳細實現
7.1 客戶端連接流程
連接流程圖
7.2 自動重連機制
重連策略
1. 前言:為什么要學習WebSocket主從架構
想象一下,你正在開發一個實時在線游戲,有成千上萬的玩家同時在線。如果只用一臺服務器,會發生什么?
- 服務器壓力巨大,可能隨時崩潰
- 玩家體驗差,延遲高
- 無法擴展,玩家數量受限
這就是為什么我們需要學習WebSocket主從架構!它能幫助我們:
- 分散壓力:把玩家分配到不同的服務器
- 提高性能:每個服務器只處理部分玩家
- 易于擴展:需要時隨時增加服務器
- 高可用性:一臺服務器掛了,其他的繼續工作
本教程將帶你從零開始,一步步掌握這個強大的架構!
第一章:基礎知識準備
2.1 什么是WebSocket
生活中的例子
想象兩種通信方式:
-
寫信(HTTP):
- 你寫一封信(請求)
- 寄給朋友
- 等待回信(響應)
- 想再說話?再寫一封信
-
打電話(WebSocket):
- 撥通電話(建立連接)
- 隨時可以說話(發送消息)
- 對方也能隨時回話(接收消息)
- 直到掛斷電話(斷開連接)
WebSocket就像打電話,一旦連接建立,雙方可以隨時通信!
技術特點
// HTTP方式(傳統)
客戶端: "服務器,現在幾點?"
服務器: "下午3點"
// 連接斷開客戶端: "服務器,現在幾點?" // 需要重新連接
服務器: "下午3點01分"
// 連接又斷開// WebSocket方式
客戶端 <--持續連接--> 服務器
客戶端: "現在幾點?"
服務器: "下午3點"
客戶端: "天氣如何?"
服務器: "晴天"
服務器: "對了,有新消息!" // 服務器主動推送
// 連接一直保持
2.2 WebSocket vs HTTP
特性 | HTTP | WebSocket |
---|---|---|
通信方式 | 請求-響應 | 全雙工 |
連接狀態 | 短連接 | 長連接 |
服務器推送 | 不支持 | 支持 |
實時性 | 低(需要輪詢) | 高 |
資源消耗 | 高(頻繁建立連接) | 低(保持連接) |
適用場景 | 普通網頁 | 實時應用 |
什么時候用WebSocket?
? 適合使用WebSocket的場景:
- 在線聊天
- 實時游戲
- 股票行情
- 協同編輯
- 直播彈幕
- IoT實時數據
? 不適合使用WebSocket的場景:
- 靜態網頁
- RESTful API
- 文件下載
- 表單提交
2.3 什么是主從架構
生活中的例子
想象一個大型餐廳的廚房:
總廚師長(主服務器)|分配任務和協調|+-----+-----+-----+| | | |廚師1 廚師2 廚師3 廚師4(從服務器)| | | |菜品1 菜品2 菜品3 菜品4(處理客戶請求)
- 總廚師長:不做菜,只負責分配訂單給各個廚師
- 廚師們:實際做菜,每個廚師負責一部分訂單
- 優勢:效率高,一個廚師累了還有其他的
這就是主從架構的核心思想!
技術架構圖
┌─────────────────┐│ 主服務器 ││ (Master) ││ ││ · 管理從服務器 ││ · 分配客戶端 ││ · 負載均衡 ││ · 健康檢查 │└────────┬────────┘│┌────────────────────┼────────────────────┐│ │ │┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐│從服務器1 │ │從服務器2 │ │從服務器3 ││(Slave 1) │ │(Slave 2) │ │(Slave 3) ││ │ │ │ │ ││·處理業務 │ │·處理業務 │ │·處理業務 ││·管理客戶端│ │·管理客戶端│ │·管理客戶端│└────┬─────┘ └────┬─────┘ └────┬─────┘│ │ │客戶端1,2,3 客戶端4,5,6 客戶端7,8,9
2.4 環境準備
需要的軟件
-
Node.js(版本 14.0 以上)
# 檢查是否安裝 node --version# 如果沒有安裝,去官網下載:https://nodejs.org
-
代碼編輯器(推薦 VS Code)
- 下載地址:https://code.visualstudio.com
-
創建項目目錄
# 創建項目文件夾 mkdir websocket-master-slave cd websocket-master-slave# 初始化項目 npm init -y# 安裝依賴 npm install ws
項目結構
websocket-master-slave/
├── package.json # 項目配置文件
├── master-server.js # 主服務器
├── slave-server.js # 從服務器
├── client.js # 客戶端
├── config/ # 配置文件夾
│ ├── master.json # 主服務器配置
│ └── slave.json # 從服務器配置
├── logs/ # 日志文件夾
├── test/ # 測試文件夾
└── README.md # 項目說明
第二章:WebSocket基礎入門
在深入主從架構之前,我們先來掌握WebSocket的基礎知識。
3.1 第一個WebSocket服務器
讓我們從最簡單的WebSocket服務器開始:
// simple-server.js
const WebSocket = require('ws');// 創建WebSocket服務器,監聽8080端口
const wss = new WebSocket.Server({ port: 8080 });console.log('WebSocket服務器啟動在端口 8080');// 當有客戶端連接時
wss.on('connection', function connection(ws) {console.log('新客戶端連接了!');// 向客戶端發送歡迎消息ws.send('歡迎連接到WebSocket服務器!');// 當收到客戶端消息時ws.on('message', function incoming(message) {console.log('收到消息:', message.toString());// 回復客戶端ws.send(`服務器收到了你的消息: ${message}`);});// 當客戶端斷開連接時ws.on('close', function close() {console.log('客戶端斷開連接');});// 錯誤處理ws.on('error', function error(err) {console.error('WebSocket錯誤:', err);});
});// 每5秒向所有連接的客戶端發送時間
setInterval(() => {wss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(`當前時間: ${new Date().toLocaleTimeString()}`);}});
}, 5000);
代碼解釋:
new WebSocket.Server({ port: 8080 })
- 創建服務器wss.on('connection', ...)
- 監聽新連接ws.on('message', ...)
- 監聽消息ws.send(...)
- 發送消息wss.clients
- 所有連接的客戶端
3.2 第一個WebSocket客戶端
// simple-client.js
const WebSocket = require('ws');// 連接到服務器
const ws = new WebSocket('ws://localhost:8080');// 連接成功時
ws.on('open', function open() {console.log('成功連接到服務器!');// 發送消息給服務器ws.send('你好,服務器!');// 每3秒發送一次消息setInterval(() => {if (ws.readyState === WebSocket.OPEN) {ws.send(`客戶端時間: ${new Date().toLocaleTimeString()}`);}}, 3000);
});// 收到服務器消息時
ws.on('message', function incoming(data) {console.log('收到服務器消息:', data.toString());
});// 連接關閉時
ws.on('close', function close() {console.log('與服務器斷開連接');
});// 錯誤處理
ws.on('error', function error(err) {console.error('連接錯誤:', err);
});// 處理進程退出
process.on('SIGINT', () => {console.log('\n正在關閉連接...');ws.close();process.exit();
});
3.3 消息通信基礎
消息格式設計
在實際應用中,我們通常使用JSON格式傳遞消息:
// 消息格式示例
const message = {type: 'chat', // 消息類型data: { // 消息數據user: '張三',text: '大家好!',time: Date.now()}
};// 發送時轉換為字符串
ws.send(JSON.stringify(message));// 接收時解析JSON
ws.on('message', (data) => {const message = JSON.parse(data);console.log(`${message.data.user}: ${message.data.text}`);
});
常見消息類型
// 定義消息類型常量
const MessageTypes = {// 系統消息CONNECT: 'connect', // 連接DISCONNECT: 'disconnect', // 斷開HEARTBEAT: 'heartbeat', // 心跳ERROR: 'error', // 錯誤// 業務消息CHAT: 'chat', // 聊天JOIN_ROOM: 'join_room', // 加入房間LEAVE_ROOM: 'leave_room', // 離開房間USER_LIST: 'user_list', // 用戶列表// 主從通信REGISTER: 'register', // 注冊SYNC: 'sync', // 同步BROADCAST: 'broadcast' // 廣播
};
完整的消息處理示例
// message-handler.js
class MessageHandler {constructor(ws) {this.ws = ws;this.handlers = new Map();this.registerHandlers();}// 注冊消息處理器registerHandlers() {this.handlers.set('chat', this.handleChat.bind(this));this.handlers.set('join_room', this.handleJoinRoom.bind(this));this.handlers.set('heartbeat', this.handleHeartbeat.bind(this));}// 處理收到的消息handleMessage(data) {try {const message = JSON.parse(data);const handler = this.handlers.get(message.type);if (handler) {handler(message.data);} else {console.log('未知消息類型:', message.type);}} catch (error) {console.error('消息解析錯誤:', error);}}// 處理聊天消息handleChat(data) {console.log(`[聊天] ${data.user}: ${data.text}`);// 廣播給其他用戶this.broadcast({type: 'chat',data: data});}// 處理加入房間handleJoinRoom(data) {console.log(`${data.user} 加入了房間 ${data.room}`);// 處理加入房間的邏輯}// 處理心跳handleHeartbeat(data) {// 回復心跳this.send({type: 'heartbeat',data: { timestamp: Date.now() }});}// 發送消息send(message) {if (this.ws.readyState === WebSocket.OPEN) {this.ws.send(JSON.stringify(message));}}// 廣播消息(需要訪問所有客戶端)broadcast(message) {// 這里需要服務器支持}
}
第三章:主從架構設計原理
4.1 架構設計思路
為什么需要主從架構?
讓我們通過一個故事來理解:
小明開了一家網絡游戲公司,最初只有100個玩家,一臺服務器輕松應對。隨著游戲火爆,玩家增長到10000人,服務器開始卡頓。小明買了更好的服務器,但玩家繼續增長到100000人,再好的單臺服務器也扛不住了。
這時,小明想到:為什么不用多臺服務器分擔壓力呢?
這就是主從架構的起源!
架構演進過程
-
單服務器階段
所有客戶端 --> 單一服務器優點:簡單 缺點:性能瓶頸、單點故障
-
簡單集群階段
客戶端 --> 負載均衡器 --> 多個獨立服務器優點:負載分散 缺點:服務器間無法通信
-
主從架構階段
客戶端 --> 從服務器 <--> 主服務器 <--> 從服務器 <-- 客戶端優點:負載分散、統一管理、服務器間可通信 缺點:架構復雜度增加
4.2 核心組件說明
1. 主服務器(Master Server)
職責:
- ?? 從服務器的注冊和管理
- ?? 負載均衡(決定客戶端連接哪個從服務器)
- ?? 健康檢查(監控從服務器狀態)
- ?? 消息路由(跨服務器消息轉發)
- ?? 全局狀態管理
類比:就像交通指揮中心,不直接運送乘客,但協調所有公交車的調度。
2. 從服務器(Slave Server)
職責:
- ?? 處理客戶端連接
- ?? 執行具體業務邏輯
- ?? 向主服務器報告狀態
- ?? 與其他從服務器同步數據
類比:就像具體的公交車,實際運送乘客,并向調度中心報告位置。
3. 客戶端(Client)
職責:
- ?? 向主服務器請求分配
- ?? 連接到指定的從服務器
- ?? 執行應用功能
類比:就像乘客,先問調度中心該上哪輛車,然后上車。
4.3 通信協議設計
通信流程詳解
1. 從服務器啟動并注冊Slave --> Master: {type: "register", name: "slave-1", capacity: 100}Master --> Slave: {type: "register_success", slaveId: "s1"}2. 心跳保活Slave --> Master: {type: "heartbeat", slaveId: "s1"}Master --> Slave: {type: "heartbeat_ack"}3. 客戶端請求分配Client --> Master: {type: "request_server"}Master --> Client: {type: "server_assigned", host: "192.168.1.2", port: 8081}4. 客戶端連接從服務器Client --> Slave: {type: "connect", userId: "u123"}Slave --> Client: {type: "connect_success"}5. 跨服務器消息Slave1 --> Master: {type: "forward", target: "slave2", data: {...}}Master --> Slave2: {type: "forwarded", from: "slave1", data: {...}}
消息協議規范
// 基礎消息結構
{id: "唯一消息ID",type: "消息類型",timestamp: "時間戳",data: {// 具體數據}
}// 主從通信消息類型
const MasterSlaveProtocol = {// 從服務器 -> 主服務器SLAVE_REGISTER: 'slave:register', // 注冊SLAVE_HEARTBEAT: 'slave:heartbeat', // 心跳SLAVE_STATUS: 'slave:status', // 狀態報告SLAVE_SYNC: 'slave:sync', // 數據同步// 主服務器 -> 從服務器MASTER_REGISTER_ACK: 'master:register_ack', // 注冊確認MASTER_HEARTBEAT_ACK: 'master:heartbeat_ack', // 心跳確認MASTER_COMMAND: 'master:command', // 命令下發MASTER_BROADCAST: 'master:broadcast', // 廣播消息// 客戶端 -> 主服務器CLIENT_REQUEST: 'client:request', // 請求分配// 主服務器 -> 客戶端MASTER_ASSIGN: 'master:assign', // 分配服務器MASTER_REJECT: 'master:reject' // 拒絕請求
};
第四章:主服務器詳細實現
5.1 主服務器架構
主服務器是整個系統的大腦,讓我們詳細實現它的每個功能。
核心類設計
// master-server-core.js
class MasterServer {constructor(config) {// 配置this.config = {port: config.port || 8080,heartbeatInterval: config.heartbeatInterval || 10000,heartbeatTimeout: config.heartbeatTimeout || 30000,...config};// 核心數據結構this.slaves = new Map(); // 從服務器集合this.clients = new Map(); // 客戶端分配記錄this.stats = { // 統計信息totalConnections: 0,totalMessages: 0,startTime: Date.now()};// 狀態this.slaveIdCounter = 0; // 從服務器ID計數器this.isRunning = false; // 運行狀態}
}
5.2 從服務器管理
從服務器信息結構
class SlaveInfo {constructor(ws, registerData) {this.id = null; // 由主服務器分配this.ws = ws; // WebSocket連接this.name = registerData.name; // 從服務器名稱this.host = registerData.host; // 主機地址this.port = registerData.port; // 端口號this.capacity = registerData.capacity || 100; // 容量this.currentLoad = 0; // 當前負載this.status = 'active'; // 狀態this.lastHeartbeat = Date.now(); // 最后心跳時間this.metadata = registerData.metadata || {}; // 額外信息this.performance = { // 性能指標cpu: 0,memory: 0,responseTime: 0};}// 獲取負載率getLoadRate() {return this.currentLoad / this.capacity;}// 是否可用isAvailable() {return this.status === 'active' && this.getLoadRate() < 0.9;}// 更新心跳updateHeartbeat() {this.lastHeartbeat = Date.now();}// 轉換為客戶端可見信息toClientInfo() {return {id: this.id,host: this.host,port: this.port,name: this.name};}
}
從服務器注冊流程
// 在 MasterServer 類中
registerSlave(ws, data) {// 驗證注冊信息if (!this.validateSlaveData(data)) {this.sendToWebSocket(ws, {type: 'register_error',error: '注冊信息不完整'});return;}// 檢查是否重復注冊const existingSlave = this.findSlaveByHostPort(data.host, data.port);if (existingSlave) {console.log(`從服務器 ${data.name} 重新連接`);// 更新現有連接existingSlave.ws = ws;existingSlave.status = 'active';existingSlave.updateHeartbeat();ws.slaveId = existingSlave.id;this.sendToWebSocket(ws, {type: 'register_success',slaveId: existingSlave.id,message: '重新連接成功'});return;}// 創建新的從服務器記錄const slaveInfo = new SlaveInfo(ws, data);slaveInfo.id = ++this.slaveIdCounter;// 保存到集合this.slaves.set(slaveInfo.id, slaveInfo);ws.slaveId = slaveInfo.id;// 發送注冊成功消息this.sendToWebSocket(ws, {type: 'register_success',slaveId: slaveInfo.id,config: {heartbeatInterval: this.config.heartbeatInterval}});console.log(`? 從服務器 ${slaveInfo.name} (ID: ${slaveInfo.id}) 注冊成功`);// 通知其他從服務器this.broadcastToSlaves({type: 'slave_joined',slave: slaveInfo.toClientInfo()}, slaveInfo.id);// 觸發事件this.emit('slave:registered', slaveInfo);
}// 驗證從服務器數據
validateSlaveData(data) {return data.name && data.host && data.port;
}// 根據主機和端口查找從服務器
findSlaveByHostPort(host, port) {for (const slave of this.slaves.values()) {if (slave.host === host && slave.port === port) {return slave;}}return null;
}
5.3 負載均衡實現
多種負載均衡策略
// load-balancer.js
class LoadBalancer {constructor() {this.strategies = new Map();this.currentStrategy = 'least_connections';this.roundRobinIndex = 0;// 注冊所有策略this.registerStrategies();}registerStrategies() {