1、实验简介

参考网址:https://gitee.com/Lockzhiner-Electronics/lz3863/tree/master/apps/c5_wifi_udp_server

1.1、实验目的

本实验旨在帮助学习者掌握 OpenHarmony 轻量系统中 WiFi + UDP 服务端 的基本使用方法。通过本实验,你将学会:

  1. 理解 UDP 协议 的无连接、报文传输特性及客户端/服务端通信模型;
  2. 在开发板上以 STA 模式连接 WiFi 热点,通过 DHCP 获取 IP 地址;
  3. 使用 lwIP Socket API 创建 UDP 服务端,完成绑定、接收数据报与回复;
  4. 掌握 UDP 服务端与客户端的完整联调流程;
  5. 完成案例代码的编译、烧录与串口现象观察。

1.2、实验内容

本案例在 LZ3863-星闪开发板上实现 UDP 服务端 功能:先连接指定 WiFi 热点并获取 IP,再在本机端口上创建 UDP 服务、等待客户端数据报,收到数据后打印客户端信息并回复测试数据,全过程通过串口打印日志。

项目 说明
源文件 wifi_udp_server_example.c(主程序)、wifi_connecter.c(WiFi 封装)
WiFi 模式 STA(站点/客户端)
目标 SSID lzdz
目标密码 88888888
UDP 监听端口 777
绑定地址 INADDR_ANY(接受任意 IP 的客户端数据报)
回复测试数据 test_data
任务线程 udp_server_demo_task(栈大小 8192 字节)
初始化入口 APP_FEATURE_INIT(udp_server_demo_entry)

典型联调拓扑:

┌─────────────────┐         WiFi          ┌─────────────────┐
│  PC / 手机热点   │ ◄──────────────────► │  LZ3863 开发板   │
│  SSID: lzdz     │                       │  (UDP Server)   │
│                 │      UDP :777         │  IP: 192.168.   │
│  (UDP Client)   │ ◄──────────────────► │      137.x      │
└─────────────────┘                       └─────────────────┘

说明:开发板作为 UDP 服务端,需先通过串口日志确认其 DHCP 获取到的 IP 地址,客户端(PC 网络调试工具、nc 命令或 c4_wifi_udp_client 案例)应向该 IP 及端口 777 发送 UDP 数据报。

1.3、实验环境

项目 说明
硬件 LZ3863-星闪开发板、USB 数据线
软件 OpenHarmony v5.1.0 源码、hb 编译工具
网络环境 可连接的 WiFi 热点(SSID/密码与代码一致)
UDP 客户端 PC 端网络调试工具 / nc / Python 脚本,或 c4_wifi_udp_client 案例
调试工具 串口助手(波特率 115200,8N1)
案例路径 applications/sample/wifi-iot/app/c5_wifi_udp_server/

2、基础知识

2.1、UDP 协议概述

UDP(User Datagram Protocol,用户数据报协议) 是一种无连接的、面向报文的传输层协议,具有以下特点:

特性 说明
无连接 通信前无需建立连接,发送方直接发送数据报
不可靠传输 不保证数据到达、顺序和完整性,无重传机制
低开销 协议头仅 8 字节,无握手/挥手过程,延迟低
面向报文 每次收发以完整数据报为单位,保留报文边界

UDP 通信同样采用 客户端/服务端(C/S) 模型,但与 TCP 不同,双方无需先建立连接:

角色 职责 本实验对应
UDP 服务端 绑定端口、接收数据报、向客户端地址回复 本案例开发板
UDP 客户端 指定目标 IP/端口、发送/接收数据报 PC 或另一块开发板

2.2、UDP 与 TCP 对比

对比项 TCP(c3 案例) UDP(本案例)
连接方式 面向连接,需三次握手 无连接,直接发送
可靠性 可靠,有确认与重传 不可靠,尽力交付
套接字类型 SOCK_STREAM SOCK_DGRAM
服务端流程 bind() + listen() + accept() bind() + recvfrom()
发送 API send() sendto()
接收 API recv() recvfrom()
适用场景 文件传输、HTTP 等需可靠传输 实时音视频、传感器上报、广播等

