梅科尔工作室-#14天鸿蒙设备开发实战#网络应用开发笔记 原创 精华
@toc
HarmonyOS网络应用开发
UDP客户端
参考:
- HarmonyOS网络应用开发—UDP客户端_哔哩哔哩_bilibili
- UDP协议详解 - 知乎 (zhihu.com)
- UDP_百度百科 (baidu.com)
- UDP协议详解 - 简书 (jianshu.com)
UDP协议相关API介绍
socket.h接口简介
这个socket.h中包含声明UDP协议相关接口函数(与UDP客户端相关api如下)。
接口名 | 功能描述 |
---|---|
socket | 创建套接字 |
sendto | 通过创建的套接字将数据由指定的socket发送到远端主机 |
recvfrom | 从远端主机接收UDP数据 |
close | 关闭套接字 |
更多api可参见以下TCP服务端等几个部分,源码在vendor\hisi\hi3861\hi3861\platform\os\Huawei_LiteOS\components\lib\libc\musl\include\sys\socket.h
目录下
socket()
int socket(int domain, int type, int protocol)
参数:
名字 | 描述 |
---|---|
domain | 指定协议族,也就是IP地址类型,常用的有AF_INET和AF_INET6,AF_INET表示IPv4地址,AF_INET6表示IPv6地址 |
type | 指定套接字类型(可以是SOCK_RAW、SOCK_DGRAM、SOCK_STREAM,SOCK_DGRAM值UDP协议,SOCK_STREAM指TCP协议) |
protocol | 指定要与套接字一起使用的协议,常用的有IPPROTO_TCP和IPPTOTO_UDP,也可以是0,系统会自动推演出应该使用什么协议,根据第二个参数确定 |
描述:
在网络编程中所需要进行的第一件事情就是创建一个socket,无论是客户端还是服务器端,都需要创建一个socket,该函数返回socket文件描述符,类似于文件描述符。socket是一个结构体,被创建在内核中。
sendto()
int sendto (socket s , const void * msg, int len, unsigned int flags,
const struct sockaddr * to , int tolen) ;
参数:
名字 | 描述 |
---|---|
s | 指定套接字文件描述符 |
msg | 指定包含要发送的消息的缓冲区 |
len | 指定要发送的消息的长度 |
flags | 指示消息传输的标志 |
to | 指定指向包含目标地址的sockaddr结构的指针 |
tolen | 指定to 结构的大小 |
描述:
sendto() 用来将数据由指定的socket传给对方主机。参数s为已建好连线的socket。参数msg指向欲连线的数据内容,参数flags 一般设0。
recvfrom()
int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
描述:
从指定地址接收UDP数据,此函数为阻塞接收,即如果没有接收到数据会一直阻塞程序运行。
参数:
名字 | 描述 |
---|---|
s | socket描述符 |
buf | UDP数据报缓存地址 |
len | UDP数据报长度 |
flags | 该参数一般为0 |
from | 对方地址 |
fromlen | 对方地址长度 |
UDP客户端创建流程介绍
实现UDP客户端
源码在applications\BearPi\BearPi-HM_Nano\sample\D3_iot_udp_client\udp_client_demo.c
路径下,以下添加了部分注释
/*烧录完成后,按一下开发板的复位按键,此时可以看到日志中打印了一些信息,也已经可以在调试工具数据接收及提示窗口中看到开发板发送的数据;在调试工具数据发送窗口中发送数据后,可以在MobaXterm的串口工具中看到UDP服务端发送的数据*/
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "wifi_device.h"
#include "lwip/netifapi.h"
#include "lwip/api_shell.h"
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include "lwip/sockets.h"
#include "wifi_connect.h"
#define _PROT_ 8888
//在sock_fd 进行监听,在 new_fd 接收新的链接
int sock_fd;
int addr_length;
static const char *send_data = "Hello! I'm BearPi-HM_Nano UDP Client!\r\n";
static void UDPClientTask(void)
{
//服务器的地址信息
struct sockaddr_in send_addr;
socklen_t addr_length = sizeof(send_addr);
char recvBuf[512];
//连接Wifi
WifiConnect("TP-LINK_65A8", "xxxxxx"); // 参数分别是热点名称和热点password,连接的WiFi和自己电脑所连的网络要在同一个局域网中,才能实现数据的收发
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) // AF_INT:ipv4, SOCK_DGRAM:udp协议,0:协议,写为0时表示由第二个参数确定,返回一个文件描述符
{
perror("create socket failed!\r\n");
exit(1);
}
//初始化预连接的服务端地址
send_addr.sin_family = AF_INET; // 协议族
send_addr.sin_port = htons(_PROT_); // 端口号
send_addr.sin_addr.s_addr = inet_addr("192.168.1.6"); // IPv4地址,应填自己电脑的IPv4地址,可以使用ipconfig查找
addr_length = sizeof(send_addr);
//总计发送 count 次数据
while (1)
{
bzero(recvBuf, sizeof(recvBuf)); // 将recvBuf清0
//发送数据到服务远端
sendto(sock_fd, send_data, strlen(send_data), 0, (struct sockaddr *)&send_addr, addr_length);
//线程休眠一段时间
sleep(10); // 10s
//接收服务端返回的字符串
recvfrom(sock_fd, recvBuf, sizeof(recvBuf), 0, (struct sockaddr *)&send_addr, &addr_length); // 阻塞接收,即如果没有从服务端接收到数据,会一直阻塞在这里
printf("%s:%d=>%s\n", inet_ntoa(send_addr.sin_addr), ntohs(send_addr.sin_port), recvBuf);
}
//关闭这个 socket
closesocket(sock_fd);
}
static void UDPClientDemo(void)
{
osThreadAttr_t attr;
attr.name = "UDPClientTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
if (osThreadNew((osThreadFunc_t)UDPClientTask, NULL, &attr) == NULL)
{
printf("[UDPClientDemo] Falied to create UDPClientTask!\n");
}
}
APP_FEATURE_INIT(UDPClientDemo);
其中WifiConnect()
驱动会放于附件中,其中的代码逻辑可参考上篇博客梅科尔工作室-#14天鸿蒙设备开发实战#无线联网开发笔记-开源基础软件社区-51CTO.COMWiFi STA联网部分
其中的_PROT_
指定了端口号为8888,后续创建UDP服务端的时候需要用到,可以自行修改,在0-65535之间即可
获取电脑的IPv4地址的方式是,打开命令行工具,输入ipconfig
,读取ip,无线局域网适配器 WLAN下的IPv4 地址
修改applications\BearPi\BearPi-HM_Nano\sample
路径下的BUILD.gn文件,指定udp_client
,编译、烧录
测试UDP客户端
使用Sockettool创建UDP服务端用于测试,工具会放在附件中,也可自行下载
Sockettool工具下载地址:百度网盘 请输入提取码 (baidu.com),提取码为1234
代码烧录到开发板中之后,打开TCP UDP Socket调试工具.exe文件,打开后界面如下
之后创建UDP服务端,如图所示步骤
创建完成后为以下界面,可以在此处接收到开发板发过来的数据,以及向开发板发送数据
打开MobaXterm的串口工具,按一下开发板的复位按键,此时可以看到日志中打印了一些信息,也已经可以在调试工具数据接收及提示窗口中看到开发板发送的数据;在调试工具数据发送窗口中发送数据后,可以在MobaXterm的串口工具中看到UDP服务端发送的数据。
日志结果如下:
调试工具中信息如下:
TCP服务端
参考:
- HarmonyOS网络应用开发—TCP服务端_哔哩哔哩_bilibili
- TCP(传输控制协议)_百度百科 (baidu.com)
- TCP协议详解 - 知乎 (zhihu.com)
- TCP和UDP的区别 - 知乎 (zhihu.com)
- TCP网络编程中connect()、listen()和accept()三者之间的关系 ( 非常重要!!) - 腾讯云开发者社区-腾讯云 (tencent.com)
TCP协议相关API介绍
这个socket.h中包含声明TCP协议相关接口函数(与TCP服务端相关api如下)。
接口名 | 功能描述 |
---|---|
socket | 创建套接字 |
bind | 为套接字关联了一个相应的地址与端口号 |
listen | 将套接字设置为监听模式 |
accept | 接受套接字上新的连接 |
recv | 接收数据 |
send | 发送数据 |
close | 关闭套接字 |
源码在vendor\hisi\hi3861\hi3861\platform\os\Huawei_LiteOS\components\lib\libc\musl\include\sys\socket.h
路径下
bind()
bind(int s, const struct sockaddr * name, socklen_t namelen)
参数:
名字 | 描述 |
---|---|
s | 指定要绑定的套接字的文件描述符 |
name | 指向包含要绑定到套接字的地址的sockaddr结构,地址的长度和格式取决于套接字的地址族。 |
namelen | 指定地址参数指向的sockaddr结构的长度 |
描述:
把一个本地协议地址和套接口绑定,比如把本机的2222端口绑定到套接口。注意:为什么在上部分中UDP客户端不需要调用bind函数?这是因为如果没有调用bind函数绑定一个端口的话,当调用connect函数时,内核会为该套接口临时选定一个端口,因此可以不用绑定。而服务器之所以需要绑定的原因就是,所有客户端都需要知道服务器使用的哪个端口,所以需要提前绑定。
listen()
int listen(int s, int backlog)
参数:
名字 | 内容 |
---|---|
s | 指定引用SOCK_STREAM类型套接字的文件描述符 |
backlog | 定义“s”的挂起连接队列可能增长到的最大长度 |
描述:
当socket创建后,它通常被默认为是主动套接口,也就是说是默认为要马上调用connect函数的,而作为服务器是需要被动接受的,所以需要调用linsten函数将主动套接口转换成被动套接口。调用linsten函数后,内核将从该套接口接收连接请求。
accept()
int accept(int s, struct sockaddr *addr, socklen_t addrlen)
描述:
此函数返回已经握手完成的连接的套接口。注意:此处的套接口不同于服务器开始创建的监听套接口,此套接口是已经完成连接的套接口,监听套接口只是用来监听。
recv()
int recv( SOCKET s, char *buf, int len, int flags)
描述:
recv函数用来从TCP连接的另一端接收数据。此函数为阻塞接收,即如果没有接收到数据会一直阻塞程序运行。
send()
int send( SOCKET s,char *buf,int len,int flags )
描述:
send函数用来向TCP连接的另一端发送数据。
TCP服务端创建流程介绍
实现TCP服务端
源码在applications\BearPi\BearPi-HM_Nano\sample\D3_iot_udp_client\udp_client_demo.c
路径下,以下添加了部分注释
/*创建客户端之后,点击连接,可以看到日志中打印出了`accept addr`,通过调试工具数据发送窗口向开发板TCP服务端发送一些数据,发送后数据会在日志中被打印出来,在调试工具数据接收及提示窗口中也可以看到开发板TCP服务端回复的数据。*/
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "lwip/sockets.h"
#include "wifi_connect.h"
#define _PROT_ 8888
#define TCP_BACKLOG 10
//在sock_fd 进行监听,在 new_fd 接收新的链接
int sock_fd, new_fd;
char recvbuf[512];
char *buf = "Hello! I'm BearPi-HM_Nano TCP Server!";
static void TCPServerTask(void)
{
//服务端地址信息
struct sockaddr_in server_sock;
//客户端地址信息
struct sockaddr_in client_sock;
int sin_size;
struct sockaddr_in *cli_addr;
//连接Wifi
WifiConnect("TP-LINK_65A8", "xxxxxx"); // 参数分别是热点名称和热点password
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) // AF_INT:ipv4, SOCK_STREAM:tcp协议,0:协议,写为0时表示由第二个参数确定,返回一个文件描述符
{
perror("socket is error\r\n");
exit(1);
}
bzero(&server_sock, sizeof(server_sock)); // 将server_sock清0
server_sock.sin_family = AF_INET; // 协议族
server_sock.sin_addr.s_addr = htonl(INADDR_ANY); // IPv4地址,应填自己电脑的IPv4地址,可以使用ipconfig查找
server_sock.sin_port = htons(_PROT_); // 端口号
//调用bind函数绑定socket和地址
if (bind(sock_fd, (struct sockaddr *)&server_sock, sizeof(struct sockaddr)) == -1)
{
perror("bind is error\r\n");
exit(1);
}
//调用listen函数监听(指定port监听)
if (listen(sock_fd, TCP_BACKLOG) == -1)
{
perror("listen is error\r\n");
exit(1);
}
printf("start accept\n");
//调用accept函数从队列中
while (1)
{
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sock_fd, (struct sockaddr *)&client_sock, (socklen_t *)&sin_size)) == -1) // 返回新创建的套接字
{
perror("accept");
continue;
}
cli_addr = malloc(sizeof(struct sockaddr)); // 向系统申请分配内存空间
printf("accept addr\r\n");
if (cli_addr != NULL)
{
memcpy(cli_addr, &client_sock, sizeof(struct sockaddr)); // 复制到cli_addr
}
//处理目标
ssize_t ret;
while (1)
{
if ((ret = recv(new_fd, recvbuf, sizeof(recvbuf), 0)) == -1) // 接收数据
{
printf("recv error \r\n");
}
printf("recv :%s\r\n", recvbuf);
sleep(2);
if ((ret = send(new_fd, buf, strlen(buf) + 1, 0)) == -1) // 发送数据
{
perror("send : ");
}
sleep(2);
}
close(new_fd);
}
}
static void TCPServerDemo(void)
{
// 创建一个任务
osThreadAttr_t attr;
attr.name = "TCPServerTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
if (osThreadNew((osThreadFunc_t)TCPServerTask, NULL, &attr) == NULL)
{
printf("[TCPServerDemo] Falied to create TCPServerTask!\n");
}
}
APP_FEATURE_INIT(TCPServerDemo);
其中的_PROT_
指定了端口号为8888,后续创建TCP客户端的时候需要用到,可以自行修改,在0-65535之间即可
修改applications\BearPi\BearPi-HM_Nano\sample
路径下的BUILD.gn文件,指定tcp_server
,编译、烧录
测试TCP服务端
代码烧录完成后,打开MobaXterm的串口工具,按一下开发板复位按键,可以看到日志中打印出开发板ip等信息(ip地址后续会用到),打开TCP UDP Socket调试工具,创建TCP客户端
创建客户端之后,点击连接,可以看到日志中打印出了accept addr
,通过调试工具数据发送窗口向开发板TCP服务端发送一些数据,发送后数据会在日志中被打印出来,在调试工具数据接收及提示窗口中也可以看到开发板TCP服务端回复的数据。
日志结果如下:
调试工具中信息如下:
TCP客户端
参考:
TCP协议相关API介绍
socket.h接口简介
这个socket.h中包含声明TCP协议相关接口函数(与TCP客户端相关api如下)。
接口名 | 功能描述 |
---|---|
socket | 创建套接字 |
connect | 连接到指定的主机 |
send | 发送数据 |
recv | 接收数据 |
close | 关闭套接字 |
源码在vendor\hisi\hi3861\hi3861\platform\os\Huawei_LiteOS\components\lib\libc\musl\include\sys\socket.h
路径下
connect()
int connect (int s, const struct sockaddr * name, socklen_t namelen)
参数:
名字 | 描述 |
---|---|
s | 指定套接字文件描述符 |
name | 指定指向标识连接的sockaddr结构的指针 |
namelen | 指定name 的大小 |
描述:
此API将文件描述符s
引用的套接字连接到名称指定的地址。
TCP客户端创建流程介绍
实现TCP客户端
代码是在UDP客户端案例的基础上进行修改的,UDP客户端案例在applications\BearPi\BearPi-HM_Nano\sample\D3_iot_udp_client\udp_client_demo.c
路径下
// 修改后的TCP客户端代码
/*烧录完成后,按一下开发板的复位按键,日志中会打印对应的信息,并且在调试工具数据接收及提示窗口中看到开发板发送的数据;在调试工具数据发送窗口中发送数据后,可以在MobaXterm的串口工具中看到TCP服务端发送的数据*/
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "wifi_device.h"
#include "lwip/netifapi.h"
#include "lwip/api_shell.h"
#include <netdb.h>
#include <string.h>
#include <stdlib.h>
#include "lwip/sockets.h"
#include "wifi_connect.h"
#define _PROT_ 8888
//在sock_fd 进行监听,在 new_fd 接收新的链接
int sock_fd;
int addr_length;
static const char *send_data = "Hello! I'm BearPi-HM_Nano TCP Client!\r\n";
static void TCPClientTask(void)
{
//服务器的地址信息
struct sockaddr_in send_addr;
socklen_t addr_length = sizeof(send_addr);
char recvBuf[512];
//连接Wifi
WifiConnect("TP-LINK_65A8", "xxxxxx"); // 参数分别是热点名称和热点password,连接的WiFi和自己电脑所连的网络要在同一个局域网中,才能实现数据的收发
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) // AF_INT:ipv4, SOCK_STREAM:tcp协议,0:协议,写为0时表示由第二个参数确定,返回一个文件描述符
{
perror("create socket failed!\r\n");
exit(1);
}
//初始化预连接的服务端地址
send_addr.sin_family = AF_INET; // 协议族
send_addr.sin_port = htons(_PROT_); // 端口号
send_addr.sin_addr.s_addr = inet_addr("192.168.1.6"); // IPv4地址,应填自己电脑的IPv4地址,可以使用ipconfig查找
addr_length = sizeof(send_addr);
connect(sock_fd, (struct sockaddr *)&send_addr, addr_length); // 连接TCP服务器,之后进行数据发送和接收是不需要再指定服务端信息
//总计发送 count 次数据
while (1)
{
bzero(recvBuf, sizeof(recvBuf)); // 将recvBuf清0
//发送数据到服务远端
send(sock_fd, send_data, strlen(send_data), 0);
//线程休眠一段时间
// sleep(10); // 10s
//接收服务端返回的字符串
recv(sock_fd, recvBuf, sizeof(recvBuf), 0); // 阻塞接收,即如果没有从服务端接收到数据,会一直阻塞在这里
printf("%s:%d=>%s\n", inet_ntoa(send_addr.sin_addr), ntohs(send_addr.sin_port), recvBuf);
}
//关闭这个 socket
closesocket(sock_fd);
}
static void TCPClientDemo(void)
{
osThreadAttr_t attr;
attr.name = "TCPClientTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
if (osThreadNew((osThreadFunc_t)TCPClientTask, NULL, &attr) == NULL)
{
printf("[TCPClientDemo] Falied to create TCPClientTask!\n");
}
}
APP_FEATURE_INIT(TCPClientDemo);
与UDP客户端不同的是,收发数据之前需要先通过connect()
api与服务端建立连接,另外就是一些api功能相近,但是名称有些许差别,如send()
与sendto()
、recv
与recvfrom
,因为TCP客户端已经与服务端建立连接,所以使用send()
和recv
时无需再指定服务端。
修改applications\BearPi\BearPi-HM_Nano\sample
路径下的BUILD.gn文件,指定udp_client
,编译、烧录
测试TCP客户端
打开TCP UDP Socket调试工具,创建TCP服务端
打开MobaXterm的串口工具,按一下开发板的复位按键,日志中会打印对应的信息,并且在调试工具数据接收及提示窗口中看到开发板发送的数据;在调试工具数据发送窗口中发送数据后,可以在MobaXterm的串口工具中看到TCP服务端发送的数据。
结果如下:
UDP服务端
参考:
UDP协议相关API介绍
socket.h接口简介
这个socket.h中包含声明TCP协议相关接口函数(与TCP客户端相关api如下)。
接口名 | 功能描述 |
---|---|
socket | 创建套接字 |
bind | 将ip和端口绑定到嵌套字 |
sendto | 将数据由指定的socket发送对方主机 |
recvfrom | 从指定主机接收UDP数据 |
close | 关闭套接字 |
源码在vendor\hisi\hi3861\hi3861\platform\os\Huawei_LiteOS\components\lib\libc\musl\include\sys\socket.h
路径下
UDP服务端创建流程介绍
实现UDP服务端
代码是在TCP服务端案例的基础上进行修改的,TCP服务端案例源码在applications\BearPi\BearPi-HM_Nano\sample\D3_iot_udp_client\udp_client_demo.c
路径下
// 修改后的UDP服务端代码
/*当通过调试工具中的数据发送窗口向开发板的UDP服务端发送数据后,在数据接收及提示窗口可以看到开发板服务端的数据反馈,并且在串口工具打印的日志中,也可以看到UDP客户端发送过来的数据。*/
#include <stdio.h>
#include <unistd.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "lwip/sockets.h"
#include "wifi_connect.h"
#define _PROT_ 8888
#define UDP_BACKLOG 10
//在sock_fd 进行监听,在 new_fd 接收新的链接
int sock_fd, new_fd;
char recvbuf[512];
char *buf = "Hello! I'm BearPi-HM_Nano UDP Server!";
static void UDPServerTask(void)
{
//服务端地址信息
struct sockaddr_in server_sock;
//客户端地址信息
struct sockaddr_in client_sock;
int sin_size;
struct sockaddr_in *cli_addr;
//连接Wifi
WifiConnect("TP-LINK_65A8", "xxxxxx"); // 参数分别是热点名称和热点password
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) // AF_INT:ipv4, SOCK_DGRAM:udp协议,0:协议,写为0时表示由第二个参数确定,返回一个文件描述符
{
perror("socket is error\r\n");
exit(1);
}
bzero(&server_sock, sizeof(server_sock)); // 将server_sock清0
server_sock.sin_family = AF_INET; // 协议族
server_sock.sin_addr.s_addr = htonl(INADDR_ANY); // IPv4地址,应填自己电脑的IPv4地址,可以使用ipconfig查找
server_sock.sin_port = htons(_PROT_); // 端口号
//调用bind函数绑定socket和地址
if (bind(sock_fd, (struct sockaddr *)&server_sock, sizeof(struct sockaddr)) == -1)
{
perror("bind is error\r\n");
exit(1);
}
// UDP服务器不需要进行监听
// //调用listen函数监听(指定port监听)
// if (listen(sock_fd, TCP_BACKLOG) == -1)
// {
// perror("listen is error\r\n");
// exit(1);
// }
// printf("start accept\n");
//调用accept函数从队列中
while (1)
{
sin_size = sizeof(struct sockaddr_in);
// if ((new_fd = accept(sock_fd, (struct sockaddr *)&client_sock, (socklen_t *)&sin_size)) == -1) // 返回新创建的套接字
// {
// perror("accept");
// continue;
// }
// cli_addr = malloc(sizeof(struct sockaddr)); // 向系统申请分配内存空间
// printf("accept addr\r\n");
// if (cli_addr != NULL)
// {
// memcpy(cli_addr, &client_sock, sizeof(struct sockaddr)); // 复制到cli_addr
// }
//处理目标
ssize_t ret;
while (1)
{
bzero(recvbuf, sizeof(recvbuf)); // 将recvbuf清0
if ((ret = recvfrom(sock_fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&client_sock, (socklen_t *)&sin_size)) == -1) // 接收数据
{
printf("recv error \r\n");
}
printf("recv :%s\r\n", recvbuf);
// sleep(2);
if ((ret = sendto(new_fd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&client_sock, sizeof(client_sock))) == -1) // 发送数据
{
perror("send : ");
}
// sleep(2);
}
close(new_fd);
}
}
static void UDPServerDemo(void)
{
// 创建一个任务
osThreadAttr_t attr;
attr.name = "UDPServerTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
if (osThreadNew((osThreadFunc_t)UDPServerTask, NULL, &attr) == NULL)
{
printf("[UDPServerDemo] Falied to create UDPServerTask!\n");
}
}
APP_FEATURE_INIT(UDPServerDemo);
与TCP服务端相比,UDP不需要使用listen()
api对服务器进行监听,也不再需要accept
api来提取挂起连接队列上的第一个连接请求,且两者进行数据收发所使用的api也不同,UDP使用recvfrom
和sendto
,TCP使用的是recv
和send
。
其中的_PROT_
指定了端口号为8888,后续创建UDP客户端的时候需要用到,可以自行修改,在0-65535之间即可
修改applications\BearPi\BearPi-HM_Nano\sample
路径下的BUILD.gn文件,指定tcp_server
,编译、烧录
测试UDP服务端
代码烧录完成后,打开MobaXterm的串口工具,按一下开发板复位按键,可以看到日志中打印出开发板联网成功信息、ip等信息(ip地址后续将会用到),打开TCP UDP Socket调试工具,创建UDP客户端
当通过调试工具中的数据发送窗口向开发板的UDP服务端发送数据后,在数据接收及提示窗口可以看到开发板服务端的数据反馈,并且在串口工具打印的日志中,也可以看到UDP客户端发送过来的数据。
结果如下:
MQTT客户端
参考:
- HarmonyOS网络应用开发—MQTT客户端_哔哩哔哩_bilibili
- MQTT_百度百科 (baidu.com)
- MQTT 入门介绍 | 菜鸟教程 (runoob.com)
- MQTT · OneOS开发者文档 (10086.cn)
- HI3861学习笔记(24)——MQTT客户端 - 简书 (jianshu.com)
- MQTT协议,终于有人讲清楚了 - 知乎 (zhihu.com)
MQTT介绍
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。
Paho是IBM在2011年建立的Eclipse开源项目,该项目包含以C、Java、Python、Javascript等语言编写的可用客户端。
本次使用的是C语言编写的客户端,开源地址是:eclipse/paho.mqtt.embedded-c: Paho MQTT C client library for embedded systems. Paho is an Eclipse IoT project (https://iot.eclipse.org/) (github.com),下载下来的源码会放在附件。
Paho MQTT 文件目录介绍
打开下载下来的源码,或者直接在GitHub上打开,
其中比较重要的3个文件夹是:
- MQTTClient:封装MQTTPacket生成的高级别C++客户端程序。
- MQTTClient-C: 封装MQTTPacket生成的高级别C客户端程序
- samples目录提供FreeRTOS和linux两个例程,分别支持FreeRTOS和Linux系统。
- src目录提供MQTTClient的代码实现能力,以及用于移植到对应平台的网络驱动
- MQTTPacket:提供MQTT数据包的序列化与反序列化,以及部分辅助函数。
MQTTClient-C\src
路径下的MQTTClient.h中包含了供开发者使用的api接口,具体见下。
从hpm官网或者gitee仓库下载的源码中,已经包含了Paho MQTT,路径为third_party\paho_mqtt
如何使用Paho MQTT
在MQTTClient.h中包含声明Paho MQTT相关接口函数。
接口名 | 功能描述 |
---|---|
MQTTClientInit | 创建一个客户端对象 |
MQTTConnect | 发送MQTT连接数据包 |
MQTTConnectWithResults | 发送MQTT连接数据包并等待返回 |
MQTTPublish | 发送MQTT发布数据包 |
MQTTSetMessageHandler | 发送每个topic消息处理函数 |
MQTTSubscribe | 发送MQTT订阅数据包 |
MQTTSubscribeWithResults | 发送MQTT订阅数据包并等待返回结果 |
MQTTUnsubscribe | 发送MQTT取消数据包 |
MQTTDisconnect | 发送MQTT断开连接数据包并关闭连接 |
源码在上文提到的MQTTClient-C\src\MQTTClient.h
路径下。但是解释的不够具体。
MQTTClientInit()
void MQTTClientInit(MQTTClient* client, Network* network, unsigned int command_timeout_ms, unsigned char* sendbuf, size_t sendbuf_size, unsigned char* readbuf, size_t readbuf_size);
参数:
名字 | 描述 |
---|---|
client | 指向需要初始化的MQTT客户端结构体 |
network | 指向需要初始化的MQTT客户端的网络结构体 |
command_timeout_ms | 命令执行超时时间 |
sendbuf | 客户端发送数据缓冲区地址 |
sendbuf_size | 发送数据缓冲区容量 |
readbuf | 客户端接收数据缓冲区地址 |
readbuf_size | 接收数据缓冲区容量 |
描述:
创建一个MQTT客户端对象
MQTTConnect()
int MQTTConnect(MQTTClient* client, MQTTPacket_connectData* options);
参数:
名字 | 描述 |
---|---|
client | 客户端结构体指针 |
options | 连接选项结构体指针 |
返回值:
返回值 | 描述 |
---|---|
int | =0初始化成功;<0初始化失败 |
描述:
在网络上发送MQTT connect数据包并等待Connack。在调用此数据包之前,必须将nework对象连接到网络端点。
MQTTConnectWithResults()
int MQTTConnectWithResults(MQTTClient* client, MQTTPacket_connectData* options, MQTTConnackData* data);
参数:
名字 | 描述 |
---|---|
client | 客户端结构体指针 |
options | 连接选项结构体指针 |
data | MQTT Connack数据 |
描述:
在网络上发送MQTT connect数据包并等待Connack。在调用此数据包之前,必须将nework对象连接到网络端点。
MQTTPublish()
int MQTTPublish(MQTTClient* client, const char* topicName, MQTTMessage* message);
参数:
名字 | 描述 |
---|---|
client | 指向需要发布消息的客户端结构体 |
topicName | 指向需要发布的消息主题 |
message | 指向需要发布的消息结构体 |
描述:
用于发布MQTT消息,发送MQTT发布数据包,并等待所有QoS的所有ACK完成。
MQTTSubscribe()
int MQTTSubscribe(MQTTClient* client, const char* topicFilter, enum QoS, messageHandler);
参数:
名字 | 描述 |
---|---|
client | 指向需要订阅消息的客户端结构体 |
topicFilter | 指向需要订阅的消息主题 |
QoS | 订阅消息的服务质量 |
messageHandler | 指向订阅消息的回调函数 |
返回值:
返回值 | 描述 |
---|---|
int | =0初始化成功;<0初始化失败 |
描述:
实现MQTT消息订阅,发送MQTT subscribe数据包并等待suback,然后返回。
实现MQTT客户端
源码在applications\BearPi\BearPi-HM_Nano\sample\D5_iot_mqtt\iot_mqtt.c
路径下,以下添加了部分注释
/*在eclipse工具中添加订阅,之后在历史记录中可以看到开发板发过来的数据;在eclipse工具中发布一个主题,在MobaXterm的串口工具日志中打印出了发布内容*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "wifi_connect.h"
#include "MQTTClient.h"
static unsigned char sendBuf[1000];
static unsigned char readBuf[1000];
Network network;
void messageArrived(MessageData* data)
{
printf("Message arrived on topic %.*s: %.*s\n", data->topicName->lenstring.len, data->topicName->lenstring.data,
data->message->payloadlen, data->message->payload);
}
/* */
static void MQTT_DemoTask(void)
{
WifiConnect("Hold","xxxxxx"); // 连接WiFi,参数分别是热点名称和热点password
printf("Starting ...\n");
int rc, count = 0;
MQTTClient client;
NetworkInit(&network); // 用于MQTT对Socket依赖关系的注册,初始化网络结构体,socket描述符、socket发送和接收函数
printf("NetworkConnect ...\n");
begin:
NetworkConnect(&network, "192.168.0.176", 1883); // 连接MQTT服务端,ip地址值为自己电脑的ip
printf("MQTTClientInit ...\n");
MQTTClientInit(&client, &network, 2000, sendBuf, sizeof(sendBuf), readBuf, sizeof(readBuf)); // 创建一个MQTT客户端对象
// 配置MQTT客户端的信息
MQTTString clientId = MQTTString_initializer;
clientId.cstring = "bearpi";
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
data.clientID = clientId; // 用户id
data.willFlag = 0; // 是否使用临终遗言
data.MQTTVersion = 3; // MQTT协议版本号
data.keepAliveInterval = 0; // 保活周期
data.cleansession = 1; // 是否清楚session信息
printf("MQTTConnect ...\n");
rc = MQTTConnect(&client, &data); // 发送连接数据包,返回值为0时,表示发送成功
if (rc != 0) {
printf("MQTTConnect: %d\n", rc);
NetworkDisconnect(&network); // 断开连接
MQTTDisconnect(&client); // 断开连接数据包并关闭连接
osDelay(200);
goto begin; // 回到begin,重新连接MQTT服务端
}
printf("MQTTSubscribe ...\n");
rc = MQTTSubscribe(&client, "substopic", 2, messageArrived); // 订阅消息,substopic为订阅的消息主题,messageArrived为订阅成功后的回调函数
if (rc != 0) {
printf("MQTTSubscribe: %d\n", rc);
osDelay(200);
goto begin;
}
while (++count) // 每500ms发布一次消息
{
MQTTMessage message;
char payload[30];
message.qos = 2; // 服务等级
message.retained = 0; // 是否保留
message.payload = payload; // 消息数据
sprintf(payload, "message number %d", count); // 发送格式化输出到payload所指向的字符串
message.payloadlen = strlen(payload); // 消息数据长度
if ((rc = MQTTPublish(&client, "pubtopic", &message)) != 0){ // 发布消息,pubtopic为发布的消息主题,message为需要发布的消息结构体
printf("Return code from MQTT publish is %d\n", rc);
NetworkDisconnect(&network); // 断开连接
MQTTDisconnect(&client); // 断开连接数据包并关闭连接
goto begin;
}
osDelay(50);
}
}
static void MQTT_Demo(void)
{
// 创建一个任务
osThreadAttr_t attr;
attr.name = "MQTT_DemoTask";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 10240;
attr.priority = osPriorityNormal;
if (osThreadNew((osThreadFunc_t)MQTT_DemoTask, NULL, &attr) == NULL) {
printf("[MQTT_Demo] Falied to create MQTT_DemoTask!\n");
}
}
APP_FEATURE_INIT(MQTT_Demo);
其中订阅的消息主题名称是substopic
,发布的消息主题名称是pubtopic
,后续还会再用到
测试MQTT客户端
工具下载安装及配置
首先需要下载两个工具,会放在附件中,也可自行下载,下载路径在后面。
MQTT消息代理软件mosquitto
下载地址:Download | Eclipse Mosquitto
根据自己的电脑选择相应的版本,本人是64位win10电脑,选择下载64位的windows版
下载完成后双击进行安装,过程较为简单,不再赘述。
安装完成之后需要修改它的配置文件,首先打开它的安装位置,找到mosquitto.conf文件并打开
- 搜索
allow_anonymous false
,将它的注释打开,并将false
改为true
- 搜索
listener
(230行左右,listener个数较多供参考),将它的注释打开,并在后面加上(有一个空格)1883 电脑ip
(电脑ip需要自行查找),另起一行加上listener 1883 localhost
,保存文件
修改完成
之后启动此服务,打开任务管理器,选择服务,右键再点击开始以启动服务。
Eclipse桌面客户端程序
下载地址:Index of /repositories/paho-releases/org/eclipse/paho/org.eclipse.paho.ui.app/1.1.1
同样需要根据自己的电脑选择相应的版本,我选择windows64位的版本
下载完成后解压,无需安装,双击paho.exe应用程序打开
测试
修改applications\BearPi\BearPi-HM_Nano\sample
路径下的BUILD.gn文件,指定iot_mqtt
,编译、烧录
订阅功能测试
在eclipse工具中添加订阅,
之后使用MobaXterm的串口工具,点击开发板复位按键,等待串口工具打印出开始订阅的日志,之后如果在eclipse工具中的历史记录中可以看到开发板发过来的数据,说明客户端已经订阅到了开发板发布的数据,客户端的订阅功能没有问题,开发板发布功能没有问题。
发布功能测试
在eclipse工具中发布一个主题,
如果在MobaXterm的串口工具日志中打印出了发布内容,则证明客户端的发布能没有问题,开发板订阅功能没有问题。
本章所用到的案例源码及相关工具见附件。
感悟
学习完本章后学习最大的感觉就是有些抽象,是之前学习过的几章中最不好理解的一部分,首先是TCP、UDP的原理不太了解,其次是里面的一些基本概念不懂,再次让我体会到了打牢基础的重要性(小白落泪)。另外在初学阶段,不能太深究底层的东西,比如里面设计到的api,我们只需要知道有什么功能,需要哪些参数,怎么去用就可以了,具体的原理不需要深入探究(而且以我目前的水平也无能为力),既然是想短时间内学习某种技术,那么实践才是最重要的。最后一章,加油!!!
UDP和TCP可以说是通信的必经之路了
另外楼主可以考虑开个专栏(https://ost.51cto.com/column)统一收录下自己的文章。