LwIP 开发指南
本文档介绍 HiDiTing SDK 中 LwIP 网络协议栈的结构、LiteOS 适配层、网络接口(Netif)接入方式,以及基于 Socket API 开发 UDP、TCP 和 DNS 网络应用的方法。文档结合 SDK 的 Netif 适配代码和网络业务代码,说明应用与底层驱动的职责边界、关键配置、代码走读和调试方法。
LwIP 背景知识
LwIP 的职责与接口层次
LwIP 负责 IPv4/IPv6、TCP、UDP、DNS、DHCP、Socket 和 Netconn 等网络能力。HiDiTing SDK 将 LwIP 内核、LiteOS 适配层和具体网络接口适配分层实现:应用优先使用 Socket API;网络接口或驱动开发才需要使用 netifapi_*、tcpip_input、pbuf 等接口。
图 1 Socket API整体流程

| 接口层次 | 面向对象 | 特点 | 主要头文件 |
|---|---|---|---|
| RAW / Callback API | 协议栈或高性能网络模块 | 直接操作 PCB 和回调,效率高,线程约束严格。 | tcp.h、udp.h |
| Netconn API | 有操作系统的中间层模块 | 使用连接对象和邮箱,易用性与效率居中。 | api.h |
| Socket API | 应用开发 | POSIX 风格的 socket、connect、send、recv 等接口,推荐用于应用。 |
sockets.h |

SDK 目录与组件关系
| 目录或组件 | 作用 | 参考 |
|---|---|---|
| LwIP 内核 | 提供 TCP/IP、Socket、DNS、Netif 等通用实现。 | /src/ohos/third_party/lwip/lwip_v2.1.3 |
| LiteOS 适配层 | 实现线程、邮箱、互斥锁、信号量、Netif 管理和不同网络接口适配。 | /src/ohos/third_party/lwip/lwip_adapter/liteos |
sys_arch.c |
为 LwIP 实现系统抽象接口,如线程、邮箱、信号量和时间。 | /src/ohos/third_party/lwip/lwip_adapter/liteos/src/arch/sys_arch.c |
| 网络接口适配 | 为 Wi-Fi、CAT1、SLE/CHBA、蓝牙桥接等网络接口创建和管理 Netif。 | /src/ohos/third_party/lwip/lwip_adapter/liteos/src/private |
| 应用 Socket 参考 | 提供 TCP 客户端/服务端、多播、HTTP 下载等网络业务代码。 | /src/middleware/utils/at/at_net_cmd/at/net_at_process.c |
说明:本文示例命令以一站式 CLI 为主。实际开发可从以下三种环境中选择,推荐使用一站式 CLI。
| 开发环境 | 适用场景 | 使用指南 |
|---|---|---|
| 一站式 CLI(推荐) | 快速完成目标选择、构建、烧录和串口监视 | 一站式 CLI 开发环境使用指南 |
| HiSpark Studio for VS Code | 图形化编辑、编译、烧录和调试 | HiSpark Studio for VS Code 开发环境使用指南 |
| WSL 与 Docker | 在 Windows 上使用一致的 Linux 容器构建环境 | WSL 与 Docker 环境使用指南 |
准备工作:按 一站式 CLI 开发环境使用指南 准备环境、构建和烧录。应用使用 Socket API 前,应确认目标配置已启用 SUPPORT_LWIP,并纳入 ohos_lwip、lwip_ext 等网络组件;当前 3322 目标配置可参考 /src/build/config/target_config/3322/config.py 与 /src/build/config/target_config/common_config.py。
快速跑通 LwIP 网络初始化 Demo
本节面向网络接口和底层适配开发。普通 Socket 应用不需要重复执行 tcpip_init() 或手动创建 Netif;应用应等待网络接口已经获得地址、链路已正常且默认路由可用后再建立连接。
线程与协议栈初始化
应用任务运行在业务线程中,LwIP 通过 tcpip_thread 处理协议栈消息和定时器。tcpip_init() 会初始化核心模块、创建 tcpip_mbox 邮箱,并启动协议栈线程;Socket/Netconn 请求通过该线程安全地进入协议栈。
图 2 LwIP线程

图 3 tcpip_thread线程

