Linux网盘程序——服务器端(完整注释版)

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

服务器

 #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//表示下载文件操作类型
 #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; //这个结构体会根据业务需求的不断变化,可能会增减新的字段。
 ​
 ​
 //扣1 实现查询文件目录的函数
 //根据网盘客户端的业务需求,客户端想要查看服务器这边目录下的文件信息
 //因此服务器必须设置一个功能,把某个目录下的文件名信息全部获取出来发给客户端
 //默认情况下服务器的目录我们默认设置为/home
 void search_server_dir(int accept_socket)//因为函数里面调用了write函数,需要用到套接字
 {
     struct dirent* dir = NULL; //存储目录信息的变量
     int res = 0;    //存储实际发送信息的字节数
     MSG info_msg = { 0 };   //定义信息的结构体并且初始化
     info_msg.type = MSG_TYPE_FILENAME;//设置类型为查询文件目录
     DIR* dp = opendir("/home/liujiajun");//打开目录并且把指针存在dp变量里面
     if (NULL == dp)
     {
         perror("open dir error:");
         return;
     }
     while (1)
     {
         
         dir = readdir(dp);//读取指定目录下的下一个目录项,并将其信息存储在 dir 结构体指针中,用于遍历读取文件
         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);
             // 使用 write 函数将 info_msg 结构体通过套接字发送给客户端
             res = write(accept_socket, &info_msg, sizeof(MSG));
             if (res < 0) {//发送失败
                 perror("send client error:");
                 return;
             }
         }
     }
 }
 ​
 ​
 //扣2 实现下载文件的函数
 //打开服务器中的某个文件,读取内容,并使用socket网络发送给客户端
 //具体文件先定一个zhuizhui.txt写死,后面延伸的时候修改
 void server_file_download(int accept_socket)
 {
     MSG file_msg = { 0 };//表示传文件内容消息的结构体定义
     int res = 0;//实际读写字节数
     int fd; //文件描述符 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用于读取文件的时候,当文件读到末尾之后,res=0
     {   //res就是实际读取文件的字节数
         //file_msg.bytes也表示此次读的消息的字节数
         file_msg.bytes = res;
         res = write(accept_socket, &file_msg, sizeof(MSG));//把file_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;//实际读写字节数变量
     char up_file_name[20] = { 0 };//文件名变量
     int fd = -1;//文件描述符变量
     MSG recv_msg = { 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)//客户端指定任务为下载文件
         {
             server_file_download(acpt_socket);//调下载文件目录的函数
             memset(&recv_msg, 0, sizeof(MSG));//清缓存
         }
         else if (recv_msg.type == MSG_TYPE_UPLOAD)//客户端指定任务为上传文件
         {
             //要从客户端传过来的数据包的文件名里面获取文件名信息,然后创建文件。默认创建的文件夹是在home目录下
             strcpy(up_file_name, recv_msg.fname);
             //然后在home目录下创建文件,名字暂时写死,
             //O_CREAT 表示如果文件不存在则创建
             //O_WRONLY 表示文件将以写入方式打开,允许你写入文件的内容。
             //0666 表示文件所有者、文件组和其他用户都有读取和写入权限。
             fd = open("/home/liujiajun/css.txt", O_CREAT | O_WRONLY, 0666);
             if (fd < 0)//创建文件失败
             {
                 perror("create up file error:");
             }
         }
         else if (recv_msg.type == MSG_TYPE_UPLOAD_DATA)//客户端上传文件数据的线程函数传消息过来了
         {
             //将 recv_msg.buffer 中的数据写入由文件描述符 fd 标识的文件
             //写入的字节数由 recv_msg.bytes 指定
             write(fd, recv_msg.buffer, recv_msg.bytes);
             if (recv_msg.bytes < sizeof(recv_msg.buffer))//说明读到最后一轮了,因为实际读取的字节数连消息的缓冲区都填不满
             {
                 printf("client uploaded file: %s\n", recv_msg.fname);
                 close(fd);
             }
             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));
     
     //将服务器套接字server_socket绑定到指定的 IP 地址和端口号
     if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
         perror("server bind error:");
         return 0;
     }
 ​
     //将服务器套接字设置为监听模式,以便它可以接收客户端的连接请求
     //10 是服务器套接字可以同时处理的等待连接的最大客户端连接数
     if (listen(server_socket, 10) < 0) {
         perror("server listen error:");
         return 0;
     }
     printf("TCP服务器准备完成,等待客户端的连接\n");
     
     //服务器将持续接收和发送数据,直到手动停止程序。
     while (1) 
     {
         //这行代码用于接受客户端的连接请求,并返回一个新的套接字 accept_socket
         accept_socket = accept(server_socket, NULL, NULL);
         printf("有客户端连接到服务器!\n");
         //创建一个线程
         //thread_id是线程标识符
         //thread_fun 是处理客户端请求的线程函数。
         //&accept_socket 是传递给线程函数的参数,其中包含与客户端通信的套接字。
         pthread_create(&thread_id, NULL, thread_fun, &accept_socket);
     }
 ​
     return 0;
 }