HarmonyOS WebSocket Native 开发:从创建连接到正确关闭的完整生命周期【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀
前言
WebSocket 适合这样一类业务:客户端与服务端之间不只是“发一次请求、收一次响应”,而是需要在一段时间内保持双向通信,例如即时消息、设备状态同步、实时协作、服务端主动推送等。
如果网络逻辑本身已经放在 C/C++ 层,或者项目需要在 Native 模块里统一管理长连接,HarmonyOS 的 Network Kit 提供了 NetStack C API。当前华为官方 API 文档中,Netstack 明确包含 WebSocket 客户端能力,并给出了它的基本调用顺序:创建实例 → 连接服务器 → 收发数据 → 关闭连接。Netstack C API 总体从 API 11 开始提供;当前文档对 WebSocket 相关能力标注的设备支持起点为 Phone 12+、Tablet 12+、PC/2in1 13+、Wearable 18+、TV 19+。
这篇文章不讨论心跳协议、断线重连框架或者业务消息协议,而是先把更基础也更容易出问题的一件事讲清楚:一个 Native WebSocket 对象从创建到销毁,到底应该怎样管理。
一、哪些业务需要 WebSocket
普通 HTTP 更适合“一次请求对应一次响应”的交互。WebSocket 建立连接以后,客户端与服务端可以在同一连接上持续进行双向通信,因此更适合实时性要求较高、消息到达时间不可预先确定的业务。
比较典型的场景包括:
- 即时聊天与客服消息;
- 在线协作状态同步;
- 游戏房间或实时对战状态;
- 设备运行状态持续上报;
- 服务端事件主动通知;
- 需要长时间保持双向消息通道的 Native 模块。
这里有一个边界需要先明确:本文讨论的是 WebSocket 客户端。华为当前 Netstack API Reference 中的 WebSocket 结构体明确描述为“WebSocket 客户端结构体”,并包含连接打开、收到消息、发生错误和关闭连接四类回调。
如果业务只是偶尔拉取数据,没有服务端主动下发需求,没有必要为了“实时”两个字就默认使用长连接。
二、先把 NetStack C API 的边界弄清楚
当前官方 Network Kit C API 中,与这一场景直接相关的是两个头文件:
#include "network/netstack/net_websocket.h"
#include "network/netstack/net_websocket_type.h"
其中 net_websocket.h 提供 WebSocket 客户端相关接口,net_websocket_type.h 定义接口所需要的数据结构。官方 Netstack 模块文档明确列出了这两个文件。
一个 WebSocket 客户端结构体内部关联了以下几个关键成员:
WebSocket_OnOpenCallback onOpenWebSocket_OnMessageCallback onMessageWebSocket_OnErrorCallback onErrorWebSocket_OnCloseCallback onCloseWebSocket_RequestOptions requestOptions
也就是说,Native WebSocket 并不是调用 Connect 之后同步等待一个结果,而是典型的事件回调模型。
此外,联网本身需要应用具备网络访问权限。Stage 工程可以在 entry/src/main/module.json5 中声明:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
华为官方文档中对联网场景给出的权限名称就是 ohos.permission.INTERNET,并明确展示了在 module.json5 的 requestPermissions 中进行声明的方式。
三、搭一个最小的 Native WebSocket 场景
这里使用一个很小的场景:
- ArkTS 页面把 WebSocket URL 交给 Native;
- Native 创建 WebSocket 客户端;
- 连接服务器;
onOpen后允许发送文本消息;onMessage接收服务端数据;onError记录错误;- 页面退出或用户主动断开时执行 Close;
- 最后 Destroy 并清空 Native 指针。
这里真正需要建立的不是“能不能连上”的 Demo,而是一个明确的资源生命周期:
nullptr
│
▼
Constructor
│
▼
WebSocket*
│
▼
Connect
│
├── onOpen
├── onMessage
├── onError
└── onClose
│
▼
Close
│
▼
Destroy
│
▼
nullptr
官方 Netstack 文档对 WebSocket 客户端同样要求按“创建实例 → 连接服务器 → 收发数据 → 关闭连接”的顺序组织调用。
四、先准备 Native 编译依赖
WebSocket Native 接口需要链接对应系统库。一个采用 NAPI 向 ArkTS 暴露方法的 Native 模块,可以把 WebSocket 系统库加入 CMakeLists.txt。
target_link_libraries(entry PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
libnet_websocket.so
)
业务代码中引入:
#include "napi/native_api.h"
#include "network/netstack/net_websocket.h"
#include "network/netstack/net_websocket_type.h"
#include "hilog/log.h"
#include <cstring>
这一层建议保持简单:WebSocket 生命周期由 Native 自己负责,ArkTS 只负责调用“连接、发送、关闭”几个明确动作,不要让 ArkTS 页面直接保存 Native 指针。
五、创建实例:先把四类回调准备好
根据官方 WebSocket 结构定义,一个客户端包含 onOpen、onMessage、onError 和 onClose 四个回调。
可以先定义:
static WebSocket *g_client = nullptr;
static void OnOpen(WebSocket *client, WebSocket_OpenResult result)
{
(void)client;
OH_LOG_INFO(LOG_APP,
"WebSocket opened, code=%{public}u, reason=%{public}s",
result.code,
result.reason);
}
static void OnMessage(WebSocket *client, char *data, uint32_t length)
{
(void)client;
if (data == nullptr || length == 0) {
return;
}
char *message = new char[length + 1];
std::memcpy(message, data, length);
message[length] = '\0';
OH_LOG_INFO(LOG_APP,
"WebSocket message, length=%{public}u, data=%{public}s",
length,
message);
delete[] message;
}
static void OnError(WebSocket *client, WebSocket_ErrorResult result)
{
(void)client;
OH_LOG_ERROR(LOG_APP,
"WebSocket error, code=%{public}u, message=%{public}s",
result.errorCode,
result.errorMessage);
}
static void OnClose(WebSocket *client, WebSocket_CloseResult result)
{
(void)client;
OH_LOG_INFO(LOG_APP, "WebSocket closed");
}
当前官方 API Reference 中,WebSocket_ErrorResult 的字段就是:
uint32_t errorCode;
const char *errorMessage;
该结构从 API 11 开始提供。
这里比较容易理解错的是 onMessage 的数据处理。回调已经提供了 length,因此业务处理应以实际长度为边界,不应假设收到的 data 天然就是一个可直接按 C 字符串处理的、以 \0 结束的缓冲区。
示例中之所以额外申请 length + 1 字节,是为了打印文本日志时自行补上字符串结尾;真正的协议解析仍然应该依据 length。
六、连接服务器:创建对象和建立连接是两个阶段
实例创建与网络连接不要混成一件事。
可以把连接过程封装为:
static int ConnectWebSocket(const char *url)
{
if (url == nullptr) {
return -1;
}
if (g_client != nullptr) {
OH_LOG_WARN(LOG_APP, "WebSocket client already exists");
return -1;
}
g_client = OH_WebSocketClient_Constructor(
OnOpen,
OnMessage,
OnError,
OnClose
);
if (g_client == nullptr) {
OH_LOG_ERROR(LOG_APP, "Create WebSocket client failed");
return -1;
}
WebSocket_RequestOptions options {};
int ret = OH_WebSocketClient_Connect(g_client, url, options);
if (ret != 0) {
OH_WebSocketClient_Destroy(g_client);
g_client = nullptr;
}
return ret;
}
这段代码解决两个问题。
一个是避免重复创建。如果 Native 层已经持有 g_client,再创建另一个实例,很容易让“哪个连接需要关闭、哪个指针还有效”变得混乱。
另一个是连接调用失败时立即回收已创建实例。创建对象成功不代表连接一定成功。如果建立连接这一步不能继续,之前已经得到的客户端资源就不能直接留在那里。
WebSocket_RequestOptions 是建立连接时使用的参数结构,官方文档明确将其描述为“WebSocket 客户端和服务端建立连接的参数”,并从 API 11 开始提供。
如果业务需要握手 Header,再单独按服务端协议添加;如果不需要,不要为了“代码完整”构造无关请求头。
七、发送数据:不要只判断指针非空
最小发送函数可以写成:
static int SendText(const char *text)
{
if (g_client == nullptr || text == nullptr) {
return -1;
}
size_t length = std::strlen(text);
return OH_WebSocketClient_Send(
g_client,
const_cast<char *>(text),
length
);
}
真正需要关注的是:对象存在和连接已经进入可正常收发的状态,不是同一个概念。
Constructor 得到 Native 对象后,Connect 仍然需要完成连接建立过程,而连接结果又通过回调通知。因此实际项目中更合适的做法是由 Native 或上层维护状态,例如:
enum class WsState {
IDLE,
CONNECTING,
OPEN,
CLOSING
};
然后:
- 调用 Connect 前切到
CONNECTING; OnOpen中切到OPEN;- 只有
OPEN时允许业务发送; - 主动关闭时切到
CLOSING; OnClose后回到IDLE。
这里的状态枚举是业务自己的封装,不是 HarmonyOS 系统 API。它解决的是“怎样正确使用官方异步接口”的问题,而不是替代系统状态。
八、异常处理:同步返回值和异步错误不能只看一个
Native 网络接口通常同时存在两个观察面。
一个是函数调用本身的返回值:
int ret = OH_WebSocketClient_Connect(...);
另一个是连接建立和运行过程中通过:
OnError(...)
收到的异步异常。
因此排错时不能只写:
if (ret == 0) {
// 以后一定不会出错
}
ret 反映的是当前接口调用结果,而 WebSocket 是一个持续存在的网络连接,后续仍可能发生网络断开、服务端关闭或者协议层异常。
官方 WebSocket 结构体专门提供 onError 和 onClose 两个独立回调,也正说明错误事件和关闭事件应该作为生命周期的一部分处理。
日志至少建议保留:
connect return value
onOpen.code
onOpen.reason
onError.errorCode
onError.errorMessage
onClose information
出问题以后,才能判断失败发生在“接口调用阶段”,还是已经进入连接生命周期之后。
九、主动关闭:Close 和 Destroy 不是同一件事
生命周期里最值得单独讲的就是这里。
主动关闭可以封装为:
static int CloseWebSocket()
{
if (g_client == nullptr) {
return 0;
}
WebSocket_CloseOption option {
.code = 0,
.reason = "Actively Close"
};
int ret = OH_WebSocketClient_Close(g_client, option);
OH_WebSocketClient_Destroy(g_client);
g_client = nullptr;
return ret;
}
这里包含两个动作:
Close → 关闭 WebSocket 连接
Destroy → 释放客户端上下文和资源
它们解决的不是同一个问题。
只 Close 而一直保留 Native 客户端资源,会让资源管理失去闭环;反过来,生命周期设计中也不应该把“业务想正常断开连接”和“释放本地实例”理解成完全相同的操作。
官方 Netstack 文档把 WebSocket 生命周期明确描述为最终需要进入关闭阶段,而 WebSocket 又是一个独立客户端结构体,因此 Native 代码必须给它设计清晰的资源所有权。
还有一个细节:Destroy 之后立刻把:
g_client = nullptr;
做掉。
这样下一次 Connect 才能准确判断当前是否存在客户端,也能避免之后误用已经释放的地址。
十、页面生命周期不要直接等价于 WebSocket 生命周期
如果 WebSocket 只服务于某一个页面,那么页面退出时关闭连接通常容易理解。
但如果连接服务于:
- 多个页面共同使用的实时消息;
- 整个应用级会话;
- Native 层公共通信模块;
那么就不能机械地在某一个页面消失时 Destroy。
更合理的设计是先确定谁拥有 WebSocket。
例如:
Page
│
▼
ArkTS WebSocketManager
│
▼
NAPI
│
▼
Native WsClient
│
▼
NetStack WebSocket
页面只是使用者,真正的资源所有者是 WebSocketManager 或 Native 管理对象。
这种做法能避免一个常见结构问题:A 页面创建连接,B 页面正在使用,A 页面销毁时却把整个 Native WebSocket 一起释放。
十一、几个容易理解错的地方
1. Connect() 返回成功,不等于已经执行了 onOpen
WebSocket 是回调驱动的长连接接口。业务真正进入可收发阶段,应围绕连接打开事件维护状态,而不是简单把调用过 Connect() 当成“已经在线”。
2. g_client != nullptr 不等于连接一定可发送
非空只能说明客户端对象还存在。连接是否已经打开,应由生命周期状态共同判断。
3. 收到错误以后不要假定对象自然消失
onError 是错误通知。资源由谁关闭、什么时候 Destroy,仍然需要应用自己建立明确规则。
4. 收到 onClose 后仍要考虑本地资源清理
远端关闭意味着连接生命周期发生了变化,但 Native 对象所有权仍然属于应用代码。
5. Destroy 之后必须停止继续使用原指针
这不仅是 WebSocket 问题,而是 Native 资源管理最基础的边界。
6. 不要为了处理文本消息忽略 length
onMessage 提供了数据长度,就应按长度消费数据。需要转换成 C 字符串时,由应用自己创建额外空间并补终止符。
十二、实际项目中建议按这个顺序排查
当 Native WebSocket 出现“连接不上”“能连但不能发”“关闭后再次连接异常”等问题时,可以按下面顺序检查。
第一步,确认 API 与设备版本。
当前官方最新文档显示 Netstack 模块总体起始于 API 11;WebSocket 当前设备标注为 Phone 12+、Tablet 12+、PC/2in1 13+、Wearable 18+、TV 19+。不要只看到“API 11”就忽略具体设备类型的最低版本标注。
第二步,检查网络权限。
确认 module.json5 中已经声明:
{
"name": "ohos.permission.INTERNET"
}
第三步,确认 Native 链接关系。
检查 WebSocket 系统库以及 net_websocket.h、net_websocket_type.h 是否按 Native 工程方式接入。
第四步,看客户端是不是重复创建。
打印 g_client 的生命周期,不要在旧对象仍存在时静默覆盖它。
第五步,区分 Connect 返回值和回调结果。
同时观察 Connect 返回值、onOpen、onError、onClose,不要只打印一处日志。
第六步,检查发送时状态。
不要只判断 g_client != nullptr,还要确认业务状态已经进入 OPEN。
第七步,检查关闭路径。
确认所有退出路径最终都能进入统一的 Close/Destroy 逻辑,包括:
用户主动退出
连接建立失败
页面/业务结束
服务端关闭
异常恢复失败
最后再检查业务消息协议本身。否则很容易把生命周期错误误判成“服务端没回消息”。
十三、一个更适合项目迁移的封装方式
Demo 里使用全局:
static WebSocket *g_client = nullptr;
便于理解接口,但真实工程最好进一步封装:
class NativeWebSocketClient {
public:
int Connect(const char *url);
int Send(const char *data, size_t length);
int Close();
private:
WebSocket *client_ = nullptr;
WsState state_ = WsState::IDLE;
};
这样可以把三个关键规则固定在一个地方:
一个实例只管理一个 WebSocket*
发送前检查状态
所有退出路径统一释放资源
如果后面继续增加鉴权 Header、业务心跳或者断线重连,也应该建立在这个生命周期已经闭环的基础上,而不是直接往页面代码里继续叠回调。
开发经验总结
Native WebSocket 的 API 数量并不多,真正容易留下问题的是资源边界。
可以把这篇文章浓缩成四句话:
- WebSocket 客户端是一个持续存在的 Native 资源,不是一次函数调用。
- Constructor、Connect、回调、Close、Destroy 要看成一个完整生命周期。
- 同步返回值、异步事件和业务连接状态需要分别处理。
- 任何创建成功的客户端,都必须能找到确定的释放路径。
如果准备在此基础上继续做心跳和自动重连,建议先做一次测试:连续执行“连接 → 收发 → 主动关闭 → 再次连接”,同时观察每一轮 Native 指针和回调顺序。很多所谓“重连问题”,实际上在第一轮连接结束时,资源生命周期就已经没有真正闭环。
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐



所有评论(0)