项目_Linux_网络编程_私人云盘

概述

项目功能总述:

该项目使用TCP进行通信,实现文件的上传和下载。云盘的文件同步有手动同步、实时同步、定时同步这三种。本项目主要实现的是手动同步的功能,重点训练在如何使用TCP进行文件传输。

选择TCP的原因:

文件的传输需要可靠,因此选择TCP传输。

项目中需要解决的问题:

  • 文件如何上传和下载
  • 文件大小不确定如何处理
  • 文件个数不确定如何处理
  • 如何获取文件事件来实现实时同步
  • 如何使用定时器和守护进程来实现定时同步

传输文件时要解决的关键问题:

  • 如何处理文件路径,从而使得创建新文件的路径名正确
  • 如何处理TCP连包问题,封包拆包的自定义协议如何设定

编写Makefile

makefile编写整体思路:

该makefile的编写参考博文"8.Linux_Makefile" - "8、makefile编写技巧" - "8.3 分文件处理"。博文连接如下:8.Linux_Makefile-CSDN博客

文件创建: 

创建一个文件夹linux_test,在该文件夹下创建 "src、inc、obj目录"、"makefile文件"、"test.c文件"

在src文件中也创建一个 "makefile文件"

主目录下的makefile代码如下:

SRCDIR = $(shell pwd)/src
INCDIR = $(shell pwd)/inc
OBJDIR = $(shell pwd)/obj
SRC = $(wildcard $(SRCDIR)/*.c)
OBJ = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRC)))
MAIN_SRCDIR = $(shell pwd)
SERVER_SRC = $(wildcard $(MAIN_SRCDIR)/server.c)
CLIENT_SRC = $(wildcard $(MAIN_SRCDIR)/client.c)
SERVER_OBJ = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SERVER_SRC)))
CLIENT_OBJ = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(CLIENT_SRC)))
CC = gcc
CFLAGS = -c -g -Wall -I $(INCDIR)
export OBJ OBJDIR CC CFLAGS all:debug $(SRCDIR) $(SERVER_OBJ) $(CLIENT_OBJ) server client
debug:
#	@echo "OBJDIR = $(OBJDIR)"
#	@echo "TEST_OBJ = $(TEST_OBJ)"
#调用其他makefile进行编译、汇编
$(SRCDIR):echo@make -C $@
echo:@echo "make start"
#编译、汇编server.c client.c 文件
$(SERVER_OBJ):server.c@$(CC) $(CFLAGS) $^ -o $@
$(CLIENT_OBJ):client.c@$(CC) $(CFLAGS) $^ -o $@
#链接全部.o生成可执行文件server client
server:$(OBJ) $(SERVER_OBJ)@$(CC) $^ -o server 
client:$(OBJ) $(CLIENT_OBJ)@$(CC) $^ -o client.PHONY:clean
clean:rm ./obj/*

src目录下的makefile代码如下:

all:debug $(OBJ)
debug:
#	@echo "OBJ = $(OBJ)"
#	@echo "OBJDIR = $(OBJDIR)"
$(OBJDIR)/%.o:%.c@$(CC) $(CFLAGS) $^ -o $@

TCP通信功能实现 

下面内容是以多进程并发形式编写服务端与客户端的代码,主要目的是实现通信的框架,使得双方可以进行通信。

 概述:

网络相关的代码封装在my_net.c、my_net.h中。

基本功能就是实现服务器与客户端可以相互通信。这里先使用单线程的方式进行框架的搭建。

具体实现逻辑见博文"13.1 Linux_网络编程_TCP/UDP" - "TCP" - "并发" - "多进程" 博文连接如下:

13.1 Linux_网络编程_TCP/UDP-CSDN博客

基本功能的my_net.h函数总览:

#ifndef __MY_NET_H
#define __MY_NET_H#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>#define BACKLOG 5 	//最大接入客户端数量//TCP Server
int Socket_TcpServerInit(int argc,char** argv);
int Socket_TcpServerAccept(int fd);
//TCP Client
int Socket_TcpClientInit(int argc,char** argv);
//通用
ssize_t Socket_Read(int fd, void *buf, size_t count);
ssize_t Socket_Write(int fd, void *buf, size_t count);
//多进程并发
void Set_SIGCHLD(void);
void SIGCHLD_Handler(int sig);#endif

1、服务端

1、初始化socket服务器

该函数实现了创建IPv4的TCPsocket、地址快速复用、绑定端口号、监听客户端的功能。

/** socket_init: 初始化socket服务器* @param argc: main中的argc* @param argv: main中的argv* @ret 创建的socket文件描述符,如果失败会自动退出进程,该值不需要判断有效性* */
int Socket_TcpServerInit(int argc,char** argv){int fd;struct sockaddr_in addr;//参数有效性判断if(argc != 3){printf("param err\n");printf("%s<ip><port>\n",argv[0]);exit(-1);}printf("server ip = %s\n",argv[1]);printf("server port = %s\n",argv[2]);//1.创建socketif((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议perror("socket");exit(-1);}//地址快速重用int flag=1,len=sizeof(int);if(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,len) == -1){perror("setsockopt");exit(-1);}//2.绑定IP、端口号addr.sin_family = AF_INET;    				  //IPv4addr.sin_port = htons(atoi(argv[2])); 		  //端口号,要转化为大端子节序addr.sin_addr.s_addr = inet_addr(argv[1]);    //IP地址:0表示在本网络上的本主机,即:自己if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){perror("bind");exit(-1);}//3.监听socketif(listen(fd,BACKLOG) == -1){ 	//允许最多接入5个客户端perror("listen");exit(-1);}return fd;
}

 2、优化的accept函数

这个函数主要功能还是accept,但是封装了一些错误处理和接入客户端信息打印的操作。

/** Socket_TcpServerAccept: 优化的accept函数* @param fd: 服务器的socket文件描述符* @ret: 接入的客户端的socket文件描述符,如果失败会自动退出进程,该值不需要判断有效性* */
int Socket_TcpServerAccept(int fd){int newfd;struct sockaddr_in newAddr;socklen_t newAddrlen;do{//printf("Debug:again accept\n");newfd = accept(fd,(struct sockaddr*)&newAddr,&newAddrlen);}while(newfd < 0 && errno == EINTR);//如果是信号中断导致的错误,则重新执行acceptif(newfd < 0){perror("accept");exit(-1);}else{printf("[%s,%d]connect,fd=%d\n",inet_ntoa(newAddr.sin_addr),ntohs(newAddr.sin_port),newfd);}return newfd;
}

2、客户端

 1、初始化socket客户端

该函数实现了创建IPv4的TCPsocket、设置要连接服务器的信息、连接服务器的操作

/** Socket_TcpClientInit: 初始化socket客户端* @param argc: main中的argc* @param argv: main中的argv* @ret 创建的socket文件描述符,如果失败会自动退出进程,该值不需要判断有效性* */
int Socket_TcpClientInit(int argc,char** argv){int fd;struct sockaddr_in addr;//参数有效性判断if(argc != 3){printf("param err\n");printf("%s<ip><port>\n",argv[0]);exit(-1);}printf("connect server ip = %s\n",argv[1]);printf("connect server port = %s\n",argv[2]);//1.创建socketif((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议perror("socket");exit(-1);}//2.设置链接服务器的IP、端口号addr.sin_family = AF_INET;    				  //IPv4addr.sin_port = htons(atoi(argv[2])); 		  //端口号,要转化为大端子节序addr.sin_addr.s_addr = inet_addr(argv[1]);    //IP地址:0表示在本网络上的本主机,即:自己//3.链接服务器if(connect(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){perror("connect");exit(-1);}return fd;
}

3、通用读/写 

1、优化的read函数

该函数主要功能还是read,但封装了一些错误处理、读前清空缓冲区、自动回收退出的客户端资源的操作。

/** Socket_Read: 优化的read函数* @param: 参数含义与read一样* @ret: 返回值含义与read一样,如果失败会自动退出进程,该值不需要判断有效性* */
ssize_t Socket_Read(int fd, void *buf, size_t count){ssize_t	read_num;//读取数据do{//printf("Debug:again read\n");//清空缓冲区memset(buf,0,count);read_num = read(fd,buf,count);}while(read_num < 0 && errno == EINTR);//如果是信号中断导致的错误,则重新执行readif(read_num < 0){perror("read");exit(-1);}else if(read_num == 0){printf("Debug:read_num = 0,close fd=%d\n",fd);close(fd);}return read_num;
}

2、 优化的write函数

该函数主要功能还是write,但封装了一些错误处理、自动回收退出的客户端资源的操作。

/** Socket_Write: 优化的write函数* @param: 参数含义与write一样* @ret: 返回值含义与write一样,如果失败会自动退出进程,该值不需要判断有效性* */
ssize_t Socket_Write(int fd, void *buf, size_t count){ssize_t	write_num;//写入数据do{//printf("Debug:again write\n");write_num = write(fd,buf,count);}while(write_num < 0 && errno == EINTR);//如果是信号中断导致的错误,则重新执行readif(write_num < 0){perror("write");exit(-1);}else if(write_num == 0){printf("Debug:write_num = 0,close fd=%d\n",fd);close(fd);}return write_num;
}

4、多进程并发

1、设置信号捕获

该函数作用是将SIGCHLD信号捕获,处理函数设置为SIGCHLD_Handler。设置后,当子进程的状态发生改变时,内核就会自动调用SIGCHLD_Handler进行处理。

/** Set_SIGCHLD:捕获SIGCHLD信号,处理函数为SIGCHLD_Handler* */
void Set_SIGCHLD(void){struct sigaction act;act.sa_handler = SIGCHLD_Handler;sigemptyset(&act.sa_mask);act.sa_flags = SA_RESTART;//让因为信号而终止的系统调用继续运行if(sigaction(SIGCHLD,&act,NULL) != 0){perror("sigaction");exit(-1);}
}

2、编写信号处理函数

该函数主要的实现就是非阻塞式的回收子进程,并打印回收时子进程退出的一些状态。这是实现多进程并发必须要做的事情,使用信号回收子进程可以使得父进程不需要阻塞等待,而是去执行其他的事情。 

/** SIGCHLD_Handler:SIGCHLD的信号处理函数* param sig:SIGCHLD的信号值* */
void SIGCHLD_Handler(int sig){int wstatus;waitpid(-1,&wstatus,WNOHANG);if(WIFEXITED(wstatus)){      //判断子进程是否正常退出printf("子进程的返回值为%d\n",WEXITSTATUS(wstatus));}else{printf("子进程是否被信号结束%d\n",WIFSIGNALED(wstatus));printf("结束子进程的信号类型%d\n",WTERMSIG(wstatus));}
}

5、main实现

server.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "cloudStorage.h"int main(int argc,char** argv){int fd,newfd;pid_t pid;fd = Socket_TcpServerInit(argc,argv);Set_SIGCHLD();//以信号方式回收子进程while(1){newfd = Socket_TcpServerAccept(fd);//父进程处理接收客户端链接的问题//子进程处理与客户端交互的问题if((pid=fork()) == -1){perror("fork");return -1;}else if(pid == 0){close(fd);//对于子进程,socket返回的fd没有用char buf[100];while(1){Socket_Read(newfd,buf,100);printf("read:%s\n",buf);}printf("child exit\n");exit(0);}else{close(newfd);//对于父进程,accept返回的newfd没有用}}return 0;
}

client.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "cloudStorage.h"/** 传参: <ip><port><mode>[pathname]* mode: u--上传 d--下载* 当mode = u时,需要填写pathname参数* */
int main(int argc,char** argv){int sockfd;sockfd = Socket_TcpClientInit(argc,argv);Socket_Write(sockfd,"hello",strlen("hello"));//开发中ingreturn 0;
}

代码执行结果如下:

忽略这里的传参,这是最终版本的传参,因此有d这个参数。重点是该代码实现了通信功能。

TCP连包与处理思路

1、TCP连包

什么是TCP连包:

TCP发送数据并不是一个接一个发的,而是以字节流的形式发送。这就代表,如果客户端连续发送多个数据,但服务器只读一次。服务器可能会读到客户端发送的两次数据。

验证实验:

server.c代码:

#include <stdio.h>
#include <string.h>
#include "my_net.h"int main(int argc,char** argv){int fd,newfd;char read_buf[1024] = {0};fd = Socket_TcpServerInit(argc,argv);while(1){newfd = Socket_TcpServerAccept(fd);while(1){if(Socket_Read(newfd,read_buf,sizeof(read_buf)-1)>0){printf("read:%s\n",read_buf);write(newfd,"hello\n",strlen("hello\n"));}else{break;}printf("block\n");getchar();//这句目的是让服务器只进行一次read}}return 0;
}

client.c代码:

#include <stdio.h>
#include <string.h> 
#include <my_net.h>int main(int argc,char** argv){int fd;fd = Socket_TcpClientInit(argc,argv);while(1){write(fd,"Client\n",strlen("Client\n"));//快速不断发送数据,当缓冲区写满时会阻塞}return 0;
}

代码运行结果如下:

2、解决方案

问题分析:

产生连包的问题,原因在于使用多个write后,使用了一个read允许读取大量的数据。比如发送数据client,这个数据的长度只有6个字节,而在read端读取了sizeof(buf),这是允许了读取1024个字节,所以一次性可以读取多个client值。

解决方案:

该问题的产生是因为读取端的读取内容太多了,从而导致多个数据被一起读出。解决方法就是读取指定的数据长度,这样就可以保证数据不会粘连。比如要连续发送数据client,那么首先发送client的长度6,再发送client数据;read端先读出client长度,再根据这个长度读出指定的字节数。流程框图如下:

实验代码:

server.c与client.c: 

代码运行结果:

新建文件/目录处理思路

1、思路分析

路径实现的问题: 

新建文件可以使用open函数,新建目录可以使用mkdir函数。但不论是文件还是目录,最重要的就是路径。比如服务器存放文件的路径是/home/download,服务器接收到文件后新建的文件/目录的路径应该是/home/download/filename。可以看到,这就是字符串的拼接,因此处理路径的思路,就是形成一系列的字符串操作函数。

获取路径的方式:

在本项目中,要获得一个文件的路径,那么首先需要文件名filename,这个文件名有客户端发来,服务器收到后调用字符串操作函数将filename进行拼接,最终实现路径。

2、相关函数实现

2.1 获取文件名

获取文件名函数是最核心的字符串处理函数,它可以将收到的字符串去掉末尾的 '/' 或者去掉前缀的一些字符。能够实现的字符串修改有:

  • ./linux/hello.c -> hello.c 
  • ./linux/hello.c -> linux/hello.c 
  • ./linux/ -> ./linux
/** GetFileName:从字符串中提取文件名 文件名格式为:xxx/xxx/文件名(以'/'分隔)* @param str:要进行处理的目录* @param mode1: 去除前缀* 0:不开启去除前缀* 1:提取最后一个'/'后的内容 ./linux/hello.c -> hello.c      适用于在当前路径下拼接* 2:提取第一个'/'后的内容 ./linux/hello.c -> linux/hello.c  适用于拼接指定路径* @param mode2: 去除后缀的'/'* 0:不开启去除后缀* 1:去除后缀的 '/' ./linux/ -> ./linux* @ret: NULL--err other--提取出来的文件名首地址* */
char* GetFileName(char* str,char mode1,char mode2){char* point = str;char* tmp = NULL;//参数有效性判断if(point == NULL){printf("str is NULL\n");return NULL;}//去除前缀if(mode1){point = str;while(*point != '\0'){if(*point == '/'){   //找到'/',这之后的一个字符就是文件名的开头tmp = point + 1;if(mode1 == 2){break;}}point++;}}//去除后缀if(mode2){point = str;if(*(point + strlen(point) - 1) == '/'){ //将文件末尾的 '/' 删除*(point + strlen(point) - 1) = '\0';}}if(tmp == NULL){ //遍历时没有找到'/',默认这个字符串就是文件名return str;}else{ 			 //找到了'/',那么tmp保存的地址就是文件名字符串的首地址return tmp;}
}

2.2 创建新文件

创建新文件就是使用open的选项O_CREAT来创建,使用选项O_EXCL来获得重名错误并进行重名操作。

重名就是在文件名后加上一个"-v1",本函数设定了可以加"-v1"~"-v9"。对于文件,它可能有后缀,比如test.txt,这时文件重名后应该为test-v1.txt。要实现这一操作首先需要对后缀.txt进行保存,之后再将v1接到test后,.txt接到-v1后。

/** CreateNewFile:在指定目录新建并打开一个文件,有重名处理操作* @param NewFilePath:新文件的目录* @ret -1--err other--新文件的文件描述符* */
int CreateNewFile(char* NewFilePath){int fd;char* point = NewFilePath;int i = 1;char suffix[30] = {0};char buf[100] = {0};if(NewFilePath == NULL){printf("NewFilePath is NULL");return -1;}//保存后缀while(*point != '\0'){if(*point == '.'){if(*(point+1) != '/' && *(point+1) != '.'){// ./xxx 和 ../xxx 这种情况排除break;}}point++;}strcpy(suffix,point);//printf("Debug:suffix = %s\n",suffix);//printf("Debug:NewFilePath = %s\n",NewFilePath);while(1){if((fd = open(NewFilePath,O_RDWR|O_CREAT|O_EXCL,0666)) < 0){//出错原因是重名,进行重名处理操作if(errno == EEXIST){sprintf(buf,"-v%d%s",i,suffix);//重名后缀-v%d 后缀%sif(i==1){ 		//第一次重名只删除后缀,不删除重名后缀i++;*(NewFilePath + strlen(NewFilePath) - strlen(suffix)) = '\0';}else if(i<10){ //之后重名,先删除全部后缀,再添加新后缀 	i++;*(NewFilePath + strlen(NewFilePath) - strlen(suffix) - strlen("-v1")) = '\0';}else{ 			//重名太多了,退出新建printf("too many same name file\n");return -1;}strcat(NewFilePath,buf);//printf("Debug:NewFilePath = %s\n",NewFilePath);}//其他出错原因直接退出else{perror("open");return -1;}}//没有出错直接退出while循环else{break;}}return fd;
}

2.3 判断是否为目录

目录与文件的处理方式不同,所以这里编写了一个根据路径判断是否为目录的函数。

/** IsFileDir:判断文件是否为目录文件* @param filename:要进行检查的文件的文件路径* @ret -1--err 0--不是dir 1--是dir* */
int IsFileDir(char* filename){struct stat statbuf;//1.获取文件属性	if(stat(filename,&statbuf) == EOF){perror("stat");return -1;}//2.判断是否为目录if(S_ISDIR(statbuf.st_mode)){//printf("Debug:this is dir\n");return 1;}else{return 0;}
}

2.4 创建新目录

创建新目录就是使用mkdir函数来创建新目录。 

/** CreateNewDir:创建新的目录,有重名处理操作* @param NewDirPath:新目录的路径* @ret -1--err 0--success* */
int CreateNewDir(char* NewDirPath){if(NewDirPath == NULL){printf("NewDirPath is NULL");return -1;}if(mkdir(NewDirPath,0777) == -1){//出错原因是重名,进行重名处理操作if(errno == EEXIST){}//其他出错原因直接退出else{perror("mkdir");return -1;}}return 0;
}

传输文件功能实现 

1、整体思路分析

设计思路:

由于TCP连包的问题,我们不能单纯的认为客户端write一些数据,服务器read就是读到客户端发来的数据。根据"TCP连包与处理思路"中的思路,我们可以先发送数据的长度,再发送数据的内容,服务器根据数据的长度,限定性的读取数据的内容,从而解决连包的问题。

整个发送过程分为:封包 -> 发送

整个接收过程分为:拆包->创建指定文件->接收文件内容

文件数据包结构体:

要发送的数据有文件名、文件内容。当目录处理时还需要将文件以链表形式进行管理,具体结构体声明如下:

//文件数据包
typedef struct tFile{int 		type; 			//类型,写TYPE_FILEint  		namelen; 		//文件名长度long long 	filelen;  		//文件大小char* 		filename; 		//文件名char* 		filedata; 		//文件数据int 		filedatalen; 	//实际申请的暂存空间char* 		filepath; 		//本地端的文件路径struct tFile* pNext;  		//用于发送目录时形成链表
}file_t;

注意:当传入的参数为./linux/file时,filename存放的值为file,filepath存放的值为./linux/file。

2、相关函数实现

注意:本代码存在缺陷,严重缺少动态开辟空间的释放,但程序功能单一,多进程模式下子进程退出后会自动回收资源,所以影响不大,但代码仍需完善。

2.1 封包

封包的内容很简单,就是申请一个结构体的空间,并将文件名、数据包类型等一些参数写入这个结构体。

/** packet_file:封包操作--文件数据包* @param filename:要封包的文件路径* @ret NULL--err other--文件数据包* */
file_t* packet_file(char* pathname){struct stat statbuf;file_t* pFile = NULL;//1.申请数据包空间,并标注"数据包类型"和"本地端的文件路径"if((pFile = (file_t*)malloc(sizeof(file_t))) == NULL){printf("pFile malloc err\n");return NULL;}pFile->type = TYPE_FILE;//数据包类型if((pFile->filepath = (char*)malloc(sizeof(char) * strlen(pathname))) == NULL){printf("pFileTmp->filepath malloc err\n");}strcpy(pFile->filepath,pathname);//本地端的文件路径//printf("Debug:pFile->filepath = %s\n",pFile->filepath);//2.获取文件属性,并将文件大小 文件名大小写入包中	if(stat(pathname,&statbuf) == EOF){perror("stat");return NULL;}pFile->filelen = statbuf.st_size;  				   //文件大小pFile->namelen = strlen(GetFileName(pathname,1,1));//文件名大小//3.申请文件名暂存空间,并赋值文件名if((pFile->filename = (char*)malloc(sizeof(char)*pFile->namelen+1)) == NULL){printf("pFile->filename malloc err\n");free(pFile);return NULL;}pFile->filename[sizeof(char)*pFile->namelen] = '\0';strcpy(pFile->filename,GetFileName(pathname,1,1));//4.申请文件暂存空间,不进行赋值,在发送时赋值if(pFile->filelen > 1024){  //申请的空间最大为1024字节if((pFile->filedata = (char*)malloc(sizeof(char)*1024)) == NULL){printf("pFile->filedata malloc err\n");free(pFile->filename);free(pFile);return NULL;}pFile->filedatalen = 1024;}else{if((pFile->filedata = (char*)malloc(sizeof(char)*pFile->filelen)) == NULL){printf("pFile->filedata malloc err\n");free(pFile->filename);free(pFile);return NULL;}pFile->filedatalen = sizeof(char)*pFile->filelen;}return pFile;
}

2.2 发送文件

发送文件首先调用封包函数获得一个数据包,之后将其按双方约定的顺序进行发出。发送顺序为:类型->名字长度->文件长度->文件名->文件内容。

/** SendFile:发送文件* @param sockfd:进行通信的socket文件描述符* @param pathname:要进行上传的文件路径* @ret -1--err 0--success* */
int SendFile(int sockfd,char* pathname){int fd,read_num;long long send_num = 0;file_t* pFile = NULL;//1.封包操作if((pFile = packet_file(pathname)) == NULL){printf("packet_file err\n");return -1;}//2.发送文件操作//2.1 发送数据包类型send_num = Socket_Write(sockfd,&pFile->type,sizeof(pFile->type));//printf("Debug:SendFile--pFile->type = %d\n",pFile->type);//printf("send_num = %lld\n",send_num);//2.2 发送名字长度send_num = Socket_Write(sockfd,&pFile->namelen,sizeof(pFile->namelen));//printf("Debug:SendFile--pFile->namelen = %d\n",pFile->namelen);//printf("send_num = %lld\n",send_num);//2.3 发送文件长度send_num = Socket_Write(sockfd,&pFile->filelen,sizeof(pFile->filelen));//printf("Debug:SendFile--pFile->filelen = %lld\n",pFile->filelen);//printf("send_num = %lld\n",send_num);//2.4 发送文件名send_num = Socket_Write(sockfd,pFile->filename,pFile->namelen);//printf("Debug:SendFile--pFile->filename = %s\n",pFile->filename);//printf("send_num = %lld\n",send_num);//2.5 发送文件内容if((fd = open(pathname,O_RDONLY)) < 0){perror("open");return -1;}send_num = 0;while(1){//当文件内容足够时,读满缓存区if(pFile->filelen >= pFile->filedatalen){read_num = Socket_Read(fd,pFile->filedata,pFile->filedatalen);}//当文件内容不够时,只读出剩余的文件的内容else{read_num = Socket_Read(fd,pFile->filedata,pFile->filelen);}//将读出的数据发送出去Socket_Write(sockfd,pFile->filedata,read_num);//减去读出的read_num,代表剩余多少内容要读pFile->filelen -= read_num;send_num += read_num;//读完后代表发送完成,退出循环	if(pFile->filelen == 0){break;}}//printf("Debug:send_num = %lld\n",send_num);return 0;
}

2.3 拆包

拆包就是将发送的数据分批次接收,并将存放到结构体中。 接收的顺序应与发送的顺序一致:名字长度->文件长度->文件名->文件内容。

/** unpacket_file:拆包操作--文件数据包* @param sockfd:进行通讯的socket文件描述符* @ret NULL--err other--文件数据包* */
file_t* unpacket_file(int sockfd){long long read_num;file_t* pFile = NULL;//1.申请数据包空间,并接收文件名大小 文件大小if((pFile = (file_t*)malloc(sizeof(file_t))) == NULL){printf("pFile malloc err\n");return NULL;}read_num = Socket_Read(sockfd,&pFile->namelen,sizeof(pFile->namelen));//文件名大小//printf("Debug:unpacket_file--pFile->namelen = %d\n",pFile->namelen);//printf("Debug:read_num = %lld\n",read_num);read_num = Socket_Read(sockfd,&pFile->filelen,sizeof(pFile->filelen));//文件大小//printf("Debug:unpacket_file--pFile->filelen = %lld\n",pFile->filelen);//printf("Debug:read_num = %lld\n",read_num);//3.申请文件名暂存空间,并赋值文件名if((pFile->filename = (char*)malloc(sizeof(char)*pFile->namelen+1)) == NULL){printf("pFile->filename malloc err\n");free(pFile);return NULL;}pFile->filename[sizeof(char)*pFile->namelen] = '\0';read_num = Socket_Read(sockfd,pFile->filename,pFile->namelen);//文件名//printf("Debug:unpacket_file--pFile->filename = %s\n",pFile->filename);//printf("Debug:read_num = %lld\n",read_num);//4.申请文件暂存空间,不进行赋值,在接收时赋值if(pFile->filelen > 1024){  //申请的空间最大为1024字节if((pFile->filedata = (char*)malloc(sizeof(char)*1024)) == NULL){printf("pFile->filedata malloc err\n");free(pFile->filename);free(pFile);return NULL;}pFile->filedatalen = 1024;}else{if((pFile->filedata = (char*)malloc(sizeof(char)*pFile->filelen)) == NULL){printf("pFile->filedata malloc err\n");free(pFile->filename);free(pFile);return NULL;}pFile->filedatalen = sizeof(char)*pFile->filelen;}read_num++;//防止报警告,该语句无实际意义return pFile;
}

2.4 接收文件

接收文件首先需要调用拆包文件获取一个数据包,之后对文件进行新建并将文件数据写入新文件。 

/** RecvFile:接收文件* @param sockfd:进行通讯的socket文件描述符* @param downloadPath:下载路径的主目录* @param mode: 1--用于接收单个文件 0--用于接收目录* @param pDir: 在接收单个文件时写NULL,接收目录时写入RecvDir的返回值* @ret -1--err 0--success* */
int RecvFile(int sockfd,char* downloadPath,char mode,dir_t* pDir){int fd;long long write_num,read_num;file_t* pFile = NULL;char NewFilePath[1024];//1.拆包操作pFile = unpacket_file(sockfd);//2.接收文件数据//2.1 创建新文件if(mode == 1){sprintf(NewFilePath,"%s/%s",downloadPath,pFile->filename);}else{sprintf(NewFilePath,"%s/%s/%s",downloadPath,pDir->filename,pFile->filename);}printf("Debug:Socket_RecvFile -- after sprintf:%s\n",NewFilePath);if((fd = CreateNewFile(NewFilePath)) < 0){return -1;}//2.2 接收文件内容并写入新文件write_num = 0;while(1){//当文件内容足够时,读满缓存区if(pFile->filelen >= pFile->filedatalen){read_num = Socket_Read(sockfd,pFile->filedata,pFile->filedatalen);}//当文件内容不够时,只读出剩余的文件的内容else{read_num = Socket_Read(sockfd,pFile->filedata,pFile->filelen);}//将读取的内容写入到文件中Socket_Write(fd,pFile->filedata,read_num);//减去读出的read_num,代表剩余多少内容要读write_num += read_num;pFile->filelen -= read_num; //读完后代表发送完成,退出循环	if(pFile->filelen == 0){break;}}//printf("Debug:write_num = %lld\n",write_num);close(fd);return 0;
}

传输目录功能实现

1、整体思路分析

设计思路:

目录的操作主要是创建目录以及递归的传输文件。目录相关的数据传送与文件一样,都是先传输数据长度再传输数据内容,这里的难点是如何将一个目录抽象为结构体。

对于一个目录,使用readdir读出的成员名称可能是目录也可能是文件,这时就需要两个链表来进行管理。因为目录下面的目录也满足上述的特点,所以目录的处理应该是一种递归处理。

目录数据包结构体:

//目录数据包
typedef struct tDir{int 			type; 		//类型,写TYPE_DIRint  			namelen; 	//本目录名长度char* 			filename; 	//本目录名char* 			filepath; 	//本地端的文件路径struct tFile* 	pFile;  	//指向目录中文件的链表struct tDir* 	pDir; 		//指向目录中目录的链表struct tDir*    pNext;      //用于形成多个目录的链表 
}dir_t;

2、相关函数实现

2.1 封包

封包操作的难点在于何时使用递归。在本函数前面一部分的操作,我们将当前目录的路径、目录名、数据包类型写入了数据包中,之后我们需要将目录下的内容写入数据包。

目录的内容分为文件和目录,如果是文件,我们就需要获取文件的数据包,并把它加入到pFile的链表中;如果是目录,我们就需要获取目录的数据包(这就是递归),并把它加入到pDir的链表中;

/** packet_dir:封包操作--目录数据包* param pathname:要进行封包的目录路径* @ret NULL--err other--目录数据包* */
dir_t* packet_dir(char* pathname){dir_t* pDir = NULL;file_t* pFileTmp = NULL;dir_t* pDirTmp = NULL;char pathnameBuf[1024];   //拷贝传入的pathnamechar memberPathBuf[2048]; //用于存放拼接之后的路径名DIR* dp = NULL;struct dirent* dt = NULL;//1.参数有效性检查,是否为目录if(IsFileDir(pathname) != 1){printf("pathname is not dir\n");return NULL;}//2.申请数据包空间,并标注"数据包类型"和"本地的目录的路径"if((pDir = (dir_t*)malloc(sizeof(dir_t))) == NULL){printf("pDir malloc err\n");return NULL;}pDir->pFile = NULL;pDir->pDir = NULL;pDir->type = TYPE_DIR;//数据包类型if((pDir->filepath = (char*)malloc(sizeof(char) * strlen(pathname))) == NULL){printf("pDir->filepath malloc err\n");return NULL;}strcpy(pDir->filepath,pathname);//本地的目录的路径//3.获取目录名字并写入包中strcpy(pathnameBuf,pathname);pDir->namelen = strlen(GetFileName(pathnameBuf,2,1));if((pDir->filename = (char*)malloc(sizeof(char) * pDir->namelen)) == NULL){printf("pDir->filename malloc err\n");free(pDir);return NULL;}strcpy(pDir->filename,GetFileName(pathnameBuf,2,1));//printf("Debug:pDir->filename = %s\n",pDir->filename);//4.获取目录中的内容if((dp = opendir(pathnameBuf)) == NULL){ //打开目录perror("opendir");return NULL;}while(1){dt = readdir(dp); //读取目录if(dt == NULL){ //读取完成break;}if(strcmp(dt->d_name,".") != 0 && strcmp(dt->d_name,"..") != 0){ // .和..这俩个目录不进行处理sprintf(memberPathBuf,"%s/%s",pathnameBuf,dt->d_name); //获取到可以访问的文件的本地路径//printf("memberPathBuf = %s\n",memberPathBuf);//这是一个文件if(IsFileDir(memberPathBuf) == 0){ 		if((pFileTmp = packet_file(memberPathBuf)) == NULL){ //封包文件break;}if(pDir->pFile == NULL){ 			//第一个目录下的成员pDir->pFile = pFileTmp; }else{ 								//不是第一个成员,头插法入链表pFileTmp->pNext = pDir->pFile;pDir->pFile = pFileTmp;}}//这是一个目录else if(IsFileDir(memberPathBuf) == 1){if((pDirTmp = packet_dir(memberPathBuf)) == NULL){//封包目录break;}if(pDir->pDir == NULL){ 			//第一个目录下的成员pDir->pDir = pDirTmp;}else{ 								//不是第一个成员,头插法入链表pDirTmp->pNext = pDir->pDir;pDir->pDir = pDirTmp;}}}}closedir(dp); //关闭目录/*//Debugfile_t* filePoint = pDir->pFile;dir_t*  dirPoint = pDir->pDir;//printf("pDir->filename = %s\n",pDir->filename);//printf("pDir->namelen = %d\n",pDir->namelen);while(filePoint!=NULL){//	printf("filePoint->namelen = %d\n",filePoint->namelen);//	printf("filePoint->filelen = %lld\n",filePoint->filelen);//	printf("filePoint->filename = %s\n",filePoint->filename);//	printf("filePoint->filepath = %s\n",filePoint->filepath);filePoint = filePoint->pNext;}while(dirPoint!=NULL){//	printf("dirPoint->namelen = %d\n",dirPoint->namelen);//	printf("dirPoint->filename = %s\n",dirPoint->filename);//	printf("dirPoint->filepath = %s\n",dirPoint->filepath);dirPoint = dirPoint->pNext;}*/return pDir;
}

2.2 发送目录

发送目录的难点也是在于何时使用递归,本函数的前面讲当前的文件包类型、目录名长度、目录名发送了出去,之后就需要发送本目录下的内容,当是文件是就直接调用发送文件的函数即可,当是目录时就需要调用发送目录的函数(这就是递归)。

/** SendDir:发送目录文件* @param sockfd:进行通讯的socket文件描述符* @param downloadPath:下载路径的主目录* @param pNowDir:递归参数,第一次时写NULL,递归时会自动写入非NULL参数* @ret NULL--err other--目录数据包* */
dir_t* SendDir(int sockfd,char *pathname,dir_t* pNowDir){dir_t* pDir = NULL;dir_t* pDirPoint = NULL;file_t* pFilePoint = NULL;dir_t* pDirHead = NULL;//1.封包操作,只有第一次进行封包if(pNowDir == NULL){if((pDir = packet_dir(pathname)) == NULL){printf("packet_dir err\n");return NULL;}}else{pDir = pNowDir;}pDirHead = pDir;//2.遍历整个数据包发送文件//2.1发送当前目录的名称Socket_Write(sockfd,&pDir->type,sizeof(pDir->type));Socket_Write(sockfd,&pDir->namelen,sizeof(pDir->namelen));Socket_Write(sockfd,pDir->filename,pDir->namelen);//2.2发送当前目录的文件内容pFilePoint = pDir->pFile;while(pFilePoint!=NULL){//printf("Debug:pFilePoint->filepath = %s\n",pFilePoint->filepath);	SendFile(sockfd,pFilePoint->filepath);pFilePoint = pFilePoint->pNext;}//2.3发送当前目录的目录内容pDirPoint = pDir->pDir;while(pDirPoint!=NULL){SendDir(sockfd,pDirPoint->filepath,pDirPoint);pDirPoint = pDirPoint->pNext;}	return pDirHead;
}

2.3 拆包

拆包操作主要是拆出目录的名字,为后面接收目录创建新目录作准备。

/** unpacket_dir:拆包操作--目录数据包* @param sockfd:进行通讯的socket文件描述符* @ret NULL--err other--目录数据包* */
dir_t* unpacket_dir(int sockfd){dir_t* pDir;//1.申请数据包空间if((pDir = (dir_t*)malloc(sizeof(dir_t))) == NULL){printf("pDir malloc err\n");return NULL;}//2.接收目录名大小Socket_Read(sockfd,&pDir->namelen,sizeof(pDir->namelen));printf("Debug:pDir->namelen = %d\n",pDir->namelen);//3.接收目录名if((pDir->filename = (char*)malloc(sizeof(char) * pDir->namelen)) == NULL){printf("pDir->filename malloc err\n");free(pDir);return NULL;}Socket_Read(sockfd,pDir->filename,pDir->namelen);printf("Debug:pDir->filename = %s\n",pDir->filename);return pDir;
}

2.4 接收目录

发送目录时是讲目录下的文件和目录都发出去,接收目录时只是讲目录下的子目录接收并创建新的目录,对于目录下的文件,这是由接收文件函数完成的。

/** RecvDir:接收目录文件* @param sockfd:进行通讯的socket文件描述符* @param downloadPath: 下载路径的主目录* @ret NULL--err other--目录数据包* */
dir_t* RecvDir(int sockfd,char* downloadPath){dir_t* pDir;char NewDirPath[2048];//1.拆包操作if((pDir = unpacket_dir(sockfd)) == NULL){printf("unpacket_dir err\n");return NULL;}//2.构建新目录的路径sprintf(NewDirPath,"%s/%s",downloadPath,pDir->filename);printf("Debug:after sprintf NewDirPath = %s\n",NewDirPath);//3.创建新的目录if(CreateNewDir(NewDirPath) == -1){return NULL;}return pDir;
}

上传与下载功能实现

1、上传

上传数据主要有上传文件、上传目录这两种。会调用IsFileDir来判断路径是文件还是目录,之后根据判断发送相应的类型信息。类型信息发送完毕后,开始调用传输函数进行数据传输。

/** upload:上传文件* @param sockfd:进行通信的socket文件描述符* @param pathname:要进行上传的文件路径* */
void upload(int sockfd,char* pathname){int mode = TRANSMODE_NONE;//1.发送传输类型if(IsFileDir(pathname) == 1){mode = TRANSMODE_DIR;}else if(IsFileDir(pathname) == 0){mode = TRANSMODE_FILE;}else{printf("IsFileDir err\n");return ;}Socket_Write(sockfd,&mode,sizeof(mode));//2.进行传输数据if(mode == TRANSMODE_FILE){ 		//传输文件SendFile(sockfd,pathname);		}else if(mode == TRANSMODE_DIR){ 	//传输目录SendDir(sockfd,pathname,NULL);	}	
}

2、下载

下载数据时,首先需要读取上传函数发送的类型信息,从而判断本次传输的是文件还是目录。之后调用接收数据函数进行数据的接收。

/** DownLoad:下载文件* @param sockfd: 进行通信的socket文件描述符* @param downloadPath: 下载路径的主目录* */
void DownLoad(int sockfd,char* downloadPath){dir_t* pDir = NULL;int type = TYPE_NONE;	int mode = TRANSMODE_NONE;//1.读取传输类型:文件/目录Socket_Read(sockfd,&mode,sizeof(mode));//2.进行传输if(mode == TRANSMODE_FILE){			//下载文件Socket_Read(sockfd,&type,sizeof(type));RecvFile(sockfd,downloadPath,1,pDir);}else if(mode == TRANSMODE_DIR){ 	//下载目录while(1){	Socket_Read(sockfd,&type,sizeof(type));//printf("type = %d\n",type);if(type == TYPE_FILE){RecvFile(sockfd,downloadPath,0,pDir);}else if(type == TYPE_DIR){pDir = RecvDir(sockfd,downloadPath);	}type = TYPE_NONE;}}
}

main代码实现

客户端与服务端代码

server.c代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "cloudStorage.h"int main(int argc,char** argv){int fd,newfd;pid_t pid;int mode = TRANSMODE_NONE;fd = Socket_TcpServerInit(argc,argv);Set_SIGCHLD();//以信号方式回收子进程while(1){newfd = Socket_TcpServerAccept(fd);//父进程处理接收客户端链接的问题//子进程处理与客户端交互的问题if((pid=fork()) == -1){perror("fork");return -1;}else if(pid == 0){close(fd);//对于子进程,socket返回的fd没有用//接收传输请求类型:上传/下载Socket_Read(newfd,&mode,sizeof(mode));if(mode == TRANSMODE_UPLOAD){ 			//客户端请求上传,则服务器下载文件DownLoad(newfd,SERVER_NEWFIREDIR);	}else if(mode == TRANSMODE_DOWNLOAD){ 	//客户端请求下载,则服务器进入浏览模式printf("get require DownLoad\n");//开发中ing//Server_ShellCommunicate(newfd,SERVER_NEWFIREDIR);}printf("child exit\n");exit(0);}else{close(newfd);//对于父进程,accept返回的newfd没有用}}return 0;
}

client.c代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "cloudStorage.h"/** 传参: <ip><port><mode>[pathname]* mode: u--上传 d--下载* 当mode = u时,需要填写pathname参数* */
int main(int argc,char** argv){int sockfd;int mode = TRANSMODE_NONE;sockfd = Socket_TcpClientInit(argc,argv);if(!strcmp(argv[3],"u")){mode = TRANSMODE_UPLOAD;Socket_Write(sockfd,&mode,sizeof(mode));	upload(sockfd,argv[4]);}else if(!strcmp(argv[3],"d")){mode = TRANSMODE_DOWNLOAD;Socket_Write(sockfd,&mode,sizeof(mode));//开发中ing//	Client_ShellCommunicate(sockfd,CLIENT_NEWFIREDIR);}return 0;
}

测试截图

1.下载路径配置

 2.上传文件测试

 3.文件重名处理测试

 4.上传目录测试

其他问题

1、视频播放器安装

安装:sudo apt-get install vlc

使用:vlc <视频文件>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/3060.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

细腻的链接:C++ list 之美的解读

细腻的链接&#xff1a;C list 之美的解读 前言&#xff1a; 小编在前几日刚写过关于vector容器的内容&#xff0c;现在小编list容器也学了一大部分了&#xff0c;小编先提前说一下学这部分的感悟&#xff0c;这个部分是我学C以来第一次感到有难度的地方&#xff0c;特别是在…

Java之包,抽象类,接口

目录 包 导入包 静态导入 将类放入包 常见的系统包 抽象类 语法规则 注意事项&#xff1a; 抽象类的作用 接口 实现多个接口 接口间的继承 接口使用实例 &#xff08;法一&#xff09;实现Comparable接口的compareTo()方法 &#xff08;法二&#xff09;实现Comp…

qt QDragEnterEvent详解

1、概述 QDragEnterEvent是Qt框架中用于处理拖放进入事件的一个类。当用户将一个拖拽对象&#xff08;如文件、文本或其他数据&#xff09;拖动到支持拖放操作的窗口部件&#xff08;widget&#xff09;上时&#xff0c;系统会触发QDragEnterEvent事件。这个类允许开发者在拖拽…

永恒之蓝漏洞复现

永恒之蓝漏洞复现 1 实验准备 1台靶机 win7 关闭防火墙 控制面板->系统和安全->Windows 防火墙 192.168.184.131 1台攻击者 kali 192.168.184.129 2 实施攻击 kali操作 1.输入msfconsole回车 2.搜索ms17_010模块 msf6 > search ms17_010 3.选择编号为3的模块 use 3…

c++拷贝构造函数

1.拷贝构造函数 拷贝构造函数的调用时机 class A { public://默认构造函数A(){m_Hp 100;cout << "A默认构造函数调用完毕" << endl;}//有参构造函数A(int hp){m_Hp hp;cout << "A有参构造函数调用完毕" << endl;}A(const A&…

排序算法的分类、时间空间复杂度

排序是计算机科学和数学中的基本操作&#xff0c;有多种不同的方式&#xff0c;每种方式都有其特定的时间复杂度和空间复杂度。以下是对排序方式的分类及其时间复杂度和空间复杂度的详细分析&#xff1a; 一、排序方式的分类 排序方式主要分为两大类&#xff1a;比较排序和非…

【MMAN-M2】基于缺失模态编码器的多多头关注网络

abstract&#xff1a; 多模态融合是多模态学习领域的研究热点。以往的多模态融合任务大多是基于完整模态的。现有的缺失多模态融合研究没有考虑模态的随机缺失&#xff0c;缺乏鲁棒性。大多数方法都是基于缺失模态和非缺失模态之间的相关性&#xff0c;而忽略了缺失模态的语境…

【AI绘画】Stable Diffusion 基础教程! 如何写出好的prompt,一些技巧和原则

前言 Stable Diffusion 教程-中文 Ask AI for ART Original txt2img and img2img modes 基础模式之 文生图/图生图 基础入门部分 所有的AI设计工具&#xff0c;安装包、模型和插件&#xff0c;都已经整理好了&#xff0c;&#x1f447;获取~ 输入一段话&#xff0c;生成一…

C++ —— 网络通信

之前在Linux系统下介绍了多种实现网络通信的方式&#xff0c;从本文开始后面的文章将在Windows系统下用C为大家介绍技术&#xff0c;敬请期待~。 话不多说&#xff0c;直接进入正文&#xff0c;我们知道&#xff0c;要完成网络通信要用到非常多的函数&#xff0c;并且函数的参数…

FPGA视频GTH 8b/10b编解码转PCIE3.0传输,基于XDMA中断架构,提供工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的PCIE方案我已有的 GT 高速接口解决方案 3、PCIE基础知识扫描4、工程详细设计方案工程设计原理框图输入Sensor之-->芯片解码的HDMI视频数据组包基于GTH高速接口的视频传输架构GTH IP 简介GTH 基本结构GTH 发送和接收处理…

java基础之 String\StringBuffer\ StringBuilder

文章目录 String字符串的创建为什么说String是不可变的&#xff1f;创建后的字符串存储在哪里&#xff1f;字符串的拼接String类的常用方法 StringBuilder & StringBuffer使用方法验证StringBuffer和StringBuilder的线程安全问题 总结三者区别什么情况下用运算符进行字符串…

深度解析阿里的Sentinel

1、前言 这是《Spring Cloud 进阶》专栏的第五篇文章&#xff0c;这篇文章介绍一下阿里开源的流量防卫兵Sentinel&#xff0c;一款非常优秀的开源项目&#xff0c;经过近10年的双十一的考验&#xff0c;非常成熟的一款产品。 文章目录如下&#xff1a; 2、什么是sentinel&…

移远通信推出全星系多频段高精度定位定向GNSS模组LG580P,引领高精度导航新时代

近日&#xff0c;全球领先的物联网整体解决方案供应商移远通信宣布&#xff0c;正式发布其全星系多频段高精度GNSS模组LG580P。该模组具备高精度、高稳定性、低功耗等特点&#xff0c;并支持20Hz RTK Heading 更新频率&#xff0c;为智能机器人、精准农业、测量测绘、自动驾驶…

Python设计模式探究:单例模式实现及应用解析

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

企业微信会话存档引用com.tencent.wework.Finance出错?

报错&#xff1a; 会话存档引用com.tencent.wework.Finance出错&#xff0c;找不到该类&#xff0c;报错如下&#xff1a;java.lang.NoClassDefFoundError: Could not initialize class com.tencent.wework.Finance 这个问题怎么解决&#xff1f; 解决方案&#xff1a;需要下载…

【前端基础】盒子模型

目标&#xff1a;掌握盒子模型组成部分&#xff0c;使用盒子模型布局网页区域 01-选择器 结构伪类选择器 基本使用 作用&#xff1a;根据元素的结构关系查找元素。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8">…

鸿蒙开发:ArkUI Toggle 组件

ArkUI提供了一套完整的UI开发工具集&#xff0c;帮助开发者高效完成页面的开发。它融合了语言、编译器、图形构建等关键的应用UI开发底座&#xff0c;为应用的UI开发提供了完整的基础设施&#xff0c;包括简洁的UI语法、丰富的UI功能以及实时界面预览工具等&#xff0c;可以支持…

【LeetCode每日一题】——802.找到最终的安全状态

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【题目提示】七【解题思路】八【时空频度】九【代码实现】十【提交结果】 一【题目类别】 图 二【题目难度】 中等 三【题目编号】 802.找到最终的安全状态 四【题目描述】 有一个有…

安利一款开源企业级的报表系统SpringReport

SpringReport是一款企业级的报表系统&#xff0c;支持在线设计报表&#xff0c;并绑定动态数据源&#xff0c;无需写代码即可快速生成想要的报表&#xff0c;可以支持excel报表和word报表两种格式&#xff0c;同时还可以支持excel多人协同编辑&#xff0c;后续考虑实现大屏设计…

考公人数攀升?地信、测绘、地质、遥感等专业,能报考哪些单位

近年来&#xff0c;考公人数持续飙升&#xff0c;国考报名人数更逐年攀升。2025年国家公务员考试共有341.6万人通过资格审查&#xff0c;报录比达86:1。国考报名人数再创新高。 国家公务员考试时间安排 地理学相关岗位分析 地信属于地理科学类&#xff0c;测绘类中不包括地信&…