鸿蒙LiteOS RK2206 LwIP Raw API 实现无阻塞UDP双向通信
B站 配套视频教程【鸿蒙 LiteOS 实战 14】LwIP Raw API实现全自动端口分配+无阻塞双向收发
一、前言
在鸿蒙LiteOS嵌入式物联网项目开发中,UDP通信是设备数据上报、远程指令控制最常用的通信方式。
日常开发中多数开发者习惯使用标准Socket接口实现UDP收发,但Socket模式存在线程阻塞、收发相互干扰、资源占用偏高等问题。
本文基于RK2206开发板 + 鸿蒙LiteOS ,使用LwIP底层Raw原生API 开发UDP客户端,采用异步回调接收+独立线程发送 架构,彻底消除阻塞问题,实现无干扰双向通信,同时支持系统自动分配本地端口,无需手动指定端口号,适配性更强,是嵌入式高性能网络开发标准方案。
主要解决问题
- 如果解决阻塞的问题
- 如果做到同时收和发
- raw 和 socket有什么区别
二、传统Socket UDP存在的两大痛点
1. 阻塞等待问题
Socket提供的recvfrom属于阻塞式接收函数,在没有服务器数据下发时,当前线程会直接挂起休眠等待,无法执行其他业务逻辑,发送周期被接收状态严重影响,实时性大打折扣。
2. 双向并发收发困难
若想要同时收发数据,必须拆分多线程运行,由于多个线程共用同一个Socket文件描述符,属于共享临界资源,必须添加互斥锁进行保护,不仅增加代码复杂度,还容易出现资源竞争、程序异常崩溃等问题。
三、Raw API核心优势与解决思路
- 摒弃阻塞读取:采用LwIP内核异步回调机制,数据到达自动触发接收函数,程序无需主动轮询等待
- 收发逻辑解耦:接收交由内核回调处理,发送独立线程定时执行,两者互不干扰
- 无需线程锁:架构天然无共享资源竞争,省去互斥锁创建、加锁、解锁流程
- 端口自动分配:绑定端口传入0,由LwIP协议栈自动分配空闲本地端口,避免端口占用冲突
- 底层高性能:直接调用LwIP原生接口,跳过Socket封装层,运行效率更高、内存占用更小
四、实现架构:真正做到同时收发
- 数据接收端 :LwIP内核层回调函数,网络数据包抵达网卡后,协议栈自动调用注册好的接收回调函数完成数据解析,不占用业务线程
- 数据发送端:独立LiteOS任务线程,按照自定义周期定时向上位机服务器发送设备数据
- 网络前置条件:程序上电自动等待WiFi连接成功并获取有效IP地址,联网成功后再初始化UDP网络
整体架构完全并行运行,接收不打断发送,发送不影响接收,实现标准意义上的全双工无阻塞UDP通信。
五、关键技术点详解
5.1 彻底解决线程阻塞
传统方案:任务主动调用接收函数 → 无数据则阻塞休眠
Raw方案:内核被动推送数据 → 有数据才执行接收逻辑,空闲状态线程全程正常运行
5.2 端口自动分配原理
调用udp_bind绑定本地端口时,第二个端口参数填写0 ,LwIP协议栈会自动从系统空闲端口池中选取未被占用的端口完成绑定,绑定成功后可直接从UDP控制块local_port成员读取实际分配端口。
c
// 自动分配本地端口
udp_bind(g_udp_pcb, IP_ADDR_ANY, 0);
// 获取分配完成的端口号
g_local_port = g_udp_pcb->local_port;
5.3 LwIP数据缓冲区pbuf使用
LwIP所有网络数据收发都依赖pbuf数据包缓冲区,发送前申请内存存放数据,发送完成必须手动调用pbuf_free释放内存,避免出现内存泄漏。
六、完整可运行源码
c
/*
* Copyright (c) 2022 FuZhou Lockzhiner Electronic Co., Ltd. All rights reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* http://www.apache.org/licenses/LICENSE-2.0
*/
#include "ohos_init.h"
#include "los_task.h"
#include "lz_hardware.h"
#include "config_network.h"
#include "lwip/udp.h"
#include "lwip/ip_addr.h"
#include "lwip/pbuf.h"
#include <string.h>
#include <stdio.h>
#define LOG_TAG "udp_raw"
#define OC_SERVER_IP "192.168.111.61" // 上位机服务器IP
#define SERVER_PORT 20108 // 上位机服务器端口
#define SEND_INTERVAL_MS 1000 // 数据发送间隔
static struct udp_pcb *g_udp_pcb = NULL;
static ip4_addr_t g_server_ip;
static u16_t g_local_port = 0; // 存储系统自动分配的本地端口
static unsigned int g_send_cnt = 0; // 发送数据计数
/**
* @brief 等待WiFi连接并获取本机IP地址
* @param info WiFi信息结构体
* @return 0连接成功,-1连接失败
*/
int udp_get_wifi_info(WifiLinkedInfo *info)
{
int ret = -1;
memset(info, 0, sizeof(WifiLinkedInfo));
unsigned int retry = 20;
while (retry--)
{
if (GetLinkedInfo(info) == WIFI_SUCCESS)
{
if (info->connState == WIFI_CONNECTED && info->ipAddress != 0)
{
LZ_HARDWARE_LOGD(LOG_TAG, "WiFi IP: %s", inet_ntoa(info->ipAddress));
ret = 0;
break;
}
}
LOS_Msleep(1000);
}
return ret;
}
/**
* @brief UDP数据异步接收回调函数
* @param arg 自定义传入参数
* @param pcb UDP控制块
* @param p 网络数据缓冲区
* @param addr 发送方IP地址
* @param port 发送方端口号
*/
static void udp_recv_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr, u16_t port)
{
if (p == NULL)
return;
printf("[Raw Recv] %.*s\n", p->len, (char *)p->payload);
pbuf_free(p); // 释放pbuf内存,防止内存泄漏
}
/**
* @brief Raw API UDP数据发送函数
*/
void udp_raw_send(void)
{
char buf[64];
snprintf(buf, sizeof(buf), "RK2206 Raw API UDP msg: %u \r\n", g_send_cnt++);
// 申请传输层pbuf缓冲区
struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, strlen(buf), PBUF_RAM);
if (p == NULL)
return;
// 拷贝发送数据
memcpy(p->payload, buf, strlen(buf));
// 指定IP与端口发送数据
udp_sendto(g_udp_pcb, p, &g_server_ip, SERVER_PORT);
printf("[Raw Send] %s\n", buf);
pbuf_free(p);
}
/**
* @brief 独立发送任务线程
*/
void udp_send_thread(void)
{
while (1)
{
if (g_udp_pcb != NULL)
{
udp_raw_send();
}
LOS_Msleep(SEND_INTERVAL_MS);
}
}
/**
* @brief UDP Raw API初始化
* @note 绑定端口填写0,实现系统自动分配本地端口
*/
void udp_raw_init(void)
{
// 创建UDP协议控制块
g_udp_pcb = udp_new();
if (g_udp_pcb == NULL)
{
printf("udp_new failed!\n");
return;
}
// 绑定任意网卡,端口0自动分配
err_t err = udp_bind(g_udp_pcb, IP_ADDR_ANY, 0);
if (err != ERR_OK)
{
printf("udp_bind failed!\n");
return;
}
// 获取协议栈自动分配的本地端口
g_local_port = g_udp_pcb->local_port;
// 注册异步接收回调函数
udp_recv(g_udp_pcb, udp_recv_callback, NULL);
// 配置服务器IP地址
IP4_ADDR(&g_server_ip, 192, 168, 111, 61);
printf("===== UDP Raw API 初始化完成 =====\n");
printf("本地端口(系统自动分配): %d\n", g_local_port);
printf("目标服务器地址: %s:%d\n", OC_SERVER_IP, SERVER_PORT);
}
/**
* @brief UDP网络业务主流程
*/
void udp_raw_example_process(void)
{
WifiLinkedInfo info;
// 循环等待WiFi联网成功
while (udp_get_wifi_info(&info) != 0)
{
LOS_Msleep(500);
}
// 初始化LwIP Raw UDP
udp_raw_init();
// 创建独立发送任务
unsigned int send_tid;
TSK_INIT_PARAM_S send_task = {0};
send_task.pfnTaskEntry = (TSK_ENTRY_FUNC)udp_send_thread;
send_task.uwStackSize = 8192;
send_task.pcName = "udp_send_task";
send_task.usTaskPrio = 24;
LOS_TaskCreate(&send_tid, &send_task);
}
/**
* @brief 系统开机自启动入口
*/
void udp_client_raw_example(void)
{
unsigned int thread_id;
TSK_INIT_PARAM_S task = {0};
task.pfnTaskEntry = (TSK_ENTRY_FUNC)udp_raw_example_process;
task.uwStackSize = 10240;
task.pcName = "udp_raw_main";
task.usTaskPrio = 23;
LOS_TaskCreate(&thread_id, &task);
}
// 鸿蒙系统应用自动初始化
APP_FEATURE_INIT(udp_client_raw_example);
七、程序运行效果
-
设备上电自动连接WiFi,打印本机局域网IP
-
联网成功后初始化UDP协议栈,打印系统自动分配的本地端口
-
每秒主动向服务器推送自定义设备消息
-
上位机下发指令可实时被回调函数捕获打印
WiFi IP: 192.168.111.105
===== UDP Raw API 初始化完成 =====
本地端口(系统自动分配): 52010
目标服务器地址: 192.168.111.61:20108
[Raw Send] RK2206 Raw API UDP msg: 0
[Raw Send] RK2206 Raw API UDP msg: 1
[Raw Recv] Server test data
[Raw Send] RK2206 Raw API UDP msg: 2

