小目标6:下载文件功能的实现

发布时间 2023-10-07 23:51:11作者: †CS.Renascence

小目标6:下载文件功能的实现

指定文件的下载功能:客户端用户输入服务器目录中的文件名,服务器打开这个文件,读取文件的内容,发送给客户端

 

实现:服务器端打开某个文件并读取文件,然后把内容传给客户端

服务器端定义一个函数server_file_download

用于打开文件读取内容+传送给客户端,我们先定死一个文件

我们把MSG稍微修改了一下,增加了一个bytes属性,buffer扩容到1024,便于后续使用(客户端和服务器都要修改)

 typedef struct msg
 {
     int type;//协议类型  0 表示登陆包  1.文件名传输包 2.文件下载包
     int flag;
     char buffer[1024];//存放除文件名之外的内容。
     char fname[50];//如果type是1  就是文件名传输包,那fname里面就存放着文件名
     int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
 }MSG; //这个结构体会根据业务需求的不断变化,这个结构体会添加新的字段。
 ​

注意补上头文件和宏定义

 #include<fcntl.h>

服务器里面定义一个下载文件的函数

实现内容:打开文件,读取内容,发送给客户端

 //通过打开服务器中的某个文件,并使用socket网络发送给客户端
 //我的文件路径是这样的:Ubantu的主文件夹/hello/hello2
 //hello2是一个文本文件,不需要后缀
 ​
 void server_file_download(int accept_socket)
 {
     MSG file_msg = { 0 };//表示文件内容的信息
     int res = 0;
     int fd; //文件描述符 linux下系统下重要概念,linux任务所有设备都是文件。对文件的打开,对设备的读写都可以使用文件描述符概念
     fd = open("/home/liujiajun/hello/hello2", O_RDONLY);//这里的文件路径要结合自己的来修改
     if (fd < 0)//表示文件打开失败,则返回失败的原因
     {
         perror("file open error:");
         return;
     }
     file_msg.type = MSG_TYPE_DOWNLOAD;//设置消息类型为下载文件
     strcpy(file_msg.fname, "hello2");//初始化文件名
     //在读取文件并把文件传到客户端 这个时候,MSG结构中的buffer就是存放文件的内容,但是一般来说文件都超过1024字节,所以要发送多个包。而且这个MSG结构中
     while ((res = read(fd, file_msg.buffer, sizeof(file_msg.buffer))) > 0) //当read用于读取文件的时候,当文件读到末尾之后,read将返回小于0的信息
     {   //res就是实际读取文件的字节数
         //file_msg.bytes也表示此次读的消息的字节数
         file_msg.bytes = res;
         res = write(accept_socket, &file_msg, sizeof(MSG));//把文件消息发送给服务器
         if (res <= 0)
         {
             perror("server send file error:");
         }
         memset(file_msg.buffer, 0, sizeof(file_msg.buffer));//清缓存便于下一次读取
     }
 }

 

服务器这边在线程函数里面调用server_file_download

更新后的线程函数代码如下,注意看while循环里面的变化

前面要补上宏定义(客户端和服务器)

 #define MSG_TYPE_DOWNLOAD 2

调用部分:

 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     MSG recv_msg = { 0 };
     char buffer[50] = { 0 };
     printf("目录信息发送客户端完成!\n");
     while (1) {
         res = read(acpt_socket, &recv_msg, sizeof(MSG));
         if (res == 0) {
             printf("客户端已经断开\n");
             break;
         }
     
         if (recv_msg.type == MSG_TYPE_FILENAME) {//说明客户端发过来的信息要的是目录
             search_server_dir(acpt_socket);
             memset(&recv_msg, 0, sizeof(MSG));
         }
         else if (recv_msg.type == MSG_TYPE_DOWNLOAD)
         {
             //printf("download函数未调用\n");
             server_file_download(acpt_socket);
             //printf("download函数已调用\n");
             memset(&recv_msg, 0, sizeof(MSG));
         }
         memset(&recv_msg, 0, sizeof(MSG));
     }
 ​
 }

 

服务器这边写好之后,客户端的switch_case里面也要补上相应的代码,按2下载

 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;

和1的差别仅仅在于send_msg.type = MSG_TYPE_DOWNLOAD;部分

 

客户端线程函数也要补上相应的内容,判断接收从服务器来的文件