2.3、Socket 服务端编程基础

Socket(套接字)是网络编程的抽象接口,lwIP 提供了与 BSD Socket 兼容的 API。UDP 服务端的典型流程:

socket(SOCK_DGRAM)  →  创建 UDP 套接字
    ↓
配置 sockaddr_in(INADDR_ANY + 端口)
    ↓
bind()  →  绑定本地 IP 和端口
    ↓
recvfrom()  →  阻塞等待客户端数据报,同时获取发送方地址
    ↓
sendto()  →  向客户端地址回复数据报
    ↓
closesocket()  →  关闭套接字

与 TCP 服务端的关键区别:

步骤 TCP 服务端(c3 案例) UDP 服务端(本实验)
地址配置 INADDR_ANY + 监听端口 INADDR_ANY + 监听端口
等待连接 listen() + accept() 被动等待连接 无需 listen/accept,直接 recvfrom() 等待数据报
通信对象 监听套接字 + 连接套接字 单一套接字,通过 recvfrom() 获取客户端地址
回复方式 send(connfd, ...) sendto(sockfd, ..., client_addr)

关键数据结构 sockaddr_in(服务端):

struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;                // IPv4
server_addr.sin_port = htons(port);              // 端口号(转网络字节序)
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡,接受任意 IP 接入

字节序转换:

函数 作用
htons() 主机字节序 → 网络字节序(16 位,用于端口)
htonl() 主机字节序 → 网络字节序(32 位,用于 IP)
inet_ntoa() 二进制网络地址 → 字符串 IP(用于打印客户端地址)
ntohs() 网络字节序 → 主机字节序(16 位,用于端口)

2.4、WiFi STA 连接与网络层

本案例在启动 UDP 服务之前,需先通过 ConnectToHotspot() 完成 WiFi 连接与 IP 获取。完整流程为:

启用 STA 模式 → 扫描热点 → 匹配 SSID → 关联连接
    → 启动 DHCP 客户端 → 获取 IP 地址 → 网络层就绪

获取 IP 后,开发板与 UDP 客户端处于同一局域网,客户端可通过该 IP 地址和监听端口发送数据报。

2.5、软件调用层次

本案例的软件调用层次如下:

应用层(wifi_udp_server_example.c)
    ├── udp_server_demo_entry()   ← APP_FEATURE_INIT 注册入口
    ├── udp_server_demo_task()    ← 任务线程:WiFi 连接 + UDP 服务端
    └── udp_server_test()         ← UDP 核心逻辑
            │
WiFi 封装层(wifi_connecter.c)
    └── ConnectToHotspot()        ← 扫描、连接、DHCP 获取 IP
            │
协议栈 / 驱动层
    ├── lwIP Socket API(socket/bind/recvfrom/sendto)
    ├── lwIP 网络协议栈(UDP/IP、DHCP)
    └── WiFi 驱动(HMAC/DMAC)

2.6、核心 API 介绍

2.6.1、头文件
#include "cmsis_os2.h"
#include "lwip/sockets.h"
#include "ohos_init.h"
#include "wifi_connecter.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
2.6.2、Socket API(UDP 服务端)
API 名称 功能说明
socket(AF_INET, SOCK_DGRAM, 0) 创建 IPv4 UDP 套接字,返回套接字描述符
bind(sockfd, addr, addrlen) 将套接字绑定到指定的 IP 地址和端口
recvfrom(sockfd, buf, len, flags, from, fromlen) 接收 UDP 数据报,同时获取发送方(客户端)地址信息
sendto(sockfd, buf, len, flags, to, tolen) 基于获取的客户端地址,发送回复数据报
closesocket(sockfd) 关闭套接字,释放资源
inet_ntoa(in_addr) 将网络字节序 IP 地址转换为字符串
2.6.3、WiFi 与应用层 API
API 名称 功能说明
ConnectToHotspot(ssid, password) 扫描并连接指定热点,完成 DHCP 获取 IP
DisconnectWithHotspot() 停止 DHCP 并断开 WiFi 连接
osThreadNew(func, arg, &attr) 创建 RTOS 线程
osDelay(ticks) 线程延时,100 ticks ≈ 1 秒(tick = 10 ms)
APP_FEATURE_INIT(func) 注册应用特性初始化入口,系统启动后自动执行

