项目——基于C/S架构的文件传输系统平台 (2)——重构

前言:

我们已经完成了

环境搭建和 MySQL 数据库初始化

客户端基本框架(连接服务器、菜单打印、注册 / 登录)

数据库操作类的完整实现

二、TCP Socket 通信完整流程详解(必懂)

服务器(接电话的人) 动作 客户端(打电话的人)
买手机 socket() 创建套接字 买手机
绑定手机号 bind() 绑定地址和端口
开机等电话 listen() 开始监听
接电话 accept() 接受连接 打电话 connect() 发起连接
通话 recv()/send() 收发数据 通话
挂电话 close() 关闭套接字 挂电话

重点理解

服务器必须先启动,等待客户端连接

accept()会创建一个新的套接字 (就是int c),专门和这个客户端通信

原来的监听套接字继续等待其他客户端连接

三、服务器端

3.1 服务器实现文件(server/server.cpp)

先写 Server 类的核心实现

cpp 复制代码
#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 类核心实现

cpp 复制代码
/************************** 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 字节数据,客户端每次请求一块,这样可以避免大文件一次性发送导致的内存问题。
相关推荐
青春喂了后端10 小时前
IntelliGit 前端入口与开发测试面板边界重构
前端·重构
量子炒饭大师10 小时前
【优化算法】滑动窗口的「义体化」重构 ——【滑动窗口】何为滑动窗口?滑动窗口算法的核心目的是什么?
c++·算法·重构·优化算法·双指针·滑动窗口
老王谈企服11 小时前
AI Agent将如何重构制造业的安全生产隐患识别模式?深度理解与实在Agent闭环实战
人工智能·安全·ai·重构
青春喂了后端1 天前
IntelliGit 前端状态层重构:把一个全局 Store 拆成清晰的状态边界
前端·重构·状态模式
青春喂了后端1 天前
IntelliGit 前端订阅边界重构
前端·重构
北京自在科技1 天前
苹果官宣 WWDC 2026:AI 重构 Siri,全新系统即将登场
人工智能·重构·wwdc
EasyDSS1 天前
私有化视频会议系统/智能会议管理系统EasyDSS以数据能力重构视频会议协同新生态
大数据·人工智能·重构
2401_853087881 天前
Confluence 替代落地复盘:存量数据迁移、权限重构、信创适配踩坑总结
开发语言·重构·c#
金融小师妹2 天前
基于AI通胀预期模型与美元流动性监测框架的黄金6周新低行分析:美元五连涨周期下贵金属定价机制重构研究
大数据·人工智能·重构·逻辑回归·线性回归