目录
前言
上一篇讲了怎么用开发板使用wifi功能,开启WiFi了要和外部通信的,这篇文章主要写TCP,UDP,MQTT三种通信协议的使用。
主要API
主要使用此文件中的函数third_party/lwip/src/include/lwip/sockets.h
socket()
sock_fd = socket(AF_INET, SOCK_STREAM, 0)) //AF_INT:ipv4, SOCK_STREAM:tcp协议
描述:
在网络编程中所需要进行的第一件事情就是创建一个socket,无论是客户端还是服务器端,都需要创建一个socket,该函数返回socket文件描述符,类似于文件描述符。socket是一个结构体,被创建在内核中。
bind()
bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))
描述:
把一个本地协议地址和套接口绑定,比如把本机的2222端口绑定到套接口。注意:为什么在使用 Socket 实现 UDP 客户端教程中的客户端不需要调用bind函数?这是因为如果没有调用bind函数绑定一个端口的话,当调用connect函数时,内核会为该套接口临时选定一个端口,因此可以不用绑定。而服务器之所以需要绑定的原因就是,所以客户端都需要知道服务器使用的哪个端口,所以需要提前绑定。
listen()
int listen(int s, int backlog)
描述:
此函数返回已经握手完成的连接的套接口。注意:此处的套接口不同于服务器开始创建的监听套接口,此套接口是已经完成连接的套接口,监听套接口只是用来监听。
一、UDP协议
UDP协议概念就不多讲了,属于数据报式通信,无连接不可靠的,但是速度快。
收发API
sendto()
int sendto ( socket s , const void * msg, int len, unsigned int flags,const struct sockaddr * to , int tolen ) ;
描述:
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数据报。
1. 通信流程
2.客户端实现
完成Wifi热点的连接后还需要以下几步
- 通过 socket 接口创建一个socket,AF_INT表示ipv4,SOCK_STREAM表示使用tcp协议,SOCK_DGRAM表示使用udp协议
- 调用 sendto 接口发送数据到服务端。
- 调用 recvfrom 接口接收服务端发来的数据
#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", "0987654321");
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
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.0.175");
addr_length = sizeof(send_addr);
//总计发送 count 次数据
while (1)
{
bzero(recvBuf, sizeof(recvBuf));
//发送数据到服务远端
sendto(sock_fd, send_data, strlen(send_data), 0, (struct sockaddr *)&send_addr, addr_length);
//线程休眠一段时间
sleep(10);
//接收服务端返回的字符串
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);
}
3.服务器端实现
服务器端需要绑定套接字,用来监听客户端的连接请求。
#define _PROT_ 8888
#define UDP_BACKLOG 10
//在sock_fd 进行监听,在 new_fd 接收新的链接
int sock_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;
//连接Wifi
WifiConnect("TP-LINK_65A8", "0987654321");
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket is error\r\n");
exit(1);
}
bzero(&server_sock, sizeof(server_sock));
server_sock.sin_family = AF_INET;
server_sock.sin_addr.s_addr = htonl(INADDR_ANY);
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);
}
//处理目标
ssize_t ret;
while (1)
{
sin_size = sizeof(struct sockaddr_in);
bzero(&recvbuf, sizeof(recvbuf));
if ((ret = recvfrom(sock_fd, recvbuf, sizeof(recvbuf), 0,(struct sockaddr *)&client_sock,&sin_size)) == -1)
{
printf("recv error \r\n");
}
printf("recv :%s\r\n", recvbuf);
if ((ret = sendto(sock_fd, buf, strlen(buf), 0,(struct sockaddr *)&client_sock,sizeof(client_sock))) == -1)
{
perror("send : ");
}
}
}
二、TCP协议
协议概念就不多讲了,属于流式通信,连接可靠的。
收发API
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连接的另一端发送数据。
1. 通信流程
2.客户端实现
#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", "0987654321");
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
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.0.175");
addr_length = sizeof(send_addr);
connect(sock_fd,(struct sockaddr *)&send_addr,addr_length);
while (1)
{
bzero(recvBuf, sizeof(recvBuf));
//发送数据到服务远端
if(ret = send(sock_fd, send_data, strlen(send_data), 0) == -1)
{
perror("send:");
}
//接收服务端返回的字符串
if(ret = recv(sock_fd, recvBuf, sizeof(recvBuf), 0) == -1)
{
printf("recv error \r\n");
}
printf("recv :%s\r\n", recvbuf);
}
//关闭这个 socket
closesocket(sock_fd);
}
3.服务器端实现
服务器端需要绑定套接字,用来监听客户端的连接请求。
完成Wifi热点的连接后还需要以下几步
- 通过
socket
接口创建一个socket
,AF_INT表示ipv4,SOCK_STREAM表示使用tcp协议 - 调用
bind
接口绑定socket和地址。 - 调用
listen
接口监听(指定port监听),通知操作系统区接受来自客户端链接请求,第二个参数:指定队列长度 - 调用
accept
接口从队列中获得一个客户端的请求链接 - 调用
recv
接口接收客户端发来的数据 - 调用
send
接口向客户端回复固定的数据
#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", "0987654321");
//创建socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket is error\r\n");
exit(1);
}
bzero(&server_sock, sizeof(server_sock));
server_sock.sin_family = AF_INET;
server_sock.sin_addr.s_addr = htonl(INADDR_ANY);
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));
}
//处理目标
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);
}
}
二、MQTT协议
协议概念就不多讲了,之前有总结过。LiteOS SDK oc流程之MQTT
Paho MQTT简介
Paho是IBM在2011年建立的Eclipse开源项目,该项目包含多种语言编写的可用客户端。
嵌入式C语言客户端地址:https://github.com/eclipse/paho.mqtt.embedded-c
gitee页面为:bearpi-hm_nano/ third_party / paho_mqtt
,在此页面可以更详细了解此库。
鸿蒙系统相关移植文件:MQTTClient-C\src\liteOS\MQTTLiteOS.c
MQTTClient
:封装MQTTPacket生成的高级别C++客户端程序。MQTTClient-C
:封装MQTTPacket生成的高级别C客户端程序- samples目录提供FreeRTOS和linux两个例程,分别支持FreeRTOS和Linux系统。
- src目录提供MQTTClient的代码实现能力,以及用于移植到对应平台的网络驱动
MQTTPacket
:提供MQTT数据包的序列化与反序列化,以及部分辅助函数。
Paho MQTT API
在MQTTClient.h文件中声明了相关接口函数。我们只需要调用接口即可,不用再自己实现MQTT协议了。
接口名 | 功能描述 |
---|---|
MQTTClientInit | 创建一个客户端对象 |
MQTTConnect | 发送MQTT连接数据包 |
MQTTConnectWithResults | 发送MQTT连接数据包并等待返回 |
MQTTPublish | 发送MQTT发布数据包 |
MQTTSetMessageHandler | 发送每个topic消息处理函数 |
MQTTSubscribe | 发送MQTT订阅数据包 |
MQTTSubscribeWithResults | 发送MQTT订阅数据包并等待返回结果 |
MQTTUnsubscribe | 发送MQTT取消数据包 |
MQTTDisconnect | 发送MQTT断开连接数据包并关闭连接 |
开发板实现MQTT客户端
代码摘自sample中的iot_mqtt工程。
- wifi要接入,使开发板处于联网状态
- 初始化Network,因为mqtt协议建立在tcp协议上,所以给定套接字,与读写接口
建立和指定IP地址与1883端口的broker的连接。
typedef struct Network
{
int my_socket;
int (*mqttread) (struct Network*, unsigned char*, int, int);
int (*mqttwrite) (struct Network*, unsigned char*, int, int);
} Network;
int NetworkConnect(Network* n, char* addr, int port)
- 使用
MQTTClientInit
建立一个MQTT客户端
DLLExport 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和data,使用
MQTTConnect
开启MQTT连接。
/** MQTT Connect - send an MQTT connect packet down the network and wait for a Connack
* The nework object must be connected to the network endpoint before calling this
* @param options - connect options
* @return success code
*/
DLLExport int MQTTConnect(MQTTClient* client, MQTTPacket_connectData* options);
- 开启订阅和发布
订阅
需要写一个回调函数作为消息处理函数。
typedef struct MessageData
{
MQTTMessage* message;
MQTTString* topicName;
} MessageData;
/** MQTT Subscribe - send an MQTT subscribe packet and wait for suback before returning.
* @param client - the client object to use
* @param topicFilter - the topic filter to subscribe to
* @param message - the message to send
* @return success code
*/
DLLExport int MQTTSubscribe(MQTTClient* client, const char* topicFilter, enum QoS, messageHandler);
发布
typedef struct MQTTMessage
{
enum QoS qos;
unsigned char retained;
unsigned char dup;
unsigned short id;
void *payload;
size_t payloadlen;
} MQTTMessage;
/** MQTT Publish - send an MQTT publish packet and wait for all acks to complete for all QoSs
* @param client - the client object to use
* @param topic - the topic to publish to
* @param message - the message to send
* @return success code
*/
DLLExport int MQTTPublish(MQTTClient* client, const char*, MQTTMessage*);
主要代码
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","0987654321");
printf("Starting ...\n");
int rc, count = 0;
MQTTClient client;
NetworkInit(&network);
printf("NetworkConnect ...\n");
begin:
NetworkConnect(&network, "192.168.0.176", 1883);
printf("MQTTClientInit ...\n");
MQTTClientInit(&client, &network, 2000, sendBuf, sizeof(sendBuf), readBuf, sizeof(readBuf));
MQTTString clientId = MQTTString_initializer;
clientId.cstring = "bearpi";
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
data.clientID = clientId;
data.willFlag = 0;
data.MQTTVersion = 3;
data.keepAliveInterval = 0;
data.cleansession = 1;
printf("MQTTConnect ...\n");
rc = MQTTConnect(&client, &data);
if (rc != 0) {
printf("MQTTConnect: %d\n", rc);
NetworkDisconnect(&network);
MQTTDisconnect(&client);
osDelay(200);
goto begin;
}
printf("MQTTSubscribe ...\n");
rc = MQTTSubscribe(&client, "substopic", 2, messageArrived);
if (rc != 0) {
printf("MQTTSubscribe: %d\n", rc);
osDelay(200);
goto begin;
}
while (++count)
{
MQTTMessage message;
char payload[30];
message.qos = 2;
message.retained = 0;
message.payload = payload;
sprintf(payload, "message number %d", count);
message.payloadlen = strlen(payload);
if ((rc = MQTTPublish(&client, "pubtopic", &message)) != 0){
printf("Return code from MQTT publish is %d\n", rc);
NetworkDisconnect(&network);
MQTTDisconnect(&client);
goto begin;
}
osDelay(50);
}
}
测试MQTT客户端
测试开发板的客户端,首先需要一个服务端也就是broker,再一个就是PC的客户端,用来订阅开发板发布的主题消息。
官方提供了两个软件,可自行尝试:
MQTT消息代理软件mosquitto下载地址: https://mosquitto.org/download/
Eclipse桌面客户端程序下载地址: https://repo.eclipse.org/content/repositories/paho-releases/org/eclipse/paho/org.eclipse.paho.ui.app/1.1.1/
编译调试的截图可参看官方案例页面
官方编译调试步骤截图页面