
HarmonyOS 里做实时通信绕不开 WebSocket。从智能家居的状态同步到 IM 聊天消息的收发再到行情推送、协作白板几乎所有需要“服务端主动找客户端”的场景都是 WebSocket 的主场。我在鸿蒙应用开发里试过轮询、也试过 SSEServer-Sent Events最后发现 WebSocket 才是那个既通用又可靠的全双工方案。这篇文章就以 HarmonyOS Next 第 15 课为主线把 WebSocket 的原理、ArkTS 侧的核心 API、完整可跑的客户端实现以及我在真机调试时踩过的一堆坑全部梳理一遍帮你少走弯路。无论你是刚接触鸿蒙开发还是已经写过几个页面但没碰过网络长连接这篇文章都值得花十分钟看完。我会从“为什么需要全双工”讲起一直讲到“怎么设计心跳、怎么应对断线、怎么抓包排查”保证是能直接抄作业的实操内容而不是干巴巴的官方文档翻译。1. 全双工通信的本质为什么 WebSocket 能把“轮询”扔进垃圾桶1.1 HTTP 的短板一问一答式的“半双工”限制在学习 WebSocket 之前先搞清楚我们到底在解决什么问题。HTTP 协议从 1.0 到 1.1再到 2.0、3.0本质上仍然是“请求-响应”模型。客户端必须主动发一个 Request服务端才能回一个 Response。如果一个应用需要实时获取服务端数据比如股票价格、聊天消息传统的做法就是客户端定时去轮询。轮询的问题很明显第一实时性差轮询间隔设得太短服务端压力巨大间隔设得太长消息延迟又不可控第二浪费资源大量 HTTP 请求携带重复的 Header 和 Cookie每次都要建立连接、解析协议、返回 200 或 304真正有效的业务数据可能只有几个字节。我在开发一个通知模块的时候实测过用 3 秒轮询接口一整天下来光无效请求就有 28800 次电池和流量都扛不住最后还是老老实实上了 WebSocket。用生活类比就是HTTP 轮询像是对讲机你按一下说一句“有人找我吗”对方回答“没有”你再按一下再问WebSocket 则像打电话线路始终通畅任何一个人随时可以开口说话不需要反复拨号。这个本质区别决定了 WebSocket 在实时场景里不可替代的位置。1.2 WebSocket 的握手从 HTTP Upgrade 开始的“电话线”WebSocket 并不是一套全新的协议它建立在 HTTP 之上。客户端发起连接时先发一个标准的 HTTP 请求里面带上了Upgrade: websocket和Connection: Upgrade两个关键 Header。服务端看到这两个字段如果同意升级协议就返回 101 状态码表示“协议切换成功”。从这一刻起这条 TCP 连接就变成了 WebSocket 通道双方可以随时互相推送数据不再受 HTTP 请求-响应模型约束。握手细节值得看一眼因为很多连接失败的案例都出在服务端对握手请求的校验上。请求头里必须有一个Sec-WebSocket-Key这是客户端随机生成的一个 Base64 编码字符串服务端拿到后会拼接一个固定 GUID258EAFA5-E914-47DA-95CA-C5AB0DC85B11做 SHA-1 哈希再 Base64 编码生成Sec-WebSocket-Accept返回给客户端。客户端收到后校验这个值校验通过连接才算真正建立。这个设计的目的是防止普通 HTTP 请求被误升级为 WebSocket同时也确保双方确实都支持 WebSocket 协议。在 HarmonyOS 里这些握手细节都被系统封装好了开发者只需要调用connect()方法传入ws://或wss://地址剩下的交给底层栈。但理解这个过程很重要因为当你排查“为什么连不上”的问题时第一反应应该是抓包看握手是否成功而不是一头扎进业务逻辑里。1.3 帧与心跳数据在 WebSocket 里是怎么跑的连接建立之后数据传输的单元叫“帧”Frame它分为多种类型文本帧、二进制帧、Ping/Pong 帧、关闭帧等。文本帧对应字符串消息二进制帧对应ArrayBuffer、Blob等二进制数据Ping/Pong 帧则用于保活探测。这里要强调一个真实开发中的设计细节WebSocket 协议虽然定义了 Ping/Pong 帧机制但很多服务端框架并没有自动处理它们。也就是说你发的 Ping 帧服务端如果没实现 Ping/Pong 响应逻辑就不会回 Pong。所以在实际项目里我更推荐在业务层设计一套“应用层心跳”客户端定时发送一个业务消息比如 JSON 字符串{type:ping,timestamp:1699999999999}服务端收到后回一个{type:pong}。这样做的好处有两个一来能确认服务端应用层确实活着而不只是 TCP 连接还挂着二来实现简单前后端都好控制。心跳间隔也不是随意定的。太频繁会浪费流量太疏又起不到保活作用。我的经验是服务端网关超时时间的一半作为心跳间隔。比如服务端 Nginx 配置的proxy_read_timeout是 60 秒那心跳就设 20 到 30 秒留足余量避免刚好卡在超时临界点导致连接被误杀。2. HarmonyOS Next 开发准备环境、权限与核心 API2.1 工程配置与 INTERNET 权限申请在 HarmonyOS Next 里创建一个新工程后第一件事不是写代码而是检查权限配置。WebSocket 属于网络通信能力默认是没有网络访问权限的必须在module.json5的requestPermissions里显式声明 INTERNET 权限。{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.INTERNET, reason: 建立WebSocket连接实现实时数据同步, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }注意usedScene里的when字段如果只填inuse表示应用在前台使用期间才能访问网络如果你的应用需要在后台维持长连接比如消息推送服务可能还需要申请ohos.permission.KEEP_BACKGROUND_RUNNING等后台任务相关权限。不过后台长连接在鸿蒙上有严格限制真机测试时我发现应用退到后台一段时间后系统会主动断开网络连接这点后文会详细讲。2.2 ohos.net.webSocket 模块核心接口盘点HarmonyOS Next 的 WebSocket API 集中在ohos.net.webSocket模块新版本也可以从kit.NetworkKit导入使用方式非常直观核心就三件事创建实例、挂回调、发消息。import { webSocket } from kit.NetworkKit; // 创建 WebSocket 实例 let ws webSocket.createWebSocket(); // 注册事件监听 ws.on(open, () { console.info(WebSocket 连接已建立); }); ws.on(message, (err, value) { if (!err) { // value 的类型是 string | ArrayBuffer console.info(收到消息: , value); } }); ws.on(close, () { console.info(WebSocket 连接已关闭); }); ws.on(error, (err) { console.error(WebSocket 错误: , JSON.stringify(err)); });创建实例后所有事件都是通过on()方法注册的支持的事件名有open、message、close、error。这个设计很接近浏览器端 WebSocket 的用法前端开发者迁移过来会非常顺手。发射消息、关闭连接、销毁实例的调用方式也都很直白// 发送文本消息 ws.send(Hello WebSocket, (err) { if (!err) { console.info(发送成功); } }); // 发送二进制消息 let buffer new ArrayBuffer(4); let view new DataView(buffer); view.setInt32(0, 123456); ws.send(buffer); // 关闭连接 ws.close((err) { if (!err) { console.info(连接已关闭); } }); // 释放资源 ws.destroy();有一点容易被忽略send()和close()是异步的一定要在回调里确认结果。我见过同事在连接还没建立成功时就调用send()结果消息直接丢失排了半天才发现是时序问题。2.3 与其他平台的对比ArkTS 的 WebSocket API 为什么更“直白”如果你写过 Node.js 的ws库或者浏览器端的 WebSocket再来看鸿蒙的 API会发现一个明显的差异鸿蒙的事件回调不需要像 Node.js 那样手动绑定connection、message、close的闭包而是统一使用on()方法并且回调参数风格保持一致都是(err, value)结构。这种设计的好处是便于上层框架做统一封装和类型管理。另外鸿蒙的connect()方法支持传入WebSocketRequestOptions可以在握手阶段携带自定义 Header、Cookie 等这在做鉴权时非常有用。比如可以在 header 里带上 Tokenlet requestOptions: webSocket.WebSocketRequestOptions { header: { Authorization: Bearer token } }; ws.connect(wss://example.com/ws, requestOptions, (err, value) { if (!err) { console.info(连接成功); } });这一点比浏览器端要灵活相当于把原生 WebSocket API 和 Node.js 客户端的自定义 Header 能力结合了起来对于对接后端网关、携带会话凭证是刚需。3. 实操从零构建一个可用的 WebSocket 客户端3.1 步骤一创建 WebSocket 实例与事件监听我把整个实现拆成五个步骤每一步都有明确的意图和细节要求照着写就能跑通。第一步创建实例并注册事件。关键点在于事件监听必须在调用connect()之前完成。如果先连上再注册open回调就可能错过首次握手成功的事件后续消息自然也就收不到了。private ws: webSocket.WebSocket | undefined undefined; initWebSocket() { if (this.ws) { this.ws.destroy(); this.ws undefined; } this.ws webSocket.createWebSocket(); this.ws.on(open, () { console.info(连接已打开); this.onConnected(); }); this.ws.on(message, (err, data) { if (err) { console.error(消息接收错误: , JSON.stringify(err)); return; } if (typeof data string) { this.handleTextMessage(data); } else { this.handleBinaryMessage(data); } }); this.ws.on(close, (err, value) { console.info(连接关闭, code${value.code}, reason${value.reason}); this.onDisconnected(); }); this.ws.on(error, (err) { console.error(WebSocket 错误: , JSON.stringify(err)); }); }这里我额外做了两件事一是每次初始化时先销毁旧实例避免内存泄漏二是对消息类型做了区分文本走文本处理二进制走二进制处理为后续业务扩展留好接口。3.2 步骤二发起连接并处理握手结果第二步调用connect()方法。这个方法需要传入 URL、可选参数和回调。回调中的err在握手失败时不为空需要立刻进入重连或错误上报流程。connectToServer() { if (!this.ws) { this.initWebSocket(); } let url wss://your-server.com/ws; let options: webSocket.WebSocketRequestOptions { header: { X-Client-Version: 1.0.0, Authorization: Bearer getToken() } }; this.ws!.connect(url, options, (err, value) { if (!err) { console.info(连接成功); } else { console.error(连接失败: , JSON.stringify(err)); this.scheduleReconnect(); } }); }关于wss://和ws://的选择生产环境必须使用wss://它相当于 HTTPS 对 HTTP 的加密升级底层走 TLS防止数据在传输过程中被窃听或篡改。真机调试时如果服务端没有配置 TLS 证书可以用ws://做临时联通测试但上线前必须切到wss://。3.3 步骤三消息发送、接收与数据格式约定第三步处理消息的发送和接收。这里最忌讳的是没有统一的报文格式前后端各写各的最终导致解析崩溃。我推荐在项目初期就约定一种 JSON 协议格式比如{ type: message, data: { text: hello, roomId: 123 }, timestamp: 1699999999999 }接收端根据type字段分发到不同的业务处理器。发送端的代码很简单sendTextMessage(text: string) { if (this.isConnected()) { let payload JSON.stringify({ type: chat, data: { text }, timestamp: Date.now() }); this.ws!.send(payload, (err) { if (err) { console.error(发送失败: , JSON.stringify(err)); // 可以在这里把消息放入重发队列 } }); } }send()回调里如果返回了err说明发送失败常见原因是连接已经断开或底层网络错误。稳妥的做法是维护一个待重发队列连接恢复后再把未确认的消息补发出去而不是直接丢消息。对于聊天这类场景用户发了消息却悄悄弄丢了体验非常差。3.4 步骤四心跳保活与优雅关闭第四步设计心跳机制。我采用一个setInterval定时器每 20 秒发一次业务层 ping同时记录服务端最后一次 pong 的时间。如果连续三次没有收到 pong就认为连接已死主动关闭并从新连接。private heartBeatTimer: number | undefined undefined; private lastPongTime: number 0; startHeartBeat() { if (this.heartBeatTimer) { clearInterval(this.heartBeatTimer); } this.heartBeatTimer setInterval(() { if (this.isConnected()) { let now Date.now(); if (now - this.lastPongTime 60000) { console.warn(心跳超时主动断开); this.disconnect(); return; } let payload JSON.stringify({ type: ping, timestamp: now }); this.ws!.send(payload, (err) { if (err) { console.error(心跳发送失败: , JSON.stringify(err)); } }); } }, 20000); } stopHeartBeat() { if (this.heartBeatTimer) { clearInterval(this.heartBeatTimer); this.heartBeatTimer undefined; } }当收到服务端 pong 时更新lastPongTime。注意这个位置很容易被忽略如果在message回调里不判断type pong而直接当成业务消息处理就会在控制台看到一堆 JSON 解析报错。优雅关闭的流程是先停心跳再调用close()最后destroy()释放资源。destroy()不能漏因为它负责释放 native 层的内存和句柄。如果直接退出页面而不调用destroy()下次创建实例时旧的还没释放完极容易造成资源泄漏严重时整个应用卡顿甚至闪退。gracefulClose() { this.stopHeartBeat(); if (this.ws) { this.ws.close((err) { if (!err) { console.info(关闭成功); } }); this.ws.destroy(); this.ws undefined; } }3.5 一个完整的最小 Demo实时推送公告把前面的步骤串起来写一个完整的示例。场景很简单应用启动后连接 WebSocket 服务器端到端接收一条文本消息并显示在页面上同时支持手动发送一条 ping 消息测试联通性。import { webSocket } from kit.NetworkKit; export class RealtimeClient { private ws: webSocket.WebSocket | undefined undefined; private heartBeatTimer: number | undefined undefined; private lastPongTime: number 0; private onMessageCallback: ((text: string) void) | undefined undefined; constructor(onMessage: (text: string) void) { this.onMessageCallback onMessage; this.init(); } private init() { this.ws webSocket.createWebSocket(); this.ws.on(open, () { console.info(连接已建立启动心跳); this.startHeartBeat(); }); this.ws.on(message, (err, data) { if (err) return; if (typeof data string) { this.handleMessage(data); } }); this.ws.on(close, () { this.stopHeartBeat(); console.info(连接已关闭); }); this.ws.on(error, (err) { console.error(连接错误: , JSON.stringify(err)); }); this.ws.connect(wss://example.com/ws, this.onConnected.bind(this)); } private handleMessage(text: string) { let jsonObj JSON.parse(text); if (jsonObj.type pong) { this.lastPongTime Date.now(); return; } this.onMessageCallback?.(text); } private onConnected() { console.info(握手成功); this.send(JSON.stringify({ type: hello, data: from harmonyos })); } send(message: string) { if (!this.ws) return; this.ws.send(message, (err) { if (err) { console.error(发送失败: , JSON.stringify(err)); } }); } private startHeartBeat() { if (this.heartBeatTimer) return; this.lastPongTime Date.now(); this.heartBeatTimer setInterval(() { if (Date.now() - this.lastPongTime 60000) { console.warn(心跳超时); this.destroy(); return; } this.send(JSON.stringify({ type: ping })); }, 20000); } private stopHeartBeat() { if (this.heartBeatTimer) { clearInterval(this.heartBeatTimer); this.heartBeatTimer undefined; } } destroy() { this.stopHeartBeat(); if (this.ws) { this.ws.close(); this.ws.destroy(); this.ws undefined; } } }这段代码已经能应付大多数实时推送场景。别以为很简单稳不稳就在这些细节上心跳、消息分发、资源销毁一个都不能少。4. 踩坑实录连接、断线、内存与抓包4.1 连接失败TLS、代理与网络权限三板斧真机调试时遇到connect() fail报错先按以下顺序排查。第一步确认网络权限配了没有漏配ohos.permission.INTERNET会直接报权限错误这是最常见的低级问题。第二步检查 URL 格式ws://和wss://协议头不能写错也不能在 URL 里带空格和非法字符。第三步如果是wss://检查证书。鸿蒙对自定义证书有严格校验如果服务端的 TLS 证书不是正规 CA 签发的应用会拒绝建立连接。开发环境临时可以到设置里信任自签名证书或者后端临时换成ws://验证连通性但上线必须用正规证书。一个容易被忽略的点是部分安卓和鸿蒙设备对网络代理敏感如果你开了抓包工具的全局代理而该工具没有正确解析 WebSocket 流量也会导致连接失败。我在一次真机调试中Charles 开着 SSL Proxying 但没安装 CA 证书结果所有wss://连接全部失败一度以为代码写错了查了半天才发现是抓包环境的问题。4.2 “stream disconnected before completion”背后服务端主动关闭的真相很多人在日志里看到stream disconnected before completion: websocket closed by server before res这种报错第一反应是客户端代码有问题。以我的经验这句话翻译成人话就是服务端已经先关了 WebSocket 连接而客户端的某项操作比如等待服务端响应还没有完成导致底层 flow 被打断。为什么服务端会主动关连接通常有三种原因一是服务端程序崩溃或重启连接被操作系统回收二是服务端网关如 Nginx、HAProxy根据空闲超时策略主动断开三是服务端在业务层主动发 close 帧并结束会话。遇到这种报错正确应对不是去代码里找逻辑漏洞而是去服务端日志和抓包里看连接是什么时候、什么原因被关闭的同时客户端要做断线重连把这个异常当成一次普通的网络中断处理不能让用户感知到卡顿或崩溃。4.3 断线重连退避策略与重连时机怎么设计断线重连是长连接应用必须具备的能力但无脑重连反而会让问题更严重。如果服务端正在重启客户端每 3 秒重连一次会形成“重连风暴”连接请求全部堆积在服务端入口拖慢系统恢复速度。我一般使用指数退避策略第一次重连延迟 1 秒第二次 2 秒第三次 4 秒最大不超过 60 秒如果连续重连 5 次还是失败就停掉重连定时器等用户手动重试或应用下一次启动再连。同时要加入随机抖动Jitter避免多个客户端同时重连产生“惊群”效应。重连时机也要注意分类如果是应用从后台回到前台可以立即主动重连一次如果是应用在前台且网络状态变化Wi-Fi 切蜂窝、断网后恢复系统网络回调会触发networkStateChange事件这时候也可以重连。鸿蒙的ohos.net.connection模块提供了网络状态监听能力可以在网络恢复时收到回调比单纯依赖 WebSocket 的 error 事件更及时。4.4 内存泄漏与页面关不掉多个 WebSocket 实例的生命周期管理在 HarmonyOS 里WebSocket 实例如果没有正确释放内存泄漏会异常隐蔽。我见过一个项目里每次重新进入页面都会创建一个新的 WebSocket 实例旧实例只有在完全退出页面时才销毁但页面用了懒加载实际路径在不同 Tab 之间来回切换导致内存里堆了七八个 WebSocket 实例每个实例都持有一堆闭包和定时器。结果就是应用越来越卡甚至被系统判定为无响应而闪退。正确的做法是把 WebSocket 实例的生命周期与页面 UI 生命周期分离推荐封装成一个单例的RealtimeManager整个应用只维护一个长连接页面切换时只做监听器的注册与注销不重建连接。同时所有on()、off()方法成对使用页面销毁时调用off()移除事件监听防止回调泄漏。如果确实需要多个连接比如同时连接多个服务端就建立连接名到实例的映射表统一管理。4.5 Charles 与抓包如何在调试中看懂 WebSocket 帧这里提一下用 Charles 抓 WebSocket 包的方法。Charles 是老牌 HTTP/HTTPS 抓包工具新版也支持 WebSocket 帧的可视化。首先打开 Proxy Settings勾选 SSL Proxying 并添加*:*安装并信任 Charles CA 证书然后在浏览器或鸿蒙模拟器上配置代理指向电脑 IP 和 8888 端口此时访问wss://地址时Charles 里的 WebSocket 标签页就能列出握手请求、每一条发送和接收的消息以及帧的类型和负载内容。鸿蒙模拟器配置代理稍微麻烦一点因为系统代理和浏览器走的是同一套网络栈配置好后记得在完成抓包时移除代理否则后续应用会出现莫名的网络问题。另外抓包只能看到客户端与 Charles 之间的通信Charles 到服务端之间的内容默认是加密的如果需要解密服务端到客户端的流量需要在服务端也配置 Charles 的 CA 或者直接抓明文端口这点要分清楚别把抓包结果当成服务端真实下发的内容。5. 工程化进阶从单聊到实时推送系统的思路5.1 服务端选型Node.js、Spring Boot 与第三方 WebSocket学会了客户端回头看服务端其实适配鸿蒙 WebSocket 的服务端技术选型非常多。Node.js 的ws库轻量好用适合快速开发中小型实时服务Spring Boot 2.x 以上的WebSocket模块适合在 Java 技术栈中做集成踩坑点主要在握手拦截器和会话管理如果不想自建也可以接入第三方云厂商的 WebSocket 网关或 IM 云服务。这里给出一个选型参考表服务端方案适合场景典型难点推荐指数Node.js ws中小型业务、原型验证进程崩溃后连接全部断开需要进程守护高Spring Boot WebSocket已有 Java 后端、团队熟悉 JVM 技术栈拦截器鉴权、连接数监控高云原生网关 / API 网关高并发、海量连接费用、跨云联通中第三方 IM / 推送服务不需要自研协议、想快速上线定制灵活性差中不管用哪种服务端都必须重点关注两件事连接数监控和心跳超时处理。连接数暴涨时要有告警心跳超时的连接要尽快回收否则大量半开连接会占满文件描述符导致服务端拒绝新连接。5.2 消息协议设计JSON、二进制与压缩字符串和二进制消息的取舍直接影响实时性和带宽成本。JSON 可读性好、调试方便适合频率低、数据结构复杂的场景二进制体积小、解析快适合音视频、传感器数据、实时行情这类对时延和带宽敏感的数据。我的建议是混合使用控制消息走 JSON大数据块走二进制同时在消息头里打一个类型标识避免接收方猜测数据格式。另外如果发送的数据量较大比如几千条数据一次性下发建议在服务端做压缩或者客户端做分页拉取。WebSocket 本身没有压缩机制应用层也不应该把所有希望寄托在服务端网关的 gzip 上因为压缩状态在长连接中是难以维持的。真机上几万条 JSON 消息连续推送如果不做节制UI 线程会直接被卡死用户看到的就是“页面白屏、App 无响应”这就是开发者口中的 WebSocket 导致崩溃其实是消息处理方式不当造成的。5.3 与鸿蒙其他能力联动后台任务、通知栏与长连接当应用退到后台系统会暂停或限制网络活动WebSocket 连接可能被底层强制断开。HarmonyOS 提供了一系列后台任务能力比如申请长任务、使用WorkScheduler定时任务但这些能力都有严格的使用门槛不会无限制允许所有应用在后台跑长连接。想让实时推送真正落地还需要配合鸿蒙的推送服务Push Kit一起使用前台时用 WebSocket 走业务实时通道退到后台时用系统推送兜底等用户回到前台再恢复 WebSocket 连接。这部分设计牵涉的细节很多但核心思想是不要在鸿蒙上硬扛后台长连接硬扛的结果不仅耗电还会被系统频繁杀掉。我个人的体会是把 WebSocket 当作“前台实时通道”把推送服务当作“后台兜底通道”两者结合既保证了用户实时性体验又符合系统的资源管理规则。我在鸿蒙上调试 WebSocket 时踩过最深的一个坑就是把断线重连当成“写个 while 循环怼上去”就能解决的事结果模拟器里跑得欢一到真机上就疯狂重连最后把服务端都拖垮了。后来才明白长连接最考验的不是“怎么连上”而是“怎么优雅地断开、合理地重连、及时地释放”。希望你在写自己的实时通信模块时能把我这些经验直接用上少踩几个坑。下次做实时功能不妨先问问自己心跳做了吗重连退避了吗连接释放干净了吗这三板斧都搞定你的 WebSocket 才算真正稳了。