Skip to content

第九章 网络编程

9.1 网络编程

9.1.1 LwIP

LwIP (A Lightweight TCP/IP stack)是一个轻量级的开源 TCP/IP协议栈。

LwIP 适用于资源有限的嵌入式系统中,对内存和计算的需求非常少,通常仅占用几十 KB 的内存空间和 40 KB 左右的代码存储空间。

LwIP 的主要功能如下:

  • 支持多种网络协议,包括 IPv4、IPv6、ICMP、ND、MLD、UDP、TCP 等。
  • 支持 DNCP 客户端、DNS 客户端、AutoIP 和 SNMP 代理。
  • 支持通过多个网络接口进行 IP 转发、TCP 拥塞控制。
  • 集成了 HTTP(S)服务器、SNTP 客户端、SMTP(S)客户端、ping、NetBIOS 名称服务器、MQTT服务端、TFTP 服务器等。

截至 OpenHarmony 3.1.4 Release 版本,在源码中有两个 LwIP 存在。

  • third_party\lwip:以源码的形式编译,供 LiteOS-A 内核使用;有一部分代码在kernel\liteos_a\net\lwip-2.1中一起参与编译。
  • device\hisilicon\hispark_pegasus\sdk_liteos\third_party\lwip_sack:这是海思 SDK 的一部分,以静态库的形式提供;因为它是预先编译好的,所以不能修改配置。

9.1.2 TCP/IP 模型

如下图所示,OSI 参考模型与 TCP/IP 模型的对照图,其中 TCP 和 UDP 通信均属于传输层。

TCP 和 UDP 均是在程序间传输数据,数据可以是文件、照片或音频等等。而 TCP 和 UDP 最大的区别在于:TCP 基于 连接 ,而 UDP 基于 非连接

关于连接与非连接,借助B站UP主的一个例子简单讲解:

  • 写信(UDP)
    • 对方是否收到,未知。
    • 对方收到的信是否完整,未知。
    • 先后发送的两封信是否按顺序收到,未知。
  • 电话(TCP)
    • 电话接通,进行下一步互相交流,然后结束通话。
    • 上诉流程均需要及时反馈。

image-20250608172126631

9.1.3 TCP 通信

TCP 确保通信顺利,需要进行关键的三个步骤:

  • ”三次握手“(建立通信)
  • 传输确认(解决丢包和乱序问题)
  • ”四次挥手“(终止连接)

🤝 三次握手,建立通信

  • 客户端发送 SYN 包到服务器,并进入 SYN_SEND 状态,等待服务器确认。
  • 服务器收到 SYN 包,必须确认客户的 SYN ,同时自己也发送一个 SYN 包,即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态。
  • 客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK,此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

image-20250608173538101


💻 传输确认,解决丢包和乱序问题

  • 每个 TCP 连接,都有一个发送缓冲区,字节序号从 0 开始,依次累加。
  • 进行数据发送时,从缓存区取出一部分数据组成发送报文,在其 TCP 协议头加上序列号和长度。
  • 接收方收到数据后,要回复确认,ACK 的内容如下图所示,同时 ACK 为下一包的起始序列号。
  • 当需要把数据拆分为多包时,接收方仅需要根据序列号重组即可,若某一包缺失可以要求发送方重新发送。

image-20250608175306415

👋 四次挥手,终止连接

  • Client 发送一个 FIN,用来关闭 ClientServer 的数据传送,Client 进入 FIN_WAIT_1 状态。
  • Server 收到 FIN 后,发送一个 ACKClient ,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server 进入 CLOSE_WAIT 状态。
  • Server 发送一个 FIN ,用来关闭 ServerClient 的数据传送,Server 进入 LAST_ACK 状态。
  • Client 收到 FIN 后,Client 进入 TIME_WAIT 状态,接着发送一个 ACKServer ,确认序号为收到序号+1,Server 进入 CLOSED 状态,完成四次挥手。

image-20250608180827966

9.1.4 UDP 通信

UDP 协议无需确认状态,仅有简单的 打包发送 。

相较于 TCP ,UDP 具备以下特点:

  • 性能损耗少
  • 资源占用少
  • 速度快,但稳定性较弱