注意要补上相关的头文件

 #include<sys/stat.h>
 #include<sys/types.h>
 #include<errno.h>
 #include<fcntl.h>

头文件的解释(ChatGPT):

  1. <sys/stat.h>:

    • 这个头文件包含了与文件状态相关的常量和结构体的定义,例如文件的大小、权限、时间戳等信息。

    • 常用的结构体包括 struct stat,它用于存储文件的各种属性信息。

  2. <sys/types.h>:

    • 这个头文件包含了一些基本的系统数据类型的定义,例如整数类型、字符类型和一些系统调用相关的特殊类型。

    • 一些常见的类型包括 size_tpid_toff_t 等。

  3. <errno.h>:

    • 这个头文件包含了一组宏和变量,用于处理错误条件。当系统调用或库函数发生错误时,它们通常会设置一个特定的错误码,程序可以检查这个错误码以确定错误的原因。

    • 常见的宏包括 errno(当前错误码的变量)、perror(将错误信息打印到标准错误输出)以及一系列的错误码宏,如 EIOEINVAL 等。

  4. <fcntl.h>:

    • 这个头文件包含了文件控制操作的常量定义,用于在打开、创建、关闭文件时设置文件属性和行为。

    • 常见的常量包括 O_RDONLY(只读模式)、O_WRONLY(只写模式)、O_RDWR(读写模式)以及各种文件标志(如 O_CREATO_TRUNC 等)。

       

并且补上相关的代码,下面是客户端的线程函数修改后的代码(具体看注释)

 int fd = -1; //这个是用来打开文件进行读写的文件描述符,默认情况下为0表示没有打开文件
 //客户端的线程函数
 void* thread_func(void* arg) {
     int client_socket = *((int*)arg);
     MSG recv_msg = { 0 };
     int res;
     char pwd[100] = { 0 };
     //接受服务器端发来的数据
     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函数 download目录
             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表示写权限
                 fd = open("./download1/hello2", O_CREAT | O_WRONLY, 0666);//打开成功之后肯定会有个文件描述符返回
                 if (fd < 0)//表示创建/写失败
                 {
                     perror("file open error:");
                 }
             }
             //通过上面的创建目录,以及文件描述符的判断通过后,就可以从MSG结构体里面的buffer取数据了
             //recv_msg.buffer存放的就是文件的部分内容,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;
             }
         }
     }
 }

 

完整代码

