一、实现原理
- 首先,
Client -> Gateway : 发送 UDP 广播包(含厂商自定义协议)
这一步表示客户端开始向网络中发送一个包含厂商自定义协议的 UDP 广播包,目的是寻找本厂商的设备(网关)。客户端此时处于活动状态activate Client
,而网关开始等待接收广播包,也进入活动状态activate Gateway
。 - 当网关接收到符合厂商协议的广播包后,
Gateway -> Client : 回复 UDP 包(若符合厂商协议)
,向客户端回复一个 UDP 包,告知自己的存在。之后客户端和网关的 UDP 交互暂时结束,状态变为不活动deactivate Client
和deactivate Gateway
。 - 接着,网关会在自己这一端
Gateway -> Gateway : 建立 TCP 服务器
,等待客户端的连接。当客户端准备好后,Client -> Gateway : 发起 TCP 连接
,向网关发起 TCP 连接请求。网关收到请求后,Gateway -> Client : 接受 TCP 连接
,接受客户端的连接,此时两者建立起了 TCP 长连接。 - 建立连接后,客户端可以向网关
Client -> Gateway : 问询信息
,询问所需的信息,网关收到请求后Gateway -> Client : 返回信息
,将相应的信息返回给客户端。 - 然后,客户端可能需要向网关发送点表文件,即
Client -> Gateway : 发送点表文件
,网关接收文件后Gateway -> Client : 接收点表文件确认
,向客户端返回接收确认信息。 - 最后,为了保持连接的活跃性,客户端会定期向网关
Client -> Gateway : 发送心跳包
,网关收到后Gateway -> Client : 回应心跳包
,表示连接仍然正常。
二、相关协议
1、设备搜索协议
客户端—>搜索响应进程
客户端发送自定义标识,比如厂家名称字符串,这里自行协定
搜索响应进程—>客户端
设备收到协定的字符串校验成功后,回复"yes"
2、交互协议
这里的通信是基于TCP完成的,格式是json。通过type
字段来标定事务类型,type
取值如下(可以继续扩展):
1:获取运行时必要的参数信息
2:点表下发
3:心跳包
3、运行状态请求协议
客户端运行后首先下发此请求包,确认设备端是否已经有了点表配置文件。如果没有,那么下一步就是给设备下发点表文件;如果设备已经有了(下发过),那么需要获取设备的某些配置信息,以便QT后续运行使用。 ** 客户端--->搜索响应进程**{"type": 1
}
**
搜索响应进程--->客户端** 设备出厂时候肯定是没有点表信息的,所以需要客户端下发,那么此时给客户端的回复up_config字段为true,后续的data无需添加。如果设备判断已经有这个点表了,那么up_config字段应该为false,并且把QT所需要的一些配置发回,添加上data字段(设备中的配置永远是最新的)。
{"type": 1,"result": 0, //成功返回0,失败返回1"up_config": false, //是否需要下发点表 true:需要下发,false:不需要下发"data": { //配置信息,当up_config为true时不需要加data字段,false需增加此字段"mqtt_config":{"mqtt_addr": "192.168.1.2", //mqtt服务器的地址"mqtt_port": 1883 //mqtt的端口 },"video_config":{"video_addr":"192.168.8.8", //监控服务的地址"video_port":8888 //监控服务的端口 },"update_config":{"type":1, //上报模式 0-不上报,客户端主动采集;1-变化上报,即连续2次值不相等;2-周期上报"period": 5 //上报周期时间,单位秒,仅在type=2时需要增加},xxx //按需可继续添加}
}
4、配置下发协议
如果设备需要客户端下发点表配置文件(设备首次运行),那么在上述协议交互后,客户端会进行配置下发请求。
客户端文件下发是一个很常规的操作,比如配置文件、后期的升级文件都可以用这种方式进行。这里我们模仿下真正的FTP协议的流程来完成文件传输,参考下面的思想。
FTP端口号20和21的区别是21端口用于连接,20端口用于传输数据。进行FTP文件传输时,客户端首先连接到FTP服务器的21端口,进行用户的认证。认证成功后,要传输文件时,服务器会专门开一个新的端口20,来进行传输数据文件。所以传输文件时,实际上是一路新的链接,传输成功后,客户端会主动断开这一路链接。
所以,我们也模仿这个流程,可以定义8000端口专门用来做命令的控制,而8001这个端口来做文件的传输。
提示:一个进程是可以同时建立多个TCP链路的,所以这个流程我们可以保持8000端口的链路常开,当收到文件传输命令后,再开启一路8001socket端口监听,等待文件传输完成后,再关闭掉这路socket。FTP的思想就是文件传输链路随用随开,用完即关。FTP也常用在固件升级等场景中,下发的文件可能会很大。
两个端口的处理你可以用多线程、多进程、IO多路复用,这些思路都可以,只要能实现就行,下面给出一个select和线程结合的例子,仅作为参考。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>#define N 64typedef struct sockaddr SA;//这个例子里模拟了两个端口的响应,第一个端口用8888,第二个用6666
//模型用的select配合着线程实现的,仅作为参考void *new_sock(void *arg)
{printf("will recv new connect\n");int sockfd, clientfd;char buf[N];int addrlen = sizeof(struct sockaddr);struct sockaddr_in addr, clientaddr;int nbytes;// 1创建一个套接字--socketsockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");exit(-1);}// 2定义套接字地址--sockaddr_inbzero(&addr, addrlen);addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(6666); //新的连接用6666端口// 3绑定套接字--bindif (bind(sockfd, (struct sockaddr *)&addr, addrlen) < 0){perror("bind err");exit(-1);}// 4启动监听--listenif (listen(sockfd, 5) < 0){perror("listen err");exit(-1);}// 5接收连接--acceptclientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);if (clientfd < 0){perror("accept err");exit(-1);}printf("recv new client\n");// 6收发数据--recv/sendwhile (1) {nbytes = recv(clientfd, buf, N, 0);if(nbytes < 0) {perror("recv err");exit(-1);} else if (nbytes > 0) {printf("recv %s-%d data = %s\n", inet_ntoa(clientaddr.sin_addr), \ntohs(clientaddr.sin_port), buf);} else {printf("peer exit\n");break;}}}int main(int argc, char *argv[])
{// 1.定义变量int i, listenfd, connfd, maxfd, n;char buf[N];fd_set rdfs, rdtmp;struct sockaddr_in myaddr;if (argc < 3){puts("server <addr> <port>");return -1;}// 2.创建套接字并监听if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0){perror("socket err");exit(-1);}bzero(&myaddr, sizeof(myaddr));myaddr.sin_family = AF_INET;myaddr.sin_addr.s_addr = INADDR_ANY;myaddr.sin_port = htons(8888); //第一个链接用8888if (bind(listenfd, (SA *)&myaddr, sizeof(myaddr)) < 0){perror("fail to bind");exit(-1);}listen(listenfd, 5);// 3.更新最大文件描述符,并设置关注集maxfd = listenfd;FD_ZERO(&rdfs); // 清空关注列表FD_SET(listenfd, &rdfs); // 将fd放入关注列表中while (1){rdtmp = rdfs;// 5.调用select进行监听if (select(maxfd + 1, &rdtmp, NULL, NULL, NULL) < 0){perror("select err");exit(-1);}// 遍历集合,如果服务器句柄有数据,那么代表有连接进来,把它加入描述符集并更新maxfd// 否则代表客户端句柄有数据,那么接收数据,如果对端退出,那么把它从描述符集清除for (i = 0; i < maxfd + 1; i++){if (FD_ISSET(i, &rdtmp)){if (i == listenfd){ // 表示有新连接进来connfd = accept(listenfd, NULL, NULL);FD_SET(connfd, &rdfs);if (connfd > maxfd){maxfd = connfd;}}else{ // 客户端有数据bzero(buf, N);n = recv(i, buf, N, 0);if (n > 0){printf("recv from %d--%s\n", i, buf); // 记得加\n冲刷缓冲区if (strncmp(buf, "new", 3) == 0){// 新建一个新的服务器,这里如果还想保证8888的端口客户端正常响应,必须开线程,否则会阻塞到新的操作中pthread_t tid;pthread_create(&tid, NULL, new_sock, NULL);}}else if (n == 0){FD_CLR(i, &rdfs);close(i);}else{}}}}}return 0;
}
客户端—>搜索响应进程
{"type": 2, "data": {"flag":"start", // start:准备发送 data:文件数据 stop:发送完毕"file_name":"node.json", //点表文件名,flag为start需要填写"file_len":560 //点表文件长度,flag为start需要填写}
}
搜索响应进程—>客户端
只有当flag为"start"或者"stop"时,需要回复;
{"type": 2"data": {"flag":"start" // start:准备接收 stop:接收完成}
}
说明:当客户端收到start请求帧的回复后,就会连接网关的8001端口,进行文件的传输。网关需要根据请求帧的信息来完成文件的命名。当文件传输完成后,客户端会主动断开8001的TCP链接,并通过控制链路发送stop帧告知已经发送完成。设备收到stop帧后关闭文件,然后回复stop帧完成点表的下发请求。
5、心跳协议
心跳机制是用来检测和设备的连接的,如果连接正常,那么客户端会回复。
心跳包疑惑解释
客户端—>搜索响应进程
{"type": 3
}
搜索响应进程—>客户端
{
"type": 3,"result": 0, //无需回复任何东西,回复即代表活着
}