👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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 onOpen
  • WebSocket_OnMessageCallback onMessage
  • WebSocket_OnErrorCallback onError
  • WebSocket_OnCloseCallback onClose
  • WebSocket_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 场景

这里使用一个很小的场景:

  1. ArkTS 页面把 WebSocket URL 交给 Native;
  2. Native 创建 WebSocket 客户端;
  3. 连接服务器;
  4. onOpen 后允许发送文本消息;
  5. onMessage 接收服务端数据;
  6. onError 记录错误;
  7. 页面退出或用户主动断开时执行 Close;
  8. 最后 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 数量并不多,真正容易留下问题的是资源边界。

可以把这篇文章浓缩成四句话:

  1. WebSocket 客户端是一个持续存在的 Native 资源,不是一次函数调用。
  2. Constructor、Connect、回调、Close、Destroy 要看成一个完整生命周期。
  3. 同步返回值、异步事件和业务连接状态需要分别处理。
  4. 任何创建成功的客户端,都必须能找到确定的释放路径。

如果准备在此基础上继续做心跳和自动重连,建议先做一次测试:连续执行“连接 → 收发 → 主动关闭 → 再次连接”,同时观察每一轮 Native 指针和回调顺序。很多所谓“重连问题”,实际上在第一轮连接结束时,资源生命周期就已经没有真正闭环。

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