将 ohos_coap 移植到仓颉:从 libcoap、C ABI 到 HarmonyOS HAR 与应用
本文完成一个具体的移植工程:保留 ohos_coap 使用的 libcoap C 核心,新增 C ABI 桥接层,再用仓颉实现客户端、服务端和应用演示。读者可以从空工程开始,按文中的目录、完整源码和命令生成 coap.har 与 HAP。
下载完整工程 ZIP(约 1.3 MB)。包内包含两套工程、固定版本的 libcoap 源码、两种架构的桥接库、许可证与教程。解压后先按第 3 节准备 SDK,再打开 ohos-coap-app,从第 10 节执行构建;若希望理解并手工搭建全部过程,从第 4 节逐步操作。
验证日期:2026 年 9 月 30 日。 Windows 上的仓颉回环、独立 UDP 对端检查、HarmonyOS arm64 与 x86_64 原生库交叉编译、双架构 HAR/HAP 构建均已通过。尚未在 HarmonyOS 设备或模拟器上运行。 本机模拟器需要启用 Hyper-V,此次未启用;下文的设备运行步骤是交付给读者的验收步骤,不能当作已经完成的运行证据。当前 HAP 未签名。
网页中的仓颉代码采用本地词法着色;主题代码框顶部仍可能显示 TEXT,实际代码与复制内容均为仓颉。C、PowerShell 等代码沿用网站现有高亮。编译结果用于验证语法正确性。
1. 移植对象和交付范围
ohos_coap 上游仓库 提供面向 ArkTS 的 NAPI 包装,底层协议处理来自 libcoap。上游页面已标记归档并提供 GitCode 新地址。本文锁定现有 Gitee 提交,避免后续默认分支变化影响复现。
| 项目 | 本文固定值 |
|---|---|
| ohos_coap 提交 | 4ad8844e1748706283721d46b8c65ca3e46a337e |
| libcoap 子模块提交 | 5fd2f89ef068214130e5d60b7087ef48711fa615 |
| libcoap 实际源码版本 | 4.3.4,以该提交的 CMakeLists.txt 为准 |
| DevEco Studio / 仓颉插件 | 26.0.0.821,Windows |
| HarmonyOS 仓颉编译器 | 1.2.0-beta.rc3 |
| 工程目标 SDK | 26.0.0 |
| 工程兼容 SDK | 6.1.1(24) |
| Native SDK Clang | 15.0.4 |
| 产物架构 | arm64-v8a、x86_64 |
上游 README 介绍中仍写着 libcoap 4.3.1;本文检出的子模块 CMake 声明为 4.3.4。构建和生命周期判断都按后者进行。本文使用的是该固定提交中的原生 C 核心,没有编译上游 NAPI 包装层;也没有把上游 thirdModule 的补丁流程自动套到不同版本上。涉及线上部署时,需要对这个固定版本及所需补丁另做维护评估。
这份移植的公开接口采用仓颉对象与函数,功能对应关系如下;它不是 ArkTS 接口的逐字兼容层。
| 功能 | 仓颉交付 |
|---|---|
| 客户端 GET / POST / PUT | requestUri(uri, method: 1 / 2 / 3) |
| CON / NON | kind: 0 / 1 |
| 文本 / 二进制载荷 | setPayload / setBinaryPayload |
| Content-Format、端口、MID、Token | setFormat、setPort、setMID、setToken |
| 等待时长、最大重传次数 | setWaitSeconds、setRetryCount |
| 自动 Block1 / Block2 | 默认块模式 3,完整消息体;由 libcoap 处理 |
| 自定义选项 | setCustomOption,只接受不由桥接层管理的选项 |
| 服务端 GET / POST | CoapServer.register(path, method, handler) |
| 请求正文、路径、查询与选项 | CoapRequest 的复制快照 |
| 服务端响应、停止 | CoapReply、stop() 后等待运行任务结束 |
| 应用与库 | entry HAP、coap HAR,均包含两种架构 |
当前未封装持续 Observe 订阅、资源属性、对端地址查询、DELETE、服务端 PUT、文件/媒体发送助手、TLS/DTLS、TCP、WebSocket、OSCORE。服务端 PUT 请求会得到 4.05;客户端 PUT 可以请求支持 PUT 的外部服务。应用运行、设备网络权限实际表现以及局域网互通仍需设备验收。
2. 三层结构与边界
调用链是:仓颉应用 → 仓颉 HAR → libcoap_cj_bridge.so → libcoap → UDP。协议报文、ACK、重传和块传输交给 libcoap,桥接层只提供一组固定宽度的 C 函数和不透明句柄。
桥接层导出的函数使用 int32_t、int64_t、uint16_t、uint32_t、uint64_t。仓颉分别声明 Int32、Int64、UInt16、UInt32、UInt64,避免让平台相关的 size_t 或 C 结构体布局进入公开 ABI。void * 对应 CPointer<Unit>,UTF-8 C 字符串对应 CString。
内存所有权需要先确定,再写调用代码:
- 传给 C 的字节数组: 仓颉只在同步 setter 或回复函数期间 pin 数组;C 在返回前复制数据,
finally释放 pin。C 不保存仓颉数组指针。 - 传给 C 的字符串:
mallocCString和LibC.free配对。URI、资源路径、绑定地址拒绝内嵌 NUL,防止 C 看到截断后的字符串。 - C 返回的指针: 在原生句柄释放或回调结束前立即复制为仓颉数组/字符串。
- 大响应正文: 桥接层分配 C 堆副本,由 libcoap 的 release callback 释放。固定的 4.3.4 实现也会在失败路径释放它,桥接层不能再释放一次。
- 服务端回调: C 只保存整数 handler ID;仓颉在受锁保护的注册表中保留闭包。
@C入口捕获Exception,未生成响应时由 C 返回 5.00。严重运行时错误仍不属于普通业务异常的恢复契约。 - 停止顺序:
stop()设置原子标志,轮询任务退出后释放 C 上下文,再清除注册表;调用方必须等待对应Future。
默认块模式 3 是 COAP_BLOCK_USE_LIBCOAP | COAP_BLOCK_SINGLE_BODY,让应用收到组装后的消息体。libcoap 4.3.4 块传输文档 说明了这些模式和大数据接口的释放回调。本实现的单次输入和最终复制正文限制为 1 MiB;这个限制不等于 libcoap 重组阶段所有内存占用的总上限。
3. 准备开发环境
先安装 DevEco Studio 26 与对应仓颉插件,登录拥有下载权限的华为账号,安装随插件提供的 HarmonyOS 仓颉 SDK。安装入口与版本要求以华为官方仓颉开发环境说明 为准。
本机曾出现“插件暂不支持非中国地区”的启动提示,最终确认是 DevEco 没有登录华为账号,登录后解决。遇到同类提示应先检查账号登录、体验版权限和插件版本;不要把这次问题直接归因于 Windows 地区。
本文的构建步骤不需要启动 Hyper-V。模拟器可能需要虚拟化支持;也可以在最后连接适配的真机。通用 Windows 仓颉 SDK 与 HarmonyOS 仓颉 SDK 的目标不同,不能拿 Windows DLL 放进 HAR。
下面约定工程根目录为 C:\dev\coap-cangjie。该目录可以替换;建议避开同步盘,减少生成目录属性和文件占用问题。在 PowerShell 7 中准备路径:
$projectRoot = 'C:\dev\coap-cangjie'
$devEco = 'C:\Program Files\Huawei\DevEco Studio'
$cjSdk = "$env:USERPROFILE\.cangjie-sdk\26.0\cangjie"
$nativeSdk = Join-Path $devEco 'sdk\default\openharmony\native'
New-Item -ItemType Directory -Path $projectRoot -Force | Out-Null
Set-Location $projectRoot
. (Join-Path $cjSdk 'build-tools\envsetup.ps1')
$env:PATH = (Join-Path $nativeSdk 'build-tools\cmake\bin') + ';' + $env:PATH
cjc -v
cjpm --version
cmake --version
ninja --version
cjc 必须来自这里的 1.2 SDK。cmake / ninja 可直接使用 Native SDK 随附的版本;交叉编译使用 SDK 的 Clang 与 sysroot。可选的 Windows 回环检查另外需要 MinGW GCC;本机版本为 GCC 15.2.0、CMake 4.2.3。不要拿安装包文件名代替实际命令版本检查。
4. 创建应用和 HAR 工程
在 DevEco 中创建 [Cangjie] Empty Ability 应用,保存到 $projectRoot\ohos-coap-app,选择 SDK 26.0.0。然后在工程中使用 New → Module → [Cangjie] Static Library,模块名填写 coap。不同版本菜单措辞可能略有区别,以当前插件模板为准。
本文工程中,模板生成的包名分别为 ohos_app_cangjie_entry 和 ohos_app_cangjie_coap。如果你的模板包名不同,要统一调整 .cj 的 package、import、cjpm.toml 中的 name、combined 构建配置和 module.json5 的 srcEntry。最简单的复现方式是沿用本文包名。
向导负责生成图标、字符串资源、构建插件、Ability 模板等基础文件。保留这些文件,下文会给出需要覆盖或核对的配置和业务源码。Static Library 是 IDE 的 HAR 模块模板名称;这个版本模板的 CJPM output-type 实际为 dynamic,打包后包含仓颉 .so,不能擅自改成纯静态目标。
完成后的主要目录如下:
coap-cangjie/
├─ ohos-coap-libcoap-port/
│ ├─ Build-Native.ps1
│ ├─ check-native.py
│ ├─ native/
│ │ ├─ CMakeLists.txt
│ │ ├─ coap_bridge.h
│ │ └─ coap_bridge.c
│ ├─ third_party/libcoap/ # 固定提交的 C 核心
│ └─ host-check/ # 可选 Windows 仓颉检查
└─ ohos-coap-app/
├─ Build-App.ps1
├─ build-profile.json5
├─ entry/
│ ├─ cjpm.toml
│ ├─ build-profile.json5
│ ├─ oh-package.json5
│ └─ src/main/{module.json5,cangjie/}
└─ coap/
├─ cjpm.toml
├─ build-profile.json5
├─ libs/{arm64-v8a,x86_64}/libcoap_cj_bridge.so
└─ src/main/
├─ cangjie/{native,client,server,demo}.cj
└─ resources/rawfile/libcoap-LICENSE.txt
5. 取得固定版本的原生核心
在工程根目录执行下面的命令。先固定上游提交,再更新子模块;不要先检出子模块的最新 HEAD。
git clone https://gitee.com/openharmony-tpc/ohos_coap.git ohos_coap-upstream
git -C ohos_coap-upstream checkout 4ad8844e1748706283721d46b8c65ca3e46a337e
git -C ohos_coap-upstream submodule update --init --recursive
git -C ohos_coap-upstream/libcoap/src/main/cpp/libcoapSource rev-parse HEAD
最后输出必须为 5fd2f89ef068214130e5d60b7087ef48711fa615。随后复制源码,排除 Git 元数据;这是一个完整源码快照,不依赖上游 ArkTS 模块的构建系统。
New-Item -ItemType Directory -Path 'ohos-coap-libcoap-port\third_party' -Force | Out-Null
robocopy 'ohos_coap-upstream\libcoap\src\main\cpp\libcoapSource' 'ohos-coap-libcoap-port\third_party\libcoap' /E /XD .git /XF .git
if ($LASTEXITCODE -ge 8) { throw 'libcoap source copy failed' }
Copy-Item -LiteralPath 'ohos_coap-upstream\LICENSE' -Destination 'ohos-coap-libcoap-port\LICENSE'
Windows 上若 Git 未创建真实符号链接,README 和 examples/contiki/coap_config.h 可能只是链接目标名。可分别复制 README.md、coap_config.h.contiki 为这两个文件的内容副本。本工程的 Windows/OHOS 目标没有编译 Contiki 示例;源码快照保留这种展开记录,不改动协议实现。
6. 编写 C ABI 桥接层
以下三个文件保存到 ohos-coap-libcoap-port/native。代码是完整实现,不需要另找隐藏的辅助函数。
6.1 CMakeLists.txt
libcoap 构建为 PIC 静态库并链接进入一个桥接 .so。关闭本文未交付的协议功能、示例和文档目标,避免引入额外的 TLS 依赖。
cmake_minimum_required(VERSION 3.16)
project(ohos_coap_cangjie_bridge LANGUAGES C)
set(LIBCOAP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/libcoap" CACHE PATH "Pinned libcoap source directory")
if(NOT EXISTS "${LIBCOAP_SOURCE_DIR}/CMakeLists.txt")
message(FATAL_ERROR "Initialize third_party/libcoap, or set LIBCOAP_SOURCE_DIR")
endif()
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(ENABLE_DTLS OFF CACHE BOOL "" FORCE)
set(ENABLE_TCP OFF CACHE BOOL "" FORCE)
set(ENABLE_WS OFF CACHE BOOL "" FORCE)
set(ENABLE_OSCORE OFF CACHE BOOL "" FORCE)
set(ENABLE_AF_UNIX OFF CACHE BOOL "" FORCE)
set(ENABLE_EXAMPLES OFF CACHE BOOL "" FORCE)
set(ENABLE_DOCS OFF CACHE BOOL "" FORCE)
set(ENABLE_TESTS OFF CACHE BOOL "" FORCE)
set(WITH_OBSERVE_PERSIST OFF CACHE BOOL "" FORCE)
set(WITH_EPOLL OFF CACHE BOOL "" FORCE)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_subdirectory("${LIBCOAP_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/libcoap")
add_library(coap_cj_bridge SHARED coap_bridge.c)
target_compile_features(coap_cj_bridge PRIVATE c_std_11)
target_compile_definitions(coap_cj_bridge PRIVATE CC_COAP_EXPORTS)
target_link_libraries(coap_cj_bridge PRIVATE coap-3)
target_include_directories(coap_cj_bridge PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
if(MINGW)
# Avoid mixing MinGW 15 clock_gettime64 with the SDK's older pthread DLL.
target_link_libraries(coap_cj_bridge PRIVATE ws2_32 "-Wl,-Bstatic" winpthread "-Wl,-Bdynamic")
target_link_options(coap_cj_bridge PRIVATE -static-libgcc)
endif()
if(NOT WIN32)
set_target_properties(coap_cj_bridge PROPERTIES C_VISIBILITY_PRESET hidden)
endif()
6.2 coap_bridge.h
客户端句柄只执行一次请求;返回正文指针属于句柄。服务端 exchange 指针仅在同步回调内有效。
#ifndef COAP_CANGJIE_BRIDGE_H
#define COAP_CANGJIE_BRIDGE_H
#include <stdint.h>
#if defined(_WIN32)
#define CC_API __declspec(dllexport)
#else
#define CC_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* Handles are single-owner. Input bytes are copied before a setter returns.
* Borrowed result pointers remain valid until the owning handle is freed.
* Client status 0 means a CoAP response, including a 4.xx or 5.xx response.
* Configuration keys: 1=format, 2=port, 3=MID, 4=timeout ms,
* 5=max retransmissions, 6=libcoap block mode. */
CC_API void *cc_client_new(const char *uri, int32_t method, int32_t type);
CC_API int32_t cc_client_set_uint(void *client, int32_t key, int64_t value);
CC_API int32_t cc_client_set_payload(void *client, const uint8_t *data, uint64_t size);
CC_API int32_t cc_client_set_token(void *client, const uint8_t *data, uint64_t size);
CC_API int32_t cc_client_add_option(void *client, uint16_t number, const uint8_t *data, uint64_t size);
CC_API int32_t cc_client_run(void *client);
CC_API int32_t cc_client_response_code(void *client);
CC_API uint64_t cc_client_response_size(void *client);
CC_API const uint8_t *cc_client_response_data(void *client);
CC_API const char *cc_client_error(void *client);
CC_API void cc_client_free(void *client);
/* Poll and registration run on the server's owner thread. The callback is
* synchronous during poll; the exchange expires as soon as it returns.
* Stop on the Cangjie side and join the polling task before freeing a server. */
typedef void (*cc_handler)(int64_t handler_id, void *exchange);
CC_API void *cc_server_new(const char *address, uint16_t port);
CC_API int32_t cc_server_register(void *server, const char *path, int32_t method, int64_t handler_id);
CC_API int32_t cc_server_poll(void *server, uint32_t wait_ms, cc_handler handler);
CC_API const char *cc_server_error(void *server);
CC_API void cc_server_free(void *server);
CC_API int32_t cc_exchange_method(void *exchange);
CC_API const char *cc_exchange_path(void *exchange);
CC_API const uint8_t *cc_exchange_payload(void *exchange);
CC_API uint64_t cc_exchange_payload_size(void *exchange);
CC_API const uint8_t *cc_exchange_query(void *exchange);
CC_API uint64_t cc_exchange_query_size(void *exchange);
CC_API const uint8_t *cc_exchange_option(void *exchange, uint16_t number);
CC_API uint64_t cc_exchange_option_size(void *exchange, uint16_t number);
CC_API int32_t cc_exchange_option_number_at(void *exchange, uint32_t index);
CC_API const uint8_t *cc_exchange_option_data_at(void *exchange, uint32_t index);
CC_API uint64_t cc_exchange_option_size_at(void *exchange, uint32_t index);
CC_API int32_t cc_exchange_reply(void *exchange, int32_t code, uint16_t format, const uint8_t *data, uint64_t size);
CC_API void cc_set_log(int32_t enabled);
#ifdef __cplusplus
}
#endif
#endif
6.3 coap_bridge.c
这里调用 libcoap 的 URI 解析、上下文、会话、PDU、选项、大数据和轮询 API。自定义选项拒绝保留编号 0、Observe 6,以及桥接层管理的 URI、Content-Format、块与大小选项;客户端结束一次请求就释放会话与上下文,不会维持观察订阅。
#include "coap_bridge.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <netdb.h>
#include <sys/socket.h>
#endif
#include <coap3/coap.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CC_MAX_BODY (1024u * 1024u)
static atomic_flag startup_lock = ATOMIC_FLAG_INIT;
static int initialized;
static void initialize(void) {
while (atomic_flag_test_and_set(&startup_lock)) {}
if (!initialized) { coap_startup(); initialized = 1; }
atomic_flag_clear(&startup_lock);
}
static int replace_bytes(uint8_t **destination, const uint8_t *source, uint64_t size) {
if (size > CC_MAX_BODY || (size && !source)) return 0;
uint8_t *copy = size ? malloc((size_t)size) : NULL;
if (size && !copy) return 0;
if (size) memcpy(copy, source, (size_t)size);
free(*destination); *destination = copy;
return 1;
}
static int resolve_address(const char *host, uint16_t port, coap_address_t *address) {
struct addrinfo hints = {0}, *result = NULL;
hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM;
if (getaddrinfo(host, NULL, &hints, &result) || !result) return 0;
struct addrinfo *entry;
for (entry = result; entry; entry = entry->ai_next) {
if (entry->ai_family == AF_INET || entry->ai_family == AF_INET6) break;
}
if (!entry || entry->ai_addrlen > sizeof(address->addr)) { freeaddrinfo(result); return 0; }
coap_address_init(address);
address->size = (socklen_t)entry->ai_addrlen;
memcpy(&address->addr, entry->ai_addr, entry->ai_addrlen);
coap_address_set_port(address, port);
freeaddrinfo(result);
return 1;
}
typedef struct {
char *uri;
int32_t method, type, format, port, mid;
uint32_t timeout_ms, retransmit, block_mode;
uint8_t *payload, token[8], *response;
size_t payload_size, token_size, response_size;
int token_set, used, done, status, response_code;
char error[192];
coap_optlist_t *options;
} client_t;
static int fail(client_t *client, int status, const char *message) {
client->status = status;
snprintf(client->error, sizeof(client->error), "%s", message);
return status;
}
void *cc_client_new(const char *uri, int32_t method, int32_t type) {
initialize();
if (!uri || method < 1 || method > 3 || type < 0 || type > 1) return NULL;
client_t *client = calloc(1, sizeof(*client));
if (!client) return NULL;
client->uri = malloc(strlen(uri) + 1);
if (!client->uri) { free(client); return NULL; }
strcpy(client->uri, uri);
client->method = method; client->type = type;
client->format = client->port = client->mid = -1;
client->timeout_ms = 10000; client->retransmit = 4;
client->block_mode = COAP_BLOCK_USE_LIBCOAP | COAP_BLOCK_SINGLE_BODY;
return client;
}
int32_t cc_client_set_uint(void *handle, int32_t key, int64_t value) {
client_t *c = handle;
if (!c || c->used) return -1;
switch (key) {
case 1: if (value < 0 || value > 65535) break; c->format = (int32_t)value; return 0;
case 2: if (value < 1 || value > 65535) break; c->port = (int32_t)value; return 0;
case 3: if (value < 0 || value > 65535) break; c->mid = (int32_t)value; return 0;
case 4: if (value < 1 || value > 600000) break; c->timeout_ms = (uint32_t)value; return 0;
case 5: if (value < 0 || value > 8) break; c->retransmit = (uint32_t)value; return 0;
case 6: if (value != 0 && value != 1 && value != 3) break; c->block_mode = (uint32_t)value; return 0;
default: break;
}
return fail(c, 1, "invalid client configuration");
}
int32_t cc_client_set_payload(void *handle, const uint8_t *data, uint64_t size) {
client_t *c = handle;
if (!c || c->used || !replace_bytes(&c->payload, data, size)) return -1;
c->payload_size = (size_t)size; return 0;
}
int32_t cc_client_set_token(void *handle, const uint8_t *data, uint64_t size) {
client_t *c = handle;
if (!c || c->used || size > 8 || (size && !data)) return -1;
if (size) memcpy(c->token, data, (size_t)size);
c->token_size = (size_t)size; c->token_set = 1; return 0;
}
int32_t cc_client_add_option(void *handle, uint16_t number, const uint8_t *data, uint64_t size) {
client_t *c = handle;
/* URI and payload options are owned by the URI parser / libcoap; option 0 is
* reserved, Observe 6 needs a subscription API we do not expose, and Size2 28
* / Size1 60 body sizing belongs to libcoap blockwise handling. */
if (!c || c->used || size > 65804 || (size && !data) || number == 0 || number == 3 || number == 6 ||
number == 7 || number == 11 || number == 12 || number == 15 || number == 23 || number == 27 ||
number == 28 || number == 60) return -1;
coap_optlist_t *option = coap_new_optlist(number, (size_t)size, data);
if (!option) return -1;
return coap_insert_optlist(&c->options, option) ? 0 : -1;
}
static coap_response_t received(coap_session_t *session, const coap_pdu_t *sent,
const coap_pdu_t *response, const coap_mid_t mid) {
(void)sent; (void)mid;
client_t *c = coap_session_get_app_data(session);
coap_bin_const_t token = coap_pdu_get_token(response);
if (!c || token.length != c->token_size ||
(token.length && memcmp(token.s, c->token, token.length))) return COAP_RESPONSE_FAIL;
size_t length = 0, offset = 0, total = 0; const uint8_t *data = NULL;
c->response_code = coap_pdu_get_code(response);
if (coap_get_data_large(response, &length, &data, &offset, &total)) {
if (offset != 0 || length != total || !replace_bytes(&c->response, data, length)) {
fail(c, 2, "response exceeds limit or block body is incomplete");
} else { c->response_size = length; }
}
c->done = 1;
return COAP_RESPONSE_OK;
}
static void rejected(coap_session_t *session, const coap_pdu_t *sent,
const coap_nack_reason_t reason, const coap_mid_t mid) {
(void)sent; (void)mid;
client_t *c = coap_session_get_app_data(session);
if (c) { fail(c, 3, reason == COAP_NACK_RST ? "peer reset the request" : "delivery failed or retries exhausted"); c->done = 1; }
}
int32_t cc_client_run(void *handle) {
client_t *c = handle;
if (!c || c->used) return -1;
c->used = 1; c->status = 0;
coap_uri_t uri; coap_address_t destination;
coap_context_t *context = NULL; coap_session_t *session = NULL;
coap_pdu_t *pdu = NULL; coap_optlist_t *options = NULL;
char *host = NULL; uint8_t *scratch = NULL;
if (coap_split_uri((const uint8_t *)c->uri, strlen(c->uri), &uri) < 0 || uri.scheme != COAP_URI_SCHEME_COAP) {
fail(c, 1, "only a valid coap:// UDP URI is supported"); goto done;
}
host = malloc(uri.host.length + 1);
if (!host) { fail(c, 2, "allocation failed"); goto done; }
memcpy(host, uri.host.s, uri.host.length); host[uri.host.length] = '\0';
if (c->port >= 0) uri.port = (uint16_t)c->port;
if (!resolve_address(host, uri.port, &destination)) { fail(c, 2, "host resolution failed"); goto done; }
context = coap_new_context(NULL);
if (!context) { fail(c, 2, "cannot create CoAP context"); goto done; }
coap_context_set_block_mode(context, c->block_mode);
coap_register_response_handler(context, received);
coap_register_nack_handler(context, rejected);
session = coap_new_client_session(context, NULL, &destination, COAP_PROTO_UDP);
if (!session) { fail(c, 2, "cannot create UDP session"); goto done; }
coap_session_set_app_data(session, c);
coap_session_set_max_retransmit(session, (uint16_t)c->retransmit);
if (!c->token_set) {
c->token_size = sizeof(c->token);
coap_session_new_token(session, &c->token_size, c->token);
}
pdu = coap_new_pdu((coap_pdu_type_t)c->type, (coap_pdu_code_t)c->method, session);
if (!pdu || !coap_add_token(pdu, c->token_size, c->token)) { fail(c, 2, "cannot create request PDU"); goto done; }
if (c->mid >= 0) coap_pdu_set_mid(pdu, (coap_mid_t)c->mid);
size_t scratch_size = strlen(c->uri) * 2 + 32;
scratch = malloc(scratch_size);
if (!scratch || coap_uri_into_options(&uri, &destination, &options, 1, scratch, scratch_size) < 0) {
fail(c, 1, "cannot encode URI options"); goto done;
}
for (coap_optlist_t *o = c->options; o; o = o->next) {
coap_optlist_t *copy = coap_new_optlist(o->number, o->length, o->data);
if (!copy || !coap_insert_optlist(&options, copy)) { fail(c, 2, "option allocation failed"); goto done; }
}
if (c->format >= 0) {
uint8_t encoded[2]; size_t length = coap_encode_var_safe(encoded, sizeof(encoded), (unsigned)c->format);
coap_optlist_t *o = coap_new_optlist(COAP_OPTION_CONTENT_FORMAT, length, encoded);
if (!o || !coap_insert_optlist(&options, o)) { fail(c, 2, "format allocation failed"); goto done; }
}
if ((options && !coap_add_optlist_pdu(pdu, &options)) ||
(c->payload_size && !coap_add_data_large_request(session, pdu, c->payload_size, c->payload, NULL, NULL))) {
fail(c, 2, "cannot add request options or payload"); goto done;
}
coap_mid_t sent_mid = coap_send(session, pdu); pdu = NULL; /* coap_send owns the PDU. */
if (sent_mid == COAP_INVALID_MID) { fail(c, 2, "cannot send request"); goto done; }
coap_tick_t start, now; coap_ticks(&start);
while (!c->done) {
coap_ticks(&now);
uint64_t elapsed = (uint64_t)(now - start) * 1000 / COAP_TICKS_PER_SECOND;
if (elapsed >= c->timeout_ms) { fail(c, 4, "request timed out"); break; }
uint32_t remaining = c->timeout_ms - (uint32_t)elapsed;
if (coap_io_process(context, remaining > 100 ? 100 : remaining) < 0) { fail(c, 2, "CoAP I/O failed"); break; }
}
done:
if (pdu) coap_delete_pdu(pdu);
if (session) coap_session_release(session);
if (context) coap_free_context(context);
coap_delete_optlist(options); free(host); free(scratch);
return c->status;
}
int32_t cc_client_response_code(void *handle) { return handle ? ((client_t *)handle)->response_code : 0; }
uint64_t cc_client_response_size(void *handle) { return handle ? ((client_t *)handle)->response_size : 0; }
const uint8_t *cc_client_response_data(void *handle) { return handle ? ((client_t *)handle)->response : NULL; }
const char *cc_client_error(void *handle) { return handle ? ((client_t *)handle)->error : "invalid client"; }
void cc_client_free(void *handle) {
client_t *c = handle; if (!c) return;
coap_delete_optlist(c->options); free(c->uri); free(c->payload); free(c->response); free(c);
}
typedef struct server_t server_t;
typedef struct resource_t {
struct resource_t *next; server_t *owner; char *path;
coap_resource_t *resource; int64_t get_id, post_id;
} resource_t;
struct server_t { coap_context_t *context; resource_t *resources; cc_handler handler; char error[192]; };
typedef struct {
resource_t *resource; coap_session_t *session;
const coap_pdu_t *request; coap_pdu_t *response; const coap_string_t *query;
const uint8_t *payload; size_t payload_size; int replied;
} exchange_t;
static void dispatched(coap_resource_t *resource, coap_session_t *session,
const coap_pdu_t *request, const coap_string_t *query,
coap_pdu_t *response) {
resource_t *r = coap_resource_get_userdata(resource);
exchange_t exchange = {0};
exchange.resource = r; exchange.session = session; exchange.request = request;
exchange.response = response; exchange.query = query;
size_t offset = 0, total = 0;
coap_get_data_large(request, &exchange.payload_size, &exchange.payload, &offset, &total);
if (offset || exchange.payload_size > CC_MAX_BODY || exchange.payload_size != total) {
coap_pdu_set_code(response, COAP_RESPONSE_CODE_REQUEST_TOO_LARGE); return;
}
int64_t id = coap_pdu_get_code(request) == COAP_REQUEST_CODE_GET ? r->get_id : r->post_id;
if (r->owner->handler && id > 0) r->owner->handler(id, &exchange);
if (!exchange.replied) coap_pdu_set_code(response, COAP_RESPONSE_CODE_INTERNAL_ERROR);
}
void *cc_server_new(const char *address, uint16_t port) {
initialize();
if (!address || !port) return NULL;
coap_address_t listen_address;
if (!resolve_address(address, port, &listen_address)) return NULL;
server_t *s = calloc(1, sizeof(*s)); if (!s) return NULL;
s->context = coap_new_context(NULL);
if (!s->context || !coap_new_endpoint(s->context, &listen_address, COAP_PROTO_UDP)) {
if (s->context) coap_free_context(s->context); free(s); return NULL;
}
coap_context_set_block_mode(s->context, COAP_BLOCK_USE_LIBCOAP | COAP_BLOCK_SINGLE_BODY);
return s;
}
int32_t cc_server_register(void *handle, const char *path, int32_t method, int64_t id) {
server_t *s = handle;
if (!s || !path || id <= 0 || (method != 1 && method != 2)) return -1;
if (*path == '/') ++path;
resource_t *r = s->resources;
while (r && strcmp(r->path, path)) r = r->next;
if (!r) {
r = calloc(1, sizeof(*r)); if (!r) return -1;
r->path = malloc(strlen(path) + 1);
if (!r->path) { free(r); return -1; }
strcpy(r->path, path); r->owner = s;
coap_str_const_t *uri = coap_new_str_const((const uint8_t *)path, strlen(path));
r->resource = uri ? coap_resource_init(uri, COAP_RESOURCE_FLAGS_RELEASE_URI) : NULL;
if (!r->resource) { if (uri) coap_delete_str_const(uri); free(r->path); free(r); return -1; }
coap_resource_set_userdata(r->resource, r);
coap_add_resource(s->context, r->resource);
r->next = s->resources; s->resources = r;
}
if (method == 1) r->get_id = id; else r->post_id = id;
coap_register_request_handler(r->resource, (coap_request_t)method, dispatched);
return 0;
}
int32_t cc_server_poll(void *handle, uint32_t wait_ms, cc_handler handler) {
server_t *s = handle; if (!s || !wait_ms || !handler) return -1;
s->handler = handler;
int status = coap_io_process(s->context, wait_ms);
s->handler = NULL;
if (status < 0) snprintf(s->error, sizeof(s->error), "server I/O failed");
return status;
}
const char *cc_server_error(void *handle) { return handle ? ((server_t *)handle)->error : "invalid server"; }
void cc_server_free(void *handle) {
server_t *s = handle; if (!s) return;
coap_free_context(s->context);
while (s->resources) { resource_t *r = s->resources; s->resources = r->next; free(r->path); free(r); }
free(s);
}
int32_t cc_exchange_method(void *handle) { return coap_pdu_get_code(((exchange_t *)handle)->request); }
const char *cc_exchange_path(void *handle) { return ((exchange_t *)handle)->resource->path; }
const uint8_t *cc_exchange_payload(void *handle) { return ((exchange_t *)handle)->payload; }
uint64_t cc_exchange_payload_size(void *handle) { return ((exchange_t *)handle)->payload_size; }
const uint8_t *cc_exchange_query(void *handle) { exchange_t *e = handle; return e->query ? e->query->s : NULL; }
uint64_t cc_exchange_query_size(void *handle) { exchange_t *e = handle; return e->query ? e->query->length : 0; }
const uint8_t *cc_exchange_option(void *handle, uint16_t number) {
coap_opt_iterator_t iterator; coap_opt_t *option = coap_check_option(((exchange_t *)handle)->request, number, &iterator);
return option ? coap_opt_value(option) : NULL;
}
uint64_t cc_exchange_option_size(void *handle, uint16_t number) {
coap_opt_iterator_t iterator; coap_opt_t *option = coap_check_option(((exchange_t *)handle)->request, number, &iterator);
return option ? coap_opt_length(option) : 0;
}
static void release_body(coap_session_t *session, void *data) { (void)session; free(data); }
static coap_opt_t *option_at(void *handle, uint32_t index, uint16_t *number) {
coap_opt_iterator_t iterator;
if (!coap_option_iterator_init(((exchange_t *)handle)->request, &iterator, COAP_OPT_ALL)) return NULL;
coap_opt_t *option;
while ((option = coap_option_next(&iterator))) {
if (index-- == 0) { *number = iterator.number; return option; }
}
return NULL;
}
int32_t cc_exchange_option_number_at(void *handle, uint32_t index) {
uint16_t number; return option_at(handle, index, &number) ? number : -1;
}
const uint8_t *cc_exchange_option_data_at(void *handle, uint32_t index) {
uint16_t number; coap_opt_t *option = option_at(handle, index, &number);
return option ? coap_opt_value(option) : NULL;
}
uint64_t cc_exchange_option_size_at(void *handle, uint32_t index) {
uint16_t number; coap_opt_t *option = option_at(handle, index, &number);
return option ? coap_opt_length(option) : 0;
}
int32_t cc_exchange_reply(void *handle, int32_t code, uint16_t format, const uint8_t *data, uint64_t size) {
exchange_t *e = handle;
if (!e || e->replied || code < 64 || code > 191 || size > CC_MAX_BODY || (size && !data)) return -1;
uint8_t *copy = NULL;
if (!replace_bytes(©, data, size)) return -1;
coap_pdu_set_code(e->response, (coap_pdu_code_t)code);
if (!coap_add_data_large_response(e->resource->resource, e->session, e->request, e->response,
e->query, format, -1, 0, (size_t)size, copy, release_body, copy)) {
/* libcoap invokes release_body on failure too. */
coap_pdu_set_code(e->response, COAP_RESPONSE_CODE_INTERNAL_ERROR); return -1;
}
e->replied = 1; return 0;
}
void cc_set_log(int32_t enabled) { initialize(); coap_set_log_level(enabled ? COAP_LOG_DEBUG : COAP_LOG_ERR); }
cc_client_run 的返回值表示传输/桥接状态:0 为已收到 CoAP 响应,1 为配置或 URI 错误,2 为创建、内存或 I/O 错误,3 为 NACK/交付失败,4 为等待截止超时。4.xx 和 5.xx 是正常收到的协议响应,仍然返回状态 0;随后读取响应 code。cc_client_set_payload 等 setter 的非零值表示配置失败。
等待计时从发送请求后开始。主机名解析使用同步 getaddrinfo,因此 setWaitSeconds 不限制 DNS 解析阶段的总耗时。
7. 编写仓颉 HAR
下面四个文件保存到 ohos-coap-app/coap/src/main/cangjie。模板的示例 index.cj 可保留;本文业务不调用其中的示例函数。
7.1 native.cj:声明 ABI 并集中处理内存
package ohos_app_cangjie_coap
foreign {
func cc_client_new(uri: CString, method: Int32, kind: Int32): CPointer<Unit>
func cc_client_set_uint(h: CPointer<Unit>, key: Int32, value: Int64): Int32
func cc_client_set_payload(h: CPointer<Unit>, data: CPointer<UInt8>, size: UInt64): Int32
func cc_client_set_token(h: CPointer<Unit>, data: CPointer<UInt8>, size: UInt64): Int32
func cc_client_add_option(h: CPointer<Unit>, number: UInt16, data: CPointer<UInt8>, size: UInt64): Int32
func cc_client_run(h: CPointer<Unit>): Int32
func cc_client_response_code(h: CPointer<Unit>): Int32
func cc_client_response_size(h: CPointer<Unit>): UInt64
func cc_client_response_data(h: CPointer<Unit>): CPointer<UInt8>
func cc_client_error(h: CPointer<Unit>): CString
func cc_client_free(h: CPointer<Unit>): Unit
func cc_server_new(address: CString, port: UInt16): CPointer<Unit>
func cc_server_register(h: CPointer<Unit>, path: CString, method: Int32, id: Int64): Int32
func cc_server_poll(h: CPointer<Unit>, ms: UInt32, handler: CFunc<(Int64, CPointer<Unit>) -> Unit>): Int32
func cc_server_error(h: CPointer<Unit>): CString
func cc_server_free(h: CPointer<Unit>): Unit
func cc_exchange_method(h: CPointer<Unit>): Int32
func cc_exchange_path(h: CPointer<Unit>): CString
func cc_exchange_payload(h: CPointer<Unit>): CPointer<UInt8>
func cc_exchange_payload_size(h: CPointer<Unit>): UInt64
func cc_exchange_query(h: CPointer<Unit>): CPointer<UInt8>
func cc_exchange_query_size(h: CPointer<Unit>): UInt64
func cc_exchange_option_number_at(h: CPointer<Unit>, index: UInt32): Int32
func cc_exchange_option_data_at(h: CPointer<Unit>, index: UInt32): CPointer<UInt8>
func cc_exchange_option_size_at(h: CPointer<Unit>, index: UInt32): UInt64
func cc_exchange_reply(h: CPointer<Unit>, code: Int32, format: UInt16, data: CPointer<UInt8>, size: UInt64): Int32
func cc_set_log(enabled: Int32): Unit
}
func copyBytes(pointer: CPointer<UInt8>, size: UInt64): Array<UInt8> {
if (size > 1048576 || (size > 0 && pointer.isNull())) {
throw Exception("invalid native buffer")
}
return Array<UInt8>(Int64(size), {i => unsafe { pointer.read(i) }})
}
// Build the closure and length before pinning. The closure must only call C.
func withBytes(bytes: Array<UInt8>, call: (CPointer<UInt8>, UInt64) -> Int32): Int32 {
let size = UInt64(bytes.size)
if (size == 0) {
return call(CPointer<UInt8>(), 0)
}
unsafe {
let pin = acquireArrayRawData(bytes)
try {
return call(pin.pointer, size)
} finally {
releaseArrayRawData(pin)
}
}
}
func check(status: Int32): Unit {
if (status != 0) {
throw Exception("invalid CoAP configuration (${status})")
}
}
7.2 client.cj:请求构建与响应
CoapClient 是可变构建器,应由一个任务独占。setter 保存配置,请求时创建新的原生句柄。二进制响应读取 payload;只有确认内容是合法 UTF-8 时才读取 text。
package ohos_app_cangjie_coap
import std.collection.ArrayList
public class CoapResponse {
public let code: Int32
public let payload: Array<UInt8>
public init(code: Int32, payload: Array<UInt8>) {
this.code = code
this.payload = payload
}
public prop isSuccess: Bool {
get() {
code >= 64 && code < 96
}
}
public prop text: String {
get() {
String.fromUtf8(payload)
}
}
}
public class CoapOption {
public let number: UInt16
public let value: Array<UInt8>
public init(number: UInt16, value: Array<UInt8>) {
this.number = number
this.value = value.clone()
}
}
// A mutable request builder belongs to one task. Each request creates a fresh C handle.
public class CoapClient {
private let settings = ArrayList<(Int32, Int64)>()
private let options = ArrayList<CoapOption>()
private var payload = Array<UInt8>()
private var token: ?Array<UInt8> = None
public init() {}
public func setFormat(value: UInt16): Unit {
settings.add((1, Int64(value)))
}
public func setPort(value: UInt16): Unit {
settings.add((2, Int64(value)))
}
public func setMID(value: UInt16): Unit {
settings.add((3, Int64(value)))
}
public func setWaitSeconds(value: Int64): Unit {
if (value < 1 || value > 600) {
throw Exception("wait seconds must be 1..600")
}
settings.add((4, value * 1000))
}
public func setRetryCount(value: Int64): Unit {
settings.add((5, value))
}
public func setBlockMode(value: Int64): Unit {
settings.add((6, value))
}
public func setPayload(value: String): Unit {
payload = value.toArray()
}
public func setBinaryPayload(value: Array<UInt8>): Unit {
payload = value.clone()
}
public func setToken(value: Array<UInt8>): Unit {
token = Some(value.clone())
}
public func setCustomOption(number: UInt16, value: Array<UInt8>): Unit {
options.add(CoapOption(number, value))
}
// method: GET=1, POST=2, PUT=3; kind: CON=0, NON=1. Run in spawn from a UI.
public func requestUri(uri: String, method!: Int32 = 1, kind!: Int32 = 0): CoapResponse {
if (uri.contains("\u{0}")) {
throw Exception("URI contains NUL")
}
let cUri = unsafe { LibC.mallocCString(uri) }
let handle = try {
unsafe { cc_client_new(cUri, method, kind) }
} finally {
unsafe { LibC.free(cUri) }
}
if (handle.isNull()) {
throw Exception("cannot create CoAP client")
}
try {
for ((key, value) in settings) {
check(unsafe { cc_client_set_uint(handle, key, value) })
}
check(withBytes(payload, {p, n => unsafe { cc_client_set_payload(handle, p, n) }}))
if (let Some(bytes) <- token) {
check(withBytes(bytes, {p, n => unsafe { cc_client_set_token(handle, p, n) }}))
}
for (option in options) {
let number = option.number
check(withBytes(option.value, {p, n => unsafe { cc_client_add_option(handle, number, p, n) }}))
}
let status = unsafe { cc_client_run(handle) }
if (status != 0) {
let message = unsafe { cc_client_error(handle).toString() }
throw Exception("CoAP request failed (${status}): ${message}")
}
let code = unsafe { cc_client_response_code(handle) }
let bytes = copyBytes(unsafe { cc_client_response_data(handle) }, unsafe { cc_client_response_size(handle) })
return CoapResponse(code, bytes)
} finally {
unsafe { cc_client_free(handle) }
}
}
}
public func setNativeLog(enabled: Bool): Unit {
unsafe { cc_set_log(if (enabled) {
1
} else {
0
}) }
}
setToken 最大 8 字节;setWaitSeconds 为 1–600 秒;重传次数为 0–8;端口必须非零。setBlockMode 接受 0、1、3,但本文公开响应对象只交付一次完整正文:需要自动重组时保持默认 3。模式 1 下收到不完整块体会报错,模式 0 不提供自动大正文分块。默认等待 10 秒、最大重传 4 次,等待截止可能早于全部重传完成。
协议响应码使用 CoAP 的整数编码。例如 2.05 为 69、2.04 为 68、4.04 为 132、4.05 为 133、5.00 为 160。isSuccess 判断 2.xx;不要用 HTTP 状态码 200/404 比较。
7.3 server.cj:资源回调与可控停止
服务端默认只绑定 127.0.0.1,适合回环检查。要接受局域网请求,可以显式使用设备接口地址或 0.0.0.0。每个实例只运行一次,先注册所有 GET/POST 路由,再 spawn 运行。回调串行执行,避免在回调中做长时间阻塞工作。
package ohos_app_cangjie_coap
import std.collection.{ArrayList, HashMap}
import std.sync.{Mutex, AtomicBool}
public class CoapRequest {
public let method: Int32
public let path: String
public let payload: Array<UInt8>
public let query: Array<UInt8>
public let options: Array<CoapOption>
public init(method: Int32, path: String, payload: Array<UInt8>, query: Array<UInt8>, options: Array<CoapOption>) {
this.method = method
this.path = path
this.payload = payload
this.query = query
this.options = options
}
public prop text: String {
get() {
String.fromUtf8(payload)
}
}
public prop queryText: String {
get() {
String.fromUtf8(query)
}
}
}
public class CoapReply {
public let code: Int32
public let format: UInt16
public let payload: Array<UInt8>
public init(code: Int32, payload: Array<UInt8>, format!: UInt16 = 0) {
this.code = code
this.format = format
this.payload = payload
}
public init(text: String, code!: Int32 = 69, format!: UInt16 = 0) {
this.code = code
this.format = format
this.payload = text.toArray()
}
}
let registryLock = Mutex()
let registry = HashMap<Int64, (CoapRequest) -> CoapReply>()
var nextID: Int64 = 1
@C
func dispatch(id: Int64, exchange: CPointer<Unit>): Unit {
// No Cangjie exception may cross a C callback boundary.
try {
let handler = synchronized(registryLock) {
registry.get(id)
}
if (let Some(call) <- handler) {
let options = ArrayList<CoapOption>()
var index: UInt32 = 0
while (true) {
let number = unsafe { cc_exchange_option_number_at(exchange, index) }
if (number < 0) {
break
}
let bytes = copyBytes(unsafe { cc_exchange_option_data_at(exchange, index) },
unsafe { cc_exchange_option_size_at(exchange, index) })
options.add(CoapOption(UInt16(number), bytes));
index++
}
let request = CoapRequest(unsafe { cc_exchange_method(exchange) },
unsafe { cc_exchange_path(exchange).toString() },
copyBytes(unsafe { cc_exchange_payload(exchange) }, unsafe { cc_exchange_payload_size(exchange) }),
copyBytes(unsafe { cc_exchange_query(exchange) }, unsafe { cc_exchange_query_size(exchange) }),
options.toArray())
let reply = call(request)
let code = reply.code
let format = reply.format
check(withBytes(reply.payload, {p, n => unsafe { cc_exchange_reply(exchange, code, format, p, n) }}))
}
} catch (error: Exception) {
// C supplies 5.00 when no response was produced.
println("CoAP handler failed: ${error.message}")
}
}
public class CoapServer {
private let address: String
private let port: UInt16
private let lock = Mutex()
private let routes = ArrayList<(String, Int32, (CoapRequest) -> CoapReply)>()
private let stopping = AtomicBool(false)
private let running = AtomicBool(false)
private var started = false
public init(address!: String = "127.0.0.1", port!: UInt16 = 5683) {
if (address.contains("\u{0}")) {
throw Exception("address contains NUL")
}
this.address = address
this.port = port
}
public prop isRunning: Bool {
get() {
running.load()
}
}
public func register(path: String, method: Int32, handler: (CoapRequest) -> CoapReply): Unit {
synchronized(lock) {
if (started || (method != 1 && method != 2) || path.contains("\u{0}")) {
throw Exception("register GET/POST resources before run")
}
routes.add((path, method, handler))
}
}
public func stop(): Unit {
stopping.store(true)
}
// One run per server; caller must stop and join its Future before discarding it.
public func run(): Unit {
synchronized(lock) {
if (started) {
throw Exception("server was already started")
}
started = true
}
let cAddress = unsafe { LibC.mallocCString(address) }
let handle = try {
unsafe { cc_server_new(cAddress, port) }
} finally {
unsafe { LibC.free(cAddress) }
}
if (handle.isNull()) {
throw Exception("cannot bind CoAP server ${address}:${port}")
}
let ids = ArrayList<Int64>()
try {
for ((path, method, handler) in routes) {
let id = synchronized(registryLock) {
let value = nextID;
nextID++;
registry.add(value, handler);
value
}
ids.add(id)
let cPath = unsafe { LibC.mallocCString(path) }
try {
check(unsafe { cc_server_register(handle, cPath, method, id) })
} finally {
unsafe { LibC.free(cPath) }
}
}
running.store(true)
while (!stopping.load()) {
if (unsafe { cc_server_poll(handle, 100, dispatch) } < 0) {
throw Exception(unsafe { cc_server_error(handle).toString() })
}
}
} finally {
running.store(false)
unsafe { cc_server_free(handle) }
synchronized(registryLock) {
for (id in ids) {
registry.remove(id)
}
}
}
}
}
queryText 来自 libcoap 重建的查询字符串,会保留必要的百分号转义。例如本文请求 a=one%20two&b=3 得到相同的编码形式,不会自动变成含空格的字符串。options 保存每一个选项的编号和原始字节值;需要业务解析时自行按照该选项协议解释,不能把所有选项都当 UTF-8 文本。
7.4 demo.cj:可重复的仓颉回环检查
这个函数启动本机服务端,验证根路径、中文 GET、二进制 POST、8 KB Block1/Block2、查询、NON 和协议错误码,最后保证 stop() 与任务等待配对。端口 15684 必须空闲。
package ohos_app_cangjie_coap
func expect(value: Bool, label: String): Unit {
if (!value) {
throw Exception("check failed: ${label}")
}
}
public func runLoopbackChecks(): String {
let server = CoapServer(port: 15684)
let large = Array<UInt8>(8192, {i => UInt8(i % 251)})
server.register("", 1, {_ => CoapReply("root")})
server.register("hello", 1, {_ => CoapReply("你好,仓颉与 libcoap")})
server.register("echo", 2, {request => CoapReply(68, request.payload, format: 42)})
server.register("large", 1, {_ => CoapReply(69, large, format: 42)})
server.register("query", 1, {request => CoapReply(request.queryText)})
let task = spawn {=> server.run()}
try {
var attempts = 0
while (!server.isRunning && attempts < 100) {
sleep(20 * Duration.millisecond);
attempts++
}
expect(server.isRunning, "server ready")
let base = "coap://127.0.0.1:15684/"
expect(CoapClient().requestUri(base).text == "root", "root URI")
expect(CoapClient().requestUri(base + "hello").text == "你好,仓颉与 libcoap", "UTF-8 GET")
let client = CoapClient()
client.setFormat(42);
client.setMID(1234);
client.setToken([1, 2, 3])
client.setWaitSeconds(10);
client.setRetryCount(2);
client.setBinaryPayload(large)
let echoed = client.requestUri(base + "echo", method: 2)
expect(echoed.code == 68 && echoed.payload.size == large.size, "Block1 POST size")
for (i in 0..large.size) {
expect(echoed.payload[i] == large[i], "Block1 POST bytes")
}
let response = CoapClient().requestUri(base + "large")
expect(response.payload.size == large.size, "Block2 GET size")
for (i in 0..large.size) {
expect(response.payload[i] == large[i], "Block2 GET bytes")
}
expect(CoapClient().requestUri(base + "query?a=one%20two&b=3").text == "a=one%20two&b=3", "URI query")
expect(CoapClient().requestUri(base + "hello", kind: 1).isSuccess, "NON GET")
expect(CoapClient().requestUri(base + "missing").code == 132, "4.04 response")
expect(CoapClient().requestUri(base + "hello", method: 3).code == 133, "PUT reaches server, 4.05")
return "PASS: root, UTF-8 GET, binary POST, Block1/Block2 8192 bytes, query, NON, 4.04, PUT/4.05"
} finally {
server.stop();
task.get()
}
}
8. 接入构建配置
8.1 HAR 的 cjpm.toml
将 coap/cjpm.toml 核对为下面的内容。最关键的是两段 目标平台自己的 ffi.c。原生库名称写 coap_cj_bridge,CJPM 会查找对应平台的 libcoap_cj_bridge.so,路径相对于库模块。
只在编译参数中手工追加 -L ./libs/... -lcoap_cj_bridge,可能让 HAR 单独构建成功,却在应用编译依赖库时按另一工作目录解析路径而失败。目标级 ffi.c 是这个工程通过两种构建的配置;仓颉 1.1 发布说明 记录了平台隔离的 FFI 配置支持,本文实际使用并验证的是 1.2 SDK。
[package]
cjc-version = "1.2.0"
compile-option = "--dy-std --cfg=\"${COMPILE_CONDITION}\""
override-compile-option = "${OVERRIDE_COMPILE_OPTION}"
description = "CangjieUI Application"
name = "ohos_app_cangjie_coap"
output-type = "dynamic"
src-dir = "./src/main/cangjie"
target-dir = ""
version = "1.0.0"
[profile]
[profile.build]
incremental = true
lto = ""
[profile.build.combined]
ohos_app_cangjie_coap = "dynamic"
[profile.customized-option]
debug = "-g -Woff all -Won apilevel-check"
release = "--fast-math -O2 -s -Woff all -Won apilevel-check"
asan = "--sanitize=address -lclang_rt.asan"
[profile.test]
[target.aarch64-linux-ohos]
compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/aarch64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
[target.aarch64-linux-ohos.ffi.c]
coap_cj_bridge = { path = "./libs/arm64-v8a" }
[target.aarch64-linux-ohos.bin-dependencies]
path-option = ["${AARCH64_LIBS}", "${AARCH64_MACRO_LIBS}", "${AARCH64_KIT_LIBS}"]
package-option = { }
[target.x86_64-linux-ohos]
compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/x86_64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
[target.x86_64-linux-ohos.ffi.c]
coap_cj_bridge = { path = "./libs/x86_64" }
[target.x86_64-linux-ohos.bin-dependencies]
path-option = ["${X86_64_OHOS_LIBS}", "${X86_64_OHOS_MACRO_LIBS}", "${X86_64_OHOS_KIT_LIBS}"]
[target.x86_64-w64-mingw32.bin-dependencies]
path-option = ["${X86_64_LIBS}", "${X86_64_MACRO_LIBS}", "${X86_64_KIT_LIBS}"]
package-option = { }
8.2 HAR 构建配置与模块清单
coap/build-profile.json5:
{
"apiType": "stageMode",
"buildOption": {
"cangjieOptions": {
"path": "./cjpm.toml",
"abiFilters": ["arm64-v8a", "x86_64"]
}
},
"buildOptionSet": [
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest"
}
]
}
coap/src/main/module.json5:
{
"module": {
"name": "coap",
"type": "har",
"deviceTypes": [
"default"
]
}
}
coap/oh-package.json5:
{
"name": "coap",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"types": "./Index.d.ets",
"license": "Apache-2.0",
"dependencies": {
}
}
8.3 应用依赖和网络权限
entry/oh-package.json5 用本地模块依赖。安装依赖后,IDE/Hvigor 会把仓颉包依赖同步到 entry/cjpm.toml。
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": { "coap": "file:../coap" }
}
同步后的 entry/cjpm.toml 如下。${coap} 是 HarmonyOS 构建集成注入的模块路径;不要在纯 Windows cjpm build 环境中期待它自动存在,也不要重复添加第二个 [dependencies] 表。
[package]
cjc-version = "1.2.0"
compile-option = "--dy-std --cfg=\"${COMPILE_CONDITION_ENTRY}\""
override-compile-option = "${OVERRIDE_COMPILE_OPTION}"
description = "CangjieUI Application"
name = "ohos_app_cangjie_entry"
output-type = "dynamic"
src-dir = "./src/main/cangjie"
target-dir = ""
version = "1.0.0"
[profile]
[profile.build]
incremental = true
lto = ""
[profile.build.combined]
ohos_app_cangjie_entry = "dynamic"
[profile.customized-option]
debug = "-g -Woff all -Won apilevel-check"
release = "--fast-math -O2 -s -Woff all -Won apilevel-check"
asan = "--sanitize=address -lclang_rt.asan"
[profile.test]
[target]
[target.aarch64-linux-ohos]
compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/aarch64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/aarch64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
[target.aarch64-linux-ohos.bin-dependencies]
path-option = [ "${AARCH64_LIBS}", "${AARCH64_MACRO_LIBS}", "${AARCH64_KIT_LIBS}" ]
[target.aarch64-linux-ohos.bin-dependencies.package-option]
[target.x86_64-linux-ohos]
compile-option = "-B \"${DEVECO_CANGJIE_HOME}/build-tools/third_party/llvm/bin\" -B \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/sysroot/usr/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/clang/15.0.4/lib/x86_64-linux-ohos\" -L \"${DEVECO_OH_NATIVE_HOME}/llvm/lib/x86_64-linux-ohos\" --sysroot \"${DEVECO_OH_NATIVE_HOME}/sysroot\""
[target.x86_64-linux-ohos.bin-dependencies]
path-option = [ "${X86_64_OHOS_LIBS}", "${X86_64_OHOS_MACRO_LIBS}", "${X86_64_OHOS_KIT_LIBS}" ]
[target.x86_64-w64-mingw32]
[target.x86_64-w64-mingw32.bin-dependencies]
path-option = [ "${X86_64_LIBS}", "${X86_64_MACRO_LIBS}", "${X86_64_KIT_LIBS}" ]
[target.x86_64-w64-mingw32.bin-dependencies.package-option]
[dependencies]
[dependencies.ohos_app_cangjie_coap]
path = "${coap}"
entry/build-profile.json5 与 HAR 使用同样的架构过滤;nativeLib.filter.enableOverride 处理应用收集库时的同名原生库覆盖。
{
"apiType": "stageMode",
"buildOption": {
"cangjieOptions": {
"path": "./cjpm.toml",
"abiFilters": ["arm64-v8a", "x86_64"]
},
"nativeLib": {
"filter": {
"enableOverride": true
}
}
},
"buildOptionSet": [
],
"targets": [
{
"name": "default"
}
]
}
entry/src/main/module.json5 加入 ohos.permission.INTERNET。srcEntry 的包名必须与后面的 Ability 代码一致。
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"requestPermissions": [{ "name": "ohos.permission.INTERNET" }],
"srcEntry": "ohos_app_cangjie_entry.MyAbilityStage",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "ohos_app_cangjie_entry.MainAbility",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
]
}
}
8.4 根配置
工程根 build-profile.json5 中同时注册 entry 和 coap。下面是未配置个人签名的版本;自己的证书、profile 和密码只在本机配置,不复制进公开工程。
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "26.0.0",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
},
{
"name": "coap",
"srcPath": "./coap"
}
]
}
根 oh-package.json5 保留模板依赖:
{
"modelVersion": "26.0.0",
"description": "Please describe the basic information.",
"dependencies": {
},
"devDependencies": {
"@ohos/hypium": "1.0.25",
"@ohos/hamock": "1.0.0"
}
}
根 hvigor/hvigor-config.json5 使用向导生成的 modelVersion: "26.0.0"。根 hvigorfile.ts 导出 appTasks;entry/hvigorfile.ts 导出 hapTasks;coap/hvigorfile.ts 导出 harTasks。这些文件由模板生成即可,不需要额外安装上游 @ohos/coap,否则会引入原来的 ArkTS/NAPI 实现。
9. 应用界面与任务调度
覆盖 entry/src/main/cangjie/index.cj。点击界面文字后,在 spawn 中执行网络检查,完成后用 ohos.base.launch 更新 UI 状态。同步网络请求和服务端轮询不能放在 UI 线程中。
package ohos_app_cangjie_entry
import kit.ArkUI.LengthProp
import kit.ArkUI.Column
import kit.ArkUI.Row
import kit.ArkUI.Text
import kit.ArkUI.CustomView
import kit.ArkUI.CJEntry
import kit.ArkUI.loadNativeView
import kit.ArkUI.FontWeight
import kit.ArkUI.SubscriberManager
import kit.ArkUI.ObservedProperty
import kit.ArkUI.LocalStorage
import ohos.arkui.state_macro_manage.Entry
import ohos.arkui.state_macro_manage.Component
import ohos.arkui.state_macro_manage.State
import ohos.base.launch
import ohos_app_cangjie_coap.runLoopbackChecks
@Entry
@Component
class EntryView {
@State
var message: String = "点击运行 CoAP 回环检查"
@State
var busy: Bool = false
func runChecks(): Unit {
if (busy) {
return
}
busy = true
message = "正在验证仓颉 + libcoap…"
spawn {
=>
let result = try {
runLoopbackChecks()
} catch (error: Exception) {
"FAIL: ${error.message}"
}
launch(
{
=>
this.message = result
this.busy = false
}
)
}
}
func build() {
Row {
Column {
Text(this.message).fontSize(20).fontWeight(FontWeight.Bold).onClick({
evt => this.runChecks()
})
}.width(100.percent)
}.height(100.percent)
}
}
两个 Ability 文件沿用向导模板,完整内容列在这里,便于核对包名和加载的视图名称。
entry/src/main/cangjie/main_ability.cj:
package ohos_app_cangjie_entry
import kit.PerformanceAnalysisKit.Hilog
import kit.AbilityKit.Want
import kit.AbilityKit.UIAbility
import kit.AbilityKit.LaunchParam
import kit.AbilityKit.LaunchReason
import kit.ArkUI.WindowStage
class MainAbility <: UIAbility {
public init() {
super()
registerSelf()
}
public override func onCreate(want: Want, launchParam: LaunchParam): Unit {
Hilog.info(1, "Cangjie", "MainAbility OnCreated.${want.abilityName}")
match (launchParam.launchReason) {
case LaunchReason.StartAbility => Hilog.info(1, "Cangjie", "START_ABILITY")
case _ => ()
}
}
public override func onWindowStageCreate(windowStage: WindowStage): Unit {
Hilog.info(1, "Cangjie", "MainAbility onWindowStageCreate.")
windowStage.loadContent("EntryView")
}
}
entry/src/main/cangjie/ability_stage.cj:
package ohos_app_cangjie_entry
import kit.AbilityKit.AbilityStage
import kit.PerformanceAnalysisKit.Hilog
class MyAbilityStage <: AbilityStage {
public override func onCreate(): Unit {
Hilog.info(1, "Cangjie", "MyAbilityStage onCreated.")
}
}
10. 构建原生库、HAR 和 HAP
10.1 原生构建脚本
保存为 ohos-coap-libcoap-port/Build-Native.ps1。不带参数时交叉编译两种 OHOS 架构,复制桥接库到 HAR 的 libs,并附带原生许可文件。-Windows 用于可选的主机检查。
param(
[string]$NativeSdk = 'C:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\native',
[string]$AppDir = (Join-Path $PSScriptRoot '..\ohos-coap-app'),
[switch]$Windows
)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$source = Join-Path $root 'third_party\libcoap'
if (-not (Test-Path -LiteralPath (Join-Path $source 'CMakeLists.txt'))) {
throw 'Initialize third_party/libcoap at commit 5fd2f89ef068214130e5d60b7087ef48711fa615 first.'
}
function Invoke-Checked([string]$Program, [string[]]$Arguments) {
& $Program @Arguments
if ($LASTEXITCODE -ne 0) { throw "$Program failed with exit code $LASTEXITCODE" }
}
Push-Location $root
try {
if ($Windows) {
Invoke-Checked 'cmake' @('-S','native','-B','build/windows','-G','Ninja',"-DLIBCOAP_SOURCE_DIR=$source",'-DCMAKE_C_COMPILER=gcc','-DCMAKE_BUILD_TYPE=Debug')
Invoke-Checked 'cmake' @('--build','build/windows','-j','4')
} else {
$toolchain = Join-Path $NativeSdk 'build\cmake\ohos.toolchain.cmake'
if (-not (Test-Path -LiteralPath $toolchain)) { throw 'HarmonyOS Native SDK toolchain not found.' }
foreach ($abi in @('arm64-v8a','x86_64')) {
$build = "build/ohos-$abi"
Invoke-Checked 'cmake' @('-S','native','-B',$build,'-G','Ninja',"-DLIBCOAP_SOURCE_DIR=$source", "-DCMAKE_TOOLCHAIN_FILE=$toolchain", "-DOHOS_ARCH=$abi",'-DCMAKE_BUILD_TYPE=Release')
Invoke-Checked 'cmake' @('--build',$build,'-j','4')
$destination = Join-Path $AppDir "coap\libs\$abi"
New-Item -ItemType Directory -Path $destination -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $build 'libcoap_cj_bridge.so') -Destination $destination
}
$licenses = Join-Path $AppDir 'coap\src\main\resources\rawfile'
New-Item -ItemType Directory -Path $licenses -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $source 'LICENSE') -Destination (Join-Path $licenses 'libcoap-LICENSE.txt')
}
} finally { Pop-Location }
在工程根目录执行:
.\ohos-coap-libcoap-port\Build-Native.ps1 -NativeSdk $nativeSdk -AppDir "$projectRoot\ohos-coap-app"
两种 .so 必须分别出现在 coap/libs/arm64-v8a 和 coap/libs/x86_64。不要把 Windows DLL 或一种架构的库复制到另一架构目录。
10.2 应用构建脚本
保存为 ohos-coap-app/Build-App.ps1:
param(
[string]$DevEco = 'C:\Program Files\Huawei\DevEco Studio',
[string]$CangjieSdk = "$env:USERPROFILE\.cangjie-sdk\26.0\cangjie"
)
$ErrorActionPreference = 'Stop'
$env:PATH = (Join-Path $DevEco 'tools\node') + ';' + $env:PATH
$env:DEVECO_SDK_HOME = Join-Path $DevEco 'sdk'
$env:DEVECO_CANGJIE_PATH = $CangjieSdk
$env:DEVECO_CANGJIE_HOME = $CangjieSdk
$env:DEVECO_OH_NATIVE_HOME = Join-Path $DevEco 'sdk\default\openharmony\native'
$env:DEVECO_CANGJIE_PLUGIN_ENABLED = 'true'
$node = Join-Path $DevEco 'tools\node\node.exe'
$hvigor = Join-Path $DevEco 'tools\hvigor\bin\hvigorw.js'
function Invoke-Hvigor([string]$Module, [string]$Task) {
& $node $hvigor --mode module -p "module=$Module@default" -p product=default -p buildMode=debug $Task --no-daemon
if ($LASTEXITCODE -ne 0) { throw "Hvigor $Task failed ($LASTEXITCODE)" }
}
Push-Location $PSScriptRoot
try {
& (Join-Path $DevEco 'tools\ohpm\bin\ohpm.bat') install --all
if ($LASTEXITCODE -ne 0) { throw 'ohpm install failed' }
Invoke-Hvigor 'entry' 'SyncCangjieResource'
Invoke-Hvigor 'coap' 'assembleHar'
Invoke-Hvigor 'entry' 'assembleHap'
} finally { Pop-Location }
调用它:
.\ohos-coap-app\Build-App.ps1 -DevEco $devEco -CangjieSdk $cjSdk
顺序是 ohpm install --all → entry:SyncCangjieResource → coap:assembleHar → entry:assembleHap。这比只在根目录执行一次 ohpm install 更明确地覆盖了两个模块。首次运行需要网络获取模板依赖;已安装后可利用缓存。
成功后得到:
ohos-coap-app/coap/build/default/outputs/default/coap.har
ohos-coap-app/entry/build/default/outputs/default/entry-default-unsigned.hap
HAP 是 ZIP 容器。除了看到 BUILD SUCCESSFUL,还应检查最终 HAP 确实包含这四个文件:
libs/arm64-v8a/libcoap_cj_bridge.so
libs/arm64-v8a/libohos_app_cangjie_coap.so
libs/x86_64/libcoap_cj_bridge.so
libs/x86_64/libohos_app_cangjie_coap.so
PowerShell 中可直接检查,不需要改名或解压整个 HAP:
Add-Type -AssemblyName System.IO.Compression.FileSystem
$hapPath = Join-Path $projectRoot 'ohos-coap-app\entry\build\default\outputs\default\entry-default-unsigned.hap'
$hapArchive = [IO.Compression.ZipFile]::OpenRead($hapPath)
try {
$hapArchive.Entries | Where-Object { $_.FullName -match '^libs/(arm64-v8a|x86_64)/lib(coap_cj_bridge|ohos_app_cangjie_coap)\.so$' } | Select-Object FullName,Length
} finally { $hapArchive.Dispose() }
HAR 的每种架构中还应有仓颉的 .cjo 接口产物。消费 HAR 的应用仍需使用兼容 SDK、配置网络权限并包含桥接 .so;本文验证的是当前工程内的本地 HAR 依赖,没有测试发布到 ohpm 仓库后的独立消费工程。
11. 不启动模拟器也能完成的检查
11.1 Windows 上运行同一份仓颉封装
这个步骤检查封装与原生核心的行为,不能证明 HarmonyOS UI、加载器或设备网络已经工作。使用 SDK 同包的 Windows 编译器和 MinGW GCC,先让 gcc、cmake、ninja 在当前 PATH 中可用。
$env:PATH = 'C:\Programs\mingw64\bin;' + $env:PATH
Set-Location "$projectRoot\ohos-coap-libcoap-port"
.\Build-Native.ps1 -Windows
cjpm init --name coap_host_check --type executable --path host-check
只初始化一次,已有项目不要再次覆盖。该命令使用当前 1.2 CJPM 的 --path 参数指定目录。
初始化后,host-check/cjpm.toml 使用下面的配置:
[package]
cjc-version = "1.2.0"
name = "coap_host_check"
organization = ""
description = "nothing here"
version = "1.0.0"
target-dir = ""
script-dir = ""
output-type = "executable"
compile-option = ""
override-compile-option = ""
link-option = ""
package-configuration = {}
[dependencies]
[ffi.c]
coap_cj_bridge = { path = "../build/windows" }
复制 HAR 中的四个仓颉文件,仅替换包名;业务代码不做平台分叉。此时当前目录仍为 ohos-coap-libcoap-port:
foreach ($cjFile in @('native.cj','client.cj','server.cj','demo.cj')) {
$sourceText = Get-Content -LiteralPath "..\ohos-coap-app\coap\src\main\cangjie\$cjFile" -Raw -Encoding utf8
$hostText = $sourceText.Replace('package ohos_app_cangjie_coap','package coap_host_check')
[IO.File]::WriteAllText((Join-Path (Get-Location) "host-check\src\$cjFile"), $hostText, [Text.UTF8Encoding]::new($false))
}
host-check/src/main.cj:
package coap_host_check
main(): Int64 {
println(runLoopbackChecks())
let client = CoapClient()
client.setWaitSeconds(1)
var timedOut = false
try { client.requestUri("coap://127.0.0.1:15685/no-listener") }
catch (error: Exception) { timedOut = true; println("PASS: timeout/error: ${error.message}") }
if (!timedOut) { throw Exception("expected transport failure") }
return 0
}
构建并运行:
Set-Location host-check
cjpm build --target-dir target-ffi
$env:PATH = (Resolve-Path '..\build\windows').Path + ';' + $env:PATH
& '.\target-ffi\release\bin\main.exe'
本机输出:
PASS: root, UTF-8 GET, binary POST, Block1/Block2 8192 bytes, query, NON, 4.04, PUT/4.05
PASS: timeout/error: CoAP request failed (3): delivery failed or retries exhausted
第二行测试没有监听者的端口,Windows 可能通过 ICMP 提前报告不可达。因此,它证明交付失败会抛出异常,不能单独证明等待截止计时。下面另用“端口开着但不回复”的对端验证真正的超时。
11.2 独立 UDP 对端:PUT、重传和截止超时
下面的 Python 程序仅依赖标准库。它加载 Windows 桥接 DLL,用独立 UDP 对端检查请求头、Token、MID、二进制正文、自定义选项和成功 PUT 响应;再故意丢掉第一包,要求第二包与第一包完全一致;最后保持 UDP 端口绑定却不回应,检查状态 4 与约 1 秒的等待截止。
保存为 ohos-coap-libcoap-port/check-native.py。这不是完整 CoAP 服务端,不能拿它替代 libcoap 或用于业务部署。
"""Independent UDP peer checks for the Windows bridge; Python standard library only."""
import ctypes as c
from pathlib import Path
import socket
import threading
import time
dll = c.CDLL(str(Path(__file__).resolve().parent / "build/windows/libcoap_cj_bridge.dll"))
dll.cc_client_new.argtypes = [c.c_char_p, c.c_int32, c.c_int32]
dll.cc_client_new.restype = c.c_void_p
dll.cc_client_set_uint.argtypes = [c.c_void_p, c.c_int32, c.c_int64]
dll.cc_client_set_payload.argtypes = [c.c_void_p, c.c_void_p, c.c_uint64]
dll.cc_client_add_option.argtypes = [c.c_void_p, c.c_uint16, c.c_void_p, c.c_uint64]
dll.cc_client_set_token.argtypes = [c.c_void_p, c.c_void_p, c.c_uint64]
for name in ("cc_client_run", "cc_client_response_code"):
getattr(dll, name).argtypes = [c.c_void_p]
getattr(dll, name).restype = c.c_int32
dll.cc_client_response_data.argtypes = [c.c_void_p]
dll.cc_client_response_data.restype = c.c_void_p
dll.cc_client_response_size.argtypes = [c.c_void_p]
dll.cc_client_response_size.restype = c.c_uint64
dll.cc_client_free.argtypes = [c.c_void_p]
dll.cc_client_free.restype = None
def packet_parts(packet):
assert packet[0] >> 6 == 1
token_size = packet[0] & 15
cursor, number, options = 4 + token_size, 0, []
def extended(value):
nonlocal cursor
if value < 13:
return value
count, base = (1, 13) if value == 13 else (2, 269)
assert value != 15
result = int.from_bytes(packet[cursor:cursor + count], "big") + base
cursor += count
return result
while cursor < len(packet) and packet[cursor] != 255:
lead = packet[cursor]
cursor += 1
number += extended(lead >> 4)
length = extended(lead & 15)
options.append((number, packet[cursor:cursor + length]))
cursor += length
payload = packet[cursor + 1:] if cursor < len(packet) else b""
return packet[4:4 + token_size], options, payload
def client(port, method=1, wait=10, payload=b"", custom=False):
handle = dll.cc_client_new(f"coap://127.0.0.1:{port}/put".encode(), method, 0)
assert handle
try:
assert dll.cc_client_set_uint(handle, 4, wait * 1000) == 0
assert dll.cc_client_set_uint(handle, 5, 2) == 0
assert dll.cc_client_set_uint(handle, 3, 321) == 0
token = c.create_string_buffer(b"abc")
assert dll.cc_client_set_token(handle, token, 3) == 0
data = c.create_string_buffer(payload)
assert dll.cc_client_set_payload(handle, data, len(payload)) == 0
if custom:
option = c.create_string_buffer(b"tag")
assert dll.cc_client_add_option(handle, 2048, option, 3) == 0
start = time.monotonic()
status = dll.cc_client_run(handle)
elapsed = time.monotonic() - start
size = dll.cc_client_response_size(handle)
body = c.string_at(dll.cc_client_response_data(handle), size) if size else b""
return status, dll.cc_client_response_code(handle), body, elapsed
finally:
dll.cc_client_free(handle)
def peer_case(drop_first=False, silent=False):
errors, seen = [], []
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.settimeout(0.1)
stop = threading.Event()
def serve():
try:
while not stop.is_set():
try:
packet, address = sock.recvfrom(65535)
except socket.timeout:
continue
seen.append(packet)
if silent or (drop_first and len(seen) == 1):
continue
token, options, payload = packet_parts(packet)
assert packet[1] == 3 and payload == b"binary\x00PUT"
assert (packet[0] >> 4) & 3 == 0
assert token == b"abc" and packet[2:4] == (321).to_bytes(2, "big")
assert (11, b"put") in options and (2048, b"tag") in options
reply = bytes([0x60 | len(token), 68]) + packet[2:4] + token + b"\xffPUT OK"
sock.sendto(reply, address)
except BaseException as error:
errors.append(error)
thread = threading.Thread(target=serve)
thread.start()
try:
result = client(port, method=3, wait=1 if silent else 10,
payload=b"binary\x00PUT", custom=True)
finally:
stop.set()
thread.join(timeout=2)
sock.close()
assert not thread.is_alive() and not errors, errors
status, code, body, elapsed = result
if silent:
assert status == 4 and 0.8 <= elapsed <= 3 and seen, result
print(f"PASS: silent bound peer produces deadline timeout, {elapsed:.2f}s")
else:
assert status == 0 and code == 68 and body == b"PUT OK", result
if drop_first:
assert len(seen) >= 2 and seen[0] == seen[1], "CON was not retransmitted unchanged"
print(f"PASS: PUT, token, MID, binary payload, elective option; packets={len(seen)}, {elapsed:.2f}s")
peer_case()
peer_case(drop_first=True)
peer_case(silent=True)
handle = dll.cc_client_new(b"coap://127.0.0.1/", 1, 0)
assert handle
try:
for option in (0, 3, 6, 7, 11, 12, 15, 23, 27, 28, 60):
assert dll.cc_client_add_option(handle, option, None, 0) != 0, option
finally:
dll.cc_client_free(handle)
print("PASS: reserved, Observe and managed URI/body options rejected")
在工程根目录运行:
python -X utf8 .\ohos-coap-libcoap-port\check-native.py
本机一次运行输出如下;重传间隔含随机性,不要求每次都是 2.61 秒。
PASS: PUT, token, MID, binary payload, elective option; packets=1, 0.00s
PASS: PUT, token, MID, binary payload, elective option; packets=2, 2.61s
PASS: silent bound peer produces deadline timeout, 1.01s
PASS: reserved, Observe and managed URI/body options rejected
两组检查覆盖了仓颉封装和独立线上的报文行为,仍不覆盖长时间运行、并发压力、所有异常内存路径、丢包网络中的完整分块组合或设备生命周期。
12. 设备运行:最后一道验收
本次没有执行这一节。准备 API 24 或以上、支持该仓颉 SDK 的 HarmonyOS 设备或模拟器,在 DevEco 登录账号并按需配置应用签名。使用 Project Structure → Signing Configs → Automatically generate signature 的实际可用选项,证书资料留在自己的环境中。
启动设备并执行:
$hdc = Join-Path $devEco 'sdk\default\openharmony\toolchains\hdc.exe'
& $hdc list targets
出现设备标识后,在 DevEco 选择设备、entry 配置并点击 Run。这个流程会生成可部署的签名 HAP。不要把本文的 entry-default-unsigned.hap 当作已经可安装的签名包;安装策略取决于设备和调试环境。
应用启动后,点击 “点击运行 CoAP 回环检查”。验收结果应是第 11 节第一行的 PASS 文本。若界面显示 FAIL,先查看 HiLog 和异常消息,定位端口占用、库加载、权限或 API 不兼容。
回环通过后,再做实际网络测试:服务端绑定可访问的设备接口,客户端 URI 使用服务端设备地址,确认网络路由和 UDP 端口可达。物理设备上的 127.0.0.1 指设备自身,不是开发电脑;模拟器到宿主的寻址也要按实际环境设置。本文没有验证这一步,因此不附上虚构的局域网成功截图。
13. 排错速查
cjc或cjpm找不到: 先检查 HarmonyOS 仓颉 SDK 是否安装,在当前 PowerShell 执行其build-tools/envsetup.ps1,再检查实际命令来源。不要混用通用 1.0.5 SDK 与本文 1.2 工程。- 找不到
coap_cj_bridge: 先运行原生构建脚本,确认 HAR 的两个libs目录有对应.so,再检查目标级ffi.c。不要仅修改临时编译输出目录中的 CJPM 配置。 - HAR 成功但 HAP 缺少仓颉依赖: 确认
entry/oh-package.json5的本地coap依赖,运行ohpm install --all和SyncCangjieResource,检查entry/cjpm.toml已出现ohos_app_cangjie_coap依赖。 - 资源编译提示 Failed to delete directory: 本机已确认过生成资源目录带
ReadOnly属性。先停止构建,检查报错指向的entry/build/...生成目录;仅清除这个已核对目录的只读属性再构建,或把工程放在普通非同步目录。不要递归修改 SDK、项目源码或用户目录的权限。 - Windows 程序退出码
0xC0000139: 本机曾是 GCC 15 的clock_gettime64与 SDK 较旧的libwinpthread-1.dll混用。本文 CMake 已为桥接 DLL 静态链接 winpthread 和 libgcc,避免这项运行时依赖冲突;不要随意覆盖 SDK DLL。 - 4.04 / 4.05: 分别核对路径和已注册方法。它们是收到的 CoAP 错误响应,要读取
code;本文服务端只注册 GET/POST。 - 超时与重传次数不符合直觉: 等待截止与重传预算是两个限制,先达到的限制结束请求。没有监听者时可能收到 ICMP/NACK,提前得到交付失败;DNS 阶段也不计入发送后的等待时长。
- 中文字符串异常: C 字符串边界使用 UTF-8,响应二进制数据不能无条件调用
text。PowerShell 读文件显式使用 UTF-8,Python 使用-X utf8。 - 停止后仍占用端口: 调用
stop()后还要等待运行任务完成。不要在其他任务里直接释放正在轮询的原生句柄。 - 模拟器要求 Hyper-V: 可以暂缓设备验收,继续构建和 Windows 检查;如需完整设备运行验证,之后启用合适的虚拟化环境或连接真机。
14. 验证记录与可交付产物
| 检查 | 本次结果 |
|---|---|
| Windows 原生桥接编译 | 通过 |
| 同一份仓颉 API 的 Windows 回环 | 通过,含 UTF-8 与 8 KB 分块 |
| 独立对端的 PUT、Token/MID、二进制和选项 | 通过 |
| 丢弃首包后的 CON 重传 | 通过,第二包与第一包一致 |
| 保持绑定但不回复的 1 秒截止 | 通过,约 1.01 秒 |
| OHOS arm64 / x86_64 原生交叉编译 | 通过 |
| 两种架构的仓颉 HAR | 构建通过 |
| 两种架构的应用 HAP | 构建通过,未签名 |
| HAP 内桥接与仓颉库检查 | 两种架构均包含 |
| HarmonyOS 设备 / 模拟器运行 | 未执行 |
| 实际局域网、独立 HAR 消费工程 | 未执行 |
到这里,读者拥有了实现客户端与服务端所需的全部新增源码、明确的上游依赖、两套构建脚本和可运行的检查入口。本文所称“完成移植工程”指上述交付范围;它不表示上游所有 API 等价迁移,也不表示已完成设备或生产环境验证。
发布和再分发时保留上游 Apache-2.0 许可、NOTICE,以及 libcoap 源码自带的完整 LICENSE 和源文件声明。本构建脚本将 libcoap LICENSE 放入 HAR 的 resources/rawfile;不要只保留一句“使用 BSD”而丢掉原文。SDK 与插件通过官方渠道安装,不随工程再分发。

Comments NOTHING