目录
1、背景引入
2、技术分析
3、过程概述
4、服务器端流程
5、客户端流程
6、效果展示
7、源码
7.1 master(主控)
7.2 device(设备)
8、注意事项
1、背景引入
在工业生产中,设备的升级和维护是非常重要的环节。随着技术的不断进步,工厂设备通常需要定期进行软件或固件的升级,以确保其性能、安全性和功能的持续改进。在这一过程中,文件传输是一个关键的步骤,因为升级文件的准确、高效地传输直接影响着设备升级的成功与否。
在工业环境中,设备通常会连接到一个局域网中,并且可能分布在不同的位置。因此,为了实现设备升级,需要一种可靠的网络通信机制,能够让设备发现服务器上的升级文件,并将文件安全地传输到设备中进行应用。
UDP(用户数据报协议)和TCP(传输控制协议)是两种常用的网络通信协议,它们分别适用于不同的场景。UDP适用于广播和简单的数据传输,而TCP适用于建立可靠的连接并进行大规模数据传输。
因此,基于UDP和TCP模拟工厂设备升级过程中的文件传输,有助于理解和展示工业设备升级过程中的网络通信原理和流程。
2、技术分析
- UDP:UDP(用户数据报协议)用于在局域网内广播消息,以便设备能够发现服务器并获取升级文件的信息。
- TCP:TCP(传输控制协议)用于建立可靠的连接,并传输升级文件的数据。
- 文件IO:通过文件I/O,程序能够从文件中读取数据,或者将数据写入文件。可以将程序中的数据持久化到文件中,以便在程序关闭后能够保存数据。
- 并发服务:三种方法使得TCP具有并发处理的能力。分别是:多线程、多进程、IO多路复用(select、poll、epoll)。
- 数据库:嵌入式常用的轻量级小型数据库(sqlite3),用来存储每次升级的设备信息以及每次升级的时间信息等,便于后期管理以及维护。
3、过程概述
- 服务器端创建UDP套接字并设置允许广播;
- 服务器端发送广播消息,告知设备有升级文件可用;
- 客户端接收到广播消息后,提取服务器地址和端口信息,并使用TCP连接请求连接服务器;
- 服务器端并发接受客户端的连接请求,并开始传输文件;
- 客户端接收文件数据,并应用升级。
4、服务器端流程
- 创建UDP套接字并设置允许广播;
- 件可发送广播消息,告知设备有升级文用;
- 创建TCP套接字并绑定端口;
- 监听客户端连接请求;
- 接受客户端连接请求;
- 读取升级文件并发送给客户端;
- 关闭客户端连接(心跳包)。
5、客户端流程
- 创建UDP套接字并绑定端口;
- 接收广播消息;
- 创建TCP套接字并连接服务器;
- 接收文件数据并保存到本地文件;
- 关闭TCP连接。
6、效果展示
7、源码
7.1 master(主控)
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sqlite3.h>
#include <time.h>
#include <sys/stat.h>
int data_base(char *);
//线程函数处理与每一台设备进行数据发送
void *handler(void *arg)
{
//获取文件描述符
int acceptfd = (int)arg;
printf("acceptfd = %d\n", acceptfd);
int fd;
//打开要发送的文件,(要是想通过命令行传参,你可以给线程传入结构体)
if ((fd = open("new.txt", O_RDONLY)) < 0)
{
perror("open:");
return NULL;
}
char buf[64] = {0};
while (1)
{
int n = read(fd, buf, 64);
if (n < 0)
{
perror("read err");
return NULL;
}
if (n == 0)
break;
//发送数据给设备
if (send(acceptfd, buf, n, 0) < 0)
{
perror("send err");
return NULL;
}
}
printf("线程处理结束\n");
close(acceptfd);
close(fd);
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
//UDP
int sockfd;
//搜索协议套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket err:");
return -1;
}
int tv = 1;
//发送方设置为发送广播
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &tv, sizeof(tv)) < 0)
{
perror("setsockopt err:");
return -1;
}
struct sockaddr_in brandaddr;
//填充结构体
brandaddr.sin_family = AF_INET;
brandaddr.sin_port = htons(8888);
brandaddr.sin_addr.s_addr = inet_addr("192.168.0.255");
int len_bra = sizeof(brandaddr);
printf("我已经准备发送\n");
int ret;
//以广播的形式发送这条信息。
ret = sendto(sockfd, "connect", 8, 0, (struct sockaddr *)&brandaddr, len_bra);
if (ret < 0)
{
perror("sendto err:");
return -1;
}
ssize_t num;
printf("我已经发送完成\n");
//TCP
int decfd;
//流式套接字
if ((decfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket err:");
return -1;
}
int optval = 1;
//设置地址重用
setsockopt(decfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
//绑定
struct sockaddr_in ser_addr;
//填充服务器的结构体
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888); //
ser_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len_ser = sizeof(ser_addr);
//绑定
if (bind(decfd, (struct sockaddr *)&ser_addr, len_ser) < 0)
{
perror("bind err is:");
return -1;
}
//启动监听,变为被动打开
if (listen(decfd, 5) < 0)
{
perror("listen err:");
return -1;
}
int dat = data_base(argv[1]);
if(dat)//判断数据库的操作
{
printf("database is err");
return -1;
}
struct sockaddr_in dev_addr;
while (1)
{
char buf[4] = {};
//接收确认信息
num = recvfrom(sockfd, buf, 4, 0, (struct sockaddr *)&dev_addr, &len_bra);
if (strncmp(buf, "yes", 3) == 0)
{
printf("wait accept\n");
int acceptfd;
//链接建立之后的信息交互文件描述符
if ((acceptfd = accept(decfd, (struct sockaddr *)&dev_addr, &len_ser)) < 0)
{
perror("accept err:");
return -1;
}
printf("链接成功 %s %s %d %p\n", inet_ntoa(dev_addr.sin_addr), buf, acceptfd, &acceptfd);
pthread_t tid;
if (pthread_create(&tid, NULL, handler, (void *)acceptfd))
{
perror("pthread create err:");
break;
}
}
}
close(sockfd);
close(decfd);
return 0;
}
int data_base(char *file)
{
sqlite3 *db = NULL; //防止野指针
if (sqlite3_open("./my.db", &db) != 0)
{
//不能用perror, perror只能打印操作系统级别的错误 sqlite是独立的
//fprintf代表 格式化输出到流内
fprintf(stderr, "sqlite_open is err: %s\n", sqlite3_errmsg(db));
//printf("sqlite3_open is err: %s\n",sqlite3_errmsg(db);
return -1;
}
printf("create is success\n");
char *errmsg = NULL;
if (sqlite3_exec(db, "create table version(time char,file_name char,size int);",
NULL, NULL, &errmsg) != SQLITE_OK)
{
fprintf(stderr, "create table is err: %s\n", errmsg);
}
printf("table is success\n");
time_t tim;
struct tm *localtim = NULL;
tim = time(NULL);
localtim = localtime(&tim);
int year = localtim->tm_year + 1900;
int mon = localtim->tm_mon;
int day = localtim->tm_mday;
int hour = localtim->tm_hour;
int min = localtim->tm_min;
int sec = localtim->tm_sec;
char timebuf[32]={0};
sprintf(timebuf, "%d-%02d-%02d %02d:%02d:%02d", year, mon, day, hour, min, sec);
struct stat buf;
stat(file, &buf);
char sql[128] = {0};
sprintf(sql, "insert into version values(\"%s\",\"%s\",%ld);", timebuf, file, buf.st_size);
if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != SQLITE_OK)
{
fprintf(stderr, "create table is err: %s\n", errmsg);
return -1;
}
return 0;
}
7.2 device(设备)
#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//当作搜索协议使用
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)//先打开数据包式套接字
{
perror("socket err:");
return -1;
}
int optval = 1;
//用于本地测试并发的。主要是设置地址重用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
struct sockaddr_in addr;
//填充结构体
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("192.168.0.255");
// addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8888);
socklen_t addrlen = sizeof(addr);
//绑定addr
if (bind(sockfd, (struct sockaddr *)&addr, addrlen) < 0)
{
perror("bind err:");
goto err;
}
ssize_t num;
char buf[8] = {0};
//接受对端ip、port等的结构体
struct sockaddr_in clientaddr;
//接受内容,之后比较接受到的内容
num = recvfrom(sockfd, buf, 8, 0, (struct sockaddr *)&clientaddr, &addrlen);
if(num <0)
{
perror("recvfrom err");
goto err;
}
if (strncmp(buf, "connect", 7) != 0)
{
printf("quit\n");
goto err;
}
sendto(sockfd, "yes", 4, 0, (struct sockaddr *)&clientaddr, addrlen);
printf("回复完成\n");
//搜索协议生命周期结束
close(sockfd);
int decfd;
//新的流式套接字
if ((decfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket err:");
return -1;
}
int optva = 1;
//设置地址重用,为了测试并发
setsockopt(decfd, SOL_SOCKET, SO_REUSEADDR, &optva, sizeof(optval));
//定义结构体,用来接受服务器发送的文件信息等。保证可靠。
struct sockaddr_in dev_addr;
dev_addr.sin_family = AF_INET;
dev_addr.sin_port = htons(8888); //
dev_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int len = sizeof(dev_addr);
//请求链接服务器
if (connect(decfd, (struct sockaddr *)&dev_addr, len) < 0)
{
perror("connect err:");
close(decfd);
return -1;
}
printf("connet success\n");
int fd;
//打开文件,用于后续接收数据
if ((fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0)
{
perror("open:");
close(decfd);
return -1;
}
char buff[64] = {0};
while (1)
{
int ret;
ret = recv(decfd, buff, 64, 0);
if (ret < 0)
{
perror("recv err:");
return -1;
}
else if (ret > 0)
{
//写入文件
write(fd, buff, ret);
memset(buff, 0, 64);
}
else
{
break;
}
}
printf("Upgrade successful\n");
close(decfd);
close(fd);
return 0;
err:
close(sockfd);
return 0;
}
8、注意事项
使用多线程的时候,因为线程的特性,不得不使用线程栈保存文件描述符来保证并发的完成。但是线程传参的时候,传入地址导致传输的地址是同一个。这是因为变量的定义在局部中,结束之后销毁,再次创建变量,编译器可能会再次为变量分配这块内存地址。导致问题一直无法解决。