9.1.5 netcat 工具

无论 TCP通信还是 UDP 通信都是在网络两端进行的,因此我们需要一个网络工具模拟 TCP 服务端。netcat是一个简单、强大且极其灵活的命令行工具,用于在网络上通过 TCP 或 UDP 协议 进行 读、写、重定向 原始网络数据。

在 Ubuntu 系统下安装网络工具 netcat

Bash
sudo apt install netcat

9.1.6 Socket 编程

TCP/IP 协议栈已经由 LwIP 实现,而 Socket 是在传输层和应用层之间的软件抽象层,为开发者提供一组简单的接口,所以我们只需要通过 Socket 就可以进行网络编程。

image-20250606132524118

9.2 TCP 编程

9.2.1 TCP 通信流程

image-20250608205916996

9.2.2 TCP 客户端

tcp_client.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
#include "lwip/sockets.h"
#include "lwip/def.h"
#include "tcp_client.h"


/* 缓冲区 */
static char request[] = "hello";
static char response[128] = "";


/**
 * @brief TCP Client测试函数
 * 
 * lwip的API分两种,一中是带"_lwip"后缀的,一种则是不带后缀的。
 * 个人建议使用带后缀的,代码比较有区分度
 * 
 */
void TcpClientDemo(const char *host, unsigned short port)
{
    // 返回值,发送/接收的字节数
    ssize_t retval = 0;

    // 创建一个 TCP Socket(网络套接字),返回文件描述符
    int sockfd = lwip_socket(AF_INET, SOCK_STREAM, 0);

    // 设置服务端的地址信息,包括协议、端口、IP地址等
    struct sockaddr_in serverAddr = {
        // 选择IPv4协议
        .sin_family = AF_INET,
        // 端口号,使用htons函数从主机字节序转为网络字节序
        // 大端序是网络标准字节序(建议转换保持兼容性)
        .sin_port = lwip_htons(port),
    };

    // 将服务端IP地址从“点分十进制”字符串,转化为标准格式(32位整数)
    if (lwip_inet_pton(AF_INET, host, &serverAddr.sin_addr) <= 0) {
        LOG_ERROR("lwip_inet_pton fail!");
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }

    // 尝试和目标主机建立连接,连接成功会返回0,失败返回-1
    if (lwip_connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
        LOG_ERROR("connect fail!");
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }
    // 建立连接成功之后,sockfd就具有了“连接状态”,
    // 后续的发送和接收,都是针对指定的目标主机和端口
    LOG_INFO("connect server %s success!", host);

    // 使用默认阻塞模式发送数据
    retval = lwip_send(sockfd, request, sizeof(request), 0);
    // 对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("send fail, %ld!", retval);
        goto do_cleanup;
    }
    LOG_INFO("send request {%s} to server %ld", request, retval);

    // 使用默认阻塞模式接收数据
    retval = lwip_recv(sockfd, response, sizeof(response), 0);
    // 对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("recv fail, %ld!", retval);
        goto do_cleanup;
    }
    // 在末尾添加字符串结束符'\0',以便后续的字符串操作
    response[retval] = '\0';
    LOG_INFO("send response {%s} to server %ld", response, retval);

    // 接收数据
do_cleanup:
    LOG_WARN("do_cleanup...");
    // 关闭Socket
    lwip_close(sockfd);
}

tcp_client.h

C
#ifndef TCP_CLIENT
#define TCP_CLIENT

/* 定义日志宏 */
#define LOG_INFO(format, ...) printf("[INFO] [%s] "format"\n", __FUNCTION__, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) printf("[ERROR] [%s:%d] "format"\n",\ 
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)
#define LOG_WARN(format, ...) printf("[WARN] [%s:%d] "format"\n",\ 
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)

void TcpClientDemo(const char *host, unsigned short port);

#endif

demo.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
#include "wifi_connecter.h"
#include "tcp_client.h"

/* WiFi信息 */
// 账号
#define  HOTSPOT_SSID           "OpenHarmony"
// 密码 
#define  HOTSPOT_PASSWD         "123456789"
// 加密方式 
#define  HOTSPOT_TYPE           WIFI_SEC_TYPE_PSK
// TCP服务器IP地址,使用虚拟机IP地址即可
#define PARAM_SERVER_ADDR "192.168.10.117"
// 用于标识TCP服务器端口
#define PARAM_SERVER_PORT 5678