服务器:

 #include<cstdio>//C++标准库的头文件
 #include<unistd.h>//Unix标准头文件
 #include<sys/types.h>//这个头文件定义了各种系统相关的数据类型
 #include<sys/socket.h>//这个头文件用于网络编程,包含了与套接字(socket)相关的函数和数据结构的声明
 #include<arpa/inet.h>//通常用于处理IP地址和套接字地址的转换
 #include<string.h>//字符串头文件,因为后面有用到memset
 #include<pthread.h>//线程相关
 #include<stdlib.h>
 #include<dirent.h>
 #include<fcntl.h>
 #define MSG_TYPE_LOGIN 0
 #define MSG_TYPE_FILENAME 1
 #define MSG_TYPE_DOWNLOAD 2
 typedef struct msg
 {
     int type;//协议类型  0 表示登陆包  1.文件名传输包 2.文件下载包
     int flag;
     char buffer[1024];//存放除文件名之外的内容。
     char fname[50];//如果type是1  就是文件名传输包,那fname里面就存放着文件名
     int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
 }MSG; //这个结构体会根据业务需求的不断变化,这个结构体会添加新的字段。
 ​
 ​
 ​
 //根据网盘客户端的业务需求,客户端想要查看下服务器这边目录下的文件信息
 //因此服务器必须设置一个功能,把某个目录下的文件名信息全部获取出来发给客户端
 //默认情况下服务器的目录我们默认设置为/home
 void search_server_dir(int accept_socket)//因为函数里面调用了write函数,需要用到套接字
 {
     //opendir是打开Linux目录的api函数
     struct dirent* dir = NULL;
     int res = 0;//存储实际发送信息的字节数
     MSG info_msg = { 0 };//定义信息的结构体并且初始化
     info_msg.type = MSG_TYPE_FILENAME;//设置文件类型
     DIR* dp = opendir("/home/liujiajun");
     if (NULL == dp)
     {
         perror("open dir error:");
         return;
     }
     while (1)
     {
         dir = readdir(dp);
         if (NULL == dir) //如果readdir函数返回是空值,全部目录读取完成
         {
             break;
         }
         if (dir->d_name[0] != '.')//把.隐藏文件过滤掉
         {
             // 清空 info_msg.fname,并将当前文件名拷贝到 info_msg.fname 中
             memset(info_msg.fname, 0, sizeof(info_msg.fname));//每一次输出之后都要把存放文件名的空间重新刷新
             strcpy(info_msg.fname, dir->d_name);//把每一个文件名拷贝到info_msg结构体里面,通过socket发送出去
             // 使用 write 函数将 info_msg 结构体发送给客户端
             res = write(accept_socket, &info_msg, sizeof(MSG));//把信息发送给客户端
             if (res < 0) {
                 perror("send client error:");
                 return;
             }
         }
     }
 }
 ​
 ​
 ​
 //通过打开服务器中的某个文件,并使用socket网络发送给客户端,至于说明文件我们先定一个zhuizhui.txt,那么可以根据实际情况
 void server_file_download(int accept_socket)
 {
     MSG file_msg = { 0 };//表示文件内容的信息
     int res = 0;
     int fd; //文件描述符 linux下系统下重要概念,linux任务所有设备都是文件。对文件的打开,对设备的读写都可以使用文件描述符概念
     fd = open("/home/liujiajun/hello/hello2", O_RDONLY);//这里的文件路径要结合自己的来修改
     if (fd < 0)//表示文件打开失败,则返回失败的原因
     {
         perror("file open error:");
         return;
     }
     file_msg.type = MSG_TYPE_DOWNLOAD;//设置消息类型为下载文件
     strcpy(file_msg.fname, "hello2");//初始化文件名
     //在读取文件并把文件传到客户端 这个时候,MSG结构中的buffer就是存放文件的内容,但是一般来说文件都超过1024字节,所以要发送多个包。而且这个MSG结构中
     while ((res = read(fd, file_msg.buffer, sizeof(file_msg.buffer))) > 0) //当read用于读取文件的时候,当文件读到末尾之后,read将返回小于0的信息
     {   //res就是实际读取文件的字节数
         //file_msg.bytes也表示此次读的消息的字节数
         file_msg.bytes = res;
         res = write(accept_socket, &file_msg, sizeof(MSG));//把文件消息发送给服务器
         if (res <= 0)
         {
             perror("server send file error:");
         }
         memset(file_msg.buffer, 0, sizeof(file_msg.buffer));//清缓存便于下一次读取
     }
 }
 ​
 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     MSG recv_msg = { 0 };
     char buffer[50] = { 0 };
     printf("目录信息发送客户端完成!\n");
     while (1) {
         res = read(acpt_socket, &recv_msg, sizeof(MSG));
         if (res == 0) {
             printf("客户端已经断开\n");
             break;
         }
     
         if (recv_msg.type == MSG_TYPE_FILENAME) {//说明客户端发过来的信息要的是目录
             search_server_dir(acpt_socket);
             memset(&recv_msg, 0, sizeof(MSG));
         }
         else if (recv_msg.type == MSG_TYPE_DOWNLOAD)
         {
             //printf("download函数未调用\n");
             server_file_download(acpt_socket);
             //printf("download函数已调用\n");
             memset(&recv_msg, 0, sizeof(MSG));
         }
         memset(&recv_msg, 0, sizeof(MSG));
     }
 ​
 }
 ​
 int main() {
     int server_socket;//这是一个唯一标识套接字的整数
     int accept_socket;//创建一个存储接受到的客户端连接的套接字文件描述符。
     int res = 0;//后续用到
     MSG recv_msg = { 0 };
     pthread_t thread_id;//线程编号
     char buffer[50] = { 0 };//定义缓冲区,用于暂时存储接收和发送的数据
     //第一步创建套接字描述符
     printf("开始创建TCP服务器\n");
     server_socket = socket(AF_INET, SOCK_STREAM, 0);
     /*
    创建了一个套接字,并将其文件描述符存储在 server_socket 变量中
    AF_INET 表示IPv4地址族
    SOCK_STREAM: 这是套接字类型,表示创建的套接字将使用面向连接的TCP协议
    0: 这是套接字的协议参数,通常设置为0
    */
     if (server_socket < 0) {
         perror("socket create failed:");
         return 0;
     }
 ​
     struct sockaddr_in server_addr;//存储套接字信息的变量
     server_addr.sin_family = AF_INET;//指定了地址族为 AF_INET
     server_addr.sin_addr.s_addr = INADDR_ANY;//表示服务器将接受来自任何可用网络接口的连接请求
     server_addr.sin_port = htons(6666);//端口号不可以直接用数字赋值,htons将主机字节序(通常是小端字节序)的端口号转换为网络字节序(大端字节序)
 ​
     //如果运行时出现了报错Address already in use如何解决
     int optvalue = 1;
     setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &optvalue, sizeof(optvalue));
 ​
     if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
         perror("server bind error:");
         return 0;
     }
 ​
     if (listen(server_socket, 10) < 0) {
         perror("server listen error:");
         return 0;
     }
     
     printf("TCP服务器准备完成,等待客户端的连接\n");
     
     
     
     while (1) //服务器将持续接收和发送数据,直到手动停止程序。
     {
         accept_socket = accept(server_socket, NULL, NULL);
         printf("有客户端连接到服务器!\n");
         //创建一个线程
         pthread_create(&thread_id, NULL, thread_fun, &accept_socket);
     }
 ​
     return 0;
 }