3、程序设计

3.1、程序架构

本案例目录结构

c5_wifi_udp_server/
├── wifi_udp_server_example.c   # UDP 服务端主程序
├── wifi_connecter.c            # WiFi 连接封装实现
├── wifi_connecter.h            # 封装接口头文件
├── BUILD.gn                    # GN 编译配置
├── README_zh.md                # 案例简要说明
└── 实验手册.md                  # 本实验手册

程序执行流程:

系统启动
    │
    ▼
udp_server_demo_entry()         ← APP_FEATURE_INIT 注册,自动执行
    │
    ▼
osThreadNew(udp_server_demo_task) ← 创建 UDP 服务端任务线程
    │
    ▼
udp_server_demo_task()
    ├── ConnectToHotspot()      ← 连接 WiFi 热点,DHCP 获取 IP
    ├── osDelay(800)            ← 等待网络稳定(约 8 秒)
    └── udp_server_test()       ← 启动 UDP 服务端
            ├── socket()        ← 创建 UDP 套接字
            ├── bind()          ← 绑定端口 777
            ├── recvfrom() 循环 ← 接收客户端数据报,获取客户端地址
            ├── sendto()        ← 向客户端地址回复 test_data
            └── closesocket()   ← 关闭套接字

3.2、源文件说明

文件 说明
wifi_udp_server_example.c UDP 服务端主程序,包含 WiFi 连接、UDP 服务及任务线程
wifi_connecter.c WiFi 封装,实现 ConnectToHotspotStartHotspotDisconnectWithHotspot
wifi_connecter.h 封装接口声明
BUILD.gn 编译配置,生成 wifi_udp_server_example 静态库

3.3、关键代码分析

(1)WiFi 与 UDP 配置参数
#define WIFI_SSID "lzdz"
#define WIFI_PASSWORD "88888888"

#define UDP_SERVER_PORT 777

static char send_data[] = "test_data";
static char recv_data[100];

实验前请根据实际网络环境修改 WIFI_SSIDWIFI_PASSWORD,确保与可用热点一致。UDP 监听端口可通过 UDP_SERVER_PORT 修改。

(2)系统入口 — udp_server_demo_entry

通过 APP_FEATURE_INIT 注册应用入口,创建 udp_server_demo_task 线程:

static void udp_server_demo_entry(void)
{
    osThreadAttr_t attr = {
        .name = "udp_server_demo_task",
        .stack_size = 8192,
        .priority = osPriorityNormal
    };

    if (osThreadNew(udp_server_demo_task, NULL, &attr) == NULL)
    {
        printf("[udp_server_demo_entry] Failed to create udp_server_demo_task!\r\n");
    }
}

APP_FEATURE_INIT(udp_server_demo_entry);
(3)任务线程 — udp_server_demo_task

任务线程先完成 WiFi 连接,延时等待网络稳定后再启动 UDP 服务端:

static void udp_server_demo_task(void *arg)
{
    (void)arg;

    printf("Starting Wi-Fi connection to SSID: %s...\r\n", WIFI_SSID);
    if (ConnectToHotspot(WIFI_SSID, WIFI_PASSWORD) != 0)
    {
        printf("Failed to connect to AP.\r\n");
        return;
    }

    printf("Wi-Fi connected successfully.\r\n");

    osDelay(800);   // 等待约 8 秒,确保网络栈稳定

    udp_server_test(UDP_SERVER_PORT);
}
(4)UDP 服务端核心 — udp_server_test