static void TcpClientTask(void *arg)
{
    (void)arg;
    /* 初始化WIFI参数 */
    WifiDeviceConfig apConfig = {
        // 热点名称
        .ssid = HOTSPOT_SSID,
        // 热点密码
        .preSharedKey = HOTSPOT_PASSWD,
        // 加密方式(PSK)
        .securityType = HOTSPOT_TYPE,
    };

    /* 连接WIFI */
    int netId = ConnectToHotspot(&apConfig);
    if (netId < 0) {
        LOG_ERROR("Connect to AP failed!");
    }

    TcpClientDemo(PARAM_SERVER_ADDR, PARAM_SERVER_PORT);

    LOG_INFO("disconnect ap ...");
    DisconnectWithHotspot(netId);
    LOG_INFO("disconnect ap done");

}

static void TcpClientEntry(void)
{
    osThreadAttr_t attr = {
        .name = "TcpClientTask",
        .stack_size = 10240,
        .priority = osPriorityNormal,
    };
    osThreadId_t thread_id = osThreadNew(TcpClientTask, NULL, &attr);
    if (thread_id == NULL) {
        printf("[Thread Create] osThreadNew(%s) failed.\r\n", "TcpClientTask");
    } else{
        printf("[Thread Create] osThreadNew(%s) success, thread id: %d.\r\n", "TcpClientTask", thread_id);
    }
}

SYS_RUN(TcpClientEntry);

模块编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
static_library("tcp_client") {
    sources = [
        "demo.c",
        "tcp_client.c"
    ]
    include_dirs = [
        # include "ohos_init.h"
        "//commonlibrary/utils_lite/include",
        # include CMSIS-RTOS API V2 for OpenHarmony5.0+
        "//kernel/liteos_m/kal/cmsis",
        # include IoT硬件设备操作接口 for OpenHarmony5.0+:
        "//base/iothardware/peripheral/interfaces/inner_api",
        # include HAL接口中的WIFI接口
        "//foundation/communication/wifi_lite/interfaces/wifiservice",
        # include EasyWiFi模块接口
        "//applications/sample/wifi-iot/3861/shared_drivers/easy_wifi/src",
    ]
}

APP 编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
  features = [
    "shared_drivers/easy_wifi/src:easy_wifi",
    "tcp_client",
  ]
}

烧录运行:

  1. 虚拟机启动服务端,nc -l -p 5678
  2. 重启开发板,启动客户端。
  3. 查看串口输出。
  4. 直接在netcat窗口输入文字,回车发送至开发板的客户端。

9.2.3 TCP 服务端

tcp_server.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
// socket函数
#include "lwip/sockets.h"
// 字节序转换函数
#include "lwip/def.h"
#include "tcp_server.h"

#define BACKLOG 1

static char response[128] = "";


