详解WebSocket及其妙用

JSON 2026-02-03 16:32:03 906

一、认知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后端库ws

2. 后端代码(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. 运行测试

  1. 启动后端:在终端执行「node server.js」,控制台显示“WebSocket服务已启动,监听ws://localhost:3000”,说明服务启动成功;
  2. 启动前端:直接用浏览器打开index.html文件,可打开多个标签页(模拟多个客户端);
  3. 测试交互:在任意一个标签页输入消息并点击“发送”,所有打开的标签页都会实时收到服务端转发的消息,实现群聊效果。

该示例覆盖了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一次连接实现全双工通信,能轻松支撑弹幕刷屏、群聊高频消息等场景,服务端压力远低于轮询。实现思路

  1. 客户端连接时,服务端用Map存储「用户ID - WebSocket连接对象」的映射,记录用户在线状态;
  2. 单聊:客户端发送消息时,携带「目标用户ID」,服务端根据映射找到目标连接,直接发送消息;
  3. 群聊:服务端维护「群ID - 群内用户连接列表」的映射,收到消息后,广播给列表内所有在线客户端;
  4. 离线消息:若目标用户未连接,将消息存入数据库(如MySQL、Redis),用户重连后,服务端主动推送离线消息。

妙用2:实时数据展示(金融/监控/大屏)

场景:股票/期货/数字货币行情、基金实时净值、服务器监控大屏(CPU/内存/QPS)、物联网设备数据(温湿度、设备运行状态)、电商实时交易大屏(销量、成交额);为什么用WebSocket:数据由服务端主动推送(客户端无需频繁查询),且更新频率高(每秒数次),WebSocket能实现毫秒级推送,且轻量帧开销极低,适合千/万级客户端同时在线。实现思路

  1. 服务端通过定时任务、消息队列、传感器等方式,采集实时数据;
  2. 客户端连接后,服务端将最新数据主动、持续推送给客户端(可根据客户端需求,推送指定类型的数据);
  3. 客户端收到数据后,通过ECharts、Canvas等工具,实时渲染图表、大屏,实现数据可视化。

妙用3:协同编辑/在线协作

场景:腾讯文档、飞书文档、石墨文档(在线编辑)、Figma(设计稿协作)、CodeSandbox(代码协同编辑);为什么用WebSocket:多用户同时编辑时,需要实时同步修改内容,一个用户的操作(输入、删除、移动)必须立刻推送给其他所有协作用户,实现“所见即所得”的协作效果。实现思路

  1. 采用「操作同步」而非「全量内容同步」(减少数据传输):将用户的每一个操作,封装为“操作指令”(如「在第5行第3列插入字符A」「删除第10行内容」);
  2. 客户端将操作指令发送给服务端,服务端验证指令合法性后,广播给所有协作客户端;
  3. 每个客户端收到操作指令后,在本地文档上执行相同操作,确保所有客户端的文档内容一致。

妙用4:服务端主动推送通知(系统告警/业务提醒)

场景:后台系统异常告警(服务器宕机、接口报错)、电商订单提醒(新订单、退款、物流更新)、社交通知(点赞、评论、关注);为什么用WebSocket:传统方案(客户端轮询数据库、第三方推送)要么延迟高,要么依赖第三方(适合APP);WebSocket适合网页端的服务端主动推送,无需依赖第三方,实时性更高,且节省客户端流量。实现思路

  1. 客户端登录后,通过Token/UserId建立WebSocket连接,服务端绑定「用户ID - 连接」的映射;
  2. 业务系统(如订单服务、监控服务)产生通知时,调用WebSocket服务的接口,传递「目标用户ID + 通知内容」;
  3. WebSocket服务根据用户ID找到对应连接,主动推送通知,客户端收到后,展示为右上角小红点、弹窗等形式。

妙用5:实时游戏(轻量级网页游戏)

场景:网页版贪吃蛇、五子棋、连连看、休闲竞技小游戏(如在线斗地主);为什么用WebSocket:游戏需要多玩家的操作实时同步(如移动、出牌、放技能),且延迟要求极低(否则出现卡顿、不同步);WebSocket基于TCP可靠传输,能保证操作不丢失,且轻量帧适合频繁的小数据传输(重度游戏用原生Socket,WebSocket适合轻量网页游戏)。实现思路

  1. 服务端维护游戏的“全局状态”(如玩家位置、游戏进度、分数、剩余时间);
  2. 玩家的操作(如方向键、点击)封装为指令,通过WebSocket发送给服务端;
  3. 服务端更新全局状态后,将最新状态广播给所有玩家客户端;
  4. 客户端根据全局状态,实时渲染游戏画面,确保所有玩家看到的游戏状态一致。

妙用6:直播互动(弹幕/点赞/礼物)

场景:B站、虎牙、抖音网页版直播的弹幕、实时点赞数、礼物飘屏、评论实时刷新;为什么用WebSocket:直播互动的实时性要求高(弹幕、点赞需秒级展示),且并发量高(上万甚至几十万观众);WebSocket单连接持续传输,帧开销极小,服务端通过广播机制,能快速将互动信息推送给所有在线观众,效率远高于轮询。实现思路

  1. 观众进入直播间时,连接该直播间对应的WebSocket“房间”,服务端维护「直播间ID - 观众连接列表」;
  2. 观众发送弹幕、点赞、赠送礼物时,客户端将数据发送给服务端,服务端做简单限流(防刷屏)后,广播给该直播间的所有观众;
  3. 客户端收到数据后,通过前端特效渲染(如弹幕滚动、点赞动画、礼物飘屏),提升互动体验。

妙用7:物联网(IoT)设备通信

场景:智能摄像头、温湿度传感器、智能家居(空调、灯光)、工业设备监控;为什么用WebSocket:IoT设备通常带宽有限、性能较低,WebSocket轻量帧适合小数据传输,且持久连接避免频繁建连的开销;双向通信同时满足“设备上报数据”(如传感器每1秒上报温湿度)和“服务端下发指令”(如手机APP控制空调开机)。实现思路

  1. IoT设备启动后,主动建立WebSocket连接,服务端记录「设备ID - 连接」的映射;
  2. 设备上报数据:定时将采集到的数据(温湿度、运行状态)通过WebSocket发送给服务端,服务端存储数据并进行分析;
  3. 服务端下发指令:用户通过手机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

转载时必须以链接形式注明原始出处及本声明。

本文主题:

如果本文对你有帮助,那么请你赞助我,让我更有激情的写下去,帮助更多的人。

关于作者
一个低调而闷骚的男人。
相关文章
SQL内连接详解及图解
Unicode编码详解
SQL右连接【RIGHT JOIN】详解及图解
SQL右连接【RIGHT JOIN】详解及图解
SQL左链接【LEFT JOIN】详解及图解
SQL内连接详解及图解
SQL左链接【LEFT JOIN】详解及图解
dns-prefetch对网站速度能提升有多少?详解dns-prefetch。
详解JavaScript中splice()方法,有Demo细介绍
Elasticsearch 教程,Elasticsearch 日期查询详解,Elasticsearch Date 查询Java API
最新文章
文件上传漏洞与防御 4058
前端构建工具选型指南:Webpack、Vite、Rollup、esbuild 深度对比 1444
物联网时代2026年时序数据库选型指南 1151
SaaS行业面临AI挑战:从“无限复用”到“灵活适应” 1269
神经网络:从构造到模型训练全链路解析 1168
一文吃透 Redis 核心存储结构:ziplist、listpack 与哈希表扩容 / 并发查询 1593
Linux sudo提权完整指南:从基础用法到生产级安全配置 691
XSS 和 CSRF 的本质区别及开发防御全解析 772
JVM垃圾回收(GC)全维度解析:从原理到调优实战 813
Linux动静态库与ELF加载全解析:从实操制作到底层原理 912
最热文章
免费天气API,天气JSON API,不限次数获取十五天的天气预报 783114
最新MyEclipse8.5注册码,有效期到2020年 (已经更新) 711464
苹果电脑Mac怎么恢复出厂系统?苹果系统怎么重装系统? 679993
Jackson 时间格式化,时间注解 @JsonFormat 用法、时差问题说明 562673
我为什么要选择RabbitMQ ,RabbitMQ简介,各种MQ选型对比 512621
Elasticsearch教程(四) elasticsearch head 插件安装和使用 484794
Jackson 美化输出JSON,优雅的输出JSON数据,格式化输出JSON数据... ... 302947
Java 信任所有SSL证书,HTTPS请求抛错,忽略证书请求完美解决 247433
Elasticsearch教程(一),全程直播(小白级别) 233097
谈谈斐讯路由器劫持,你用斐讯路由器,你需要知道的事情 228329
支付扫码

所有赞助/开支都讲公开明细,用于网站维护:赞助名单查看

查看我的收藏

正在加载... ...