| 关键对象 | 作用 |
|---|---|
| tcpip_init | 初始化 TCP/IP 核心和协议栈线程。 |
| /src/ohos/third_party/lwip/lwip_adapter/liteos/src/arch/sys_arch.c | 为 tcpip_thread 提供线程、邮箱、互斥锁和信号量适配。 |
| TCPIP_MBOX_SIZE | 定义 tcpip_thread 接收消息的邮箱容量。 |
Netif 南向适配流程
网络接口适配需准备 IP 地址、子网掩码、网关和 MAC 地址;将 Netif 加入协议栈后,设置默认路由并在链路已就绪时将接口置为 up。SDK 网络接口代码优先使用 netifapi_netif_add()、netifapi_netif_set_up() 等线程安全接口,将操作切换到 LwIP 协议栈线程处理。
图 4 IP地址、网关和网络掩码


| 操作 | 接口或实现 | 说明 |
|---|---|---|
| 创建 Netif | netifapi_netif_add | 将 Netif、地址、初始化函数和 tcpip_input 加入协议栈。 |
| 设置链路和接口状态 | netifapi_netif_set_link_up、netifapi_netif_set_up | 链路物理可用后设置 link up,再将接口置为运行状态。 |
| 设置默认路由 | netifapi_netif_set_default | 使无特定路由的报文经该接口发送。 |
| 查询/监听 CAT1 网络状态 | lwip_get_connect_status、lwip_register_connect_listener | 应用或网络管理模块应以链路状态为准,而不是只判断接口创建成功。 |
SDK 参考代码与走读:
- /src/ohos/third_party/lwip/lwip_adapter/liteos/src/private/lwip_wifi_adapter.c 使用
netifapi_netif_add()创建 Wi-Fi Netif,再将链路置为 up、设置默认接口。 - /src/ohos/third_party/lwip/lwip_adapter/liteos/src/private/lwip_volte_adapter.c 在 CAT1 网络状态变化时调用
netifapi_netif_set_link_up()、netifapi_netif_set_up(),并通过信号量协调地址获取。 - /src/ohos/third_party/lwip/lwip_adapter/liteos/src/private/lwip_sle_adapter.c 展示 SLE/CHBA 网络接口的地址、MAC 和
tcpip_input注册方式。
预期结果与调试
Netif 创建成功后,应具备有效 IP/网关/DNS,并在链路状态回调中显示已连接。若 Socket 建连失败,优先确认接口是否已经 link up 和 up、默认路由是否指向正确 Netif、IP/掩码/网关是否匹配当前网络。
4G CAT1 公网访问时,SDK 网络适配层使用预置公共 DNS;若业务使用私有 DNS 或企业网络,应由网络管理侧按实际网络设置 DNS 并验证域名解析结果。
快速跑通基于 LwIP 的 Socket JS Demo
本节使用仓库中的 /samples/js_samples/sockettest/README.md 建立应用层 TCP/UDP 收发闭环。JS 代码调用 @ohos.net.socket,SDK 通过 InitSocketClientModule 注册该模块,最终由 Native 实现调用 lwip_socket()、lwip_connect()、lwip_send() 和 lwip_recv()。因此,该示例既能验证 JS Socket API,也能覆盖下层 LwIP 数据通路。
Demo 归档路径与文件结构
samples/js_samples/sockettest/
├── entry/src/main/config.json
└── entry/src/main/js/MainAbility/
├── common/
│ ├── codec.js # 字符串与二进制数据转换
│ ├── socketCore.js # 创建、连接、发送、接收和销毁
│ └── testRunner.js # 批量、压力和异常场景
└── pages/index/
├── index.hml # TCP/UDP 与收发操作按钮
├── index.css
└── index.js # 服务地址、端口和页面生命周期
对应 SDK 入口如下:
| 层次 | 代码位置 | 作用 |
|---|---|---|
| JS 应用 | /samples/js_samples/sockettest/entry/src/main/js/MainAbility/common/socketCore.js | 引入 @ohos.net.socket,管理 Socket 实例和异步回调。 |
| 模块注册 | /src/ohos/foundation/arkui/ace_engine_lite/frameworks/module_manager/ohos_module_config.h | 在 FEATURE_MODULE_SOCKET 打开时注册 net.socket。 |
| JS/Native 桥接 | /src/ohos/foundation/arkui/ace_engine_lite/frameworks/src/core/modules/socket/socket_module.cpp | 将 TCP、UDP 映射为 SOCK_STREAM、SOCK_DGRAM 并解析服务地址。 |
| LwIP 调用 | /src/ohos/foundation/arkui/ace_engine_lite/frameworks/src/core/modules/socket/socket_module_client.cpp | 执行 lwip_socket()、连接、收发、关闭和超时处理。 |
准备 TCP/UDP 回显服务
- 准备一台与开发板网络互通的电脑或服务器,同时启动 TCP 回显服务和 UDP 回显服务。
- 为两个服务分别选择未占用端口,并放行操作系统防火墙中的对应 TCP/UDP 入站规则。
- 在电脑上确认监听地址不是仅本机可见的
127.0.0.1,记录开发板可访问的 IPv4 地址。 - 先在同一网络中的另一台主机验证回显服务,再部署应用,以便区分服务端、防火墙和开发板网络问题。
配置服务地址
打开 /samples/js_samples/sockettest/entry/src/main/js/MainAbility/pages/index/index.js,将 onInit() 中的三项空值替换为实际测试地址。例如:
这里必须填写 IPv4 地址和有效端口。当前 Native 桥接层按 AF_INET 解析地址,不能把示例中的地址改成 URL,也不要填写只对服务端本机有效的 127.0.0.1。
构建、安装与启动
- 使用 HiSpark Studio for VS Code 或兼容的 OpenHarmony IDE 打开 samples/js_samples/sockettest。
- 按 OpenHarmony JS 应用开发用户指南 配置工程、签名和目标设备,构建 HAP 并安装到开发板。
- 确认开发板已获得有效 IP 地址,默认路由能够到达回显服务地址。
- 启动“Socket通信测试”应用,并同时观察应用日志和服务端日志。
快速验证 TCP 与 UDP
- 点击“新建 TCP 通信”。页面状态变为
S1 [TCP] OK后,服务端应收到Hello from Client [S1]!,并将它原样回传。 - 点击“发送单条消息”。服务端应收到形如
[S1] Test msg 2的报文,应用日志中的发送、接收计数随之增加。 - 点击“关闭当前连接”,确认服务端连接关闭,页面不再保留该实例。
- 点击“新建 UDP 通信”,页面状态变为
S1 [UDP] OK后,UDP 服务端应收到欢迎报文并回传。 - 再次点击“发送单条消息”,确认 UDP 报文内容和接收计数正确;完成后点击“关闭当前连接”。
基础回显通过后,再使用“发送二进制数据”“批量发送消息”和压力测试按钮。若页面始终显示 ...,按“开发板 IP/默认路由 → 服务端监听地址 → 防火墙 → 服务端端口 → 应用配置”的顺序排查。
核心代码走读
页面点击 TCP 或 UDP 按钮后,index.js 创建业务对象并把协议类型交给 socketCore.js:
runTCPDemo: function () {
this.addSocket('TCP');
},
runUDPDemo: function () {
this.addSocket('UDP');
}
prepareClient() 创建 Native Socket 客户端,再调用底层 socket();创建成功后 _onCreated() 使用配置的 IP 和端口连接服务端:
sk.client = socket.createSocket();
sk.client.socket(type, sk._onCreated);
var address = { address: ctx._serverIp, port: parseInt(port) };
sk.client.connect(address, sk._onConnected);
连接成功后,示例先发送欢迎消息,再启动持续接收。发送侧将字符串显式转换成 Uint8Array;接收侧完成一轮回调后继续调用 recv(),形成异步接收循环:
sendMsg(ctx, sk, 'Hello from Client [S' + sk.id + ']!', null);
startRecv(ctx, sk);
sk.client.send(binary, cb || sk._onSendDone);
sk.client.recv(sk._onRecvResult);
页面销毁时 onDestroy() 调用 destroyAll(),逐个停止定时器、关闭连接并释放 Native 客户端。基于该示例开发业务时,应继续保持“配置地址 → 创建 → 连接 → 收发 → 关闭 → 销毁”的顺序,并为协议报文补充长度边界和业务分帧规则。
快速跑通 UDP 客户端与服务端 Demo
UDP 适合低开销、允许应用自行处理丢包、乱序或重传的业务,例如广播发现、状态上报和实时控制。客户端使用 sendto() 指定对端地址发送报文;服务端通过 bind() 固定监听端口,并在循环中调用 recvfrom() 取得数据和来源地址。
图 5 客户端和服务端线程