void TcpServerDemo(unsigned short port)
{
    // 返回值,发送/接收的字节数
    ssize_t retval = 0;

    // 创建一个 TCP Socket(网络套接字),返回文件描述符
    // 用于监听客户端的连接请求
    int sockfd = lwip_socket(AF_INET, SOCK_STREAM, 0);

    // 记录客户端的IP地址和端口号
    struct sockaddr_in clientAddr = {0};

    // 配置服务端的地址信息
    struct sockaddr_in servertAddr = {
        // 选择IPv4协议族
        .sin_family = AF_INET,
        // 端口号,使用htons函数从主机字节序转为网络字节序
        // 大端序是网络标准字节序(建议转换保持兼容性)
        .sin_port = lwip_htons(port),
        // 允许任意主机接入,0.0.0.0
        // 使用htonl函数将32位整数从主机字节序转换为网络字节序
        .sin_addr.s_addr = lwip_htonl(INADDR_ANY), // 宏定义在inet.h
    };

    // 将sockfd与服务器IP、端口号绑定
    retval = lwip_bind(sockfd, (struct sockaddr *)&servertAddr, sizeof(servertAddr));
    if (retval < 0) {
        LOG_ERROR("bind fail, %ld!", retval);
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }
    LOG_INFO("bind server port %d success!", port);

    // 开始监听,最大等待队列长度为BACKLOG
    retval = lwip_listen(sockfd, BACKLOG);
    if (retval < 0) {
        LOG_ERROR("listen fail, %ld!", retval);
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }
    LOG_INFO("listen server port %d success!", port);

    // 阻塞式的等待客户端连接
    socklen_t clientAddrLen = sizeof(clientAddr);
    int connfd = lwip_accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrLen);
    if (connfd < 0) {
        LOG_ERROR("accept fail, %d!", connfd);
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }
    // inet_ntoa函数在inet.h,lwip_ntohs在def.h
    // 将客户端的IP地址和端口号转换为字符串格式
    LOG_INFO("accept client %s:%d success!", inet_ntoa(clientAddr.sin_addr), lwip_ntohs(clientAddr.sin_port));

    // 使用默认阻塞模式接收数据
    retval = lwip_recv(connfd, response, sizeof(response), 0);
    // 对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("recv fail, %ld!", retval);
        goto do_disconnect;
    }
    LOG_INFO("recv response {%s} from client", response);


    // 使用默认阻塞模式发送数据
    retval = lwip_send(connfd, response, sizeof(response), 0);
    // 对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("send fail, %ld!", retval);
        goto do_disconnect;
    }
    LOG_INFO("send request {%s} to server %ld", response, retval);

do_disconnect:
    LOG_WARN("do_cleanup...");
    // 关闭Socket
    lwip_close(connfd);

do_cleanup:
    LOG_WARN("do_cleanup...");
    // 关闭Socket
    lwip_close(sockfd);
}

tcp_server.h

C
#ifndef TCP_SERVER
#define TCP_SERVER

/* 定义日志宏 */
#define LOG_INFO(format, ...) printf("[INFO] [%s] "format"\n",\
                                        __FUNCTION__,\
                                        ##__VA_ARGS__)
#define LOG_ERROR(format, ...) printf("[ERROR] [%s:%d] "format"\n",\
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)
#define LOG_WARN(format, ...) printf("[WARN] [%s:%d] "format"\n",\
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)

void TcpServerDemo(unsigned short port);

#endif

demo.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
#include "wifi_connecter.h"
#include "tcp_server.h"

/* WiFi信息 */
// 账号
#define  HOTSPOT_SSID           "CMCC-XPeA"
// 密码 
#define  HOTSPOT_PASSWD         "cyjj7346"
// 加密方式 
#define  HOTSPOT_TYPE           WIFI_SEC_TYPE_PSK
// 用于标识TCP服务器端口
#define PARAM_SERVER_PORT 5678


static void TcpServerTask(void *arg)
{
    (void)arg;
    /* 初始化WIFI参数 */
    WifiDeviceConfig apConfig = {
        // 热点名称
        .ssid = HOTSPOT_SSID,
        // 热点密码
        .preSharedKey = HOTSPOT_PASSWD,
        // 加密方式(PSK)
        .securityType = HOTSPOT_TYPE,
    };

    /* 连接WIFI */
    int netId = ConnectToHotspot(&apConfig);
    if (netId < 0) {
        LOG_ERROR("Connect to AP failed!");
    }

    TcpServerDemo(PARAM_SERVER_PORT);

    LOG_INFO("disconnect ap ...");
    DisconnectWithHotspot(netId);
    LOG_INFO("disconnect ap done");

}

static void TcpServerEntry(void)
{
    osThreadAttr_t attr = {
        .name = "TcpServerTask",
        .stack_size = 10240,
        .priority = osPriorityNormal,
    };
    osThreadId_t thread_id = osThreadNew(TcpServerTask, NULL, &attr);
    if (thread_id == NULL) {
        printf("[Thread Create] osThreadNew(%s) failed.\r\n", "TcpServerTask");
    } else{
        printf("[Thread Create] osThreadNew(%s) success, thread id: %d.\r\n", "TcpServerTask", thread_id);
    }
}

SYS_RUN(TcpServerEntry);

