WebSocket 技术详解
引言
在现代Web应用中,实时通信已经成为不可或缺的一部分。想象一下聊天应用、在线游戏、股票交易平台或协作工具,这些应用都需要服务器能够即时将更新推送给客户端,而不仅仅是等待客户端请求。WebSocket技术应运而生,它提供了一种在客户端和服务器之间建立持久连接的方法,实现了真正的双向通信。
传统的HTTP通信模式是"请求-响应"式的,客户端必须主动发送请求,服务器才能响应。这种模式对于实时应用来说效率低下且资源消耗大。相比之下,WebSocket在建立连接后,允许服务器主动向客户端推送数据,为实时应用提供了更高效、更低延迟的通信方式。
本文将带你从零开始,深入理解WebSocket的工作原理、如何在前端实现WebSocket连接、处理消息传输、使用常见的WebSocket库,以及在实际开发中需要注意的各种细节和最佳实践。
什么是WebSocket?
WebSocket是一种在单个TCP连接上进行全双工通信的协议。它在2011年被IETF标准化为RFC 6455,现在已得到所有主流浏览器的支持。
WebSocket与HTTP的区别
特性 | HTTP | WebSocket |
---|---|---|
连接性质 | 非持久连接 | 持久连接 |
通信方式 | 单向(请求-响应) | 双向(全双工) |
开销 | 每次请求都有HTTP头 | 建立连接后的消息无需额外头信息 |
实时性 | 依赖轮询,延迟高 | 真正的实时,延迟低 |
URL前缀 | http:// 或 https:// | ws:// 或 wss:// (安全WebSocket) |
WebSocket的工作流程
- 握手阶段:客户端通过HTTP请求发起WebSocket连接
- 协议升级:服务器接受连接请求,协议从HTTP升级到WebSocket
- 数据传输:建立连接后,双方可以随时发送消息
- 关闭连接:任何一方都可以发起关闭连接的请求
从零开始:原生WebSocket API
现代浏览器内置了WebSocket API,让我们先来看看如何使用原生API创建和管理WebSocket连接。
创建WebSocket连接
// 创建WebSocket连接
const socket = new WebSocket('ws://example.com/socketserver');// 连接建立时触发
socket.onopen = function(event) {console.log('WebSocket连接已建立');// 可以立即发送消息socket.send('你好,服务器!');
};// 接收到消息时触发
socket.onmessage = function(event) {console.log('收到消息:', event.data);
};// 连接关闭时触发
socket.onclose = function(event) {console.log('WebSocket连接已关闭');console.log('关闭码:', event.code);console.log('关闭原因:', event.reason);
};// 发生错误时触发
socket.onerror = function(error) {console.error('WebSocket发生错误:', error);
};
发送不同类型的数据
WebSocket支持发送文本和二进制数据:
// 发送文本数据
socket.send('这是一个文本消息');// 发送JSON数据(需要先转换为字符串)
const jsonData = { type: 'userInfo', name: 'Zhang San', age: 30 };
socket.send(JSON.stringify(jsonData));// 发送二进制数据(例如ArrayBuffer)
const buffer = new ArrayBuffer(4);
const view = new Int32Array(buffer);
view[0] = 42;
socket.send(buffer);// 发送Blob对象
const blob = new Blob(['Hello world'], {type: 'text/plain'});
socket.send(blob);
关闭WebSocket连接
// 正常关闭连接
socket.close();// 带关闭码和原因关闭连接
socket.close(1000, '操作完成');/*
常见的关闭码:
1000: 正常关闭
1001: 离开(例如用户关闭浏览器)
1002: 协议错误
1003: 数据类型不支持
1008: 消息违反策略
1011: 服务器遇到未知情况
*/
WebSocket连接状态
WebSocket对象有一个readyState属性,表示连接的当前状态:
// 检查WebSocket的状态
const checkState = () => {switch(socket.readyState) {case WebSocket.CONNECTING: // 0 - 连接正在建立console.log('正在连接...');break;case WebSocket.OPEN: // 1 - 连接已建立,可以通信console.log('已连接');break;case WebSocket.CLOSING: // 2 - 连接正在关闭console.log('正在关闭...');break;case WebSocket.CLOSED: // 3 - 连接已关闭或无法打开console.log('已关闭');break;}
};
使用Socket.io:更强大的WebSocket库
原生WebSocket API虽然简单易用,但在处理复杂场景时仍显不足。Socket.io是一个广泛使用的WebSocket库,它提供了更多功能和更好的兼容性。
安装Socket.io客户端
# 使用npm安装
npm install socket.io-client# 或使用yarn
yarn add socket.io-client
使用Socket.io的基本示例
// 导入Socket.io客户端
import io from 'socket.io-client';// 创建Socket.io连接
const socket = io('http://example.com');// 连接事件
socket.on('connect', () => {console.log('Socket.io连接已建立');console.log('连接ID:', socket.id);// 发送事件到服务器socket.emit('greeting', { message: '你好,服务器!' });
});// 自定义事件监听
socket.on('welcome', (data) => {console.log('收到欢迎消息:', data);
});// 断开连接事件
socket.on('disconnect', (reason) => {console.log('Socket.io连接断开:', reason);
});// 重新连接事件
socket.on('reconnect', (attemptNumber) => {console.log(`第${attemptNumber}次重连成功`);
});// 重连尝试事件
socket.on('reconnect_attempt', (attemptNumber) => {console.log(`正在尝试第${attemptNumber}次重连`);
});// 重连错误
socket.on('reconnect_error', (error) => {console.error('重连错误:', error);
});// 连接错误
socket.on('connect_error', (error) => {console.error('连接错误:', error);
});
Socket.io的高级功能
命名空间和房间
Socket.io支持命名空间和房间,用于组织和分类连接:
// 连接到特定的命名空间
const chatSocket = io('http://example.com/chat');
const gameSocket = io('http://example.com/game');// 加入房间
socket.emit('join', 'room1');// 向特定房间发送消息
socket.to('room1').emit('message', '你好,房间1的成员!');
事件确认
Socket.io支持确认事件接收:
// 客户端发送带确认的事件
socket.emit('createUser', { name: 'Li Si', email: 'lisi@example.com' }, (response) => {if (response.success) {console.log('用户创建成功:', response.userId);} else {console.error('用户创建失败:', response.error);}
});
二进制数据支持
// 发送二进制数据
const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);
view.set([1, 2, 3, 4]);
socket.emit('binaryData', buffer);// 接收二进制数据
socket.on('binaryResponse', (data) => {const view = new Uint8Array(data);console.log('收到二进制数据:', Array.from(view));
});
实现一个聊天应用
让我们结合前面所学,实现一个简单的聊天应用:
HTML结构
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>WebSocket聊天室</title><style>#chatbox {height: 300px;overflow-y: scroll;border: 1px solid #ccc;padding: 10px;margin-bottom: 10px;}.message {margin-bottom: 5px;padding: 5px;border-radius: 5px;}.sent {background-color: #e3f2fd;text-align: right;}.received {background-color: #f1f1f1;}.system {background-color: #fff9c4;text-align: center;font-style: italic;}</style>
</head>
<body><h1>WebSocket聊天室</h1><div id="connectionStatus">正在连接...</div><div id="chatbox"></div><div><input type="text" id="messageInput" placeholder="输入消息..."><button id="sendButton">发送</button></div><script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script><script src="app.js"></script>
</body>
</html>
JavaScript实现
// app.js
document.addEventListener('DOMContentLoaded', () => {const statusElement = document.getElementById('connectionStatus');const chatbox = document.getElementById('chatbox');const messageInput = document.getElementById('messageInput');const sendButton = document.getElementById('sendButton');// 创建一个唯一的用户IDconst userId = 'user_' + Math.random().toString(36).substr(2, 9);const username = prompt('请输入你的昵称') || '访客' + userId.substr(-4);// 连接到Socket.io服务器const socket = io('http://localhost:3000');// 连接建立socket.on('connect', () => {statusElement.textContent = '已连接';statusElement.style.color = 'green';// 发送加入消息socket.emit('join', { userId, username });// 添加系统消息addMessage('系统', `欢迎来到聊天室, ${username}!`, 'system');});// 断开连接socket.on('disconnect', () => {statusElement.textContent = '已断开连接';statusElement.style.color = 'red';addMessage('系统', '与服务器的连接已断开', 'system');});// 接收消息socket.on('message', (data) => {const messageType = data.userId === userId ? 'sent' : 'received';addMessage(data.username, data.message, messageType);});// 有新用户加入socket.on('userJoined', (data) => {addMessage('系统', `${data.username} 加入了聊天室`, 'system');});// 用户离开socket.on('userLeft', (data) => {addMessage('系统', `${data.username} 离开了聊天室`, 'system');});// 发送消息const sendMessage = () => {const message = messageInput.value.trim();if (message) {socket.emit('sendMessage', {userId,username,message});messageInput.value = '';}};// 添加消息到聊天框const addMessage = (sender, content, type) => {const messageElement = document.createElement('div');messageElement.className = `message ${type}`;if (type !== 'system') {messageElement.innerHTML = `<strong>${sender}:</strong> ${content}`;} else {messageElement.textContent = content;}chatbox.appendChild(messageElement);chatbox.scrollTop = chatbox.scrollHeight; // 滚动到底部};// 点击发送按钮sendButton.addEventListener('click', sendMessage);// 按回车键发送messageInput.addEventListener('keypress', (e) => {if (e.key === 'Enter') {sendMessage();}});// 页面关闭前window.addEventListener('beforeunload', () => {socket.emit('leave', { userId, username });});
});
服务器端实现(Node.js + Socket.io)
// server.js
const http = require('http');
const { Server } = require('socket.io');const server = http.createServer();
const io = new Server(server, {cors: {origin: '*', // 在生产环境中应该限制为特定域名methods: ['GET', 'POST']}
});// 在线用户
const onlineUsers = new Map();io.on('connection', (socket) => {console.log('新连接:', socket.id);// 用户加入socket.on('join', (userData) => {const { userId, username } = userData;// 存储用户信息onlineUsers.set(socket.id, { userId, username });// 广播新用户加入socket.broadcast.emit('userJoined', { userId, username, onlineCount: onlineUsers.size });// 发送当前在线用户信息socket.emit('currentUsers', Array.from(onlineUsers.values()));});// 发送消息socket.on('sendMessage', (data) => {// 广播消息给所有客户端io.emit('message', data);});// 用户离开socket.on('leave', (userData) => {handleDisconnect(socket);});// 断开连接socket.on('disconnect', () => {handleDisconnect(socket);});// 处理断开连接逻辑const handleDisconnect = (socket) => {// 检查用户是否在记录中if (onlineUsers.has(socket.id)) {const userData = onlineUsers.get(socket.id);// 移除用户onlineUsers.delete(socket.id);// 广播用户离开io.emit('userLeft', {userId: userData.userId,username: userData.username,onlineCount: onlineUsers.size});console.log('用户断开连接:', userData.username);}};
});// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {console.log(`WebSocket服务器运行在端口 ${PORT}`);
});
WebSocket监听心跳和重连机制
在实际应用中,网络可能不稳定,WebSocket连接可能会意外断开。实现心跳检测和重连机制是很重要的。
心跳检测机制
class HeartbeatWebSocket {constructor(url, options = {}) {this.url = url;this.options = options;this.socket = null;this.heartbeatInterval = options.heartbeatInterval || 30000; // 默认30秒this.heartbeatTimer = null;this.reconnectTimer = null;this.reconnectAttempts = 0;this.maxReconnectAttempts = options.maxReconnectAttempts || 5;this.reconnectInterval = options.reconnectInterval || 5000; // 默认5秒this.connect();}connect() {this.socket = new WebSocket(this.url);this.socket.onopen = (event) => {console.log('WebSocket连接已建立');this.reconnectAttempts = 0; // 重置重连次数this.startHeartbeat(); // 开始心跳if (typeof this.options.onopen === 'function') {this.options.onopen(event);}};this.socket.onmessage = (event) => {// 如果是心跳响应,重置心跳计时器if (event.data === 'pong') {this.resetHeartbeat();return;}if (typeof this.options.onmessage === 'function') {this.options.onmessage(event);}};this.socket.onclose = (event) => {console.log('WebSocket连接已关闭');this.stopHeartbeat();if (event.code !== 1000) { // 非正常关闭this.reconnect();}if (typeof this.options.onclose === 'function') {this.options.onclose(event);}};this.socket.onerror = (error) => {console.error('WebSocket错误:', error);if (typeof this.options.onerror === 'function') {this.options.onerror(error);}};}startHeartbeat() {this.stopHeartbeat(); // 确保没有多余的心跳计时器this.heartbeatTimer = setInterval(() => {if (this.socket.readyState === WebSocket.OPEN) {console.log('发送心跳');this.socket.send('ping');// 设置心跳超时检测this.heartbeatTimeout = setTimeout(() => {console.log('心跳超时');this.socket.close(3000, 'Heart beat timeout');}, 5000); // 5秒内没收到回应则认为连接已断开}}, this.heartbeatInterval);}resetHeartbeat() {// 清除心跳超时检测if (this.heartbeatTimeout) {clearTimeout(this.heartbeatTimeout);this.heartbeatTimeout = null;}}stopHeartbeat() {if (this.heartbeatTimer) {clearInterval(this.heartbeatTimer);this.heartbeatTimer = null;}this.resetHeartbeat();}reconnect() {if (this.reconnectTimer) {clearTimeout(this.reconnectTimer);}if (this.reconnectAttempts < this.maxReconnectAttempts) {this.reconnectAttempts++;console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);this.reconnectTimer = setTimeout(() => {console.log('重新连接...');this.connect();}, this.reconnectInterval);} else {console.log('达到最大重连次数,放弃重连');if (typeof this.options.onreconnectfailed === 'function') {this.options.onreconnectfailed();}}}send(data) {if (this.socket && this.socket.readyState === WebSocket.OPEN) {this.socket.send(data);return true;}return false;}close(code, reason) {this.stopHeartbeat();if (this.reconnectTimer) {clearTimeout(this.reconnectTimer);this.reconnectTimer = null;}if (this.socket) {this.socket.close(code, reason);}}
}// 使用示例
const wsClient = new HeartbeatWebSocket('ws://example.com/socket', {heartbeatInterval: 15000, // 15秒发送一次心跳maxReconnectAttempts: 10,reconnectInterval: 3000,onopen: (event) => {console.log('连接已建立,可以发送消息');},onmessage: (event) => {console.log('收到消息:', event.data);},onclose: (event) => {console.log('连接已关闭:', event.code, event.reason);},onerror: (error) => {console.error('发生错误:', error);},onreconnectfailed: () => {alert('无法连接到服务器,请检查您的网络连接或稍后再试。');}
});// 发送消息
wsClient.send('Hello!');// 关闭连接
// wsClient.close(1000, 'Normal closure');
WebSocket安全性考虑
实现WebSocket时,安全性是一个重要的考虑因素:
1. 使用WSS而非WS
始终使用安全的WebSocket(WSS)连接,就像使用HTTPS而非HTTP一样:
// 安全连接
const secureSocket = new WebSocket('wss://example.com/socket');// 不安全连接(避免使用)
const insecureSocket = new WebSocket('ws://example.com/socket');
2. 实现身份验证和授权
// 前端:在WebSocket连接中包含身份验证token
const token = localStorage.getItem('authToken');
const socket = new WebSocket(`wss://example.com/socket?token=${token}`);// 或者在连接后通过消息发送身份验证
socket.onopen = () => {socket.send(JSON.stringify({type: 'authenticate',token: localStorage.getItem('authToken')}));
};
3. 验证和消毒输入数据
// 在处理接收到的消息之前,总是验证数据格式
socket.onmessage = (event) => {try {const data = JSON.parse(event.data);// 验证数据结构if (!data.type || typeof data.type !== 'string') {console.error('无效的消息格式');return;}switch (data.type) {case 'chat':// 验证聊天消息的必要字段if (!data.message || typeof data.message !== 'string' || data.message.length > 1000) {console.error('无效的聊天消息');return;}displayChatMessage(data);break;// 其他消息类型处理...}} catch (e) {console.error('解析消息失败:', e);}
};
4. 限速和资源保护
在服务器端实现限速机制,防止洪水攻击:
// 服务器端示例(Node.js)
const messageRateLimits = new Map(); // 用户ID -> 消息计数// 在收到消息时检查限速
socket.on('message', (data) => {const userId = getUserId(socket);// 初始化或增加计数if (!messageRateLimits.has(userId)) {messageRateLimits.set(userId, {count: 1,lastReset: Date.now()});} else {const userLimit = messageRateLimits.get(userId);// 如果超过10秒,重置计数if (Date.now() - userLimit.lastReset > 10000) {userLimit.count = 1;userLimit.lastReset = Date.now();} else {userLimit.count++;// 如果10秒内发送超过20条消息,拒绝处理if (userLimit.count > 20) {socket.send(JSON.stringify({type: 'error',message: '发送消息过于频繁,请稍后再试'}));return;}}messageRateLimits.set(userId, userLimit);}// 处理消息...
});
WebSocket相关库和框架
除了Socket.io,还有其他几个流行的WebSocket库和框架:
1. SockJS
SockJS是一个JavaScript库,提供了一个类似WebSocket的对象,即使在不支持WebSocket的浏览器中也能工作。
// 安装SockJS
// npm install sockjs-client// 使用SockJS
import SockJS from 'sockjs-client';const sockjs = new SockJS('http://example.com/sockjs');sockjs.onopen = function() {console.log('SockJS连接已打开');sockjs.send('Hello, SockJS!');
};sockjs.onmessage = function(e) {console.log('收到消息:', e.data);
};sockjs.onclose = function() {console.log('SockJS连接已关闭');
};
2. ws (Node.js)
ws是一个Node.js的WebSocket库,速度快且易于使用。
// 服务器端(Node.js)
// npm install wsconst WebSocket = require('ws');const wss = new WebSocket.Server({ port: 8080 });wss.on('connection', function connection(ws) {ws.on('message', function incoming(message) {console.log('收到消息:', message);// 回显消息ws.send(`您发送的消息: ${message}`);});ws.send('欢迎连接到WebSocket服务器');
});
3. STOMP
STOMP(Simple Text Oriented Messaging Protocol)是一个简单的消息协议,通常与WebSocket一起使用。
// 安装STOMP客户端
// npm install @stomp/stompjsimport { Client } from '@stomp/stompjs';const client = new Client({brokerURL: 'ws://example.com/stomp',connectHeaders: {login: 'user',passcode: 'password',},debug: function (str) {console.log(str);},reconnectDelay: 5000,heartbeatIncoming: 4000,heartbeatOutgoing: 4000,
});client.onConnect = function (frame) {console.log('STOMP连接已建立');// 订阅消息const subscription = client.subscribe('/topic/messages', function (message) {console.log('收到消息:', message.body);});// 发送消息client.publish({destination: '/app/send',headers: {},body: JSON.stringify({ content: 'Hello, STOMP!' }),});
};client.onStompError = function (frame) {console.error('STOMP错误:', frame.headers['message']);
};client.activate();
性能优化和最佳实践
为了获得最佳的WebSocket性能,请考虑以下建议:
1. 消息压缩
对于大型消息,考虑使用压缩:
// 压缩消息(使用pako压缩库)
// npm install pako
import pako from 'pako';// 发送前压缩消息
function sendCompressedMessage(socket, data) {// 将数据转换为字符串const jsonString = JSON.stringify(data);// 转换为Uint8Array (pako需要)const uint8Array = new TextEncoder().encode(jsonString);// 压缩数据const compressed = pako.deflate(uint8Array);// 发送压缩数据socket.send(compressed);
}// 接收并解压消息
socket.onmessage = (event) => {// 判断是否为二进制数据if (event.data instanceof ArrayBuffer || event.data instanceof Blob) {// 处理二进制数据const processBlob = async (blob) => {try {// 将Blob或ArrayBuffer转换为Uint8Arrayconst arrayBuffer = blob instanceof Blob ? await blob.arrayBuffer() : blob;const compressedData = new Uint8Array(arrayBuffer);// 解压数据const decompressed = pako.inflate(compressedData);// 转换回字符串const jsonString = new TextDecoder().decode(decompressed);// 解析JSONconst data = JSON.parse(jsonString);console.log('收到并解压的消息:', data);processMessage(data);} catch (error) {console.error('解压消息失败:', error);}};if (event.data instanceof Blob) {processBlob(event.data);} else {processBlob(event.data);}} else {// 处理普通文本消息try {const data = JSON.parse(event.data);console.log('收到文本消息:', data);processMessage(data);} catch (e) {console.log('收到非JSON消息:', event.data);}}
};### 2. 批量处理消息当需要发送多个小消息时,将它们批量处理可以减少开销:```javascript
// 不好的做法:发送多个小消息
function sendIndividually(socket, items) {items.forEach(item => {socket.send(JSON.stringify({type: 'update',data: item}));});
}// 好的做法:批量发送
function sendBatch(socket, items) {socket.send(JSON.stringify({type: 'batchUpdate',data: items}));
}
3. 使用二进制数据格式
对于大型数据传输,使用二进制格式(如Protocol Buffers或MessagePack)比JSON更有效:
// 使用MessagePack (需要安装msgpack库)
// npm install @msgpack/msgpack
import { encode, decode } from '@msgpack/msgpack';// 编码并发送
function sendWithMessagePack(socket, data) {const encoded = encode(data);socket.send(encoded);
}// 接收并解码
socket.onmessage = (event) => {if (event.data instanceof ArrayBuffer) {const data = decode(event.data);console.log('接收到的MessagePack数据:', data);}
};
4. 合理设置重连策略
实现指数退避算法进行重连:
class ReconnectingWebSocket {constructor(url) {this.url = url;this.socket = null;this.reconnectAttempts = 0;this.maxReconnectAttempts = 10;this.baseReconnectDelay = 1000; // 1秒起始延迟this.maxReconnectDelay = 30000; // 最大30秒延迟this.connect();}connect() {this.socket = new WebSocket(this.url);this.socket.onopen = () => {console.log('连接成功');this.reconnectAttempts = 0; // 重置重连次数};this.socket.onclose = () => {this.reconnect();};}reconnect() {if (this.reconnectAttempts >= this.maxReconnectAttempts) {console.log('达到最大重连次数');return;}// 计算指数退避延迟const delay = Math.min(this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts),this.maxReconnectDelay);// 添加随机抖动,避免多客户端同时重连const jitter = Math.random() * 0.5 + 0.75; // 0.75-1.25之间的随机值const actualDelay = Math.floor(delay * jitter);console.log(`将在${actualDelay}ms后尝试第${this.reconnectAttempts + 1}次重连`);setTimeout(() => {this.reconnectAttempts++;this.connect();}, actualDelay);}
}
5. 优化负载均衡
在大型应用中,实现WebSocket集群和负载均衡是很重要的:
// 前端:在连接WebSocket时携带会话信息
const sessionId = localStorage.getItem('sessionId');
const socket = new WebSocket(`wss://example.com/socket?sessionId=${sessionId}`);// 后端伪代码(使用Redis进行会话共享)
// 保持相同sessionId的客户端连接到相同的服务器
常见的WebSocket问题及解决方案
1. 浏览器限制同域名的WebSocket连接数
现代浏览器通常将同一域名的WebSocket连接限制在6-8个。
解决方案:
- 使用WebSocket子协议合并多个逻辑连接
- 使用不同的子域名
- 实现消息优先级,确保重要消息先发送
2. 防火墙和代理问题
一些公司网络和代理可能会阻止WebSocket连接。
解决方案:
- 使用Socket.io或SockJS等库,它们具有自动降级功能
- 使用WSS(WebSocket Secure)连接,更可能通过防火墙
- 提供长轮询作为备选方案
3. 连接被意外关闭
解决方案:
- 实现健壮的重连机制
- 使用心跳检测保持连接活跃
- 在后端设置更长的超时时间
// 简单心跳实现
function setupHeartbeat(socket, intervalMs = 30000) {const heartbeatInterval = setInterval(() => {if (socket.readyState === WebSocket.OPEN) {socket.send(JSON.stringify({ type: 'heartbeat' }));} else {clearInterval(heartbeatInterval);}}, intervalMs);return {stop: () => clearInterval(heartbeatInterval)};
}// 使用
const socket = new WebSocket('wss://example.com/socket');
let heartbeat;socket.onopen = () => {heartbeat = setupHeartbeat(socket);
};socket.onclose = () => {if (heartbeat) {heartbeat.stop();}
};
4. 消息顺序问题
WebSocket通常保持消息顺序,但在网络问题或重连时可能出现问题。
解决方案:
- 为消息添加序列号
- 在客户端实现重排序逻辑
- 考虑使用确认机制
// 添加序列号的消息发送
let messageCounter = 0;function sendOrderedMessage(socket, data) {const message = {...data,seq: messageCounter++};socket.send(JSON.stringify(message));
}// 客户端接收和排序
const messageBuffer = [];
let expectedSeq = 0;socket.onmessage = (event) => {const message = JSON.parse(event.data);// 如果是期望的下一个消息,直接处理if (message.seq === expectedSeq) {processMessage(message);expectedSeq++;// 检查缓冲区是否有可以处理的消息checkBufferedMessages();} else if (message.seq > expectedSeq) {// 收到了未来的消息,先缓存messageBuffer.push(message);messageBuffer.sort((a, b) => a.seq - b.seq);}// 忽略已处理的消息(seq < expectedSeq)
};function checkBufferedMessages() {while (messageBuffer.length > 0 && messageBuffer[0].seq === expectedSeq) {const message = messageBuffer.shift();processMessage(message);expectedSeq++;}
}
总结与最佳实践
WebSocket是实现实时Web应用的强大工具,它彻底改变了我们构建互动性应用的方式。总结一下使用WebSocket的最佳实践:
1. 连接管理
- 使用WSS而非WS以确保安全
- 实现健壮的重连机制
- 使用心跳保持连接活跃
2. 消息处理
- 为大型消息使用压缩
- 考虑批量处理多个小消息
- 对于高流量应用,使用二进制格式如MessagePack或Protocol Buffers
- 实现消息确认机制以确保可靠性
3. 安全性
- 始终验证用户身份和权限
- 限制消息速率和大小
- 验证所有输入数据
- 不要通过WebSocket传输敏感信息,除非使用端到端加密
4. 可扩展性
- 设计支持水平扩展的架构
- 使用消息队列系统如RabbitMQ或Kafka管理大量连接
- 考虑使用Redis等工具进行会话共享
5. 架构考虑
- 为不支持WebSocket的环境提供降级方案
- 考虑将推送通知与WebSocket结合,以便在应用未运行时通知用户
- 监控WebSocket服务器性能和连接状态
结语
通过本文,我们从零开始详细探讨了WebSocket技术,从基本概念到实际应用,再到高级主题和最佳实践。WebSocket为Web应用提供了强大的实时通信能力,使开发者能够创建更具交互性和响应性的用户体验。
随着物联网、在线游戏和协作工具的发展,WebSocket技术将继续扮演重要角色。掌握WebSocket不仅可以丰富你的技术栈,还能帮助你构建下一代的实时Web应用。
希望这篇文章对你理解和应用WebSocket技术有所帮助。如果你有任何问题或建议,欢迎在评论区留言讨论!
参考资料
- WebSocket API - MDN Web Docs
- RFC 6455 - WebSocket协议
- Socket.io 官方文档
- SockJS 官方文档
- ws: Node.js WebSocket库
相关文章:
WebSocket 技术详解
引言 在现代Web应用中,实时通信已经成为不可或缺的一部分。想象一下聊天应用、在线游戏、股票交易平台或协作工具,这些应用都需要服务器能够即时将更新推送给客户端,而不仅仅是等待客户端请求。WebSocket技术应运而生,它提供了一…...
微服务即时通信系统---(四)框架学习
目录 ElasticSearch 介绍 安装 安装kibana ES客户端安装 头文件包含和编译时链接库 ES核心概念 索引(Index) 类型(Type) 字段(Field) 映射(mapping) 文档(document) ES对比MySQL Kibana访问ES测试 创建索引库 新增数据 查看并搜索数据 删除索引 ES…...
日常记录-CentOS 9安装java17
文章目录 前言一、手动安装 Oracle JDK 17 或 OpenJDK 17(适合自定义路径)二、使用 CentOS 9 系统包安装 OpenJDK 17(简单稳定)三、使用 SDKMAN(管理多个版本)总结 前言 CentOS 9安装java17 一、手动安装 …...
Python 导出 PDF(ReportLab )
文章目录 1. ReportLab 使用1.1. 安装 ReportLab1.2. 创建 PDF 文件1.3. 使用文档模板 DocTemplate1.4. 使用页面模板 PageTemplate1.5. 继承 BaseDocTemplate1.6. 使用 SimpleDocTemplate1.7. 继承Canvas1.8. 直接使用Canvas 2. 字体与编码3. PLATYPUS - 页面布局和排版3.1. 设…...
私域运营的底层逻辑:从流量到留存的进阶之路
私域流量已成为企业营销的新战场,但盲目跟风只会事倍功半。 接下来,我将深入剖析私域运营的底层逻辑,从几个关键环节,助你构建高效稳定的私域体系。 一、价值优先:以用户需求为核心 私域运营并非简单的粉丝积累&…...
【数据结构 · 初阶】- 带头双向循环链表
目录 1.尾插 2.初始化 3.尾删、头插、头删 4.查找,返回 pos 指针 5.pos 前插入 优化头插,直接复用 优化尾插,直接复用 6.pos 位删除 头删尾删简化 7.销毁 整体代码 List.h List.c Test.c 循环:1.尾 next 指向哨兵位…...
Cube IDE常用快捷键
STM32CubeIDE常用快捷键 STM32CubeIDE快捷键很多,可以通过 Help > Show Active Keybindings… 查看当前可用快捷键;也可以在 Window > Preferences > General > Keys 中查看修改快捷键 快捷键快捷键说明Ctrl/注释行/取消注释行CtrlD删除行…...
C++开发中的DUMP文件:解决崩溃与性能问题的利器(全文字数2w+)
[外链图片转存中…(img-mf6LznjF-1744717065188)] 文章目录 前言为什么需要了解DUMPDUMP在C开发中的重要性 一、DUMP基础概念1. 什么是DUMP文件2. DUMP文件的类型3. DUMP文件的作用(1)调试程序崩溃(2)分析程序性能(3&a…...
Golang|接口并发测试和压力测试
文章目录 这里出现某些奖品和数据库中库存量不一致的问题原因就是在并发的情况下,sync.Map仍然会出现脏写问题,就是在同时操作下的操作覆盖问题可以先把数据放到channel里,然后用一个单一的协程负责读取channel并写入map...
解决 Maven 500 错误:无法传输 maven-metadata.xml 文件
在使用 Maven 构建和管理 Java 项目时,可能会遇到类似以下的错误信息: [WARNING] Could not transfer metadata com.ha:xxx-model:2025.0.1.SNAPSHOT/maven-metadata.xml from/to public (http://xxx.xx.xx.xx/repository/maven-public): status code: …...
鸿蒙应用开发—鸿蒙app一键安装脚本
背景 当鸿蒙App开发完后需要提测,如何将App文件发给QA安装测试,是一件麻烦事,因为鸿蒙App并不能像Android Apk那样可以直接安装到设备中,能想到的方式有: 直接叫测试拿手机过来安装让测试安装DevEco Studio 拉代码编…...
opencv二值化实验
二值化实验 1二值化说明2 阈值法(THRESH_BINARY)3.反阈值法(THRESH_BINARY_INV)4截断阈值法(THRESH_TRUNC)5 低阈值零处理(THRESH_TOZERO)6 超阈值零处理(THRESH_TOZERO_…...
3DGS之渲染管线
渲染管线(Rendering Pipeline)是计算机图形学中将三维场景转换为二维屏幕图像的核心流程,涉及CPU与GPU的分工协作。计算机图形学把渲染管线分为三个阶段:应用程序阶段、几何阶段、光栅化阶段。渲染管线的一般流程是:顶…...
C#设计模式-状态模式
状态模式案例解析:三态循环灯的实现 案例概述 本案例使用 状态模式(State Pattern) 实现了一个 三态循环灯 的功能。每点击一次按钮,灯的状态会按顺序切换(状态1 → 状态2 → 状态3 → 状态1...)ÿ…...
泛微相关文档以及相关安装包下载
泛微相关文档以及相关安装包下载 泛微相关安装包下载泛微相关安装包下载 泛微E10登录网址:https://www.e-cology.com.cn/login?service=https%3A%2F%2Fwww.e-cology.com.cn%2F Ecode使用说明:https://e-cloudstore.com/doc.html 泛微组件库:https://cloudstore.e-cology…...
软件包安装管理Gitlab
官方提供了非常详尽的系统及自动化脚本安装教程 Gitlab官网下载地址:https://gitlab.cn/install/ 1、安装配置 今天我们说一下包安装管理,这样方便我们自己更精确的制定符合我们自己需要的Gitlab仓库 配置:ubuntu2004(focal) 4C8G 下载程…...
在Java使用rest Client操作ES
1. 导入restClient依赖 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.12.1</version></dependency> 2. 了解ES核心客户端API 核心区别…...
深入解析Linux软件包管理:apt/yum源配置与Vim编辑器高效使用指南
一、Linux软件包管理与开发工具 1.软件包管理器与Linux软件生态 软件包管理器的作用与分类 什么是软件包? 在Linux下安装软件,一个通常的办法是下载到程序的源代码,并进行编译,得到可执行程序。但是这样太麻烦了,于…...
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
<view class"gundongBox"><!-- 滚动展示信息的模块 --><image class"imgWid" :src"imgurlgundong.png" mode"widthFix"></image><view class"gundongView"><view class"container&qu…...
记录 | Pycharm中如何调用Anaconda的虚拟环境
目录 前言一、步骤Step1 查看anaconda 环境名Step2 Python项目编译器更改 更新时间 前言 参考文章: 参考视频:如何在pycharm中使用Anaconda创建的python环境 自己的感想 这里使用的Pycharm 2024专业版的。我所使用的Pycharm专业版位置:【仅用…...
静态站点生成
以下是关于 静态站点生成(SSG) 的系统知识梳理,涵盖核心概念、核心实现、数据管理与优化等内容: 一、核心概念与优势 定义 静态站点生成(SSG)是在构建阶段预生成所有静态HTML文件的技术,用户访问时直接获取预渲染内容,无需服务器动态生成。 核心优势 性能卓越:CDN缓存…...
Android Jni(二)加载调用第三方 so 库
文章目录 Android Jni(二)加载调用第三方 so 库前置知识CPU架构 ABI 基本步骤1、将第三方 SO 库文件放入项目中的正确位置:2. 创建 JNI 接口3. 实现 JNI 层代码4、配置 CMake 常见问题解决1、UnsatisfiedLinkError:2、函数找不到&…...
解锁元生代:ComfyUI工作流与云原生后端的深度融合
目录 蓝耘元生代:智算新势力崛起 ComfyUI 工作流创建详解 ComfyUI 初印象 蓝耘平台上搭建 ComfyUI 工作流 构建基础工作流实操 代码示例与原理剖析 云原生后端技术全景 云原生后端概念解析 核心技术深度解读 蓝耘元生代中两者的紧密联系…...
LeetCode算法题(Go语言实现)_47
题目 给你一个 m x n 的迷宫矩阵 maze (下标从 0 开始),矩阵中有空格子(用 ‘.’ 表示)和墙(用 ‘’ 表示)。同时给你迷宫的入口 entrance ,用 entrance [entrancerow, entrancecol…...
树莓派_利用Ubuntu搭建gitlab
树莓派_利用Ubuntu搭建gitlab 一、给树莓派3A搭建基本系统 1、下载系统镜像 https://cdimage.ubuntu.com/ubuntu/releases/18.04/release/ 2、准备系统SD卡 二、给树莓派设备联网 1、串口后台登录 使用串口登录后台是最便捷的,因为前期网络可能不好直接成功 默…...
vi(vim)编辑器和root用户与普通用户之间的转换
vim编辑器是vi编辑器的加强版,以vi为例: vi编辑器: vi编辑器可以编辑文件内容 如何进入vi编辑器? 语法: vi 文件路径 如何退出? 语法: wq:保存退出 w:保存 q&…...
【vscode】vscode链接关联github/gitlab
一、windows下载安装git Git - Downloading Package 二、配置Git的用户名和邮箱 Git Bash运行以下命令来配置Git的用户名和邮箱: git config --global user.name "你的用户名" git config --global user.email "你的邮箱地址" 生成本机秘钥…...
Redis面试问题缓存相关详解
Redis面试问题缓存相关详解 一、缓存三兄弟(穿透、击穿、雪崩) 1. 穿透 问题描述: 缓存穿透是指查询一个数据库中不存在的数据,由于缓存不会保存这样的数据,每次都会穿透到数据库,导致数据库压力增大。例…...
Web三漏洞学习(其一:文件上传漏洞)
靶场:云曦历年考核题 一、文件上传 在此之前先准备一个一句话木马 将其命名为muma.txt 23年秋期末考 来给师兄上个马 打开环境以后直接上传muma.txt,出现js弹窗,说明有前端验证 提示只能上传.png .jpg 和 .gif文件,那就把muma.txt的后缀…...
冲刺高分!挑战7天一篇nhanes机器学习SCI!DAY1-7
医学生集合啦,继续挑战 7天一篇nhanes机器学习SCI! Day 1 进展:确定选题、期刊、文献 前面挑战了一期NHANES机器学习,大家使用NHANES的发文章的热情,火爆程度远超想象!我在下面的评论区看到大家的学习欲…...
高并发三剑客-本地缓存之王Caffeine-01缓存应用
1 分布式缓存使用及导致的问题 1.1 hotkey典型业务场景 常规性hotkey:可以提前评估出hotkey的场景,比如:重要节假日、促销活动等 突发性hotkey:没法提前评估,突发性行为,比如:突然新闻、爆炸信息…...
基于Java,SpringBoot,Vue,HTML家政服务预约系统设计
摘要 本文聚焦于基于Java、SpringBoot、Vue和HTML技术的家政服务预约系统的设计与实现。该系统旨在为家政服务的供需双方搭建一个便捷、高效的在线交互平台。后端采用Java语言结合SpringBoot框架,充分利用SpringBoot的自动配置和快速开发特性,实现系统业…...
系统架构设计师:系统架构概述知识体系、考点详解、高效记忆要点、练习题并提供答案与解析
一、系统架构概述知识体系、考点详解 系统架构概述、定义与作用 1. 系统架构的定义与核心要素 系统架构是复杂系统的高层次组织结构,包含硬件/软件组件、交互关系、设计原则及演进策略。其核心要素包括: 构件与模式:现代架构三要素为构件…...
汽配快车道解决chrome backgroud.js(Service Worker) XMLHttpRequest is not defined问题
Chrome 扩展开发:Service Worker 中如何优雅地发送 HTTP 请求 在 Chrome 扩展开发中,Service Worker 是一个非常重要的部分,它可以帮助我们实现很多强大的功能。然而,如果你在 Service Worker 中尝试使用 XMLHttpRequest 来发送 …...
VMware Ubuntu挂载Windows机器的共享文件
https://www.dong-blog.fun/post/2029 在VMware Ubuntu中访问Windows共享文件夹:完整指南 在使用VMware运行Ubuntu虚拟机时,访问Windows主机上的文件是常见需求。本文将详细介绍如何通过网络共享方式,让Ubuntu虚拟机直接访问Windows主机的文…...
LeNet神经网络
一、LeNet概述 1. 历史地位 开创性模型:首个成功应用的卷积神经网络(1998年)应用场景:手写数字识别(MNIST数据集)、银行支票识别提出者:Yann LeCun团队(论文《Gradient-Based Lear…...
Visio绘图工具全面科普:解锁专业图表绘制新境界[特殊字符]
Visio绘图工具全面科普:解锁专业图表绘制新境界🌟 在信息爆炸的时代,清晰、直观地呈现复杂信息变得至关重要。无论是绘制流程图📊、组织结构图👥,还是规划网络拓扑🖧,一款强大的绘图…...
ECharts散点图-散点图3,附视频讲解与代码下载
引言: ECharts散点图是一种常见的数据可视化图表类型,它通过在二维坐标系或其它坐标系中绘制散乱的点来展示数据之间的关系。本文将详细介绍如何使用ECharts库实现一个散点图,包括图表效果预览、视频讲解及代码下载,让你轻松掌握…...
D3路网图技术文档
在本文档中,我们将探讨如何使用 D3.js,结合 SVG(可缩放矢量图形)和 Canvas,来实现高效、交互性强的路网图效果。D3.js 是一个强大的 JavaScript 数据可视化库,可以基于数据驱动文档对象模型(DOM…...
Unity 一些小功能(屏幕画画,)
利用 Line Renderer 实现屏幕画画并保存图片 // 当前正在绘制的 LineRendererprivate LineRenderer currentLineRenderer;// 用于保存所有笔触的列表private List<LineRenderer> allLineRenderers new List<LineRenderer>();// 当前笔触顶点计数器private int ve…...
列表、字符串、heapq堆对列算法
1、列表 1.1 访问最后一个元素 list [1, 2, 3, 4, 5, 6] print(list[-1]) # 61.2 访问列表指定范围,例如第1-4个元素 print(list[0, 4]) # [1, 2, 3, 4]1.3 将字符列表以字符串形式输出 list [h, e, l, l, o] ls "".join(list) print(ls) # "…...
实战指南:封装Whisper为FastAPI接口并实现高并发处理
下面给出一个详细的示例,说明如何使用 FastAPI 封装 OpenAI 的 Whisper 模型,提供一个对外的 REST API 接口,并支持一定的并发请求。 下面是主要步骤和示例代码。 1. 环境准备 Python 环境: 建议使用 Python 3.8。依赖库&#x…...
Arm系统ubuntu20.04中自带的火狐浏览器打开网页B站视频和百度网盘网页视频,视频无法打开,并且没有声音——(本质上解决)
自己工作过程中有一台Orin nx arm设备,所以希望能够用这台设备看视频学习,发现自带的firefox打开网页版百度网盘和B站的时候无法打开视频,而且用有线耳机发现没有声音,最后换掉浏览器,用Chromium。 1、如果还想继续用…...
Qt 自定义控件
在 Qt 中,自定义控件是通过继承现有的 Qt 控件类(如 QWidget、QPushButton、QLabel 等)并重载相关的事件处理函数或绘制函数来实现的。自定义控件允许你根据需求添加特定的功能或样式。 自定义控件的基本步骤 1. 继承 Qt 控件类:…...
Java使用WebSocket视频拆帧进度处理与拆帧图片推送,结合Apipost进行调试
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>Configuration public class WebSocketConfig {/*** 启动 WebSocket 服务器*/Beanpublic ServerEndpointE…...
Java项目之基于ssm的QQ村旅游网站的设计(源码+文档)
项目简介 QQ村旅游网站实现了以下功能: 管理员权限操作的功能包括管理景点路线,板块信息,留言板信息,旅游景点信息,酒店信息,对景点留言,景点路线留言以及酒店留言信息等进行回复,…...
《 Reinforcement Learning for Education: Opportunities and Challenges》全文阅读
Reinforcement Learning for Education: Opportunities and Challenges 面向教育的强化学习:机遇与挑战 摘要 本综述文章源自作者在 Educational Data Mining (EDM) 2021 会议期间组织的 RL4ED 研讨会。我们组织了这一研讨会,作为一项社区建设工作的组…...
Apache Kafka UI :一款功能丰富且美观的 Kafka 开源管理平台!!
Apache Kafka UI 是一个免费的开源 Web UI,用于监控和管理 Apache Kafka 集群,可方便地查看 Kafka Brokers、Topics、消息、Consumer 等情况,支持多集群管理、性能监控、访问控制等功能。 1 特征 多集群管理: 在一个地方监控和管理…...
无参数RCE
无参数RCE(Remote Code Execution,远程代码执行) 是一种通过利用目标系统中的漏洞,在不直接传递用户可控参数的情况下,实现远程执行任意代码的攻击技术。与传统的RCE攻击不同,无参数RCE不依赖外部输入参数…...
设计模式之状态模式:优雅管理对象行为变化
引言 状态模式(State Pattern)是一种行为型设计模式,它允许对象在其内部状态改变时改变它的行为,使对象看起来似乎修改了它的类。状态模式将状态转移逻辑和状态相关行为封装在独立的状态类中,完美解决了复杂条件判断问…...