UDP 客户端发送场景
图 6 申请套接字

图 7 设置服务器地址信息

图 8 发送数据

| 操作 | 接口 | 说明 |
|---|---|---|
| 创建 UDP 套接字 | socket | 使用 AF_INET、SOCK_DGRAM、IPPROTO_UDP。 |
| 设置地址与端口 | sockaddr_in、inet_addr、htons | 地址采用网络字节序,端口通过 htons() 转换。 |
| 发送数据 | sendto | 返回值小于 0 时读取错误码并执行关闭路径。 |
| 关闭套接字 | closesocket | 每个成功创建的套接字都必须关闭。 |
#include "lwip/sockets.h"
#include "lwip/inet.h"
#include "lwip/def.h"
int lwip_udp_send(const char *peer_ip, uint16_t peer_port,
const void *payload, size_t payload_len)
{
int fd = -1;
int ret = -1;
struct sockaddr_in peer = {0};
if (peer_ip == NULL || payload == NULL || payload_len == 0) {
return -1;
}
fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (fd < 0) {
return -1;
}
peer.sin_family = AF_INET;
peer.sin_port = htons(peer_port);
peer.sin_addr.s_addr = inet_addr(peer_ip);
if (peer.sin_addr.s_addr == INADDR_NONE) {
goto exit;
}
ret = sendto(fd, payload, payload_len, 0,
(const struct sockaddr *)&peer, sizeof(peer));
exit:
closesocket(fd);
return ret;
}
UDP 服务端接收场景
图 9 绑定socket