模块编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
static_library("tcp_server") {
    sources = [
        "demo.c",
        "tcp_server.c",
    ]
    include_dirs = [
        # include "ohos_init.h"
        "//commonlibrary/utils_lite/include",
        # include CMSIS-RTOS API V2 for OpenHarmony5.0+
        "//kernel/liteos_m/kal/cmsis",
        # include IoT硬件设备操作接口 for OpenHarmony5.0+:
        "//base/iothardware/peripheral/interfaces/inner_api",
        # include HAL接口中的WIFI接口
        "//foundation/communication/wifi_lite/interfaces/wifiservice",
        # include EasyWiFi模块接口
        "//applications/sample/wifi-iot/3861/shared_drivers/easy_wifi/src",
    ]
}

APP 编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
  features = [
    "shared_drivers/easy_wifi/src:easy_wifi",
    "tcp_server",
  ]
}

烧录运行:

  1. 重启开发板。
  2. 虚拟机终端启动客户端,nc <开发板IP> 5678,注意虚拟机和开发板需连接同一个网络。
  3. 查看串口输出。
  4. 直接在netcat窗口输入文字,回车发送至开发板的服务端,同时会收到服务端返回的消息。

9.3 UDP 编程

9.3.1 UDP 通信流程

由于 UDP 基于非连接,可以看出 UDP 的收发函数均带有一定的指向性。

image-20250608212118775

9.3.2 UDP 客户端

udp_client.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
#include "lwip/sockets.h"
#include "lwip/def.h"
#include "udp_client.h"


/* 缓冲区 */
static char request[] = "hello";
static char response[128] = "";


/**
 * @brief Udp Client测试函数
 * 
 * lwip的API分两种,一中是带"_lwip"后缀的,一种则是不带后缀的。
 * 个人建议使用带后缀的,代码比较有区分度
 * 
 */
void UdpClientDemo(const char *host, unsigned short port)
{
    // 返回值,发送/接收的字节数
    ssize_t retval = 0;

    // ⭐创建一个 TCP Socket(网络套接字),返回文件描述符
    int sockfd = lwip_socket(AF_INET, SOCK_DGRAM, 0); 

    // 设置服务端的地址信息,包括协议、端口、IP地址等
    struct sockaddr_in serverAddr = {
        // 选择IPv4协议
        .sin_family = AF_INET,
        // 端口号,使用htons函数从主机字节序转为网络字节序
        // 大端序是网络标准字节序(建议转换保持兼容性)
        .sin_port = lwip_htons(port),
    };

    // 将服务端IP地址从“点分十进制”字符串,转化为标准格式(32位整数)
    if (lwip_inet_pton(AF_INET, host, &serverAddr.sin_addr) <= 0) {
        LOG_ERROR("lwip_inet_pton fail!");
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }

    // ⭐尝试和目标主机建立连接,连接成功会返回0,失败返回-1
    // if (lwip_connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
    //     LOG_ERROR("connect fail!");
    //     // 跳转到cleanup部分,主要是关闭连接
    //     goto do_cleanup;
    // }
    // 建立连接成功之后,sockfd就具有了“连接状态”,
    // 后续的发送和接收,都是针对指定的目标主机和端口
    // LOG_INFO("connect server %s success!", host);

    // ⭐使用默认阻塞模式发送数据
    retval = lwip_sendto(sockfd, request, sizeof(request), 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    // 对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("send fail, %ld!", retval);
        goto do_cleanup;
    }
    LOG_INFO("sendto request {%s} to server %ld", request, retval);

    // ⭐记录发送方的地址信息
    struct sockaddr_in fromAddr = {0};
    socklen_t fromLen = sizeof(fromAddr);
    // ⭐使用默认阻塞模式接收数据
    retval = lwip_recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&fromAddr, &fromLen);
    // 对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("recv fail, %ld!", retval);
        goto do_cleanup;
    }
    // 在末尾添加字符串结束符'\0',以便后续的字符串操作
    response[retval] = '\0';
    LOG_INFO("recvfrom response {%s} to server %ld", response, retval);

    // ⭐显示发送方的地址信息
    LOG_INFO("peer info %s:%d success!", inet_ntoa(fromAddr.sin_addr), lwip_ntohs(fromAddr.sin_port));

    // 接收数据
