项目——基于C/S架构的文件传输系统平台 (2)——重构
前言我们已经完成了环境搭建和 MySQL 数据库初始化客户端基本框架连接服务器、菜单打印、注册 / 登录数据库操作类的完整实现二、TCP Socket 通信完整流程详解必懂服务器接电话的人动作客户端打电话的人买手机socket()创建套接字买手机绑定手机号bind()绑定地址和端口无开机等电话listen()开始监听无接电话accept()接受连接打电话connect()发起连接通话recv()/send()收发数据通话挂电话close()关闭套接字挂电话重点理解服务器必须先启动等待客户端连接accept()会创建一个新的套接字就是int c专门和这个客户端通信原来的监听套接字继续等待其他客户端连接三、服务器端3.1 服务器实现文件server/server.cpp先写 Server 类的核心实现#include server.h Server::Server() { cout 服务器启动中... endl; ip 127.0.0.1; port 6000; sockfd -1; base NULL; } Server::Server(string ip, int port) { this-ip ip; this-port port; sockfd -1; base NULL; } Server::~Server() { cout 服务器关闭 endl; if (sockfd ! -1) { close(sockfd); } if (base ! NULL) { event_base_free(base); // 释放Libevent事件基础 } } //创建监听套接字 bool Server::Create_Socket() { sockfd socket(AF_INET, SOCK_STREAM, 0); if (-1 sockfd) { perror(socket create failed); return false; } // 重点设置端口复用 // 解决服务器重启后地址已被使用的问题 int opt 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); // 2. 填充服务器地址结构 struct sockaddr_in saddr; memset(saddr, 0, sizeof(saddr)); // 清空结构防止垃圾数据 saddr.sin_family AF_INET; // 地址族IPv4 saddr.sin_port htons(port); // 端口号主机字节序转网络字节序 // INADDR_ANY监听所有网卡的地址 saddr.sin_addr.s_addr htonl(INADDR_ANY); // 3. 绑定地址和端口到套接字 int res bind(sockfd, (struct sockaddr*)saddr, sizeof(saddr)); if (-1 res) { perror(bind failed); close(sockfd); return false; } // 4. 开始监听客户端连接 // LIS_MAX监听队列的最大长度即同时等待连接的客户端数量 res listen(sockfd, LIS_MAX); if (-1 res) { perror(listen failed); close(sockfd); return false; } cout 监听套接字创建成功端口 port endl; return true; } // 初始化Libevent事件驱动框架 bool Server::Libevent_Init() { // 1. 创建事件基础对象Libevent的核心管理所有事件 // 相当于Libevent的大脑 base event_base_new(); if (nullptr base) { cout event_base_new failed endl; return false; } // 2. 创建监听套接字的读事件 // 参数说明 // base事件基础对象 // sockfd要监听的文件描述符监听套接字 // EV_READ监听读事件有客户端连接时触发 // EV_PERSIST持久事件触发后不会自动删除 // Accept_CallBack事件触发时的回调函数 // this传递给回调函数的参数当前Server对象的指针 struct event* sock_ev event_new(base, sockfd, EV_READ | EV_PERSIST, Accept_CallBack, static_castvoid*(this)); if (sock_ev nullptr) { cout event_new failed endl; return false; } // 3. 将事件添加到事件基础中开始监听 event_add(sock_ev, nullptr); cout Libevent初始化成功 endl; return true; } // 服务器总初始化函数 bool Server::Ser_Init() { // 1. 创建文件存储根目录如果不存在 if (access(PATH.c_str(), F_OK) -1) { if (mkdir(PATH.c_str(), 0775) -1) { cout 创建根目录失败 PATH endl; return false; } cout 创建文件根目录 PATH endl; } // 2. 创建监听套接字 if (!Create_Socket()) { return false; } // 3. 初始化Libevent if (!Libevent_Init()) { return false; } return true; } // 接受客户端连接 bool Server::Accept_Client() { // 接受一个新的客户端连接 // 返回值新的套接字int c专门和这个客户端通信 int c accept(sockfd, NULL, NULL); if (c -1) { perror(accept failed); return false; } cout 新客户端连接成功套接字 c endl; // ⚠️ 重点为每个新客户端创建一个Con_Client对象 // 这个对象会管理这个客户端的所有状态 Con_Client* p new Con_Client(c, base); // 为这个客户端创建读事件监听它发来的数据 struct event* c_ev event_new(base, c, EV_READ | EV_PERSIST, Read_CallBack, static_castvoid*(p)); if (c_ev NULL) { delete p; close(c); return false; } // 将事件对象保存到Con_Client中方便后续释放 p-Set_event(c_ev); // 将事件添加到事件基础中开始监听这个客户端的数据 event_add(c_ev, NULL); return true; } // 启动服务器事件循环阻塞函数一直运行 void Server::Run() { cout 服务器启动成功开始监听客户端连接... endl; // 启动事件循环等待事件发生 // 这个函数会一直阻塞直到event_base被销毁 event_base_dispatch(base); } /************************** 全局回调函数 **************************/ // 监听套接字的读事件回调有新客户端连接时触发 void Accept_CallBack(int fd, short event, void* arg) { // 将void*参数转换回Server对象指针 Server* ser static_castServer*(arg); // 调用Server类的Accept_Client方法接受连接 ser-Accept_Client(); } // 客户端套接字的读事件回调有客户端发来数据时触发 void Read_CallBack(int fd, short event, void* arg) { // 将void*参数转换回Con_Client对象指针 Con_Client* cli static_castCon_Client*(arg); // 调用Con_Client类的Recv_Data方法接收数据 cli-Recv_Data(); }重点代码讲解setsockopt(SO_REUSEADDR)非常重要解决服务器重启后 地址已被使用 的问题因为 TCP 有 TIME_WAIT 状态端口不会立即释放。event_baseLibevent 的核心相当于一个事件管理器所有事件都要注册到它上面。event_new()创建一个事件对象指定要监听的文件描述符、事件类型和回调函数。回调函数Libevent 是事件驱动的当事件发生时会自动调用你注册的回调函数。每个客户端一个 Con_Client 对象这是面向对象设计的精髓把每个客户端的状态和行为封装在一起。3.2 Con_Client 类核心实现/************************** Con_Client类实现 **************************/ // 构造函数初始化客户端连接对象 Con_Client::Con_Client(int c, struct event_base* b) { this-c c; // 保存客户端套接字 base b; // 保存事件基础对象 ev NULL; // 事件对象初始化为空 fd -1; // 文件描述符初始化为无效值 mypath ; // 当前工作目录初始化为空 userpath ; // 用户根目录初始化为空 } // 析构函数释放客户端资源 Con_Client::~Con_Client() { if (ev ! NULL) { event_free(ev); // 释放事件对象 } close(c); // 关闭客户端套接字 if (fd ! -1) { close(fd); // 关闭打开的文件 } cout 客户端断开连接套接字 c endl; } // 设置事件对象 void Con_Client::Set_event(struct event* e) { ev e; } // 判断数据是否是JSON格式 bool Con_Client::is_json(const char buff[]) { // 简单判断JSON以{开头 return buff[0] {; } // 发送成功响应 void Con_Client::send_ok() { Json::Value v; v[status] OK; send(c, v.toStyledString().c_str(), strlen(v.toStyledString().c_str()), 0); } // 发送失败响应 void Con_Client::send_err() { Json::Value v; v[status] ERR; send(c, v.toStyledString().c_str(), strlen(v.toStyledString().c_str()), 0); } // 发送JSON响应 void Con_Client::send_Json(Json::Value v) { send(c, v.toStyledString().c_str(), strlen(v.toStyledString().c_str()), 0); } // 处理注册请求 void Con_Client::Register() { // 从JSON中提取用户信息 string usertel val[usertel].asString(); string username val[username].asString(); string userpasswd val[passwd].asString(); // 创建数据库客户端 mysqlclient mysqlcli; if (!mysqlcli.connectserver()) { send_err(); return; } // 将用户信息插入数据库 if (!mysqlcli.db_register(usertel, username, userpasswd)) { send_err(); return; } // 为新用户创建专属目录 mypath PATH usertel; userpath mypath; if (mkdir(mypath.c_str(), 0775) -1) { send_err(); return; } cout 用户注册成功 username ( usertel ) endl; send_ok(); } // 处理登录请求 void Con_Client::Login() { // 从JSON中提取用户信息 string usertel val[usertel].asString(); string passwd val[passwd].asString(); string username ; // 输入验证 if (usertel.empty() || passwd.empty()) { send_err(); return; } // 创建数据库客户端 mysqlclient cli; if (!cli.connectserver()) { send_err(); return; } // 验证用户登录 if (!cli.db_login(usertel, username, passwd)) { send_err(); return; } // 初始化用户目录 mypath PATH usertel; userpath mypath; // 如果用户目录不存在比如数据库是手动导入的创建它 if (access(mypath.c_str(), F_OK) -1) { if (mkdir(mypath.c_str(), 0775) -1) { send_err(); return; } } cout 用户登录成功 username ( usertel ) endl; // 返回登录成功响应和用户名 Json::Value v; v[status] OK; v[username] username; send_Json(v); } // 处理查看文件列表请求 void Con_Client::showfiles() { // 打开用户当前目录 DIR* ptr opendir(mypath.c_str()); if (ptr NULL) { cout 打开目录失败 mypath endl; send_err(); return; } int ndirs 0; // 目录数量 int nfiles 0; // 文件数量 Json::Value resval; struct dirent *s nullptr; struct stat st; // 遍历目录中的所有文件和子目录 while ((s readdir(ptr)) ! nullptr) { // 跳过.和..目录 if (strncmp(s-d_name, ., 1) 0) { continue; } // 构建文件的完整路径 string filename mypath / s-d_name; // 获取文件属性 if (lstat(filename.c_str(), st) -1) { cout 获取文件属性失败 filename endl; continue; } // 判断是目录还是普通文件 if (S_ISDIR(st.st_mode)) { // 是目录添加到arrdir数组 Json::Value tmp; tmp[filename] string(s-d_name); resval[arrdir].append(tmp); ndirs; } else { // 是普通文件添加到arrfile数组 Json::Value tmp; tmp[filename] string(s-d_name); resval[arrfile].append(tmp); nfiles; } } // 关闭目录 closedir(ptr); // 构建响应JSON resval[ndirs] ndirs; resval[status] OK; resval[nfiles] nfiles; // 发送响应给客户端 send(c, resval.toStyledString().c_str(), strlen(resval.toStyledString().c_str()), 0); } // 处理下载文件请求 void Con_Client::get_file(char* ptr) { // 解析命令get start filename 或 get continue 或 get stop char *status strtok_r(NULL, , ptr); if (status nullptr) { send(c, ERR, 3, 0); return; } if (strcmp(status, start) 0) { // 开始下载获取文件名 char* fname strtok_r(NULL, , ptr); if (fname nullptr) { send(c, ERR, 3, 0); return; } // 构建文件完整路径 string pathname mypath / fname; // 以只读方式打开文件 fd open(pathname.c_str(), O_RDONLY); if (fd -1) { send(c, ERR, 3, 0); return; } // 获取文件大小移动文件指针到末尾返回偏移量 int filesize lseek(fd, 0, SEEK_END); // 移动文件指针回到开头 lseek(fd, 0, SEEK_SET); // 发送响应OK 文件大小 string r_str OK to_string(filesize); send(c, r_str.c_str(), strlen(r_str.c_str()), 0); cout 开始发送文件 fname 大小 filesize 字节 endl; } else if (strcmp(status, continue) 0) { // 继续下载发送下一块数据 if (fd -1) { send(c, ERR, 3, 0); return; } char buff[128] {0}; // 读取128字节数据 int num read(fd, buff, 128); if (num 0) { // 文件读完或出错关闭文件 close(fd); fd -1; send(c, , 0, 0); cout 文件发送完成 endl; return; } // 发送数据给客户端 send(c, buff, num, 0); } else if (strcmp(status, stop) 0) { // 停止下载关闭文件 if (fd ! -1) { close(fd); fd -1; } cout 下载被客户端终止 endl; } } // 处理新建目录请求 void Con_Client::Mkdir() { string dname val[dirname].asString(); string filepath mypath / dname; // 创建目录权限0775 if (mkdir(filepath.c_str(), 0775) -1) { send_err(); return; } cout 创建目录成功 dname endl; send_ok(); } // 处理删除文件/目录请求 void Con_Client::Rmfile() { string filename val[filename].asString(); string filepath mypath / filename; // 检查文件是否存在 if (access(filepath.c_str(), F_OK) -1) { send_err(); return; } // 获取文件属性 struct stat st; if (stat(filepath.c_str(), st) -1) { send_err(); return; } // 判断是目录还是普通文件 if (S_ISDIR(st.st_mode)) { // 删除空目录 if (rmdir(filepath.c_str()) -1) { send_err(); return; } } else { // 删除普通文件 if (unlink(filepath.c_str()) -1) { send_err(); return; } } cout 删除成功 filename endl; send_ok(); } // 处理重命名请求 void Con_Client::Rename() { string s_name val[sname].asString(); string t_name val[tname].asString(); string s_filepath mypath / s_name; string t_filepath mypath / t_name; // 重命名文件/目录 if (rename(s_filepath.c_str(), t_filepath.c_str()) -1) { send_err(); return; } cout 重命名成功 s_name - t_name endl; send_ok(); } // 处理进入目录请求 void Con_Client::Chdir() { string dname val[dirname].asString(); string testpath mypath / dname; // 尝试打开目录检查是否存在 DIR* ptr opendir(testpath.c_str()); if (ptr nullptr) { send_err(); return; } // 更新当前工作目录 mypath testpath; closedir(ptr); cout 切换目录成功 mypath endl; send_ok(); } // 处理返回上级目录请求 void Con_Client::Ret() { // 如果已经在用户根目录不能再返回 if (userpath mypath) { send_ok(); return; } // 找到最后一个/的位置 size_t pos mypath.find_last_of(/); if (pos string::npos) { send_err(); return; } // 截取到最后一个/之前的部分就是上级目录 mypath mypath.substr(0, pos); cout 返回上级目录成功 mypath endl; send_ok(); } // 请求分发函数根据操作类型调用对应的处理函数 void Con_Client::do_run(int op) { switch (op) { case REGISTER: Register(); break; case LOGIN: Login(); break; case SHOWFILES: showfiles(); break; case GET: // GET请求已经在Recv_Data中单独处理了 break; case POST: send_err(); break; case MKDIR: Mkdir(); break; case RMFILE: Rmfile(); break; case MVNAME: Rename(); break; case CHDIR: Chdir(); break; case RET: Ret(); break; case MVFILE: send_err(); break; case USEREXIT: break; default: send_err(); break; } } // 接收并处理客户端数据 void Con_Client::Recv_Data() { char buff[256] {0}; // 接收客户端数据 int n recv(c, buff, 255, 0); if (n 0) { // 客户端断开连接或出错删除Con_Client对象 delete this; return; } cout 收到客户端 c 数据 buff endl; // 判断数据类型JSON或自定义协议 if (is_json(buff)) { // 解析JSON数据 Json::Reader Read; if (!Read.parse(buff, val)) { cout JSON解析失败 endl; send_err(); return; } // 获取操作类型 int op val[type].asInt(); // 分发请求 do_run(op); } else { // 自定义协议目前只有下载协议 char* ptr nullptr; char* s strtok_r(buff, , ptr); if (s nullptr) { send(c, ERR, 3, 0); return; } if (strcmp(s, get) 0) { // 处理下载请求 get_file(ptr); } else if (strcmp(s, up) 0) { cout 上传功能暂未实现 endl; send(c, ERR, 3, 0); } else { send(c, ERR, 3, 0); } } } // 主函数 int main() { Server ser; if (!ser.Ser_Init()) { cout 服务器初始化失败 endl; return 1; } ser.Run(); return 0; }重点代码讲解opendir()/readdir()/closedir()Linux 下目录操作的三个核心函数用于遍历目录中的文件。lstat()获取文件属性通过S_ISDIR(st.st_mode)判断是否是目录。strtok_r()线程安全的字符串分割函数用于解析自定义的下载协议。lseek(fd, 0, SEEK_END)获取文件大小的常用技巧将文件指针移动到末尾返回的偏移量就是文件大小。分块下载服务器每次发送 128 字节数据客户端每次请求一块这样可以避免大文件一次性发送导致的内存问题。