客户端

 #include<cstdio>
 #include<unistd.h>
 #include<arpa/inet.h>
 #include<string.h>
 #include<stdlib.h>
 #include<pthread.h>//线程相关
 #include<sys/stat.h>
 #include<sys/types.h>
 #include<errno.h>
 #include<fcntl.h>
 ​
 ​
 #define MSG_TYPE_LOGIN 0
 #define MSG_TYPE_FILENAME 1
 #define MSG_TYPE_DOWNLOAD 2
 typedef struct msg
 {
     int type;//协议类型  0 表示登陆包  1.文件名传输包 2.文件下载包
     int flag;
     char buffer[1024];//存放除文件名之外的内容。
     char fname[50];//如果type是1  就是文件名传输包,那fname里面就存放着文件名
     int bytes;//这个字段用来记录传输文件时每个数据包实际的文件字节数
 }MSG; //这个结构体会根据业务需求的不断变化,这个结构体会添加新的字段。
 ​
 ​
 int fd = -1; //这个是用来打开文件进行读写的文件描述符,默认情况下为0表示没有打开文件
 //客户端的线程函数
 void* thread_func(void* arg) {
     int client_socket = *((int*)arg);
     MSG recv_msg = { 0 };
     int res;
     char pwd[100] = { 0 };
     //接受服务器端发来的数据
     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函数 download目录
             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表示写权限
                 fd = open("./download1/hello2", O_CREAT | O_WRONLY, 0666);//打开成功之后肯定会有个文件描述符返回
                 if (fd < 0)//表示创建/写失败
                 {
                     perror("file open error:");
                 }
             }
             //通过上面的创建目录,以及文件描述符的判断通过后,就可以从MSG结构体里面的buffer取数据了
             //recv_msg.buffer存放的就是文件的部分内容,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\t0、退出系统\n");
     printf("=====================================================================\n");
     printf("请选择你要执行的操作: ");
 ​
 }
 int main() {
     int client_socket;
     pthread_t thread_id;
     int res;
     char c;
     char buffer[50] = { 0 };//创建缓冲区,后面会用到
     struct sockaddr_in server_addr;// server_addr,用于存储套接字的地址信息。
 ​
     MSG send_msg = { 0 };//定义发送给服务器消息的结构体,表示要执行哪一个命令的消息
     //MSG recv_msg = { 0 };/*定义接受的结构体*/
     
     client_socket = socket(AF_INET, SOCK_STREAM, 0);
     if (client_socket < 0) {
         perror("client socket failed:");
         return 0;
     }
 ​
 ​
     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();
     
     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':
                 break;
             case '0':
                 return 0;
             
         }
         sleep(1);//延迟一会儿再输出UI界面,这样可以让UI界面在输出目录之后执行
         net_disk_ui();
     }
     
     return 0;
 }

运行结果

服务器:

客户端:

最关键的是在Ubantu的client运行的目录下确实多了一个download1文件夹和hello2的文件