图 10 接收数据

int lwip_udp_receive_once(uint16_t local_port, void *buffer, size_t buffer_size)
{
int fd = -1;
int received = -1;
struct sockaddr_in local = {0};
struct sockaddr_in peer = {0};
socklen_t peer_len = sizeof(peer);
if (buffer == NULL || buffer_size == 0) {
return -1;
}
fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (fd < 0) {
return -1;
}
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(local_port);
if (bind(fd, (const struct sockaddr *)&local, sizeof(local)) < 0) {
goto exit;
}
received = recvfrom(fd, buffer, buffer_size, 0,
(struct sockaddr *)&peer, &peer_len);
exit:
closesocket(fd);
return received;
}
SDK 参考代码与走读:/src/middleware/utils/at/at_net_cmd/at/net_at_process.c 的多播发送逻辑在构造 sockaddr_in 后调用 sendto();接收逻辑先 bind() 本地端口,再调用 recvfrom(),并依据返回长度在缓冲区末尾补充字符串结束符。应用处理二进制协议时不应补充结束符,应严格使用返回长度。
预期结果与调试
客户端发送成功时 sendto() 返回实际发送字节数;服务端收到数据时 recvfrom() 返回实际接收字节数。若服务端无法接收,检查端口、协议类型、接口 IP、主机防火墙及 Netif 链路状态。阻塞式 recvfrom() 应运行在独立业务线程中,建议为网络任务预留至少 4 KB 栈空间。
快速跑通 TCP 客户端与服务端 Demo
TCP 提供有序、可靠的字节流传输。客户端依次创建套接字、配置对端地址、调用 connect(),然后使用 send()/recv();服务端依次执行 socket()、bind()、listen()、accept(),并对每个客户端连接单独收发数据。