udp_server_test() 实现完整的 UDP 服务端通信流程:

void udp_server_test(unsigned short port)
{
    ssize_t ret = 0;

    // 1. 创建 UDP 套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        printf("Failed to create UDP socket! errno=%d\r\n", errno);
        return;
    }

    // 2. 配置服务端地址(绑定所有网卡)
    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 3. 绑定端口
    ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (ret < 0)
    {
        printf("Bind to port %d failed! errno=%d\r\n", port, errno);
        closesocket(sockfd);
        return;
    }
    printf("Bind to port %d success! Waiting for client data...\r\n", port);

    // 4. 循环接收客户端数据并回复
    struct sockaddr_in client_addr = {0};
    socklen_t client_addr_len = sizeof(client_addr);
    while (running)
    {
        memset(recv_data, 0, sizeof(recv_data));
        ret = recvfrom(sockfd, recv_data, sizeof(recv_data) - 1, 0,
                       (struct sockaddr *)&client_addr, &client_addr_len);
        if (ret <= 0)
        {
            printf("Recv data failed or connection closed! ret=%ld, errno=%d\r\n", ret, errno);
            continue;
        }
        recv_data[ret] = '\0';

        // 打印接收日志和客户端信息
        printf("Received data{%s} %ld bytes from client! Peer info: IP=%s, Port=%d\r\n",
               recv_data, ret, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 向客户端回复数据
        ret = sendto(sockfd, send_data, strlen(send_data), 0,
                     (struct sockaddr *)&client_addr, client_addr_len);
        if (ret < 0)
        {
            printf("Reply to client failed! errno=%d\r\n", errno);
        }
        else
        {
            printf("Sent reply data{%s} %ld bytes to client!\r\n", send_data, ret);
        }

        osDelay(100);   // 防止忙等待
    }

    // 5. 关闭套接字
    closesocket(sockfd);
}

设计要点:

  • INADDR_ANY 表示绑定到所有可用网卡,客户端可通过开发板的任意 IP 地址发送数据报;
  • UDP 无需 listen()accept(),绑定端口后直接进入 recvfrom() 等待数据;
  • recvfrom() 返回时,client_addr 中保存了发送方的 IP 和端口,可直接用于 sendto() 回复;
  • 服务端采用被动响应模式:收到客户端数据后才回复,不会主动发送首包;
  • running 全局变量控制循环,设为 0 可退出服务循环。
(5)WiFi 封装层 — ConnectToHotspot 概要

ConnectToHotspot()wifi_connecter.c 中实现,主要步骤:

  1. 调用 wifi_sta_enable() 启用 STA 模式;
  2. 循环执行 wifi_sta_scan() 扫描,get_match_network() 匹配目标 SSID;
  3. 调用 wifi_sta_connect() 发起关联,等待 WIFI_CONNECTED 状态;
  4. wlan0 接口上启动 DHCP 客户端,获取 IP 地址并打印。

3.4、程序执行流程

UDP 客户端 (PC/开发板) WiFi 热点 开发板 (UDP Server) UDP 客户端 (PC/开发板) WiFi 热点 开发板 (UDP Server) UDP 无连接,每次收发独立进行 APP_FEATURE_INIT → udp_server_demo_entry osThreadNew(udp_server_demo_task) ConnectToHotspot("lzdz") 关联成功 + DHCP 分配 IP osDelay(800) 等待网络稳定 bind(:777) sendto("send_data" → 开发板IP:777) recvfrom() 获取客户端地址 sendto("test_data" → 客户端地址)

4、编译步骤

以下步骤只需在首次编译时完成 4.1~4.3 的配置注册。

4.1、确认案例目录

确认案例已位于 OpenHarmony 源码目录下:

