小目标5:查询文件功能实现

发布时间 2023-10-07 14:58:41作者: †CS.Renascence

小目标5:查询文件功能实现

这次我们来实现一下如何通过键盘按下不同的键来执行查询文件和下载文件

 

获取按键的循环

我们在客户端程序里面写一个循环,来获取按键

 char c;
 while (1) 
 {
     c = getchar();
     switch (c) {
         case '1':
             //要让服务器给我们发送目录信息
 ​
             printf("1 press\n");
             break;
         case '2':
             break;
         case '3':
             break;
         case '0':
             return 0;
     }
 }

因为这个while循环本身也是死循环,那么我们要让客户端也创建线程,让接受服务器的数据的代码放到线程里面,所以再增加一个线程函数

 

之前代码的修改

首先之前的代码不支持多个客户端下的查询文件,所以我们在客户端也使用多线程的方式,我们在客户端里面增加下面的代码

 //客户端的线程函数
 void* thread_func(void* arg) {
     int client_socket = *((int*)arg);
     MSG recv_msg = { 0 };
     int res;
     while (1) {
         res = read(client_socket, &recv_msg, sizeof(MSG));
         if (res == 0) {//说明客户端已经端口服务器的连接了,read默认状况下是堵塞模式
             printf("客户端已经断开\n");
         }
         if (recv_msg.type == MSG_TYPE_FILENAME) {
             printf("server path filename=%s\n", recv_msg.fname);
             memset(&recv_msg, 0, sizeof(MSG));
         }
     }
 }

写好线程函数之后就要调用,在客户端中该位置补上下面的代码

 //补上头文件
 #include<pthread.h>//线程相关
 //补上下面代码
 ……
 printf("客户端连接服务器成功!\n");
 pthread_t thread_id;//创建线程ID
 pthread_create(&thread_id, NULL, thread_func, &client_socket);//调用创建线程的函数
 net_disk_ui();
 ……

 

实现键盘按1,查询文件的功能

客户端部分增加代码

 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':
             break;
         case '3':
             break;
         case '0':
             return 0;
     }
 }

 

服务器也要修改一下接受客户端消息的机制,改成要接收MSG结构体的消息

我们在thread_fun函数中进行修改

 void* thread_fun(void* arg) {
     int acpt_socket = *(int*)arg;
     int res;
     MSG recv_msg = { 0 };
     char buffer[50] = { 0 };
     //search_server_dir(acpt_socket);//调用发送函数
     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));
         }
     }
 ​
         //printf("client read %s\n", buffer);
         ////向accept_socket写入buffer中数据,写的数据的字节数为res
         //write(acpt_socket, buffer, res);
         //memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
         
         memset(&recv_msg, 0, sizeof(MSG));
     }
 ​
 }

 

一些小小的修改:

在客户端里面把UI函数的内容和位置都进行一下修改

修改后的UI函数位置——放到了循环里面

修改后的UI函数内容——去掉了system那一行

 

完整代码(有较大的改动,有的上面没有写到)

客户端部分

 #include<cstdio>
 #include<unistd.h>
 #include<arpa/inet.h>
 #include<string.h>
 #include<stdlib.h>
 #include<pthread.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* thread_func(void* arg) {
     int client_socket = *((int*)arg);
     MSG recv_msg = { 0 };
     int res;
     //接受服务器端发来的数据
     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));
         }
     }
 }
 ​
 void net_disk_ui()
 {
     //清空屏幕并且打印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':
                 break;
             case '3':
                 break;
             case '0':
                 return 0;
             
         }
         sleep(1);//延迟一会儿再输出UI界面,这样可以让UI界面在输出目录之后执行
         net_disk_ui();
     }
     
     return 0;
 }

服务器部分

 #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;
     MSG recv_msg = { 0 };
     char buffer[50] = { 0 };
     //search_server_dir(acpt_socket);//调用发送函数
     
     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));
         }
     
         //printf("client read %s\n", buffer);
         ////向accept_socket写入buffer中数据,写的数据的字节数为res
         //write(acpt_socket, buffer, res);
         //memset(buffer, 0, sizeof(buffer));//缓冲区清零,便于接收下一次的数据
         
         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;
 }

 

运行结果

服务器端显示:

客户端显示: