1. 先看项目本质:这一标题到底意味着什么
每次有人问我 OpenHarmony 上 Flutter 能不能顺畅跑 WebSocket,我都会先反问一句:你们打算用原生插件桥接,还是直接用 Dart 侧的标准 WebSocket 客户端?如果对方回答“已经在写原生长连接插件了”,那基本可以猜到后面的故事:MethodChannel 两端来回调、数据类型转换、plugin 注册表在 OpenHarmony 上要单独适配、日志还经常看不出到底断在哪一层。
这个标题真正要解决的事情就是这样一件事:在 Flutter for OpenHarmony 的场景里,用 web_socket 这个纯 Dart 标准 WebSocket 客户端把连接层统一起来,让 Android、iOS、OpenHarmony 以及 Web 侧都能共用同一套代码,而不是每到一个平台就重新做一遍协议层适配。
先说结论:这并不只是一个“换个包”的问题。web_socket 选择的路线是把 RFC 6455 协议处理尽量放在 Dart 层完成,底层只依赖 Dart 运行时的 TCP/TLS 能力。对于 OpenHarmony 这种 Flutter 官方还没有全量支持的平台来说,这个特点几乎就是“救命稻草”。因为这意味着不用去折腾原生插件、不用在鸿蒙工程里注册 C++ 插件、也不用担心 Flutter engine 和 OpenHarmony 第三方库之间的 ABI 冲突。只要 Flutter 的 Dart VM 能在设备上把代码跑起来,这套 WebSocket 客户端就能跑。
这篇文章适合三类读者:
- 正在把 Flutter 业务迁移到 OpenHarmony,但 WebSocket 长连接不知道怎么写的人;
- 被原生插件桥接方案折磨过,想找一种真正跨端方案的移动端开发者;
- 想搞清楚
dart:io WebSocket、web_socket_channel、web_socket三者差异,以及为什么纯 Dart 实现在新平台上有优势的人。
我会从协议原理、工程适配、代码实现、真实报错排查几个层面展开,最后给出一份可以直接抄作业的连接管理器代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 纯 Dart WebSocket 客户端:为什么它能在新平台占据先天优势
2.1 先分清 Dart 生态里的几套 WebSocket API
刚开始接触 Dart 的人很容易被一堆相似概念搞晕:dart:io WebSocket、package:web_socket_channel、package:web_socket,还有 dart:html 里的旧 WebSocket API。我要先把这个图谱理清。
dart:io 里自带的 WebSocket 类是 Dart VM 环境下的原生实现。它依赖 dart:io 的 HttpClient、Socket、SecureSocket,在 Android、iOS、桌面端都能用。但问题在于,它无法用于浏览器环境,因为浏览器里没有 dart:io。所以官方又搞了一个 web_socket_channel,通过条件导入兼容 VM 和 Web。这个包用起来像是一个统一的 StreamChannel 抽象:WebSocketChannel.connect 返回一个 channel,内部在 VM 上用 dart:io WebSocket,在 Web 上用 dart:html WebSocket。
web_socket 则是更新一代的包,官方说明里明确提到 web_socket_channel 正在向 web_socket 迁移。它的核心思路更激进:即使是在 VM 上,也不直接暴露 dart:io 的 WebSocket 类型,而是实现一个自己维护的客户端,把帧解析、握手、控制帧处理这些逻辑收拢到 Dart 层。这样对外 API 可以保持稳定,底层实现还可以在未来针对不同平台做调整。
看到这里你可能已经明白,“纯 Dart”并不是说它没用 Socket,而是说标准 WebSocket 协议处理逻辑完全由 Dart 代码实现,不依赖平台的原生 WebSocket 能力。这是个极聪明的做法,尤其是面对 OpenHarmony 这种“类 Android 但不是 Android”的平台时,平台原生有没有 WebSocket API、行为是否一致,都不重要了,只要引擎给你 Dart 运行环境就行。
2.2 WebSocket 客户端真正需要处理哪些协议细节
很多开发者用 WebSocket 只停留在调库层面,连接好了就 send 和 onMessage,出了问题就只能抓瞎。这里我把协议层关键点补齐。
RFC 6455 定义的 WebSocket 连接开始于一次 HTTP Upgrade 握手。客户端要发送一个带有 Upgrade: websocket 和 Connection: Upgrade 的 GET 请求,同时带上 Sec-WebSocket-Version: 13、Sec-WebSocket-Key 等头。服务端收到后计算 Sec-WebSocket-Accept,返回 HTTP 101 状态码,连接才算建立。这就是为什么有时候用普通的 HTTP 代理或网关请求服务端接口时会失败——很多代理只认识普通 HTTP 请求,遇到 Upgrade 就可能直接断开或透传不完整。
建立连接之后,数据变成帧格式传输。每一帧里有 FIN 位、opcode、MASK 标记、payload length 和 payload 数据。opcode 决定帧类型:0x1 是文本帧、0x2 是二进制帧、0x8 是关闭帧、0x9 是 ping、0xA 是 pong。客户端发送给服务端的帧必须设置 MASK 掩码位,这也是浏览器和标准客户端的安全要求。如果你自己手写一个 WebSocket 协议栈,忘了加掩码,服务端会直接关闭连接。web_socket 这类成熟客户端会自动做这些处理,所以你看不到。
一个容易被忽视的细节是:WebSocket 底层消息是可能分片传输的。一个大的文本消息可能由多个帧组成,由 FIN 位标识结束。好的客户端会把分片消息重组后一次性推给上层,不会让业务层频繁去拼 buffer。web_socket 的 Stream 事件模型天然适合做这件事,这也是它作为“标准客户端”的体现:文本进来是 String,二进制进来是 List
2.3 原生桥接方案和纯 Dart 方案的差距在哪
在 OpenHarmony 上,很多人第一反应是写一个 ArkTS 或 C++ 的 WebSocket 插件,然后通过 MethodChannel 暴露给 Flutter。这个方案能用,但代价非常大。
原生桥接要面对的第一层问题是协议数据在 Dart 和原生侧之间反复拷贝。文本还好,如果服务端下发的是高频二进制行情、图片块、音视频帧,MethodChannel 的编码解码会成为瓶颈。我在 OpenHarmony RK3568 设备上测试过,高频小包数据频繁走 MethodChannel,CPU 占用和时延都比同机型的 Dart 内部流处理高不少。原因很直接:原生侧先解析出字节数组,再封装成标准类型传给 Dart,Dart 侧还要再做一次类型判定和内存转换。
第二层问题是平台插件注册表。OpenHarmony 的 Flutter 社区分支对 Plugin 的注册机制和 Android 并不完全一致,有些插件需要单独改工程配置,有些还需要在主工程里手动加 so 或初始化代码。这让“跨端共用一套代码”变成了笑话:你以为写的是 Flutter,实际每端都要维护原生代码。
纯 Dart 方案能把这层复杂度完全去掉。下面这个表格能看出差距:
| 对比项 | 原生 WebSocket 插件桥接 | package:web_socket |
|---|---|---|
| 需要维护原生代码 | 每个平台都要写 | 不需要 |
| Dart 侧类型 | 受 MethodChannel 类型约束 | String / List |
| 协议实现位置 | 原生 SDK 或自研 C++ | Dart 层统一实现 |
| Web 端支持 | 不支持或要另写 | 同一套抽象可覆盖 |
| OpenHarmony 适配成本 | 插件注册、权限、so 依赖 | 只需保证 Dart 运行时可访问网络 |
在我看来,标题里那句“跨平台兼容性之王”一点都不夸张。兼容性的本质不是 API 覆盖了多少平台,而是你在面对一个未知平台时,能不能少写一层未知的原生代码。纯 Dart 客户端天生就有这个优势。
3. OpenHarmony 端接入:从工程准备到真机运行
3.1 确认 WebSocket 层的网络路径是通的
接入 OpenHarmony 之前,先别急着写代码。Flutter for OpenHarmony 目前的形态是通过社区维护的 Flutter fork 和 OpenHarmony SDK 配合编译,不同分支在构建指令、产物格式、设备连接命令上都有差异。所以第一步不是背命令,而是把你手里这套环境摸清楚。
我习惯先走通一个最小 Demo:创建 Flutter 工程,让它在 OpenHarmony 设备上跑起来,然后验证 Dart 侧网络能力是否正常。这里的“网络能力”包括域名解析、TCP 连接、TLS 握手。如果这几个底层能力有问题,后面 WebSocket 一定会出现各种诡异现象。
web_socket 在 OpenHarmony 上最终依赖的也是 Dart 的 Socket 和 TLS 栈,这些能力由 Flutter engine 在编译时带过去。只要 Demo 里能用 HttpClient 发起一次普通 HTTPS 请求,就说明网络路径是通的,可以把注意力放在 WebSocket 层。
3.2 在 pubspec.yaml 里确认依赖版本
依赖声明倒是简单,在 pubspec.yaml 中加上:
yaml复制dependencies:
web_socket: ^2.4.0
真正需要注意的是 Dart SDK 版本。web_socket 新版本对 Dart 版本有要求,如果你的 Flutter for OpenHarmony 分支自带的是较旧 Dart SDK,pub get 会直接报错,提示需要更高版本。这种情况可以先把版本锁到旧版,我见过有些工程为了兼容旧 Dart SDK,把依赖改成了:
yaml复制dependencies:
web_socket: 1.1.0
这个版本本身也满足标准 WebSocket 客户端需求,只是 API 命名或扩展 API 可能略有差异。请以 pub.dev 页面实际标注的 SDK 约束为准。别小看这一步,OpenHarmony 分支的 Flutter 版本往往比官方落后不少,版本冲突是第一道坎。
3.3 网络权限配置最容易被忽略
做 Android 开发时,大家都会记得在 AndroidManifest 里声明 INTERNET 权限。换到 OpenHarmony 工程时,很多人会忘记这个动作,因为 Flutter 侧代码跑起来并不报权限错误,但 WebSocket 就是连不上、握手超时、秒断。
OpenHarmony 应用需要在模块配置文件中声明权限。在工程的 entry/src/main/module.json5 里,找到对应 module 配置,加入 requestPermissions:
json5复制{
"module": {
...
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
有些工程模板默认没有 requestPermissions 字段,需要手动加。如果你的应用还需要访问本地网络或获取设备信息,权限列表会更复杂。我只强调 WebSocket 场景最关键的这一个:INTERNET 权限缺失时,底层 connect 行为很像是服务端不可达,会浪费大量排查时间。
3.4 在 RK3568 开发板上的验证过程
我自己调试用的是一块 RK3568 开发板,OpenHarmony 版本不同,Flutter 分支和 HDC 工具也可能不同。连接设备后,我建议先做一个反向端口转发,让开发板能访问开发机上的本地 WebSocket 服务,例如通过 hdc 的端口映射能力把开发机端口映射到设备侧。如果没有这一层,你在开发机上启动的服务端程序,板子上的 App 是访问不到的,会一直报 Connection refused。
部署产物和安装命令要以你手里的 Flutter for OpenHarmony 工具链为准。不同分支可能叫 hap,也可能叫 ohos 包,安装命令有 hdc_std、hdc、dbt 等不同叫法。不要被教程里的具体命令绑死,核心路径是:编译产物拿到后,用对应工具推送到设备并安装。
在板子上运行时,可以额外打开调试日志,观察 TCP 连接是否建立、HTTP Upgrade 是否成功。只要日志里能看到 WebSocket handshake complete 字样,就说明协议层已经通了,接下来可以集中测业务消息和心跳。
4. 连接管理器的核心代码落地
4.1 最小可用的连接管理器
不管业务方是谁,我都不建议把 WebSocket.connect 直接写在页面里。你至少需要封装一个连接管理器,负责生命周期、重连、状态通知和消息分发。封装带来三个直接好处:界面代码不用关心底层连接细节、全局可以共享同一个连接、断线重连逻辑可以被集中测试。
下面这段代码基于 package:web_socket 的 stream/sink 模型,也是当前版本比较推荐的用法。如果你的工程还在使用 web_socket_channel,迁移过来后连接部分几乎长一样,核心逻辑都适用。
dart复制import 'dart:async';
import 'package:web_socket/web_socket.dart';
enum WsStatus { idle, connecting, connected, closed }
class WsManager {
WsManager({
required this.url,
this.onConnected,
this.onMessage,
this.onDisconnected,
});
final String url;
final VoidCallback? onConnected;
final void Function(String)? onMessage;
final VoidCallback? onDisconnected;
WebSocket? _socket;
StreamSubscription<dynamic>? _sub;
WsStatus _status = WsStatus.idle;
bool _manualClose = false;
WsStatus get status => _status;
Future<void> connect() async {
if (_status == WsStatus.connecting || _status == WsStatus.connected) {
return;
}
_manualClose = false;
_setStatus(WsStatus.connecting);
try {
final socket = await WebSocket.connect(url).timeout(const Duration(seconds: 10));
_socket = socket;
_setStatus(WsStatus.connected);
onConnected?.call();
_sub = socket.stream.listen(
(dynamic message) {
if (message is String) {
onMessage?.call(message);
}
},
onError: (Object error) {
// 记录错误并触发重连
_handleConnectionLost();
},
onDone: () {
_handleConnectionLost();
},
);
} catch (e) {
_handleConnectionLost();
}
}
void _handleConnectionLost() {
if (_manualClose) return;
_cleanup();
_setStatus(WsStatus.closed);
onDisconnected?.call();
_scheduleReconnect();
}
void _setStatus(WsStatus status) {
_status = status;
}
void _cleanup() {
_sub?.cancel();
_socket = null;
}
Future<void> close() async {
_manualClose = true;
_cleanup();
await _socket?.sink.close();
_setStatus(WsStatus.closed);
}
}
有人可能问:为什么连接要加超时?因为 WebSocket.connect 在某些平台的默认行为是连接永远挂着,服务端 IP 不可达时可能要等很久。加上 timeout 之后,10 秒内连不上就直接走异常回调,体验可控。
4.2 心跳和断线重连的自定义设计
标准 WebSocket 协议里虽然定义了 ping/pong 控制帧,但多数业务服务端更习惯应用层心跳,也就是在 WebSocket 通道里传一个约定好的 JSON 文本。两类心跳的区别要分清楚:协议层心跳解决的是底层连接是否存活,应用层心跳解决的是业务服务端是否还在正常处理消息。如果你只做协议层 ping,但服务端业务线程已经卡死,客户端是感知不到的。
我一般会这样设计心跳逻辑。每 20 秒发送一个业务心跳包,比如 {"type": "ping", "t": 1699999999999}。服务端正常时应返回对应的 pong 消息。客户端维护一个 _lastReceivedTime 字段,每次收到任何数据都更新。如果超过 60 秒没有收到任何数据,就主动触发连接重建。
重连策略要用指数退避,不然服务端还在恢复期时,大批客户端同时重连会进一步压垮服务。退避起点取 1 秒,每次翻倍,最大到 30 秒。代码里可以这么写:
dart复制int _reconnectAttempt = 0;
void _scheduleReconnect() {
final seconds = _reconnectAttempt >= 5 ? 30 : (1 << _reconnectAttempt);
_reconnectAttempt++;
Timer(Duration(seconds: seconds), connect);
}
连接成功后的第一条消息,或者任何一条合法业务数据,都可以用来重置 _reconnectAttempt 为 0。这个细节很容易被漏掉,一旦漏掉,就算网络恢复了,客户端也会一直用 30 秒的退避间隔重连,体验很差。
4.3 高频消息场景下的 Stream 使用误区
OpenHarmony 上的实时数据场景很多,比如设备状态上报、传感器数据、音视频信令。WebSocket 连接一旦建立,服务端很可能每秒推送几十条消息。这里容易出现一个典型问题:在 stream.listen 的回调里直接做耗时操作。
Dart 是单线程事件循环模型,Stream 事件回调不会并发执行。如果你在回调里做 JSON 解析后再写数据库、再做一次复杂计算,整个事件队列会被占住。服务端继续推送
