小目标4:网盘UI界面+查询文件功能

发布时间 2023-10-06 21:22:01作者: †CS.Renascence

网盘UI界面

大致的逻辑是这样的,定义一个函数,清空当前屏幕然后print界面内容

 void net_disk_ui()
 {
     //清空屏幕并且打印UI界面
     system("clear");
     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("请选择你要执行的操作: ");
 ​
 }

注意system("clear");函数的调用需要头文件

 #include<stdlib.h>

 

写完net_disk_ui函数之后我们在连接客户端之后调用一下即可

 ……
 printf("客户端连接服务器成功!\n");
 net_disk_ui();
 ……

 

查询功能的实现

查询功能:客户端按下1这个字符,来获取服务器端默认目录下的所有文件信息,并且把这些文件信息传递给客户端,客户端显示出来(文件信息包括文件名)

但是我们这一节先不管按下1的过程,我们实现的是一打开服务器,服务器就执行函数来查询文件并且发送给客户端(按下1的之后再实现)

服务器查询文件逐条打印

我们在服务器定义一个函数search_server_dir来读取服务器文件目录下(默认设置为home目录 )的文件名,并且发给客户端

我们需要如下函数:

  1. opendir:这是一个用于打开目录的系统调用。它接受一个目录路径作为参数,然后返回一个指向 DIR 结构的指针,该结构用于表示目录流。如果打开失败,它将返回 NULL

  2. readdir:这是一个用于读取目录中的文件和子目录的库函数。它接受一个指向 DIR 结构的指针,并返回一个指向 struct dirent 结构的指针,该结构包含了目录项的信息,包括文件名。每次调用 readdir,它返回下一个目录项,直到目录中的所有项都被读取完毕,此时返回 NULL

 void search_server_dir(int accept_socket)
 {
  
   
     DIR* dp = opendir("/home/liujiajun");//定义一个变量表示文件的路径地址
     if (NULL == dp)//如果打开目录失败
     {
         perror("open dir error:");
         return;
     }
    struct dirent* dir = NULL;//定义一个变量表示读到的内容地址
     while (1)//不停读取目录文件
     {
         dir = readdir(dp);
         if (NULL == dir) //如果readdir函数返回是空值,全部目录读取完成
         {
             break;
         }
         if (dir->d_name[0] != '.')//把.隐藏文件过滤掉
         {
             printf("name=%s\n", dir->d_name);
         }
     }
 }

写完之后在服务器启动之前来调用这个函数

 ……
 int server_socket;//这是一个唯一标识套接字的整数
 server_socket = socket(AF_INET, SOCK_STREAM, 0);
 search_server_dir();
 printf("开始创建TCP服务器\n");
 ……
消息的结构化+服务器内容传递给客户端的实现

接下来要做的就是把服务器的内容传递给客户端,但是传统的信息传递用的是字符串,我们如果把所有的文件名都拼接成字符串然后传递的话不是很方便,我们可以在服务器和客户端中都定义一个结构体MSG用来表示传递的信息(注意服务器和客户端的结构体的内容要一致)

 #define MSG_TYPE_LOGIN 0
 #define MSG_TYPE_FILENAME 1//宏定义表示类型增加可读性
 typedef struct msg {
     int type;//协议类型: 0 表示登录协议包,1表示文件名传输包
     int flag;//后面用到
     char buffer[128];//存放除了文件名以外的内容
     char fname[50];//如果type是1(上面用宏定义增加可读性),就是文件名传输包,那么fname里面就存放文件名
 ​
 }MSG;//这个结构体会根据业务需求而不断变化,结构体后面可能会添加新的字段
 ​

 

原来文件名只是在服务器中打开然后直接printf,并没有整理成信息传递给客户端,所以我们要在服务器之前的search_server_dir()中增加一些内容把信息放到MSG结构体里面。修改后的代码如下,新增的部分添加了注释

 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] != '.')//把.隐藏文件过滤掉
         {
             printf("name=%s\n", dir->d_name);
             memset(info_msg.fname, 0, sizeof(info_msg.fname);//每一次输出之后都要把存放文件名的空间重新刷新
             strcpy(info_msg.fname, dir->d_name);//把每一个文件名拷贝到info_msg结构体里面,通过socket发送出去
             res = write(accept_socket, &info_msg, sizeof(MSG));//把信息发送给客户端
             if (res < 0) {
                 perror("send client error:");
                 return ;
             }
         }
     }
 }
 ​