| 场景 | 关键接口 | 说明 |
|---|---|---|
| TCP 客户端 | socket、connect、send、recv | connect() 成功后才进行收发;send() 可能部分发送,业务应循环直到全部发送或失败。 |
| TCP 服务端 | bind、listen、accept | 监听套接字和每个 accept() 返回的客户端套接字都需要单独关闭。 |
| 域名解析 | getaddrinfo、freeaddrinfo | 解析完成后使用返回的地址连接,并释放结果。 |
| 超时与多路复用 | select、poll | 避免单个阻塞连接永久占用业务线程。 |
int lwip_tcp_connect(const char *server_ip, uint16_t server_port)
{
int fd = -1;
struct sockaddr_in server = {0};
if (server_ip == NULL) {
return -1;
}
fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
return -1;
}
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip);
if (server.sin_addr.s_addr == INADDR_NONE ||
connect(fd, (const struct sockaddr *)&server, sizeof(server)) < 0) {
closesocket(fd);
return -1;
}
return fd;
}
SDK 参考代码与走读:/src/middleware/utils/at/at_net_cmd/at/net_at_process.c 中的 TCP 客户端逻辑按照“创建 Socket → 设置 sockaddr_in → connect() → 收发 → 关闭”处理;TCP 服务端逻辑按照“创建监听 Socket → bind() → listen() → accept() → 处理客户端连接”的顺序执行。该文件也提供 HTTP 下载和多播逻辑,可用于比较 TCP/UDP 的错误处理和关闭路径。
预期结果与调试
客户端 connect() 成功后返回有效文件描述符;服务端 accept() 成功后返回新的客户端描述符。若连接失败,依次检查 DNS/IP、端口、路由、服务端监听状态、TCP 缓冲区和 Socket 数量。若发送慢或接收吞吐不足,先测量网络链路和任务优先级,再调整 TCP 相关配置,不要仅通过无限增大缓冲区解决问题。
LwIP 配置与调试
适配层目录与关键配置
图 11 适配层目录

LiteOS 适配层中,sys_arch.c 负责系统抽象,ethernetif.c 提供网卡初始化、发送和接收入口,lwipopts_default.h 提供平台默认配置。网卡接收的数据应封装为 pbuf 后提交协议栈;发送路径由 low_level_output() 将协议栈的 pbuf 转交给硬件接口。应用层不应直接修改这些底层函数。
| 配置项 | 当前作用 | 配置位置 |
|---|---|---|
NO_SYS |
取值为 0 时启用线程、邮箱、互斥锁和 Socket/Netconn API。 | /src/ohos/third_party/lwip/lwip_adapter/liteos/src/include/lwip/lwipopts_default.h |
LWIP_SOCKET / LWIP_NETCONN |
启用 Socket 和 Netconn API。 | 同上 |
TCPIP_MBOX_SIZE |
协议栈线程的消息邮箱容量。 | 同上 |
LWIP_NUM_SOCKETS_MAX |
Socket 最大数量;应覆盖应用并发连接数和系统保留量。 | 同上 |
MEMP_NUM_PBUF / PBUF_POOL_SIZE |
影响 pbuf 分配能力和接收缓存资源。 | 同上 |
TCP_SND_BUF / TCP_SND_QUEUELEN |
影响 TCP 发送缓存与队列长度。 | 同上 |
TCP_WND |
TCP 接收窗口,影响高吞吐接收能力。 | 同上 |
配置修改应以目标业务的并发 Socket 数、报文大小、发送频率、可用内存和链路带宽为依据。每次调整后应重新构建,并通过连接数、发送失败率、接收吞吐和内存占用验证结果。
图 12 组件库选项

协议栈任务与性能调优
图 13 lwip_main任务

tcpip_thread 持续处理邮箱消息、超时事件和协议栈回调。出现吞吐低、发送阻塞或接收丢包时,按以下顺序排查:
- 确认应用没有在
tcpip_thread或网络回调中执行耗时操作。 - 检查业务 Socket 是否被阻塞接收长期占用;使用独立线程、
select()或poll()控制阻塞。 - 结合并发连接数检查
LWIP_NUM_SOCKETS_MAX、TCPIP_MBOX_SIZE、pbuf 池和任务栈。 - 根据真实网络吞吐和内存余量调整
TCP_SND_BUF、TCP_SND_QUEUELEN、TCP_WND,并进行回归测试。
图 14 配置项

日志调试
LwIP 默认配置文件为 /src/ohos/third_party/lwip/lwip_adapter/liteos/src/include/lwip/lwipopts_default.h。开启全局调试类型掩码和目标协议模块日志后重新构建,才能看到对应调试信息。
图 15 LwIP日志宏LWIP_DEBUG

