鸿蒙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成员读取实际分配端口。
// 自动分配本地端口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释放内存,避免出现内存泄漏。
六、完整可运行源码
/* * 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>#defineLOG_TAG"udp_raw"#defineOC_SERVER_IP"192.168.111.61"// 上位机服务器IP#defineSERVER_PORT20108// 上位机服务器端口#defineSEND_INTERVAL_MS1000// 数据发送间隔staticstructudp_pcb*g_udp_pcb=NULL;staticip4_addr_tg_server_ip;staticu16_tg_local_port=0;// 存储系统自动分配的本地端口staticunsignedintg_send_cnt=0;// 发送数据计数/** * @brief 等待WiFi连接并获取本机IP地址 * @param info WiFi信息结构体 * @return 0连接成功,-1连接失败 */intudp_get_wifi_info(WifiLinkedInfo*info){intret=-1;memset(info,0,sizeof(WifiLinkedInfo));unsignedintretry=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);}returnret;}/** * @brief UDP数据异步接收回调函数 * @param arg 自定义传入参数 * @param pcb UDP控制块 * @param p 网络数据缓冲区 * @param addr 发送方IP地址 * @param port 发送方端口号 */staticvoidudp_recv_callback(void*arg,structudp_pcb*pcb,structpbuf*p,constip_addr_t*addr,u16_tport){if(p==NULL)return;printf("[Raw Recv] %.*s\n",p->len,(char*)p->payload);pbuf_free(p);// 释放pbuf内存,防止内存泄漏}/** * @brief Raw API UDP数据发送函数 */voidudp_raw_send(void){charbuf[64];snprintf(buf,sizeof(buf),"RK2206 Raw API UDP msg: %u \r\n",g_send_cnt++);// 申请传输层pbuf缓冲区structpbuf*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 独立发送任务线程 */voidudp_send_thread(void){while(1){if(g_udp_pcb!=NULL){udp_raw_send();}LOS_Msleep(SEND_INTERVAL_MS);}}/** * @brief UDP Raw API初始化 * @note 绑定端口填写0,实现系统自动分配本地端口 */voidudp_raw_init(void){// 创建UDP协议控制块g_udp_pcb=udp_new();if(g_udp_pcb==NULL){printf("udp_new failed!\n");return;}// 绑定任意网卡,端口0自动分配err_terr=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网络业务主流程 */voidudp_raw_example_process(void){WifiLinkedInfo info;// 循环等待WiFi联网成功while(udp_get_wifi_info(&info)!=0){LOS_Msleep(500);}// 初始化LwIP Raw UDPudp_raw_init();// 创建独立发送任务unsignedintsend_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 系统开机自启动入口 */voidudp_client_raw_example(void){unsignedintthread_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与端口即可直接编译烧录,可直接用于物联网数据采集、无线遥控、局域网设备通信等实际项目开发。
十、思考
- 如果客户端不给服务端发数据,服务端能发送数据给客户端吗?