八、LwIP Raw API 与 Socket API详细对比
| 对比维度 | 标准Socket API | LwIP Raw原生API |
|---|---|---|
| 接收模式 | 主动调用函数阻塞等待 | 内核异步回调被动接收 |
| 阻塞特性 | 存在阻塞,影响业务时序 | 全程无阻塞,线程运行流畅 |
| 并发收发 | 多线程需互斥锁保护 | 天然线程安全,无需加锁 |
| 调用层级 | LwIP上层封装接口 | 直接调用协议栈底层接口 |
| 运行性能 | 中等,存在封装损耗 | 性能最优,资源占用极低 |
| 端口使用 | 手动指定固定端口 | 支持填0自动分配空闲端口 |
| 开发难度 | 入门简单,上手快 | 熟悉协议栈后开发更灵活 |
| 适用场景 | 快速调试、简易通信项目 | 工业物联网、高并发、低功耗设备 |
九、开发总结
- 采用LwIP Raw异步回调彻底解决传统UDP接收阻塞问题,保障设备业务逻辑稳定运行
- 发送任务+内核回调的分离架构,轻松实现UDP全双工同时收发,互不干扰
- 绑定端口置0实现协议栈自动分配端口,有效解决多设备同网段端口冲突问题
- Raw API跳过Socket封装层,更贴合嵌入式底层开发需求,在低配置IoT芯片中优势明显
- 开发使用
pbuf缓冲区务必及时释放,长期运行项目必须做好内存管理,避免内存泄漏
本文代码完全适配RK2206鸿蒙LiteOS原生工程,修改服务器IP与端口即可直接编译烧录,可直接用于物联网数据采集、无线遥控、局域网设备通信等实际项目开发。
十、思考
- 如果客户端不给服务端发数据,服务端能发送数据给客户端吗?