因为现在要把套接字发送出去,所以调用这个函数的地方也要改一下,不能在服务器启动之前就直接调用,我们改成在线程函数里面调用

 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     char buffer[50];
     search_server_dir(acpt_socket);/*这里调用发送函数!!!!*/
     printf("目录信息发送给客户端完成\n");
     while (1) {
         res = read(acpt_socket, buffer, sizeof(buffer));
         printf("client read %s\n", buffer);
         //向accept_socket写入buffer中数据,写的数据的字节数为res
         write(acpt_socket, buffer, res);
         memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
 ​
 }

我们服务器修改了,客户端也要修改一下代码来接收数据,增加的代码如下:

 int res = 0;
 MSG recv_msg = { 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));
     }
 ​
 }
 ​
 //并且把原来的接受信息的代码注释掉
 /*现在改用MSG来接受数据,这部分代码就不用啦!
 //我们用户在客户端终端连续输入字符串,回车表示把数据发送出去
 int res = 0;
 while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
     res = write(client_socket, buffer, sizeof(buffer));
     printf("send bytes=%d\n", res);
     memset(buffer, 0, sizeof(buffer));
     res = read(client_socket, buffer, sizeof(buffer));
     printf("recv from server info:%s\n", buffer);
     memset(buffer, 0, sizeof(buffer));
 ​
 }
 */

 

补充内容:如果运行时出现了报错Address already in use如何解决

如果我们服务器程序退出之后,然后又立刻打开服务器程序,就是报这个错,这是因为ip地址和端口号是系统资源,必须把它设置为端口号可以重复使用

在服务器中增加下面的代码即可解决该问题

 //如果运行时出现了报错Address already in use如何解决
 int optvalue = 1;
 setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &optvalue, sizeof(optvalue));
 ​

上述代码的解释:

  1. int optvalue = 1;:整数变量optvalue将用于设置SO_REUSEADDR选项的值。

  2. setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &optvalue, sizeof(optvalue));:这一行代码使用setsockopt函数来设置套接字选项。下面是各个参数的解释:

    • server_socket:这是一个套接字描述符,表示你想要设置选项的套接字。

    • SOL_SOCKET:这是套接字选项的级别,它表示我们要设置的选项属于通用套接字选项。SO_REUSEADDR是通用选项之一。

    • SO_REUSEADDR:这是要设置的具体选项,表示允许重用本地地址。这个选项的作用是,在套接字关闭后,允许其他套接字快速绑定到相同的本地地址,而不会出现"Address already in use"错误。

    • &optvalue:这是一个指向存储选项值的内存位置的指针,这里是optvalue变量的地址。1表示启用套接字重用

    • sizeof(optvalue):这是指定存储选项值的内存块的大小,通常使用sizeof操作符来获取变量的大小。

 

全部代码如下