applications/sample/wifi-iot/app/c5_wifi_udp_server/
├── wifi_udp_server_example.c
├── wifi_connecter.c
├── wifi_connecter.h
├── BUILD.gn
└── 实验手册.md

若从外部复制,请将 c5_wifi_udp_server 目录放到上述 app/ 路径下。

4.2、修改 BUILD.gn(注册编译组件)

编辑 applications/sample/wifi-iot/app/BUILD.gn,在 features 列表中添加本案例:

lite_component("app") {
  features = [
    "startup",
    "c5_wifi_udp_server:wifi_udp_server_example",   // 添加此行
  ]
}

4.3、修改 SDK 配置文件

步骤 1:编辑 device/soc/hisilicon/ws63v100/sdk/build/config/target_config/ws63/config.py

找到 'ws63-liteos-app' 配置段,在其 'ram_component' 列表中添加:

"wifi_udp_server_example"

步骤 2:编辑 device/soc/hisilicon/ws63v100/sdk/libs_url/ws63/cmake/ohos.cmake

找到 "ws63-liteos-app" 对应的 set(COMPONENT_LIST 部分,添加:

"wifi_udp_server_example"

4.4、编译固件

在 OpenHarmony 源码根目录下执行编译:

rm -rf out
hb set -root .
# 通过上下方向键选择 ws63 对应的编译分支(如 nearlink_dk_3863 / ws63-liteos-app)
hb build -f

编译成功后,使用开发板配套烧录工具将固件烧写到 LZ3863-星闪开发板。

4.5、修改网络参数(可选)

烧录前,若实际 WiFi 热点与默认值不同,请编辑 wifi_udp_server_example.c 中的宏定义:

#define WIFI_SSID "lzdz"              // 改为实际热点名称
#define WIFI_PASSWORD "88888888"      // 改为实际热点密码
#define UDP_SERVER_PORT 777           // 改为实际监听端口

修改后需重新编译并烧录。


5、运行结果

5.1、硬件与网络准备

方式一:PC 移动热点 + PC 端 UDP 客户端(推荐)

  1. 在 PC 上开启移动热点,SSID 设为 lzdz,密码设为 88888888

  2. 开发板上电或复位,烧录本案例固件,通过 USB 连接 PC 打开串口助手(115200,8N1);

  3. 观察串口日志中打印的开发板 IP 地址(如 STA IP 192.168.137.x);

  4. 在 PC 上使用 UDP 客户端向该 IP 及端口 777 发送数据:

    # 方式 A:使用 netcat(Linux / macOS)
    echo "Hello UDP" | nc -u 192.168.137.x 777
    
    # 方式 B:使用 Python
    python3 -c "
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.sendto(b'Hello UDP', ('192.168.137.x', 777))
    data, addr = s.recvfrom(1024)
    print(f'Received from {addr}: {data.decode()}')
    s.close()
    "
    

    192.168.137.x 替换为串口日志中实际打印的开发板 IP。

方式二:使用 c4_wifi_udp_client 案例联调

  1. 准备可连接的 WiFi 路由器或手机热点(SSID/密码与代码一致);
  2. 本开发板烧录 c5_wifi_udp_server 固件作为 UDP 服务端;
  3. 另一块开发板烧录 c4_wifi_udp_client 固件作为 UDP 客户端;
  4. 查看服务端串口日志中的 IP 地址,将其填入客户端案例的 UDP_SERVER_IP 宏定义后重新编译烧录客户端固件。

建议:先确认开发板 WiFi 连接成功并打印 IP 地址,再启动 UDP 客户端发送数据,以提高首次联调成功率。

5.2、串口配置

参数
波特率 115200
数据位 8
停止位 1
校验位
流控

5.3、预期串口输出

烧录固件并复位后,串口助手可观察到完整的 WiFi 连接与 UDP 服务过程:

Starting Wi-Fi connection to SSID: lzdz...
Start Scan !
[WIFI_STA_SAMPLE] Scan done!.
STA try connect.
[WIFI_STA_SAMPLE] Connect succ!.
STA DHCP start.
STA DHCP bound success.
STA IP 192.168.137.x
Connect success.
Wi-Fi connected successfully.
Bind to port 777 success! Waiting for client data...
Received data{Hello UDP} 9 bytes from client! Peer info: IP=192.168.137.1, Port=xxxxx
Sent reply data{test_data} 9 bytes to client!

其中:

  • Start Scan ! / Scan done! 表示 WiFi 热点扫描完成;
  • Connect succ! / STA IP 192.168.137.x 表示 WiFi 关联成功并获取 IP;
  • Wi-Fi connected successfully. 表示应用层确认 WiFi 就绪;
  • Bind to port 777 success! 表示 UDP 端口绑定成功,进入等待状态;
  • Received data{...} from client! 表示收到客户端发送的 UDP 数据报;
  • Peer info: IP=..., Port=... 表示正确解析了客户端地址信息;
  • Sent reply data{test_data} 表示向客户端回复成功。

5.4、PC 端 UDP 客户端预期现象

若使用 Python 脚本作为客户端,PC 端可观察到:

Received from ('192.168.137.x', 777): test_data

表示开发板 UDP 服务端已成功接收数据并回复 test_data

5.5、结果分析

现象 说明
输出 Wi-Fi connected successfully. WiFi 连接与 DHCP 获取 IP 成功
输出 Bind to port 777 success! UDP 端口绑定成功
输出 Waiting for client data... UDP 服务已进入等待接收状态
输出 Received data{...} from client! 客户端数据报接收成功
输出 Peer info: IP=..., Port=... 正确解析了客户端地址信息
输出 Sent reply data{test_data} 回复数据发送成功
输出 Failed to connect to AP. WiFi 连接失败,检查 SSID/密码
输出 Bind to port 777 failed! 端口被占用或权限不足
输出 Recv data failed 接收异常,检查网络状态
输出 Reply to client failed! 回复发送失败,检查客户端地址或网络

5.6、常见问题排查

问题 可能原因 解决方法
反复 Can not find AP 热点未开启或 SSID/密码不匹配 确认热点已开启,宏定义与实际一致
WiFi 成功但客户端无响应 使用了错误的 IP 地址 以串口打印的 STA IP 为准,勿使用网关 IP
客户端发送后服务端无日志 服务端尚未完成 bind 或端口错误 等待 Bind to port 777 success! 后再发送
Bind to port 777 failed! 端口已被其他程序占用 更换 UDP_SERVER_PORT 或重启开发板
PC 端发送成功但收不到回复 防火墙拦截 UDP 回复 确认 PC 与开发板连接同一热点,关闭防火墙
编译报错找不到组件 BUILD.gn 或 config.py 未修改 逐步核对 4.2、4.3 节的配置项
与 c4 联调无响应 客户端 UDP_SERVER_IP 配置错误 以服务端串口打印的 STA IP 为准填入客户端

6、实验扩展

完成基本实验后,可尝试以下扩展练习:

  1. 修改回复内容:更改 send_data 字符串,观察客户端接收到的数据变化;
  2. 自定义响应逻辑:根据接收到的不同数据内容,回复不同的响应报文;
  3. 配合 UDP 客户端案例:使用 c4_wifi_udp_client 案例,实现两块开发板之间的 UDP 通信;
  4. 对比 TCP 与 UDP 服务端:分别运行 c3_wifi_tcp_server 与本案例,对比 bind/listen/accept 与 bind/recvfrom 的差异;
  5. 多客户端支持:利用 recvfrom() 每次获取不同客户端地址的特性,实现一对多通信;
  6. 设置接收超时:在 recvfrom() 前通过 setsockopt 设置 SO_RCVTIMEO,避免无限阻塞;
  7. UDP 广播服务:绑定广播地址或发送广播回复,探索 UDP 广播通信特性。
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