do_cleanup:
    LOG_WARN("do_cleanup...");
    // 关闭Socket
    lwip_close(sockfd);
}

udp_client.h

C
#ifndef TCP_CLIENT
#define TCP_CLIENT

/* 定义日志宏 */
#define LOG_INFO(format, ...) printf("[INFO] [%s] "format"\n", __FUNCTION__, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) printf("[ERROR] [%s:%d] "format"\n",\ 
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)
#define LOG_WARN(format, ...) printf("[WARN] [%s:%d] "format"\n",\ 
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)

void UdpClientDemo(const char *host, unsigned short port);

#endif

demo.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
#include "wifi_connecter.h"
#include "udp_client.h"

/* WiFi信息 */
// 账号
#define  HOTSPOT_SSID           "OpenHarmony"
// 密码 
#define  HOTSPOT_PASSWD         "123456789"
// 加密方式 
#define  HOTSPOT_TYPE           WIFI_SEC_TYPE_PSK
// TCP服务器IP地址
#define PARAM_SERVER_ADDR "192.168.10.117"
// 用于标识TCP服务器端口
#define PARAM_SERVER_PORT 5678


static void UdpClientTask(void *arg)
{
    (void)arg;
    /* 初始化WIFI参数 */
    WifiDeviceConfig apConfig = {
        // 热点名称
        .ssid = HOTSPOT_SSID,
        // 热点密码
        .preSharedKey = HOTSPOT_PASSWD,
        // 加密方式(PSK)
        .securityType = HOTSPOT_TYPE,
    };

    /* 连接WIFI */
    int netId = ConnectToHotspot(&apConfig);
    if (netId < 0) {
        LOG_ERROR("Connect to AP failed!");
    }

    UdpClientDemo(PARAM_SERVER_ADDR, PARAM_SERVER_PORT);

    LOG_INFO("disconnect ap ...");
    DisconnectWithHotspot(netId);
    LOG_INFO("disconnect ap done");

}

static void UdpClientEntry(void)
{
    osThreadAttr_t attr = {
        .name = "UdpClientTask",
        .stack_size = 10240,
        .priority = osPriorityNormal,
    };
    osThreadId_t thread_id = osThreadNew(UdpClientTask, NULL, &attr);
    if (thread_id == NULL) {
        printf("[Thread Create] osThreadNew(%s) failed.\r\n", "UdpClientTask");
    } else{
        printf("[Thread Create] osThreadNew(%s) success, thread id: %d.\r\n", "UdpClientTask", thread_id);
    }
}

SYS_RUN(UdpClientEntry);

模块编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
static_library("udp_client") {
    sources = [
        "demo.c",
        "udp_client.c"
    ]
    include_dirs = [
        # include "ohos_init.h"
        "//commonlibrary/utils_lite/include",
        # include CMSIS-RTOS API V2 for OpenHarmony5.0+
        "//kernel/liteos_m/kal/cmsis",
        # include IoT硬件设备操作接口 for OpenHarmony5.0+:
        "//base/iothardware/peripheral/interfaces/inner_api",
        # include HAL接口中的WIFI接口
        "//foundation/communication/wifi_lite/interfaces/wifiservice",
        # include EasyWiFi模块接口
        "//applications/sample/wifi-iot/3861/shared_drivers/easy_wifi/src",
    ]
}

APP 编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
  features = [
    "shared_drivers/easy_wifi/src:easy_wifi",
    "udp_client",
  ]
}

烧录运行:

  1. 虚拟机启动服务端,nc -u -l -p 5678
  2. 重启开发板,启动客户端。
  3. 查看串口输出。
  4. 直接在netcat窗口输入文字,回车发送至开发板的客户端。

9.3.3 UDP 服务端

udp_server.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
// socket函数
#include "lwip/sockets.h"
// 字节序转换函数
#include "lwip/def.h"
#include "udp_server.h"

#define BACKLOG 1

static char response[128] = "";