服务器部分

 #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>
 #define MSG_TYPE_LOGIN 0
 #define MSG_TYPE_FILENAME 1//宏定义表示类型增加可读性
 typedef struct msg {
     int type;//协议类型: 0 表示登录协议包,1表示文件名传输包
     int flag;//后面用到
     char buffer[128];//存放除了文件名以外的内容
     char fname[50];//如果type是1,就是文件名传输包,那么fname里面就存放文件名
 ​
 }MSG;//这个结构体会根据业务需求而不断变化,结构体后面可能会添加新的字段
 ​
 ​
 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] != '.')//把.隐藏文件过滤掉
         {
             printf("name=%s\n", dir->d_name);
             memset(info_msg.fname, 0, sizeof(info_msg.fname));//每一次输出之后都要把存放文件名的空间重新刷新
             strcpy(info_msg.fname, dir->d_name);//把每一个文件名拷贝到info_msg结构体里面,通过socket发送出去
             res = write(accept_socket, &info_msg, sizeof(MSG));//把信息发送给客户端
             if (res < 0) {
                 perror("send client error:");
                 return ;
             }
         }
     }
 }
 ​
 ​
 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     char buffer[50] = { 0 };
     search_server_dir(acpt_socket);//调用发送函数
     printf("目录信息发送给客户端完成\n");
     while (1) {
         res = read(acpt_socket, buffer, sizeof(buffer));
         printf("client read %s\n", buffer);
         //向accept_socket写入buffer中数据,写的数据的字节数为res
         write(acpt_socket, buffer, res);
         memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
 ​
 }
 ​
 int main() {
 ​
     //创建一个套接字描述符
     int server_socket;//这是一个唯一标识套接字的整数
     server_socket = socket(AF_INET, SOCK_STREAM, 0);
     /*
    创建了一个套接字,并将其文件描述符存储在 server_socket 变量中
    AF_INET 表示IPv4地址族
    SOCK_STREAM: 这是套接字类型,表示创建的套接字将使用面向连接的TCP协议
    0: 这是套接字的协议参数,通常设置为0
    */
     printf("开始创建TCP服务器\n");
     
     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("~~~~~~~~~~~~~\n");
     printf("TCP服务器准备完成,等待客户端的连接\n");
     //printf("~~~~~~~~~~~~~\n");
     int accept_socket;//创建一个存储接受到的客户端连接的套接字文件描述符。
     int res = 0;//后续用到
     char buffer[50] = { 0 };//定义缓冲区,用于暂时存储接收和发送的数据
     
     pthread_t thread_id;//线程编号
     while (1) //服务器将持续接收和发送数据,直到手动停止程序。
     {
         accept_socket = accept(server_socket, NULL, NULL);
         printf("有客户端连接到服务器!\n");
         //创建一个线程
         //第三个参数是执行线程的函数
         pthread_create(&thread_id, NULL, thread_fun, &accept_socket);
         //read函数就是接受客户端发来的数据,存储到buffer里面,返回值表示实际上从accept_socket那边读取到的字节数
         res = read(accept_socket, buffer, sizeof(buffer));
         printf("client read %s\n", buffer);
         //向accept_socket写入buffer中数据,写的数据的字节数为res
         write(accept_socket, buffer, res);
         memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
     }
 ​
     return 0;
 }

客户端部分:

 #include<cstdio>
 #include<unistd.h>
 #include<arpa/inet.h>
 #include<string.h>
 #include<stdlib.h>
 #define MSG_TYPE_LOGIN 0
 #define MSG_TYPE_FILENAME 1//宏定义表示类型增加可读性
 typedef struct msg {
     int type;//协议类型: 0 表示登录协议包,1表示文件名传输包
     int flag;//后面用到
     char buffer[128];//存放除了文件名以外的内容
     char fname[50];//如果type是1,就是文件名传输包,那么fname里面就存放文件名
 ​
 }MSG;//这个结构体会根据业务需求而不断变化,结构体后面可能会添加新的字段
 ​
 ​
 void net_disk_ui()
 {
     //清空屏幕并且打印UI界面
     system("clear");
     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;
     int res = 0;
     MSG recv_msg = { 0 };/*定义接受的结构体*/
     char buffer[50] = { 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");
     net_disk_ui();
 ​
     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));
         }
 ​
     }
     /*现在改用MSG来接受数据,这部分代码就不用啦!
     //我们用户在客户端终端连续输入字符串,回车表示把数据发送出去
     int res = 0;
     while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
         res = write(client_socket, buffer, sizeof(buffer));
         printf("send bytes=%d\n", res);
         memset(buffer, 0, sizeof(buffer));
         res = read(client_socket, buffer, sizeof(buffer));
         printf("recv from server info:%s\n", buffer);
         memset(buffer, 0, sizeof(buffer));
 ​
     }
     */
     return 0;
 }

 

运行结果如下

服务器:

客户端;