图 16 TCP模块相关宏定义开启

调试时重点关注 LWIP_DBG_TYPES_ON、TCP/UDP/DNS 等模块宏,以及 Socket 返回值和错误码。建议仅在定位问题时开启所需模块,完成后关闭高频调试输出,避免日志影响实时性和内存。
基于 LwIP Demo 开发自己的应用
下面以“UDP 状态上报 + TCP 配置通道”为例说明如何建立自己的 LwIP 应用。
- 创建应用组件。 在 /samples/native_samples/ 下创建目录,参考 /samples/native_samples/adc/CMakeLists.txt 建立 C 组件,并在 /samples/native_samples/CMakeLists.txt 中通过功能宏加入
add_subdirectory_if_exist()。 -
加入 LwIP 头文件目录。 在应用组件
CMakeLists.txt的PRIVATE_HEADER中加入: -
纳入目标配置。 为组件添加独立功能宏,并在目标的
ram_component中加入该组件;确保目标已启用SUPPORT_LWIP、ohos_lwip与lwip_ext。ohos_lwip会导出LWIP_CONFIG_FILE="lwip/lwipopts_default.h"和LWIP_SHARED_CONFIG_FILE="lwip/lwipopts_shared.h",使应用与目标使用同一套 LwIP 配置;不要在应用目录自行新增lwipopts.h或覆盖这两个定义。参考 /src/build/config/target_config/3322/config.py 和 /src/build/cmake/open_source/lwip.cmake。 - 分离网络任务。 UDP 状态上报和 TCP 配置连接使用独立状态机;阻塞接收、重连、DNS 和业务解析不得运行在协议栈回调中。
- 实现可恢复关闭路径。 任一阶段失败时关闭已创建 Socket;网络断开后使本地连接状态失效,待 Netif 恢复后重新解析地址并建连。
推荐的最小验证顺序:先验证 Netif 地址和 DNS,再运行 UDP 单包发送/接收,最后增加 TCP 建连、收发和重连。HTTP、MQTT、WebSocket 等上层协议应建立在已验证的 TCP/DNS 能力之上。
注意事项
- 普通应用不应直接调用
tcpip_init()、netifapi_netif_add()或修改ethernetif.c;这些接口属于协议栈和网络接口适配层。应用应等待 Netif 已就绪后再使用 Socket。 - Socket API 依赖
NO_SYS=0、LWIP_SOCKET=1等配置。关闭 Socket 或 Netconn 支持后,应用无法使用 POSIX 风格网络接口。 - Socket 是有限资源。应确保每条成功创建的连接均在所有退出路径调用
closesocket(),并避免超过LWIP_NUM_SOCKETS_MAX。 - TCP 是字节流,
send()和recv()不保证一次完整收发一条业务消息;应用协议必须自行定义长度、分帧和重传策略。 recvfrom()、accept()、recv()等接口可能阻塞,需运行在独立任务中,并设置超时、多路复用或退出机制。- 修改 pbuf、TCP 缓冲或调试宏会影响内存和性能;必须在目标板上完成并发、压力和断网恢复验证。
常见编译错误
| 现象 | 排查方法 |
|---|---|
找不到 lwip/sockets.h、lwip/netdb.h 或 lwip/netifapi.h |
检查组件 CMakeLists.txt 是否加入 LwIP 内核 src/include 与 LiteOS 适配层 src/include 目录。 |
| 链接阶段找不到 Socket 或 Netif 符号 | 确认目标开启 SUPPORT_LWIP,并纳入 ohos_lwip、lwip_ext 等组件。 |
socket() 返回负值 |
检查 Netif 状态、LWIP_NUM_SOCKETS_MAX、系统文件描述符资源和之前是否遗漏 closesocket()。 |
connect()、sendto() 或 DNS 解析失败 |
检查 IP/端口/网关/DNS、链路状态、默认路由及对端服务状态。 |
| TCP 收发不完整或阻塞 | 按字节流协议循环处理 send()/recv(),增加超时、select()/poll() 或独立网络任务。 |
| 修改宏后行为未变化 | 确认修改的是当前目标实际使用的 lwipopts_default.h,并已重新构建和烧录。 |