第九章 网络编程
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)
- 电话接通,进行下一步互相交流,然后结束通话。
- 上诉流程均需要及时反馈。
9.1.3 TCP 通信¶
TCP 确保通信顺利,需要进行关键的三个步骤:
- ”三次握手“(建立通信)
- 传输确认(解决丢包和乱序问题)
- ”四次挥手“(终止连接)
🤝 三次握手,建立通信
- 客户端发送
SYN
包到服务器,并进入SYN_SEND
状态,等待服务器确认。 - 服务器收到
SYN
包,必须确认客户的SYN
,同时自己也发送一个SYN
包,即SYN+ACK
包,此时服务器进入SYN_RECV
状态。 - 客户端收到服务器的
SYN+ACK
包,向服务器发送确认包ACK
,此包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手。
💻 传输确认,解决丢包和乱序问题
- 每个 TCP 连接,都有一个发送缓冲区,字节序号从 0 开始,依次累加。
- 进行数据发送时,从缓存区取出一部分数据组成发送报文,在其 TCP 协议头加上序列号和长度。
- 接收方收到数据后,要回复确认,ACK 的内容如下图所示,同时 ACK 为下一包的起始序列号。
- 当需要把数据拆分为多包时,接收方仅需要根据序列号重组即可,若某一包缺失可以要求发送方重新发送。
👋 四次挥手,终止连接
Client
发送一个FIN
,用来关闭Client
到Server
的数据传送,Client
进入FIN_WAIT_1
状态。Server
收到FIN
后,发送一个ACK
给Client
,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server
进入CLOSE_WAIT
状态。Server
发送一个FIN
,用来关闭Server
到Client
的数据传送,Server
进入LAST_ACK
状态。Client
收到FIN
后,Client
进入TIME_WAIT
状态,接着发送一个ACK
给Server
,确认序号为收到序号+1,Server
进入CLOSED
状态,完成四次挥手。
9.1.4 UDP 通信¶
UDP 协议无需确认状态,仅有简单的 打包发送 。
相较于 TCP ,UDP 具备以下特点:
- 性能损耗少
- 资源占用少
- 速度快,但稳定性较弱
9.1.5 netcat 工具¶
无论 TCP通信还是 UDP 通信都是在网络两端进行的,因此我们需要一个网络工具模拟 TCP 服务端。netcat
是一个简单、强大且极其灵活的命令行工具,用于在网络上通过 TCP 或 UDP 协议 进行 读、写、重定向 原始网络数据。
在 Ubuntu 系统下安装网络工具 netcat
:
9.1.6 Socket 编程¶
TCP/IP 协议栈已经由 LwIP 实现,而 Socket 是在传输层和应用层之间的软件抽象层,为开发者提供一组简单的接口,所以我们只需要通过 Socket 就可以进行网络编程。
9.2 TCP 编程¶
9.2.1 TCP 通信流程¶
9.2.2 TCP 客户端¶
tcp_client.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
#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
#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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"shared_drivers/easy_wifi/src:easy_wifi",
"tcp_client",
]
}
烧录运行:
- 虚拟机启动服务端,
nc -l -p 5678
。 - 重启开发板,启动客户端。
- 查看串口输出。
- 直接在netcat窗口输入文字,回车发送至开发板的客户端。
9.2.3 TCP 服务端¶
tcp_server.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
#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
#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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"shared_drivers/easy_wifi/src:easy_wifi",
"tcp_server",
]
}
烧录运行:
- 重启开发板。
- 虚拟机终端启动客户端,
nc <开发板IP> 5678
,注意虚拟机和开发板需连接同一个网络。 - 查看串口输出。
- 直接在netcat窗口输入文字,回车发送至开发板的服务端,同时会收到服务端返回的消息。
9.3 UDP 编程¶
9.3.1 UDP 通信流程¶
由于 UDP 基于非连接,可以看出 UDP 的收发函数均带有一定的指向性。
9.3.2 UDP 客户端¶
udp_client.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
#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
#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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"shared_drivers/easy_wifi/src:easy_wifi",
"udp_client",
]
}
烧录运行:
- 虚拟机启动服务端,
nc -u -l -p 5678
。 - 重启开发板,启动客户端。
- 查看串口输出。
- 直接在netcat窗口输入文字,回车发送至开发板的客户端。
9.3.3 UDP 服务端¶
udp_server.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
#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
#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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
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 有出入。建议读者自行修改这部分,一方面检验一下自己所学,另一方面我逃个懒。
import("//build/lite/config/component/lite_component.gni")
lite_component("app") {
features = [
"shared_drivers/easy_wifi/src:easy_wifi",
"udp_server",
]
}
烧录运行:
- 重启开发板。
- 虚拟机终端启动客户端,
nc -u <开发板IP> 5678
,注意虚拟机和开发板需连接同一个网络。 - 查看串口输出。
- 直接在netcat窗口输入文字,回车发送至开发板的服务端,同时会收到服务端返回的消息。
附录¶
重点函数解析:
domain
:指定协议族AF_INET
:IPv4 协议族 (最常用)。AF_INET6
:IPv6 协议族 (如果 lwIP 启用了 IPv6 支持)。
type
:指定 Socket 类型SOCK_STREAM
:提供 面向连接的、可靠的、基于字节流的 双向通信信道 (对应 TCP )。SOCK_DGRAM
:提供 无连接的、不可靠的、固定最大长度报文传输 的信道 (对应 **UDP **)。SOCK_RAW
:提供对 底层协议(如 IP 或 ICMP)的直接访问 。
protocol
:指定 Socket 使用的协议- 通常在
domain
和type
已经指定出唯一协议(如AF_INET
+SOCK_STREAM
隐含 TCP)的情况下,此参数为0
。 - 极少在应用层使用
lwip_socket
时显式指定。
- 通常在