void UdpServerDemo(unsigned short port)
{
    // 返回值,发送/接收的字节数
    ssize_t retval = 0;

    // ⭐创建一个 TCP Socket(网络套接字),返回文件描述符
    // 用于监听客户端的连接请求
    int sockfd = lwip_socket(AF_INET, SOCK_DGRAM, 0);

    // 记录客户端的IP地址和端口号
    struct sockaddr_in clientAddr = {0};

    // 配置服务端的地址信息
    struct sockaddr_in servertAddr = {
        // 选择IPv4协议族
        .sin_family = AF_INET,
        // 端口号,使用htons函数从主机字节序转为网络字节序
        // 大端序是网络标准字节序(建议转换保持兼容性)
        .sin_port = lwip_htons(port),
        // 允许任意主机接入,0.0.0.0
        // 使用htonl函数将32位整数从主机字节序转换为网络字节序
        .sin_addr.s_addr = lwip_htonl(INADDR_ANY), // 宏定义在inet.h
    };

    // 将sockfd与服务器IP、端口号绑定
    retval = lwip_bind(sockfd, (struct sockaddr *)&servertAddr, sizeof(servertAddr));
    if (retval < 0) {
        LOG_ERROR("bind fail, %ld!", retval);
        // 跳转到cleanup部分,主要是关闭连接
        goto do_cleanup;
    }
    LOG_INFO("bind server port %d success!", port);

    // ⭐开始监听,最大等待队列长度为BACKLOG
    // retval = lwip_listen(sockfd, BACKLOG);
    // if (retval < 0) {
    //     LOG_ERROR("listen fail, %ld!", retval);
    //     // 跳转到cleanup部分,主要是关闭连接
    //     goto do_cleanup;
    // }
    // LOG_INFO("listen server port %d success!", port);

    // ⭐阻塞式的等待客户端连接
    socklen_t clientAddrLen = sizeof(clientAddr);
    // int connfd = lwip_accept(sockfd, (struct sockaddr *)&clientAddr, &clientAddrLen);
    // if (connfd < 0) {
    //     LOG_ERROR("accept fail, %d!", connfd);
    //     // 跳转到cleanup部分,主要是关闭连接
    //     goto do_cleanup;
    // }
    // inet_ntoa函数在inet.h,lwip_ntohs在def.h
    // 将客户端的IP地址和端口号转换为字符串格式
    // LOG_INFO("accept client %s:%d success!", inet_ntoa(clientAddr.sin_addr), lwip_ntohs(clientAddr.sin_port));

    // ⭐使用默认阻塞模式接收数据
    retval = lwip_recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);
    // ⭐对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("recvfrom fail, %ld!", retval);
        goto do_cleanup;
    }
    LOG_INFO("recvfrom response {%s} from client", response);
    LOG_INFO("peer info %s:%d success!", inet_ntoa(clientAddr.sin_addr), lwip_ntohs(clientAddr.sin_port));


    // ⭐使用默认阻塞模式发送数据
    retval = lwip_sendto(sockfd, response, sizeof(response), 0, (struct sockaddr *)&clientAddr, sizeof(clientAddr));
    // ⭐对方的通信端关闭时,返回值为0;返回值小于0表示接收失败
    if (retval <= 0 ) {
        LOG_ERROR("sendto fail, %ld!", retval);
        goto do_cleanup;
    }
    LOG_INFO("sendto request {%s} to server %ld", response, retval);


do_cleanup:
    LOG_WARN("do_cleanup...");
    // 关闭Socket
    lwip_close(sockfd);
}

udp_server.h

C
#ifndef TCP_SERVER
#define TCP_SERVER

/* 定义日志宏 */
#define LOG_INFO(format, ...) printf("[INFO] [%s] "format"\n",\
                                        __FUNCTION__,\
                                        ##__VA_ARGS__)
#define LOG_ERROR(format, ...) printf("[ERROR] [%s:%d] "format"\n",\
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)
#define LOG_WARN(format, ...) printf("[WARN] [%s:%d] "format"\n",\
                                        __FUNCTION__,\
                                        __LINE__,\
                                        ##__VA_ARGS__)

void UdpServerDemo(unsigned short port);

#endif

demo.c

C
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
#include "cmsis_os2.h"
#include "wifi_connecter.h"
#include "udp_server.h"

