详解WebSocket及其妙用
一、认知WebSocket:它为何能替代HTTP轮询?
要理解WebSocket的价值,首先要明确它解决的核心痛点——HTTP协议在实时通信场景中的天然缺陷。我们最熟悉的HTTP协议(尤其是1.1/2.0版本),本质是「请求-响应」模式的单向、无状态、短连接协议,即便HTTP/1.1支持Keep-Alive伪长连接,也只是复用连接而非真正持久,且每次通信都需携带完整的HTTP头部,开销较大。
1. HTTP实时通信的折中方案及其弊端
为了实现“服务端主动推送数据”的实时需求,早期开发者只能采用「轮询」和「长轮询」两种折中方案,但弊端极为明显:
- 短轮询:客户端每隔几秒发送一次HTTP请求,服务端无论有无新数据都返回响应。这种方式资源浪费严重——频繁建立/释放TCP连接、重复传输冗余的HTTP头部,且存在明显延迟(数据更新后需等待下一次轮询才能获取);
- 长轮询:客户端发送请求后,服务端挂起连接,直到有新数据或超时才返回响应,客户端收到后立即重连。虽减少了请求次数,但仍存在HTTP头部开销,挂起的连接会占用服务端大量资源,且易出现连接超时问题。
这两种方案本质是“用HTTP模拟实时通信”,属于治标不治本。而WebSocket的出现,彻底解决了这些痛点——它将“频繁的单向请求”转化为“一次连接的双向通信”,实现了开销极低、延迟毫秒级、全双工的实时交互。
2. WebSocket核心定义
WebSocket是一种基于TCP协议的全双工、持久化的应用层通信协议,它通过一次HTTP握手建立客户端与服务端的持久连接,之后的所有通信都直接在这个TCP连接上进行,脱离HTTP协议的束缚,支持客户端与服务端随时互发文本、二进制数据。
3. 一句话看懂HTTP与WebSocket的区别
- HTTP:客户端问、服务端答,单向通信,像“写信”(必须客户端先发起请求,服务端才能响应);- WebSocket:客户端与服务端随时互发消息,双向通信,像“打电话”(一次接通,双方可随时发言)。
二、WebSocket核心原理:一次握手,终身连通
WebSocket并非完全脱离HTTP,而是借助HTTP完成“握手升级”,之后切换到独立的WebSocket帧协议进行数据传输,整个生命周期分为「握手建立连接→双向数据传输→连接关闭」三步,全程基于TCP可靠连接。
1. 第一步:HTTP握手升级(核心环节)
客户端通过浏览器原生API或第三方库创建WebSocket实例时,会向服务端发送一个特殊的HTTP GET请求,核心目的是“告知服务端要升级为WebSocket协议”。
客户端握手请求(关键头部缺一不可)
GET /ws HTTP/1.1
Host: localhost:3000
Upgrade: websocket # 声明要升级的目标协议为WebSocket
Connection: Upgrade # 声明连接类型为“协议升级”
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== # 随机字符串,用于服务端验证(非加密)
Sec-WebSocket-Version: 13 # 固定版本,主流浏览器均支持
Origin: https://your-domain.com # 跨域时的客户端源,用于CORS校验服务端握手响应(协议切换确认)
服务端解析到上述请求头后,若同意升级,会返回HTTP 101 Switching Protocols(协议切换成功)状态码,并通过响应头返回验证结果,此时HTTP连接正式升级为WebSocket连接。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= # 服务端加密后的验证结果验证逻辑(防误连接)
服务端将客户端发送的Sec-WebSocket-Key,拼接上固定的“魔法字符串”(258EAFA5-E914-47DA-95CA-C5AB0DC85B11),经过SHA1加密+Base64编码后,生成Sec-WebSocket-Accept,客户端验证该值与自己计算的结果一致后,确认握手成功,避免普通HTTP请求被当作WebSocket连接处理。
2. 第二步:双向数据传输(WebSocket帧协议)
握手成功后,TCP连接保持打开状态,双方不再使用HTTP协议,而是通过WebSocket专属的「帧(Frame)」传输数据——帧是WebSocket的最小通信单位,仅包含2~10字节的帧头(记录数据类型、长度、掩码等信息)和数据体,开销远小于HTTP头部(几百字节)。- 客户端发消息:通过send()方法发送文本或二进制数据,浏览器自动将其封装为帧,发送给服务端;- 服务端发消息:直接通过连接对象发送帧,浏览器自动解析帧数据,触发onmessage事件,供客户端处理。
3. 第三步:连接关闭(优雅释放资源)
任意一方均可主动发起关闭请求:发送「Close控制帧」(opcode=0x08),对方收到后返回确认帧,之后基于TCP的四次挥手关闭连接,触发双方的onclose事件。若网络中断、服务端宕机等导致被动断开,客户端和服务端会通过后续的心跳机制感知。
三、WebSocket核心特点(决定其“妙用”的关键)
结合HTTP和轮询的短板,WebSocket的核心优势一目了然,具体对比如下:
特性 | HTTP 1.1 | 轮询/长轮询 | WebSocket |
通信方向 | 单向(客户端主动请求) | 伪双向(客户端触发) | 全双工(双向同时通信) |
连接状态 | 短连接/伪长连接 | 频繁重连 | 持久连接(一次建连,长期复用) |
头部开销 | 大(每次~几百字节) | 大(同HTTP) | 极小(帧头仅2~10字节) |
服务端主动推送 | 不支持 | 模拟支持(挂起连接) | 原生支持 |
延迟 | 无(请求-响应即时) | 高(轮询间隔/超时) | 极低(毫秒级实时传输) |
除此之外,WebSocket还有两个实用特性,适配更多场景:
- 跨域支持:遵循与HTTP一致的CORS规则,服务端配置跨域允许后,即可实现跨域通信,无需额外代理;
- 多数据类型支持:可传输文本数据(如JSON、字符串)和二进制数据(如图片、视频片段、设备数据);
- 加密传输:通过wss://协议实现(对应HTTP的https://),基于TLS/SSL加密,生产环境必须使用(ws://为明文传输,存在安全风险)。
四、WebSocket实操实现:前端+后端完整可运行示例
WebSocket前端有浏览器原生API(无需引入任何库),后端主流语言(Node.js、Java、Go、Python)均有成熟库,以下采用「Node.js + ws库」(最轻量、最流行的WebSocket后端库)+「原生JS前端」,实现简单的双向群聊功能,覆盖WebSocket核心操作。
1. 前置准备(后端)
首先安装Node.js(已安装可跳过),然后初始化项目并安装ws库:
npm init -y # 初始化项目(生成package.json)
npm install ws # 安装WebSocket后端库ws2. 后端代码(server.js)
创建WebSocket服务,监听客户端连接、接收消息、广播消息(群聊核心)、处理连接关闭和错误:
// 引入ws库
const WebSocket = require('ws');
// 创建WebSocket服务,监听3000端口
const wss = new WebSocket.Server({ port: 3000 });
console.log('WebSocket服务已启动,监听ws://localhost:3000');
// 监听「新客户端连接」事件
wss.on('connection', (ws) => {
console.log('一个新客户端已连接');
// 监听「接收客户端消息」事件
ws.on('message', (data) => {
console.log(`收到客户端消息:${data}`);
// 广播消息:发给所有已连接的客户端(群聊核心逻辑)
wss.clients.forEach((client) => {
// 仅向处于“打开状态”的客户端发送消息
if (client.readyState === WebSocket.OPEN) {
client.send(`服务端转发:${data}`);
}
});
});
// 监听「客户端连接关闭」事件
ws.on('close', () => {
console.log('一个客户端已断开连接');
});
// 监听「连接错误」事件
ws.on('error', (err) => {
console.log('连接出错:', err.message);
});
// 服务端主动向客户端发送欢迎消息(连接成功后)
ws.send('恭喜你,成功连接WebSocket服务!');
});3. 前端代码(index.html)
使用浏览器原生WebSocket API,创建连接、发送消息、接收消息、展示消息,实现简单的聊天界面:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 前端示例(群聊)</title>
<style>
.container { width: 600px; margin: 30px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; }
.title { text-align: center; color: #333; margin-bottom: 20px; }
.chat-box { height: 400px; border: 1px solid #eee; border-radius: 4px; padding: 10px; margin-bottom: 20px; overflow-y: auto; }
.msg-item { margin: 10px 0; padding: 8px 12px; border-radius: 6px; max-width: 70%; }
.system-msg { background: #f5f5f5; color: #666; text-align: center; margin: 10px auto; }
.self-msg { background: #4CAF50; color: white; margin-left: auto; }
.other-msg { background: #e9e9eb; color: #333; }
.input-area { display: flex; gap: 10px; }
#msgInput { flex: 1; padding: 10px; border: 1px solid #eee; border-radius: 4px; outline: none; }
#sendBtn { padding: 10px 20px; border: none; background: #4CAF50; color: white; border-radius: 4px; cursor: pointer; }
#sendBtn:disabled { background: #ccc; cursor: not-allowed; }
</style>
</head>
<body>
<div class="container">
<h1 class="title">WebSocket 群聊示例</h1>
<div class="chat-box" id="chatBox">
<div class="msg-item system-msg">欢迎进入群聊,可发送消息与所有在线用户互动!</div>
</div>
<div class="input-area">
<input type="text" id="msgInput" placeholder="请输入消息..." autocomplete="off">
<button id="sendBtn">发送</button>
</div>
</div>
<script>
// 连接后端WebSocket服务(与server.js监听的端口一致)
const ws = new WebSocket('ws://localhost:3000');
// DOM元素
const chatBox = document.getElementById('chatBox');
const msgInput = document.getElementById('msgInput');
const sendBtn = document.getElementById('sendBtn');
// 连接成功事件
ws.onopen = () => {
addMsg('系统提示:成功连接群聊服务!', 'system');
sendBtn.disabled = false; // 连接成功后启用发送按钮
};
// 接收服务端消息事件
ws.onmessage = (e) => {
// 服务端转发的消息,作为其他用户消息展示
addMsg(e.data, 'other');
};
// 连接关闭事件
ws.onclose = () => {
addMsg('系统提示:群聊连接已关闭!', 'system');
sendBtn.disabled = true;
};
// 连接错误事件
ws.onerror = (err) => {
addMsg(`系统提示:连接出错!${err.message}`, 'system');
sendBtn.disabled = true;
};
// 发送消息按钮点击事件
sendBtn.addEventListener('click', sendMsg);
// 输入框回车发送消息
msgInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') sendMsg();
});
// 发送消息函数
function sendMsg() {
const msg = msgInput.value.trim();
if (!msg) {
alert('请输入有效消息!');
return;
}
// 向服务端发送消息
ws.send(msg);
// 本地展示自己发送的消息
addMsg(`我:${msg}`, 'self');
// 清空输入框
msgInput.value = '';
}
// 追加消息到聊天框
function addMsg(content, type) {
const msgItem = document.createElement('div');
msgItem.className = `msg-item ${type}-msg`;
msgItem.textContent = content;
chatBox.appendChild(msgItem);
// 滚动到最新消息
chatBox.scrollTop = chatBox.scrollHeight;
}
</script>
</body>
</html>4. 运行测试
- 启动后端:在终端执行「node server.js」,控制台显示“WebSocket服务已启动,监听ws://localhost:3000”,说明服务启动成功;
- 启动前端:直接用浏览器打开index.html文件,可打开多个标签页(模拟多个客户端);
- 测试交互:在任意一个标签页输入消息并点击“发送”,所有打开的标签页都会实时收到服务端转发的消息,实现群聊效果。
该示例覆盖了WebSocket的核心操作:建立连接、发送消息、接收消息、关闭连接、错误处理,是所有WebSocket实战场景的基础。
五、必备技巧:心跳保活+断线重连(解决连接假死)
实际开发中,网络波动(如断网、路由器重启)、中间设备(防火墙、代理)超时、服务端重启等,会导致WebSocket连接被动断开,但客户端和服务端可能“不知情”(连接处于“假死”状态),此时发送消息会失败。因此,心跳保活+断线重连是生产环境WebSocket应用的必备功能。
1. 心跳保活(检测连接状态)
核心逻辑:客户端每隔固定时间(如10~30秒)向服务端发送一个“心跳包”(如空消息、{type: 'ping'}),服务端收到后返回“pong”响应;若客户端超过指定时间(如30秒)未收到pong响应,判定连接假死,触发断线重连。
2. 断线重连(恢复连接)
核心逻辑:当触发onclose、onerror事件,或心跳检测判定连接假死后,自动尝试重新建立连接;为避免频繁重连压垮服务端,需采用「指数退避策略」(如1秒、2秒、4秒、8秒,最多重连5次,超过则停止)。
3. 前端集成心跳+重连代码(修改index.html的script部分)
const ws = new WebSocket('ws://localhost:3000');
const msgInput = document.getElementById('msgInput');
const msgList = document.getElementById('msgList');
// 心跳相关配置
let heartBeatTimer = null; // 心跳定时器
const HEART_BEAT_INTERVAL = 10000; // 心跳间隔:10秒
let reconnectTimer = null; // 重连定时器
let reconnectCount = 0; // 重连次数
const MAX_RECONNECT_COUNT = 5; // 最大重连次数
// 连接成功:启动心跳
ws.onopen = () => {
addMsg('系统提示:WebSocket连接成功!');
startHeartBeat();
reconnectCount = 0; // 连接成功,重置重连次数
clearTimeout(reconnectTimer); // 清除重连定时器
};
// 接收服务端消息
ws.onmessage = (e) => {
addMsg(e.data);
};
// 连接关闭:触发重连
ws.onclose = (e) => {
addMsg(`系统提示:连接已关闭,状态码:${e.code}`);
stopHeartBeat(); // 停止心跳
reconnect(); // 触发重连
};
// 连接错误:触发重连
ws.onerror = (err) => {
addMsg(`系统提示:连接出错!${err.message}`);
stopHeartBeat();
reconnect();
};
// 监听服务端的pong响应(心跳正常)
ws.onpong = () => {
console.log('收到服务端pong,心跳正常');
};
// 启动心跳:定时发送ping
function startHeartBeat() {
clearInterval(heartBeatTimer);
heartBeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping(); // 发送ping心跳包(原生API)
console.log('发送心跳ping');
}
}, HEART_BEAT_INTERVAL);
}
// 停止心跳
function stopHeartBeat() {
clearInterval(heartBeatTimer);
}
// 断线重连(指数退避策略)
function reconnect() {
if (reconnectCount >= MAX_RECONNECT_COUNT) {
addMsg('系统提示:重连5次失败,停止重连,请手动刷新页面');
return;
}
reconnectCount++;
const delay = Math.pow(2, reconnectCount) * 1000; // 1s→2s→4s→8s...
reconnectTimer = setTimeout(() => {
addMsg(`系统提示:第${reconnectCount}次重连...`);
// 重新创建WebSocket连接
window.location.reload(); // 简单方式:刷新页面重建连接
// 更优雅方式:new WebSocket('ws://localhost:3000'),重新绑定所有事件
}, delay);
}
// 发送消息、追加消息方法(不变,省略重复代码)
// ...4. 后端心跳处理(无需额外代码)
ws库会自动接收客户端的ping心跳包,并返回pong响应,无需开发者编写额外代码;若需要服务端主动发送ping心跳,逻辑与客户端一致,调用ws.ping()即可。
六、WebSocket的妙用:实战场景解析(核心价值体现)
WebSocket的“全双工、低延迟、长连接、轻开销”特性,使其成为所有实时通信场景的首选,以下是开发中最常用的实战场景,每个场景均说明「为什么用WebSocket」和「核心实现思路」,覆盖前端、后端、IoT等多个领域。
妙用1:实时聊天/IM系统(最经典场景)
场景:单聊(微信网页版、私信)、群聊(企业微信、钉钉)、在线客服(电商平台)、弹幕聊天(直播平台);为什么用WebSocket:消息需要毫秒级双向推送,轮询的延迟和资源开销完全无法接受;WebSocket一次连接实现全双工通信,能轻松支撑弹幕刷屏、群聊高频消息等场景,服务端压力远低于轮询。实现思路:
- 客户端连接时,服务端用Map存储「用户ID - WebSocket连接对象」的映射,记录用户在线状态;
- 单聊:客户端发送消息时,携带「目标用户ID」,服务端根据映射找到目标连接,直接发送消息;
- 群聊:服务端维护「群ID - 群内用户连接列表」的映射,收到消息后,广播给列表内所有在线客户端;
- 离线消息:若目标用户未连接,将消息存入数据库(如MySQL、Redis),用户重连后,服务端主动推送离线消息。
妙用2:实时数据展示(金融/监控/大屏)
场景:股票/期货/数字货币行情、基金实时净值、服务器监控大屏(CPU/内存/QPS)、物联网设备数据(温湿度、设备运行状态)、电商实时交易大屏(销量、成交额);为什么用WebSocket:数据由服务端主动推送(客户端无需频繁查询),且更新频率高(每秒数次),WebSocket能实现毫秒级推送,且轻量帧开销极低,适合千/万级客户端同时在线。实现思路:
- 服务端通过定时任务、消息队列、传感器等方式,采集实时数据;
- 客户端连接后,服务端将最新数据主动、持续推送给客户端(可根据客户端需求,推送指定类型的数据);
- 客户端收到数据后,通过ECharts、Canvas等工具,实时渲染图表、大屏,实现数据可视化。
妙用3:协同编辑/在线协作
场景:腾讯文档、飞书文档、石墨文档(在线编辑)、Figma(设计稿协作)、CodeSandbox(代码协同编辑);为什么用WebSocket:多用户同时编辑时,需要实时同步修改内容,一个用户的操作(输入、删除、移动)必须立刻推送给其他所有协作用户,实现“所见即所得”的协作效果。实现思路:
- 采用「操作同步」而非「全量内容同步」(减少数据传输):将用户的每一个操作,封装为“操作指令”(如「在第5行第3列插入字符A」「删除第10行内容」);
- 客户端将操作指令发送给服务端,服务端验证指令合法性后,广播给所有协作客户端;
- 每个客户端收到操作指令后,在本地文档上执行相同操作,确保所有客户端的文档内容一致。
妙用4:服务端主动推送通知(系统告警/业务提醒)
场景:后台系统异常告警(服务器宕机、接口报错)、电商订单提醒(新订单、退款、物流更新)、社交通知(点赞、评论、关注);为什么用WebSocket:传统方案(客户端轮询数据库、第三方推送)要么延迟高,要么依赖第三方(适合APP);WebSocket适合网页端的服务端主动推送,无需依赖第三方,实时性更高,且节省客户端流量。实现思路:
- 客户端登录后,通过Token/UserId建立WebSocket连接,服务端绑定「用户ID - 连接」的映射;
- 业务系统(如订单服务、监控服务)产生通知时,调用WebSocket服务的接口,传递「目标用户ID + 通知内容」;
- WebSocket服务根据用户ID找到对应连接,主动推送通知,客户端收到后,展示为右上角小红点、弹窗等形式。
妙用5:实时游戏(轻量级网页游戏)
场景:网页版贪吃蛇、五子棋、连连看、休闲竞技小游戏(如在线斗地主);为什么用WebSocket:游戏需要多玩家的操作实时同步(如移动、出牌、放技能),且延迟要求极低(否则出现卡顿、不同步);WebSocket基于TCP可靠传输,能保证操作不丢失,且轻量帧适合频繁的小数据传输(重度游戏用原生Socket,WebSocket适合轻量网页游戏)。实现思路:
- 服务端维护游戏的“全局状态”(如玩家位置、游戏进度、分数、剩余时间);
- 玩家的操作(如方向键、点击)封装为指令,通过WebSocket发送给服务端;
- 服务端更新全局状态后,将最新状态广播给所有玩家客户端;
- 客户端根据全局状态,实时渲染游戏画面,确保所有玩家看到的游戏状态一致。
妙用6:直播互动(弹幕/点赞/礼物)
场景:B站、虎牙、抖音网页版直播的弹幕、实时点赞数、礼物飘屏、评论实时刷新;为什么用WebSocket:直播互动的实时性要求高(弹幕、点赞需秒级展示),且并发量高(上万甚至几十万观众);WebSocket单连接持续传输,帧开销极小,服务端通过广播机制,能快速将互动信息推送给所有在线观众,效率远高于轮询。实现思路:
- 观众进入直播间时,连接该直播间对应的WebSocket“房间”,服务端维护「直播间ID - 观众连接列表」;
- 观众发送弹幕、点赞、赠送礼物时,客户端将数据发送给服务端,服务端做简单限流(防刷屏)后,广播给该直播间的所有观众;
- 客户端收到数据后,通过前端特效渲染(如弹幕滚动、点赞动画、礼物飘屏),提升互动体验。
妙用7:物联网(IoT)设备通信
场景:智能摄像头、温湿度传感器、智能家居(空调、灯光)、工业设备监控;为什么用WebSocket:IoT设备通常带宽有限、性能较低,WebSocket轻量帧适合小数据传输,且持久连接避免频繁建连的开销;双向通信同时满足“设备上报数据”(如传感器每1秒上报温湿度)和“服务端下发指令”(如手机APP控制空调开机)。实现思路:
- IoT设备启动后,主动建立WebSocket连接,服务端记录「设备ID - 连接」的映射;
- 设备上报数据:定时将采集到的数据(温湿度、运行状态)通过WebSocket发送给服务端,服务端存储数据并进行分析;
- 服务端下发指令:用户通过手机APP、网页端发送控制指令(如开机、调节温度),服务端通过WebSocket将指令推送给对应设备,设备执行指令后,返回执行结果。
妙用7 补充:IoT设备通信完整可运行代码
以下采用「Node.js + ws库」(后端)+「原生JS前端(模拟IoT设备+控制端)」,实现IoT温湿度采集+设备控制的完整可运行示例,贴合真实IoT场景,代码可直接复制运行。
1. 前置准备(与前文一致,无需重复安装)
确保已安装Node.js,且项目中已安装ws库(npm install ws),项目结构如下:
websocket-iot-demo/
├─ server.js # WebSocket后端服务(核心)
├─ device.html # 模拟IoT设备(温湿度采集、接收控制指令)
└─ control.html # 控制端(查看设备数据、下发控制指令)2. 后端代码(server.js,IoT专属服务)
核心功能:维护设备连接映射、接收设备上报的温湿度数据、转发控制端指令给指定设备、存储设备最新状态,支持多设备同时在线。
// 引入ws库
const WebSocket = require('ws');
// 创建WebSocket服务,监听3001端口(与群聊示例区分,避免端口冲突)
const wss = new WebSocket.Server({ port: 3001 });
// 存储设备连接:key=设备ID,value=WebSocket连接对象
const deviceMap = new Map();
// 存储设备最新状态:key=设备ID,value={temperature, humidity, power}
const deviceStatus = new Map();
console.log('IoT WebSocket服务已启动,监听ws://localhost:3001');
// 监听新连接(设备/控制端都会连接,通过消息类型区分)
wss.on('connection', (ws) => {
let currentDeviceId = null; // 当前连接的设备ID(控制端为空)
let isControl = false; // 是否是控制端
console.log('有新连接接入');
// 接收客户端(设备/控制端)消息
ws.on('message', (data) => {
try {
// 所有消息统一用JSON格式传输(IoT场景规范)
const msg = JSON.parse(data.toString());
const { type, deviceId, data: msgData, command } = msg;
// 1. 设备注册(设备启动后,先发送注册消息)
if (type === 'deviceRegister') {
currentDeviceId = deviceId;
deviceMap.set(deviceId, ws); // 存储设备连接
// 初始化设备状态(默认关机、温湿度默认值)
deviceStatus.set(deviceId, {
temperature: 25.0, // 默认温度
humidity: 60.0, // 默认湿度
power: false // 默认关机状态
});
console.log(`设备注册成功:${deviceId}`);
// 向设备返回注册成功响应
ws.send(JSON.stringify({ type: 'registerSuccess', msg: '设备注册成功' }));
}
// 2. 设备上报温湿度数据
else if (type === 'reportData') {
if (!currentDeviceId) {
ws.send(JSON.stringify({ type: 'error', msg: '设备未注册,请先注册' }));
return;
}
// 更新设备最新状态
const newStatus = { ...deviceStatus.get(currentDeviceId), ...msgData };
deviceStatus.set(currentDeviceId, newStatus);
console.log(`设备${currentDeviceId}上报数据:`, newStatus);
// (可选)向所有控制端推送设备最新数据(实时同步)
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN && client.isControl) {
client.send(JSON.stringify({
type: 'deviceDataUpdate',
deviceId: currentDeviceId,
data: newStatus
}));
}
});
}
// 3. 控制端注册
else if (type === 'controlRegister') {
isControl = true;
ws.isControl = true; // 标记当前连接为控制端
console.log('控制端注册成功');
// 向控制端返回所有在线设备及最新状态
const allDeviceStatus = Array.from(deviceStatus.entries()).map(([id, status]) => ({
deviceId: id,
...status
}));
ws.send(JSON.stringify({
type: 'controlRegisterSuccess',
onlineDevices: allDeviceStatus
}));
}
// 4. 控制端下发控制指令(如开机、关机、调节阈值)
else if (type === 'sendCommand') {
if (!isControl) {
ws.send(JSON.stringify({ type: 'error', msg: '非控制端,无权下发指令' }));
return;
}
if (!deviceId || !command) {
ws.send(JSON.stringify({ type: 'error', msg: '设备ID和指令不能为空' }));
return;
}
// 查找目标设备连接
const targetDevice = deviceMap.get(deviceId);
if (!targetDevice || targetDevice.readyState !== WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'error', msg: '目标设备离线或未注册' }));
return;
}
// 向目标设备下发指令
targetDevice.send(JSON.stringify({
type: 'controlCommand',
command: command,
from: 'control'
}));
console.log(`控制端向设备${deviceId}下发指令:`, command);
// 向控制端返回指令下发成功响应
ws.send(JSON.stringify({
type: 'commandSuccess',
msg: `指令已下发至设备${deviceId}`,
deviceId,
command
}));
}
// 5. 设备返回指令执行结果
else if (type === 'commandResult') {
if (!currentDeviceId) return;
// 将执行结果转发给所有控制端
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN && client.isControl) {
client.send(JSON.stringify({
type: 'deviceCommandResult',
deviceId: currentDeviceId,
result: msgData
}));
}
});
}
// 未知消息类型
else {
ws.send(JSON.stringify({ type: 'error', msg: '未知消息类型' }));
}
} catch (err) {
console.log('消息解析失败:', err);
ws.send(JSON.stringify({ type: 'error', msg: '消息格式错误,请使用JSON格式' }));
}
});
// 连接关闭(移除设备连接和状态)
ws.on('close', () => {
console.log('连接已断开');
if (currentDeviceId) {
deviceMap.delete(currentDeviceId);
// (可选)保留设备状态,设备重连后恢复
// deviceStatus.delete(currentDeviceId);
console.log(`设备${currentDeviceId}已离线`);
}
});
// 连接错误处理
ws.on('error', (err) => {
console.log('连接出错:', err.message);
});
});3. 模拟IoT设备代码(device.html)
功能:模拟温湿度传感器,自动注册设备、每隔2秒采集并上报温湿度数据、接收控制端下发的指令(开机/关机)、返回指令执行结果,页面实时展示设备状态。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>IoT设备模拟(温湿度传感器)</title>
<style>
.container { width: 600px; margin: 30px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; }
.status { margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 4px; }
.data-item { margin: 10px 0; font-size: 16px; }
.title { text-align: center; color: #333; }
.error { color: red; }
.success { color: green; }
</style>
</head>
<body>
<div class="container">
<h1 class="title">IoT设备模拟(温湿度传感器)</h1>
<div class="status">
<div class="data-item">设备ID:<span id="deviceId">device_001</span></div>
<div class="data-item">设备状态:<span id="deviceStatus" class="error">未连接</span></div>
<div class="data-item">当前温度:<span id="temperature">--</span> ℃</div>
<div class="data-item">当前湿度:<span id="humidity">--</span> %</div>
<div class="data-item">运行状态:<span id="powerStatus">关机</span></div>
<div class="data-item">消息日志:<br/><span id="log" style="font-size: 14px; color: #666;"></span></div>
</div>
</div>
<script>
// 设备配置(可修改设备ID)
const DEVICE_ID = 'device_001';
const WS_URL = 'ws://localhost:3001';
let ws = null;
let reportTimer = null; // 数据上报定时器
let devicePower = false; // 设备运行状态(开机/关机)
// DOM元素
const deviceIdEl = document.getElementById('deviceId');
const deviceStatusEl = document.getElementById('deviceStatus');
const temperatureEl = document.getElementById('temperature');
const humidityEl = document.getElementById('humidity');
const powerStatusEl = document.getElementById('powerStatus');
const logEl = document.getElementById('log');
// 初始化设备ID
deviceIdEl.textContent = DEVICE_ID;
// 建立WebSocket连接
function connectWebSocket() {
ws = new WebSocket(WS_URL);
// 连接成功
ws.onopen = () => {
deviceStatusEl.textContent = '已连接';
deviceStatusEl.className = 'success';
addLog('WebSocket连接成功,开始注册设备...');
// 设备注册(连接成功后立即注册)
ws.send(JSON.stringify({
type: 'deviceRegister',
deviceId: DEVICE_ID
}));
};
// 接收消息(处理服务端/控制端指令)
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
addLog(`收到消息:${JSON.stringify(msg)}`);
// 注册成功:开始上报温湿度数据
if (msg.type === 'registerSuccess') {
startReportData();
}
// 控制端下发的指令
if (msg.type === 'controlCommand') {
const { command } = msg;
handleControlCommand(command);
}
// 错误消息
if (msg.type === 'error') {
addLog(`错误:${msg.msg}`, 'error');
}
};
// 连接关闭:重连
ws.onclose = () => {
deviceStatusEl.textContent = '未连接';
deviceStatusEl.className = 'error';
powerStatusEl.textContent = '关机';
devicePower = false;
clearInterval(reportTimer);
addLog('WebSocket连接关闭,正在重连...');
// 3秒后重连
setTimeout(connectWebSocket, 3000);
};
// 连接错误
ws.onerror = (err) => {
deviceStatusEl.textContent = '连接出错';
deviceStatusEl.className = 'error';
addLog(`连接出错:${err.message}`, 'error');
};
}
// 处理控制端指令(开机/关机)
function handleControlCommand(command) {
switch (command) {
case 'powerOn': // 开机
devicePower = true;
powerStatusEl.textContent = '开机';
addLog('设备开机成功,开始采集温湿度数据');
startReportData(); // 开机后开始上报数据
// 向服务端返回执行结果
ws.send(JSON.stringify({
type: 'commandResult',
deviceId: DEVICE_ID,
data: { command: 'powerOn', result: 'success', msg: '设备开机成功' }
}));
break;
case 'powerOff': // 关机
devicePower = false;
powerStatusEl.textContent = '关机';
addLog('设备关机成功,停止采集数据');
clearInterval(reportTimer); // 关机后停止上报
// 向服务端返回执行结果
ws.send(JSON.stringify({
type: 'commandResult',
deviceId: DEVICE_ID,
data: { command: 'powerOff', result: 'success', msg: '设备关机成功' }
}));
break;
default:
addLog(`未知指令:${command}`);
ws.send(JSON.stringify({
type: 'commandResult',
deviceId: DEVICE_ID,
data: { command, result: 'fail', msg: '未知指令' }
}));
}
}
// 模拟温湿度采集(随机生成合理范围的数值)
function collectSensorData() {
// 温度范围:20~30℃,保留1位小数
const temperature = (20 + Math.random() * 10).toFixed(1);
// 湿度范围:50~70%,保留1位小数
const humidity = (50 + Math.random() * 20).toFixed(1);
return { temperature, humidity };
}
// 开始上报温湿度数据(每隔2秒上报一次)
function startReportData() {
if (!devicePower) return;
clearInterval(reportTimer);
// 立即上报一次
reportData();
// 每隔2秒上报一次
reportTimer = setInterval(reportData, 2000);
}
// 上报温湿度数据到服务端
function reportData() {
if (ws.readyState !== WebSocket.OPEN || !devicePower) return;
const sensorData = collectSensorData();
// 更新页面显示
temperatureEl.textContent = sensorData.temperature;
humidityEl.textContent = sensorData.humidity;
// 发送数据到服务端
ws.send(JSON.stringify({
type: 'reportData',
deviceId: DEVICE_ID,
data: sensorData
}));
}
// 添加日志
function addLog(content, type = 'normal') {
const time = new Date().toLocaleTimeString();
const logItem = `[${time}] ${content}`;
if (type === 'error') {
logEl.innerHTML += `${logItem}`;
} else {
logEl.innerHTML += `${logItem}`;
}
// 滚动到最新日志
logEl.scrollTop = logEl.scrollHeight;
}
// 初始化连接
connectWebSocket();
</script>
</body>
</html>4. 控制端代码(control.html)
功能:连接WebSocket服务、查看所有在线IoT设备、实时接收设备上报的温湿度数据、向指定设备下发开机/关机指令、查看指令执行结果。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>IoT设备控制端</title>
<style>
.container { width: 800px; margin: 30px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; }
.title { text-align: center; color: #333; margin-bottom: 30px; }
.control-panel { margin-bottom: 30px; padding: 20px; background: #f8f8f8; border-radius: 6px; }
.device-list { margin-bottom: 20px; }
.device-item { padding: 15px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
.device-info span { margin-right: 20px; font-size: 16px; }
.control-btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin-left: 10px; }
.power-on { background: #4CAF50; color: white; }
.power-off { background: #f44336; color: white; }
.log-panel { padding: 20px; background: #f5f5f5; border-radius: 6px; height: 300px; overflow-y: auto; }
.log-item { margin: 5px 0; font-size: 14px; }
.success { color: #4CAF50; }
.error { color: #f44336; }
.info { color: #2196F3; }
</style>
</head>
<body>
<div class="container">
<h1 class="title">IoT设备控制端</h1>
<div class="control-panel">
<h2>在线设备列表</h2>
<div id="deviceList" class="device-list">
<div class="device-item">
<span>暂无在线设备,请等待设备连接...</span>
</div>
</div>
</div>
<div class="log-panel">
<h3>操作日志</h3>
<div id="logContainer"></div>
</div>
</div>
<script>
const WS_URL = 'ws://localhost:3001';
let ws = null;
const deviceListEl = document.getElementById('deviceList');
const logContainerEl = document.getElementById('logContainer');
// 建立WebSocket连接,注册控制端
function connectWebSocket() {
ws = new WebSocket(WS_URL);
// 连接成功:注册控制端
ws.onopen = () => {
addLog('WebSocket连接成功,正在注册控制端...', 'info');
ws.send(JSON.stringify({
type: 'controlRegister'
}));
};
// 接收服务端消息(设备状态、指令结果等)
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
addLog(`收到服务端消息:${JSON.stringify(msg)}`, 'info');
// 控制端注册成功:渲染在线设备
if (msg.type === 'controlRegisterSuccess') {
const { onlineDevices } = msg;
renderDeviceList(onlineDevices);
addLog(`控制端注册成功,当前在线设备${onlineDevices.length}台`, 'success');
}
// 设备数据更新:实时刷新设备状态
if (msg.type === 'deviceDataUpdate') {
const { deviceId, data } = msg;
updateDeviceStatus(deviceId, data);
}
// 设备指令执行结果:提示用户
if (msg.type === 'deviceCommandResult') {
const { deviceId, result } = msg;
addLog(`设备${deviceId}指令执行结果:${result.msg}(${result.result})`, 'success');
}
// 指令下发成功提示
if (msg.type === 'commandSuccess') {
const { deviceId, command } = msg;
addLog(`向设备${deviceId}下发${command}指令,下发成功`, 'success');
}
// 错误消息提示
if (msg.type === 'error') {
addLog(`错误:${msg.msg}`, 'error');
}
};
// 连接关闭:重连
ws.onclose = () => {
addLog('WebSocket连接关闭,正在重连...', 'error');
deviceListEl.innerHTML = '<div class="device-item"><span>暂无在线设备,请等待设备连接...</span></div>';
setTimeout(connectWebSocket, 3000);
};
// 连接错误
ws.onerror = (err) => {
addLog(`连接出错:${err.message}`, 'error');
};
}
// 渲染在线设备列表
function renderDeviceList(devices) {
if (devices.length === 0) {
deviceListEl.innerHTML = '<div class="device-item"><span>暂无在线设备,请等待设备连接...</span></div>';
return;
}
let html = '';
devices.forEach(device => {
const { deviceId, temperature, humidity, power } = device;
html += `
<div class="device-item" data-device-id="${deviceId}">
<div class="device-info">
<span>设备ID:${deviceId}</span>
<span>温度:${temperature} ℃</span>
<span>湿度:${humidity} %</span>
<span>运行状态:${power ? '开机' : '关机'}</span>
</div>
<div class="control-btn-group">
<button class="control-btn power-on" onclick="sendCommand('${deviceId}', 'powerOn')" ${power ? 'disabled' : ''}>开机</button>
<button class="control-btn power-off" onclick="sendCommand('${deviceId}', 'powerOff')" ${!power ? 'disabled' : ''}>关机</button>
</div>
</div>
`;
});
deviceListEl.innerHTML = html;
}
// 更新单个设备状态
function updateDeviceStatus(deviceId, newStatus) {
const deviceItemEl = deviceListEl.querySelector(`.device-item[data-device-id="${deviceId}"]`);
if (!deviceItemEl) return;
const { temperature, humidity, power } = newStatus;
// 更新设备信息文本
const infoEls = deviceItemEl.querySelectorAll('.device-info span');
infoEls[1].textContent = `温度:${temperature} ℃`;
infoEls[2].textContent = `湿度:${humidity} %`;
infoEls[3].textContent = `运行状态:${power ? '开机' : '关机'}`;
// 更新按钮状态(禁用/启用)
const powerOnBtn = deviceItemEl.querySelector('.power-on');
const powerOffBtn = deviceItemEl.querySelector('.power-off');
powerOnBtn.disabled = power;
powerOffBtn.disabled = !power;
}
// 向设备下发控制指令
function sendCommand(deviceId, command) {
if (ws.readyState !== WebSocket.OPEN) {
addLog('WebSocket未连接,无法下发指令', 'error');
return;
}
ws.send(JSON.stringify({
type: 'sendCommand',
deviceId: deviceId,
command: command
}));
}
// 添加操作日志
function addLog(content, type = 'normal') {
const time = new Date().toLocaleTimeString();
const logItem = document.createElement('div');
logItem.className = `log-item ${type}`;
logItem.textContent = `[${time}] ${content}`;
logContainerEl.appendChild(logItem);
// 滚动到最新日志
logContainerEl.scrollTop = logContainerEl.scrollHeight;
}
// 暴露sendCommand方法到全局,供按钮点击调用
window.sendCommand = sendCommand;
// 初始化连接
connectWebSocket();
</script>
</body>
</html>
版权所属:SO JSON在线解析
原文地址:https://www.sojson.com/blog/577.html
转载时必须以链接形式注明原始出处及本声明。
如果本文对你有帮助,那么请你赞助我,让我更有激情的写下去,帮助更多的人。
