当前位置: 首页 > news >正文

项目——基于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_cast<void*>(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_cast<void*>(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_cast<Server*>(arg); // 调用Server类的Accept_Client方法接受连接 ser->Accept_Client(); } // 客户端套接字的读事件回调:有客户端发来数据时触发 void Read_CallBack(int fd, short event, void* arg) { // 将void*参数转换回Con_Client对象指针 Con_Client* cli = static_cast<Con_Client*>(arg); // 调用Con_Client类的Recv_Data方法接收数据 cli->Recv_Data(); }

重点代码讲解

  1. setsockopt(SO_REUSEADDR):非常重要!解决服务器重启后 "地址已被使用" 的问题,因为 TCP 有 TIME_WAIT 状态,端口不会立即释放。
  2. event_base:Libevent 的核心,相当于一个事件管理器,所有事件都要注册到它上面。
  3. event_new():创建一个事件对象,指定要监听的文件描述符、事件类型和回调函数。
  4. 回调函数:Libevent 是事件驱动的,当事件发生时,会自动调用你注册的回调函数。
  5. 每个客户端一个 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; }

重点代码讲解

  1. opendir()/readdir()/closedir():Linux 下目录操作的三个核心函数,用于遍历目录中的文件。
  2. lstat():获取文件属性,通过S_ISDIR(st.st_mode)判断是否是目录。
  3. strtok_r():线程安全的字符串分割函数,用于解析自定义的下载协议。
  4. lseek(fd, 0, SEEK_END):获取文件大小的常用技巧,将文件指针移动到末尾,返回的偏移量就是文件大小。
  5. 分块下载:服务器每次发送 128 字节数据,客户端每次请求一块,这样可以避免大文件一次性发送导致的内存问题。
http://www.jsqmd.com/news/855545/

相关文章:

  • 保姆级教程:在S32G274ARDB2上,用IPCF点亮RGB LED(附源码解析)
  • AI 写代码总跑偏?mirrorai 让 Claude Code、Cursor、Copilot 严格遵守你项目的真实规范
  • 2026年自助建站平台哪个好?推荐这4个知名建站平台!
  • Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
  • 【正式版上线】Open Claw 2.7.5 桌面端一键安装部署教程
  • 三步告别键盘连击:KeyboardChatterBlocker高效使用全攻略
  • C#如何优雅处理引用类型的深拷贝 (十一)
  • Kimi、DeepSeek、阶跃星辰三天融资超百亿,中国AI的“中场战事”刚刚开始
  • 掌握Linux网络设计中的WebSocket服务器
  • 港科大沈劭劼、谭平团队最新成果:开源280万全景数据集,实现零样本立体匹配
  • 测试经理为保障项目按期交付,主动规划核心内容
  • 我开发了一个 AI 表单填写 Chrome 插件:AutoFormX,提升 Web 测试和表单联调效率
  • 3步搞定OFD兼容难题:Ofd2Pdf实战手册
  • Cursor试用限制终极解决方案:3分钟快速重置设备标识实战指南
  • STM32 HAL库驱动中景园0.96寸OLED(SSD1306)避坑指南:从IIC地址到GRAM刷新的完整流程
  • 别再傻傻分不清:一张图看懂BLDC六步换相与PMSM FOC的本质区别与应用选型
  • 不止是省9.9刀:解锁特斯拉Model 3的‘行驶中保持WiFi’功能,打造家庭移动娱乐中心
  • 告别臃肿UI!5K行代码的GuiLite在STM32 HAL库上跑起来了(附工程源码)
  • 避开这3个坑,你的C# + VisionPro相机采集程序才算稳定(WinForm实战)
  • 告别接线混乱!用ESP32的I2C接口驱动LCD1602,5分钟搞定温湿度显示(附完整代码)
  • 从音箱分频到电源净化:聊聊RLC低通滤波器那些意想不到的实用场景
  • 操作系统概述(4)--操作系统运行机制(1):处理机双重模式与中断
  • FPGA管脚不够用?手把手教你用74HC595级联驱动8位数码管(附Verilog代码与仿真)
  • C++ STL常用函数一览表(快速记忆版本)
  • 多模态协作:文本、图像、语音Agent配合
  • Odrive运动控制实战:用STM32的CAN总线读取电机位置和发送位置指令
  • Perplexity历史资料搜索效率提升300%:实测验证的5步精准检索法(附2024最新API调用参数)
  • 构建AI应用时如何借助Taotoken实现模型的灵活选型与降级
  • 《Linux系统编程》Linux基础开发工具 (三):从零实现动态进度条(附回车、换行与缓冲区详解)
  • TPU核心引擎的‘血管网络’:用RTL仿真动画可视化脉动阵列数据流