Linux网盘程序——客户端(完整注释版)

发布时间 2023-10-09 11:00:50作者: †CS.Renascence

客户端

 #include<cstdio>//C++标准库的头文件
 #include<unistd.h>//Unix标准头文件
 #include<arpa/inet.h>//通常用于处理IP地址和套接字地址的转换
 #include<string.h>//字符串头文件
 #include<stdlib.h>//包含了一些标准库函数,用于内存分配、释放以及其他一些通用的实用功能
 #include<pthread.h>//线程相关,用于支持多线程编程
 #include<sys/stat.h>//用于获取文件状态信息的函数和数据结构,例如文件的大小、权限
 #include<sys/types.h>//包含了一些系统相关的数据类型和常量,例如文件描述符
 #include<errno.h>//包含了错误码(errno)的定义,用于在程序中处理错误情况
 #include<fcntl.h>//包含了文件控制操作相关的常量和函数,例如打开文件、关闭文件、读取文件等
 ​
 ​
 ​
 ​
 #define MSG_TYPE_LOGIN 0//表示登录类型
 #define MSG_TYPE_FILENAME 1//表示查询文件目录操作类型
 #define MSG_TYPE_DOWNLOAD 2//表示下载文件操作类型
 #define MSG_TYPE_UPLOAD   3//表示上传文件操作类型
 #define MSG_TYPE_UPLOAD_DATA  4//表示上传文件数据类型
 ​
 ​
 typedef struct msg
 {
 int type;//协议类型  0 表示登陆包  1.文件名传输包 2.文件下载包 ……
 char fname[50];//存放文件名
 char buffer[1024];//存放文件数据
 int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
 }MSG; //这个结构体会根据业务需求的不断变化,可能会增减新的字段。
 ​
 ​
 char up_file_name[20] = { 0 };
 int fd = -1; //这个是用来打开文件进行读写的文件描述符,默认情况下为0表示没有打开文件
 //客户端的线程函数
 ​
 ​
 //上传文件数据的线程函数
 void* upload_file_thread(void* args)
 {
 // 客户端实现上传文件到服务器的逻辑思路
 //1 首先要打开文件
 MSG up_file_msg = { 0 };//上传文件消息的结构体变量
 char buffer[1024] = { 0 }; // 用来保存读取文件的数据缓冲区
 int client_socket = *((int*)args); //定义套接字描述符接收形参
 int res = 0;//实际读写字节数
 int fd = -1;//文件描述符
 fd = open("./download/css.txt", O_RDONLY); //打开文件, O_RDONLY表示只读
 if (fd < 0)
 {
 perror("open up file error : ");
 return NULL;
 }
 ​
 ​
 up_file_msg.type = MSG_TYPE_UPLOAD_DATA;//类型设置为上传数据文件,便于服务器识别
 while ((res = read(fd, buffer, sizeof(buffer))) > 0)//从文件中读取数据,并将读取的数据存储到 buffer 中。读取的字节数保存在 res 变量中
 {
 // 要把文件数据内容拷贝到 MSG 结构体中的 buffer 中
 memcpy(up_file_msg.buffer, buffer, res);//将从文件读取的数据复制到 up_file_msg.buffer 中
 up_file_msg.bytes = res;
 //下面的res是复用,和上面的res无关
 res = write(client_socket, &up_file_msg, sizeof(MSG));// write 函数将 up_file_msg 结构体中的数据通过套接字发送给服务器
 memset(buffer, 0, sizeof(buffer));//清缓存
 memset(up_file_msg.buffer, 0, sizeof(up_file_msg.buffer));
 }
 close(fd);//关目录
 }
 ​
 ​
 //客户端的多线程的线程函数
 void* thread_func(void* arg) {
 int client_socket = *((int*)arg);//定义套接字描述符接收形参
 MSG recv_msg = { 0 };//定义接收消息的结构体变量
 int res;//实际读写字节数
 ​
 ​
 //循环read不断接收从服务器端发来的数据
 while (1) {
 res = read(client_socket, &recv_msg, sizeof(MSG));
 if (recv_msg.type == MSG_TYPE_FILENAME) {//服务器发过来的是包含文件名的数据报,就输出文件名
 printf("server path filename=%s\n", recv_msg.fname);
 memset(&recv_msg, 0, sizeof(MSG));
 }
 else if (recv_msg.type == MSG_TYPE_DOWNLOAD)  ///服务器发过来的是包含文件的数据,就做好接收准备
 {
 //1.要确定下这个文件放在哪个目录下,我们可以调mkdir函数创建一个目录:默认东西都放在download1目录下
 if (mkdir("download1", S_IRWXU) < 0)
 {
 if (errno == EEXIST)//如果目录已经存在就无视,否则输出报错信息
 {
 //printf("dir exist continue!\n");
 }
 else
 {
 perror("mkdir error");
 }
 }
 //2.目录创建没问题之后,就要开始创建文件
 if (fd == -1)//如果文件还没有打开过
 {
 //O_CREAT 表示如果文件不存在则创建
 //O_WRONLY 表示文件将以写入方式打开,允许你写入文件的内容。
 //0666 表示文件所有者、文件组和其他用户都有读取和写入权限。
 fd = open("./download1/hello2", O_CREAT | O_WRONLY, 0666);//打开成功之后肯定会有个文件描述符返回
 if (fd < 0)//表示创建/写入失败
 {
 perror("file open error:");
 }
 }
 //通过上面的创建目录,以及文件描述符的判断通过后,就可以从MSG结构体里面的buffer取数据了
 //将 recv_msg.buffer 中的数据写入由文件描述符 fd 标识的文件
 //写入的字节数由 recv_msg.bytes 指定
 res = write(fd, recv_msg.buffer, recv_msg.bytes);
 if (res < 0)//表示写入失败
 {
 perror("file write error:");
 }
 //那么我们怎么判断文件内容都全部发完了呢?可以通过recv_msg.bytes是否小于recv_buffer的最大字节数1024
 if (recv_msg.bytes < sizeof(recv_msg.buffer))
 {
 printf("file download finish!\n");
 close(fd);
 fd = -1;
 }
 }
 }
 }
 ​
 ​
 void net_disk_ui()
 {
 printf("=========================TCP网盘程序=================================\n");
 printf("=========================功能菜单=================================\n");
 printf("\t\t\t1、查询文件\n");
 printf("\t\t\t2、下载文件\n");
 printf("\t\t\t3、上传文件\n");
 printf("\t\t\t4、UI界面\n");
 printf("\t\t\t0、退出系统\n");
 printf("=====================================================================\n");
 printf("请选择你要执行的操作: ");
 ​
 ​
 }
 int main() {
 int client_socket;//创建客户端套接字描述符
 pthread_t thread_id;
 pthread_t thread_send_id;
 int res;
 char c;
 MSG send_msg = { 0 };//定义发送给服务器消息的结构体变量
 ​
 ​
 client_socket = socket(AF_INET, SOCK_STREAM, 0);//创建客户端套接字
 if (client_socket < 0) {
 perror("client socket failed:");
 return 0;
 }
 ​
 ​
 struct sockaddr_in server_addr;// server_addr,用于存储套接字的地址信息
 server_addr.sin_family = AF_INET;//AF_INET 表示IPv4地址族
 server_addr.sin_addr.s_addr = inet_addr("192.168.43.128");//这里填Ubantu虚拟机的网卡IP地址,如果服务器和客户端在同一台机子上,则IP地址可以写成127.0.0.1
 server_addr.sin_port = htons(6666);//端口号赋值
 //创建好套接字之后,通过connect连接到服务器
 if (connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
 perror("connect error!");
 return 0;
 }
 printf("客户端连接服务器成功!\n");
 ​
 ​
 //创建线程,支持多线程
 pthread_create(&thread_id, NULL, thread_func, &client_socket);
 net_disk_ui();//输出UI界面
 ​
 ​
 while (1) 
 {
 c = getchar();
 switch (c) {//根据键盘输入来执行不同的操作
 case '1':
 //要让服务器给我们发送目录信息
 //这个while循环本身也是死循环,那么我们要让客户端也创建线程,让接受服务器的数据的代码放到线程里面
 send_msg.type = MSG_TYPE_FILENAME;//指定命令类型为查询文件目录类型
 res=write(client_socket, &send_msg, sizeof(MSG));//把send_msg数据发送出去
 if (res < 0) {//表示发送失败
 perror("send msg error:");
 }
 memset(&send_msg, 0, sizeof(MSG));//清缓存
 break;
 case '2':
 send_msg.type = MSG_TYPE_DOWNLOAD;//指定命令类型为下载文件操作类型
 res = write(client_socket, &send_msg, sizeof(MSG));
 if (res < 0)
 {
 perror("send msg error:");
 }
 memset(&send_msg, 0, sizeof(MSG));
 break;
 case '3':
 send_msg.type = MSG_TYPE_UPLOAD;//指定命令类型为上传文件操作类型
 strcpy(up_file_name, "css.txt");//上传文件的文件名
 printf("input upload filename:",);
 puts(up_file_name);
 //在上传文件给服务器之前,你要先发送一个数据包给服务器,告诉服务器我这边准备上传文件了
 strcpy(send_msg.fname, up_file_name);
 res = write(client_socket, &send_msg, sizeof(MSG));
 if (res < 0)
 {
 perror("send upload package error:");
 continue;
 }
 memset(&send_msg, 0, sizeof(MSG));
 pthread_create(&thread_send_id, NULL, upload_file_thread, &client_socket);
 break;
 //由于考虑到上传文件是需要比较长的时间,考虑到如果文件很大,那么就需要非常长的时间,这个时候如果写
 // //在这里那么久卡诺导致其他功能卡住排队等待,因此需要把发送文件内容的代码放进线程里面,因此需要创建线程
 // //还需要创建一个新的线程,来专门处理文件上传任务
 case '\n':
 net_disk_ui();
 break;
 case '0':
 return 0;
 ​
 ​
 }
 printf("按4即可显示UI界面\n");
 }
 ​
 ​
 return 0;
 }