/* WiFi信息 */
// 账号
#define  HOTSPOT_SSID           "CMCC-XPeA"
// 密码 
#define  HOTSPOT_PASSWD         "cyjj7346"
// 加密方式 
#define  HOTSPOT_TYPE           WIFI_SEC_TYPE_PSK
// 用于标识TCP服务器端口
#define PARAM_SERVER_PORT 5678


static void UdpServerTask(void *arg)
{
    (void)arg;
    /* 初始化WIFI参数 */
    WifiDeviceConfig apConfig = {
        // 热点名称
        .ssid = HOTSPOT_SSID,
        // 热点密码
        .preSharedKey = HOTSPOT_PASSWD,
        // 加密方式(PSK)
        .securityType = HOTSPOT_TYPE,
    };

    /* 连接WIFI */
    int netId = ConnectToHotspot(&apConfig);
    if (netId < 0) {
        LOG_ERROR("Connect to AP failed!");
    }

    UdpServerDemo(PARAM_SERVER_PORT);

    LOG_INFO("disconnect ap ...");
    DisconnectWithHotspot(netId);
    LOG_INFO("disconnect ap done");

}

static void UdpServerEntry(void)
{
    osThreadAttr_t attr = {
        .name = "UdpServerTask",
        .stack_size = 10240,
        .priority = osPriorityNormal,
    };
    osThreadId_t thread_id = osThreadNew(UdpServerTask, NULL, &attr);
    if (thread_id == NULL) {
        printf("[Thread Create] osThreadNew(%s) failed.\r\n", "UdpServerTask");
    } else{
        printf("[Thread Create] osThreadNew(%s) success, thread id: %d.\r\n", "UdpServerTask", thread_id);
    }
}

SYS_RUN(UdpServerEntry);

模块编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
static_library("udp_server") {
    sources = [
        "demo.c",
        "udp_server.c",
    ]
    include_dirs = [
        # include "ohos_init.h"
        "//commonlibrary/utils_lite/include",
        # include CMSIS-RTOS API V2 for OpenHarmony5.0+
        "//kernel/liteos_m/kal/cmsis",
        # include IoT硬件设备操作接口 for OpenHarmony5.0+:
        "//base/iothardware/peripheral/interfaces/inner_api",
        # include HAL接口中的WIFI接口
        "//foundation/communication/wifi_lite/interfaces/wifiservice",
        # include EasyWiFi模块接口
        "//applications/sample/wifi-iot/3861/shared_drivers/easy_wifi/src",
    ]
}

APP 编译脚本:

由于本章节我是隔了挺久重新学习的,现在使用的源码为 5.1.0 release 版本,一些目录与 3.0.7 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。

Text Only
import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
  features = [
    "shared_drivers/easy_wifi/src:easy_wifi",
    "udp_server",
  ]
}

烧录运行:

  1. 重启开发板。
  2. 虚拟机终端启动客户端,nc -u <开发板IP> 5678,注意虚拟机和开发板需连接同一个网络。
  3. 查看串口输出。
  4. 直接在netcat窗口输入文字,回车发送至开发板的服务端,同时会收到服务端返回的消息。

附录

重点函数解析:

C
int lwip_socket(int domain, int type, int protocol);
  • domain:指定协议族
    • AF_INET:IPv4 协议族 (最常用)。
    • AF_INET6:IPv6 协议族 (如果 lwIP 启用了 IPv6 支持)。
  • type:指定 Socket 类型
    • SOCK_STREAM:提供 面向连接的、可靠的、基于字节流的 双向通信信道 (对应 TCP )。
    • SOCK_DGRAM:提供 无连接的、不可靠的、固定最大长度报文传输 的信道 (对应 **UDP **)。
    • SOCK_RAW:提供对 底层协议(如 IP 或 ICMP)的直接访问
  • protocol:指定 Socket 使用的协议
    • 通常在 domaintype 已经指定出唯一协议(如 AF_INET + SOCK_STREAM 隐含 TCP)的情况下,此参数为 0
    • 极少在应用层使用 lwip_socket 时显式指定。

参考资料

一条视频讲清楚TCP协议与UDP协议-什么是三次握手与四次挥手_哔哩哔哩_bilibili

网络通信总结(TCP/IP协议、HTTP协议等) - 知乎

网络通信基础(入门知识总结)_通信基础知识-CSDN博客