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

UNIX网络编程笔记:客户/服务器程序示例

服务器实例

有个著名的项目,TinyWebServer,本项目将其改到windows下,并使用RAII重构,原因是用C编写过程中对于内存泄漏的问题确实很头疼:

 /*
 * 基于C++17的Windows平台高性能HTTP服务器实现
 * 特性:
 * 1. 使用RAII管理资源(Winsock、Socket)
 * 2. 采用现代C++特性(移动语义、智能指针等)
 * 3. 使用IOCP(IO完成端口)模型优化文件传输
 * 4. 多线程处理客户端请求
 * 5. 支持基本的HTTP GET请求和文件服务
 * 6. 包含日志系统和错误处理
 */

 // 系统头文件
#include <WinSock2.h>       // Windows Socket API
#include <WS2tcpip.h>       // TCP/IP相关函数
#include <Windows.h>        // Windows API
#include <Mswsock.h>        // TransmitFile扩展函数
// 标准库头文件
#include <algorithm>        // 算法函数
#include <chrono>           // 时间相关
#include <cstring>          // C字符串处理
#include <fstream>          // 文件流操作
#include <iomanip>          // 格式化输出
#include <iostream>         // 输入输出流
#include <memory>           // 智能指针
#include <mutex>            // 互斥锁
#include <sstream>          // 字符串流
#include <string>           // 字符串类
#include <unordered_map>    // 哈希表
#include <vector>           // 动态数组
#include <filesystem>       // 文件系统操作

// 链接库
#pragma comment(lib, "WS2_32.lib")      // Winsock库
#pragma comment(lib, "Mswsock.lib")      // TransmitFile函数库

namespace fs = std::filesystem; // 文件系统命名空间别名

// 日志系统 ======================================================================
std::mutex g_log_mutex;  // 全局日志互斥锁,保证多线程日志安全

/*
 * 日志记录器类
 * 特性:
 * - 自动记录时间戳和线程ID
 * - 线程安全输出
 * - 支持流式日志记录
 */
class Logger {
    std::ostringstream oss_;          // 日志内容缓冲区
    std::chrono::system_clock::time_point start_;  // 程序启动时间基准

    // 生成带时间戳的日志前缀
    std::string timestamp() {
        auto now = std::chrono::system_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - start_);
        auto t = std::chrono::system_clock::to_time_t(now);

        std::tm tm_local;
        localtime_s(&tm_local, &t);  // 安全版本的时间转换

        std::ostringstream oss;
        // 格式化:YYYY-MM-DD HH:MM:SS.fff [T线程ID]
        oss << std::put_time(&tm_local, "%Y-%m-%d %H:%M:%S")
            << "." << std::setfill('0') << std::setw(3) << ms.count() % 1000
            << " [T" << std::setw(5) << GetCurrentThreadId() << "] ";
        return oss.str();
    }

public:
    Logger() : start_(std::chrono::system_clock::now()) {
        oss_ << timestamp();  // 构造时初始化时间戳
    }

    ~Logger() {
        std::lock_guard<std::mutex> lock(g_log_mutex);  // 加锁保证输出原子性
        std::clog << oss_.str() << std::endl;           // 输出到标准错误
    }

    // 流式输出运算符重载
    template <typename T>
    Logger& operator<<(T&& value) {
        oss_ << value;  // 将数据追加到缓冲区
        return *this;
    }
};

// 日志宏,方便使用
#define LOG Logger()

// RAII封装 =====================================================================
/*
 * Winsock初始化RAII封装
 * 特性:
 * - 构造函数初始化Winsock
 * - 析构函数自动清理
 * - 禁止拷贝
 */
class WSAInitialize {
    WSADATA wsa_data_;  // Winsock初始化信息
public:
    explicit WSAInitialize(WORD version = MAKEWORD(2, 2)) {
        LOG << "Initializing Winsock...";
        if (WSAStartup(version, &wsa_data_) != 0) {
            LOG << "WSAStartup failed: " << WSAGetLastError();
            throw std::runtime_error("WSAStartup failed");
        }
        LOG << "Winsock initialized. Version: "
            << (wsa_data_.wVersion & 0xFF) << "."
            << (wsa_data_.wVersion >> 8);  // 打印主次版本号
    }

    ~WSAInitialize() {
        LOG << "Cleaning up Winsock...";
        WSACleanup();
    }

    // 禁止拷贝
    WSAInitialize(const WSAInitialize&) = delete;
    WSAInitialize& operator=(const WSAInitialize&) = delete;
};

/*
 * Socket RAII封装类
 * 特性:
 * - 自动关闭socket
 * - 支持移动语义
 * - 异常安全
 */
class AutoSocket {
    SOCKET socket_ = INVALID_SOCKET;  // 管理的socket句柄

    // 抛出带有错误信息的异常
    static void throw_wsa_error(const char* context) {
        int err = WSAGetLastError();
        LOG << "Socket error (" << context << "): " << err << " - " << get_wsa_error_str(err);
        throw std::runtime_error(context);
    }

    // 获取错误码对应的描述信息
    static std::string get_wsa_error_str(int code) {
        LPSTR msg = nullptr;
        FormatMessageA(
            FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
            nullptr, code, 0,
            reinterpret_cast<LPSTR>(&msg), 0, nullptr);
        std::string result(msg ? msg : "Unknown error");
        LocalFree(msg);
        return result;
    }

public:
    AutoSocket() = default;

    // 创建新socket
    explicit AutoSocket(int domain, int type, int protocol) {
        LOG << "Creating socket...";
        socket_ = socket(domain, type, protocol);
        if (socket_ == INVALID_SOCKET) {
            throw_wsa_error("socket creation");
        }
        LOG << "Socket created (fd: " << socket_ << ")";
    }

    ~AutoSocket() {
        close();  // 确保资源释放
    }

    // 移动语义支持
    AutoSocket(AutoSocket&& other) noexcept : socket_(other.socket_) {
        other.socket_ = INVALID_SOCKET;  // 转移所有权
        LOG << "Socket moved (from " << other.socket_ << " to " << socket_ << ")";
    }

    AutoSocket& operator=(AutoSocket&& other) noexcept {
        if (this != &other) {
            close();  // 先关闭现有socket
            socket_ = other.socket_;
            other.socket_ = INVALID_SOCKET;
        }
        return *this;
    }

    // 显式关闭socket
    void close() noexcept {
        if (socket_ != INVALID_SOCKET) {
            LOG << "Closing socket " << socket_;
            closesocket(socket_);
            socket_ = INVALID_SOCKET;
        }
    }

    // 隐式转换为SOCKET类型
    operator SOCKET() const noexcept { return socket_; }

    // Socket配置方法 ----------------------------------------------------------
    void set_reuse_addr() const {
        LOG << "Setting SO_REUSEADDR on socket " << socket_;
        int opt = 1;
        if (setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR,
            reinterpret_cast<const char*>(&opt), sizeof(opt)) == SOCKET_ERROR) {
            throw_wsa_error("setsockopt(SO_REUSEADDR)");
        }
    }

    // 绑定地址
    void bind(const sockaddr_in& addr) const {
        char ip_str[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));

        LOG << "Binding to " << ip_str << ":" << ntohs(addr.sin_port);
        if (::bind(socket_, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR) {
            throw_wsa_error("bind");
        }
    }

    // 开始监听
    void listen(int backlog = SOMAXCONN) const {
        LOG << "Listening on socket " << socket_ << " with backlog " << backlog;
        if (::listen(socket_, backlog) == SOCKET_ERROR) {
            throw_wsa_error("listen");
        }
    }

    // 接受连接
    AutoSocket accept(sockaddr_in& client_addr) const {
        int addr_len = sizeof(client_addr);
        LOG << "Waiting for incoming connections on socket " << socket_;

        SOCKET client_socket = ::accept(socket_, reinterpret_cast<sockaddr*>(&client_addr), &addr_len);
        if (client_socket == INVALID_SOCKET) {
            throw_wsa_error("accept");
        }

        // 记录客户端信息
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        LOG << "Accepted connection from " << client_ip << ":" << ntohs(client_addr.sin_port)
            << " (client fd: " << client_socket << ")";

        return AutoSocket(client_socket);  // 返回包装后的socket
    }

private:
    // 私有构造函数,用于包装已存在的socket
    explicit AutoSocket(SOCKET s) : socket_(s) {}
};

// HTTP相关功能 =================================================================
namespace http {
    /*
     * 根据文件扩展名获取MIME类型
     * 参数:path - 文件路径
     * 返回:对应的MIME类型字符串
     */
    std::string get_mime_type(const fs::path& path) {
        // MIME类型映射表
        static const std::unordered_map<std::string, std::string> mime_types{
            {".html", "text/html"}, {".htm", "text/html"},   {".txt", "text/plain"},
            {".css", "text/css"},   {".js", "application/javascript"},
            {".jpg", "image/jpeg"}, {".jpeg", "image/jpeg"}, {".png", "image/png"},
            {".gif", "image/gif"},  {".json", "application/json"},
            {".ico", "image/x-icon"}
        };

        std::string ext = path.extension().string();
        std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);  // 转为小写

        auto it = mime_types.find(ext);
        return it != mime_types.end() ? it->second : "application/octet-stream";  // 默认二进制流
    }

    /*
     * 发送HTTP响应
     * 参数:
     *   client        - 客户端socket
     *   content       - 响应内容
     *   content_type  - MIME类型
     *   status_code   - HTTP状态码
     */
    void send_response(SOCKET client, const std::string& content,
        const std::string& content_type = "text/html",
        unsigned status_code = 200) {
        std::ostringstream response;
        // 构建响应头
        response << "HTTP/1.1 " << status_code << " "
            << (status_code == 200 ? "OK" : "Not Found") << "\r\n"
            << "Content-Type: " << content_type << "\r\n"
            << "Content-Length: " << content.size() << "\r\n"
            << "Connection: close\r\n\r\n"
            << content;

        const std::string resp_str = response.str();
        LOG << "Sending response (" << resp_str.size() << " bytes)"
            << "\nHeaders:" << resp_str.substr(0, resp_str.find("\r\n\r\n"));

        send(client, resp_str.data(), static_cast<int>(resp_str.size()), 0);
    }

    /*
     * 发送文件(使用TransmitFile优化大文件传输)
     * 参数:
     *   client - 客户端socket
     *   path   - 文件路径
     */
    void send_file(SOCKET client, const fs::path& path) {
        LOG << "Sending file: " << path;

        // 尝试打开文件
        std::ifstream file(path, std::ios::binary | std::ios::ate);
        if (!file) {
            LOG << "File open failed: " << path;
            send_response(client, "<h1>404 Not Found</h1>", "text/html", 404);
            return;
        }

        // 准备响应头
        const auto file_size = file.tellg();
        file.seekg(0);

        std::ostringstream headers;
        headers << "HTTP/1.1 200 OK\r\n"
            << "Content-Type: " << get_mime_type(path) << "\r\n"
            << "Content-Length: " << file_size << "\r\n"
            << "Connection: close\r\n\r\n";

        std::string header_str = headers.str();
        LOG << "Sending file headers (" << header_str.size() << " bytes)";

        // 先发送头
        if (send(client, header_str.data(), static_cast<int>(header_str.size()), 0) == SOCKET_ERROR) {
            LOG << "Header send error: " << WSAGetLastError();
            return;
        }

        // 使用TransmitFile高效传输文件(Windows特有)
        HANDLE file_handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ,
            nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
        if (file_handle != INVALID_HANDLE_VALUE) {
            LOG << "Using TransmitFile for large file transfer (" << file_size << " bytes)";
            TransmitFile(client, file_handle, 0, 0, nullptr, nullptr, TF_DISCONNECT);
            CloseHandle(file_handle);
            return;
        }

        // 小文件分块传输
        std::vector<char> buffer(65536);  // 64KB缓冲区
        while (file.read(buffer.data(), buffer.size())) {
            const int bytes_read = static_cast<int>(file.gcount());
            if (send(client, buffer.data(), bytes_read, 0) == SOCKET_ERROR) {
                LOG << "File send error: " << WSAGetLastError();
                break;
            }
            LOG << "Sent " << bytes_read << " bytes (total: " << file.tellg() << "/" << file_size << ")";
        }
        LOG << "File transfer completed";
    }
}

// 客户端会话处理 ==============================================================
/*
 * 客户端会话处理类
 * 职责:
 * - 读取HTTP请求
 * - 解析请求
 * - 验证路径安全性
 * - 发送响应或文件
 */
class ClientSession {
    AutoSocket client_;       // 客户端socket
    sockaddr_in client_addr_; // 客户端地址信息

    // 验证请求路径是否在文档根目录内
    static bool is_valid_path(const fs::path& requested, const fs::path& doc_root) {
        const auto abs_requested = fs::absolute(requested);
        const auto abs_root = fs::absolute(doc_root);

        // 计算相对路径,防止目录遍历攻击
        const auto rel_path = fs::relative(abs_requested, abs_root);
        return !rel_path.empty() && *rel_path.begin() != "..";  // 禁止上级目录访问
    }

    // 读取HTTP请求(简化版,仅读取头)
    std::string read_request() {
        std::vector<char> buffer(4096);  // 4KB缓冲区
        std::string request;

        while (true) {
            int bytes_received = recv(client_, buffer.data(), buffer.size(), 0);
            if (bytes_received > 0) {
                request.append(buffer.data(), bytes_received);
                // 检查是否收到完整头部
                if (request.find("\r\n\r\n") != std::string::npos) {
                    LOG << "Received full request headers:\n"
                        << request.substr(0, request.find("\r\n\r\n") + 4);
                    return request;
                }
            }
            else {
                throw std::runtime_error("Connection closed or error");
            }
        }
    }

public:
    ClientSession(AutoSocket&& socket, sockaddr_in addr)
        : client_(std::move(socket)), client_addr_(addr) {
        char ip_str[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));
        LOG << "New session: " << ip_str << ":" << ntohs(addr.sin_port);
    }

    // 处理客户端请求
    void process() {
        try {
            // 读取并解析请求
            const std::string request = read_request();
            std::istringstream iss(request);
            std::string method, url, version;
            iss >> method >> url >> version;

            LOG << "Request: " << method << " " << url;

            // 仅处理GET请求
            if (method != "GET") {
                LOG << "Unsupported method: " << method;
                http::send_response(client_, "<h1>501 Not Implemented</h1>", "text/html", 501);
                return;
            }

            // 构造文件路径
            fs::path doc_root = "htdocs";          // 文档根目录
            fs::path request_path = url.substr(1); // 去除前导斜杠

            // 路径安全检查
            if (!is_valid_path(doc_root / request_path, doc_root)) {
                LOG << "Invalid path: " << request_path;
                http::send_response(client_, "<h1>403 Forbidden</h1>", "text/html", 403);
                return;
            }

            // 处理默认文件
            if (request_path.empty() || fs::is_directory(doc_root / request_path, doc_root)) {
                request_path /= "index.html";  // 自动添加默认文件
            }

            const fs::path full_path = doc_root / request_path;  // 构造完整文件路径

            // 检查文件是否存在且是普通文件
            if (!fs::exists(full_path) || !fs::is_regular_file(full_path)) {
                LOG << "File not found or not a regular file: " << full_path;
                http::send_response(client_, "<h1>404 Not Found</h1>", "text/html", 404);
                return;
            }

            // 发送文件内容(内部处理大文件优化传输)
            http::send_file(client_, full_path);
        }
        catch (const std::exception& e) {
            // 捕获所有处理过程中的异常,记录日志并返回500错误
            LOG << "Processing error: " << e.what();
            http::send_response(client_, "<h1>500 Server Error</h1>", "text/html", 500);
        }
    }
};

// 服务器主程序 ================================================================
int main() {
    try {
        // RAII方式初始化Winsock(作用域结束时自动清理)
        WSAInitialize wsa;

        // 创建TCP监听socket(IPv4,流式套接字,TCP协议)
        AutoSocket listener(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        listener.set_reuse_addr();  // 设置地址重用,避免TIME_WAIT状态影响重启

        // 配置服务器地址结构
        sockaddr_in server_addr{};
        server_addr.sin_family = AF_INET;          // IPv4地址族
        server_addr.sin_port = htons(8880);        // 监听端口8880
        server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口

        // 绑定socket到指定地址
        listener.bind(server_addr);

        // 获取实际绑定的端口(适用于端口随机分配的情况)
        sockaddr_in actual_addr{};
        int addr_len = sizeof(actual_addr);
        getsockname(listener, reinterpret_cast<sockaddr*>(&actual_addr), &addr_len);
        LOG << "Server listening on port " << ntohs(actual_addr.sin_port);

        // 开始监听,设置最大等待连接数为SOMAXCONN(系统最大值)
        listener.listen();

        // 主循环:接受并处理客户端连接
        while (true) {
            try {
                // 接受客户端连接
                sockaddr_in client_addr{};
                AutoSocket client = listener.accept(client_addr);

                // 创建新线程处理客户端请求(使用移动语义转移socket所有权)
                std::thread([client = std::move(client), client_addr]() mutable {
                    LOG << "Client handler thread started";
                    ClientSession session(std::move(client), client_addr);
                    session.process();  // 处理客户端请求
                    LOG << "Client handler thread exiting";
                    }).detach();  // 分离线程,自主运行
            }
            catch (const std::exception& e) {
                // 处理单个连接异常,防止服务器崩溃
                LOG << "Accept error: " << e.what();
                continue;  // 继续接受新连接
            }
        }
    }
    catch (const std::exception& e) {
        // 捕获初始化阶段的致命错误
        LOG << "Fatal server error: " << e.what();
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

服务器流程

1.网络协议初始化:

  • 1.配置端口;
  • 2.网络协议初始化,首先创建一个能存储版本协议等信息的结构体,然后初始化这个结构体(有轮子,不用自己实现);
  • 3.配置服务器地址、创建socket、设置端口复用;
  • 4.绑定地址和套接字、动态分配端口(防止端口被占用)、设置监听队列;
  • 5.初始化成功后进入主事件循环,设置存储客户端地址的结构体,然后开始接收连接;
  • 6.2345步除了配置服务器地址外都属于对socket的配置,因此可以将用到的函数整合成一个类;
  • 7.然后就是创建新线程处理请求,实际应该使用线程池。

总结起来就是主线程开启监听 socket 之后,进入无限循环调用 accept 处理客户端连接,accept 返回新的客户端 socket 后封装成任务交给线程池处理,线程池的线程共用一个队列,当有任务产生时,从任务队列中取出任务执行。
这里会扩展出几个问题:

1.如何实现在主线程投递N个任务时同时唤醒N个工作线程?

结合条件变量和任务队列的设计,任务队列与条件变量:工作线程在队列为空时阻塞在条件变量上。​精准唤醒机制:主线程每投递一个任务,立即调用一次条件变量的signal,总共调用N次。每次signal唤醒一个工作线程,确保N个任务对应N次唤醒。

2.如果 accept 之后就将客户端 socket 封装成任务交给线程池处理,此时严格来说是没有任务需要执行的,因为客户端 socket 上不一定有数据需要收发,如果有数据需要收发,任务线程如何处理?如果在工作线程中将客户端 socket 挂载到某 IO 复用函数上去,那么为了保证效率,这些任务就常驻线程池了,这样几个连接之后,线程池的所有线程都被占用了,无法继续处理其他任务了?

核心矛盾:直接将客户端 socket 作为任务交给线程池,会导致工作线程被阻塞在无数据的 socket 上,浪费线程资源。关键需求:工作线程不应阻塞等待数据,而应有选择地处理已就绪的 socket。
高效解决方案:Reactor + 线程池分层
架构分层设计:
主线程(Accept) → IO 复用线程(Reactor) → 线程池(Worker Threads)
​主线程:仅负责接受新连接,将客户端 socket ​设置为非阻塞模式。
​IO 复用线程​(1个或多个):
使用 epoll/kqueue 监听所有客户端 socket 的 ​IO 就绪事件​(如可读/可写)。
当 socket 有数据可读时,生成实际可执行的任务​(如读取请求数据)。
​线程池:仅处理 IO 就绪后的具体任务(如解析请求、业务计算、发送响应)。
“Reactor” 指的是 IO 复用线程,在系统架构中起到连接主线程和线程池的作用,负责处理输入 / 输出操作的复用。一般来说,Reactor 模式是一种事件驱动的设计模式,用于处理并发的服务请求。

3.既然是 Web 服务,那么解析 HTTP 数据包是一个核心功能, HTTP 协议的格式, 包头和包体如何分界,HTTP 是基于 TCP 协议的,TCP 是流式协议,包头可以通过 \r\n\r\n 确定边界,包体如何确定边界呢?

在 HTTP 协议中,包体(Body)边界的确定是网络编程的核心难点,尤其当基于流式传输的 TCP 协议时,需要根据协议规范设计精准的解析逻辑。以下是 HTTP 包体边界判断的完整技术方案:


一、HTTP 包体边界的三种判定方式

1. Content-Length 字段(固定长度)

规范依据:HTTP/1.1 RFC 7230。
工作流程
1. 解析包头时,提取 Content-Length: N 字段值。
2. 从包头结束符 \r\n\r\n 后开始读取固定 N 字节作为包体。
代码示例
cpp // 伪代码:解析 Content-Length size_t content_length = parse_header_field("Content-Length"); read_body(fd, content_length); // 精确读取 N 字节
注意事项
◦ 必须验证 Content-Length 是否为合法非负整数。
◦ 若实际数据不足 N 字节,需等待后续数据到达(需设计缓冲区暂存)。

2. Transfer-Encoding: chunked(分块传输)

规范依据:HTTP/1.1 RFC 7230。
工作流程
1. 包头中存在 Transfer-Encoding: chunked 字段。
2. 按分块格式逐块解析:
▪ 每块以 16进制长度值 + \r\n 开头。
▪ 读取长度值对应字节后,以 \r\n 结束当前块。
▪ 最终以 0\r\n\r\n 标识包体结束。
代码示例
cpp // 伪代码:解析 chunked 数据 while (true) { read_chunk_size(); // 读取块长度(例如 "1A" 表示 26 字节) if (chunk_size == 0) break; read_exact_bytes(chunk_size); // 读取块数据 read_crlf(); // 跳过 \r\n } read_trailer(); // 可选尾部头字段(通常为空)
注意事项
◦ 必须处理块扩展(如 1A;key=value)但通常可忽略。
◦ 需合并所有分块数据才能得到完整包体。

3. 连接关闭(Fallback 方案)

规范依据:HTTP/1.0 默认行为。
工作流程
1. 若包头中Content-Length 且非 chunked,则持续读取数据直到 TCP 连接关闭。
注意事项
不适用于持久连接(HTTP/1.1 默认 Keep-Alive)
◦ 仅用于兼容老旧客户端或特定场景(如服务器主动推送)。


二、协议解析状态机设计

1. 解析器状态转移
Start → 解析包头 → 检查包头字段 → 选择包体解析模式 → 解析包体 → 完成
2. 关键数据结构

缓冲区管理
cpp struct http_parser { enum { PARSE_HEADER, PARSE_BODY } state; size_t content_length; // 用于固定长度模式 size_t bytes_read; // 已读取字节计数 bool chunked_mode; // 分块传输标记 size_t chunk_remaining; // 当前块剩余字节 };

3. 解析逻辑伪代码
while (true) {
    switch (parser.state) {
        case PARSE_HEADER:
            if (找到 \r\n\r\n) {
                parse_headers();
                if (has_content_length) {
                    parser.content_length = content_length;
                    parser.state = PARSE_BODY;
                } else if (is_chunked) {
                    parser.chunked_mode = true;
                    parser.state = PARSE_BODY;
                } else {
                    // 无包体或 Fallback 模式
                }
            }
            break;
        case PARSE_BODY:
            if (parser.chunked_mode) {
                parse_chunked_data();
            } else {
                read_fixed_length();
            }
            break;
    }
}

三、工程实践中的关键问题

1. 数据不完整处理

问题:TCP 数据可能分多次到达(如包头和包体分离到达)。
方案
◦ 设计环形缓冲区(如 4KB~1MB)暂存未处理数据。
◦ 每次读取后更新解析状态,保留未消费数据。

2. 恶意攻击防御

Content-Length 超大值:限制最大允许值(如 10MB)。
分块长度溢出:校验 16 进制值为合法整数。
缓冲区溢出:限制单次读取数据量。

3. 与 Reactor 模式结合

非阻塞读取:当数据不完整时,将 socket 重新挂载到 epoll,等待下次可读事件。
高效触发:仅在数据足够时唤醒线程池处理业务逻辑。


四、对比与选型

方法适用场景复杂度资源占用
Content-Length包体长度已知(如文件下载)内存可预测
Chunked Encoding流式生成数据(如 ChatGPT)需动态合并数据
连接关闭兼容旧客户端无法复用连接

五、扩展讨论:HTTP/2 的帧结构

帧格式:HTTP/2 将数据封装为带长度字段的二进制帧(Length + Type + Flags + Stream ID + Payload),彻底解决流式边界问题。
优势:单连接多路复用,头部压缩,更精确的流控制。


3 既然是 HTTP 协议,那么肯定可以处理 GET 和 POST 请求,那么 GET 请求和 POST 请求有什么区别,你在处理的时候,如何区分的,分别又是如何解包的

在 HTTP 协议中,GET 和 POST 是最常用的两种请求方法,它们的核心区别体现在 语义、数据传递方式、安全性、编码规范 等方面。以下是详细的技术解析和具体处理方案:


一、GET 与 POST 的核心区别

特性GETPOST
语义获取资源(幂等操作)提交数据(非幂等操作)
数据位置URL 查询字符串(Query String)请求体(Body)
数据长度限制受 URL 长度限制(通常 ≤ 2KB)无限制(服务器可配置最大限制)
安全性参数暴露在 URL 中(易被缓存/记录)参数在 Body 中(相对安全)
缓存行为可被浏览器/代理缓存默认不缓存
编码类型仅支持 application/x-www-form-urlencoded支持多种类型(如 multipart/form-dataapplication/json
幂等性幂等(多次请求结果相同)非幂等(可能产生副作用)

二、区分 GET 与 POST 的请求方法

1. 解析 HTTP 请求行

• 从请求的第一行提取方法类型:
http GET /path?name=value HTTP/1.1 POST /path HTTP/1.1
代码示例
cpp // 伪代码:解析请求行 std::string method = parse_request_line(buffer).method; if (method == "GET") { /* 处理 GET */ } else if (method == "POST") { /* 处理 POST */ }

2. 区分逻辑

GET:无需检查请求头 Content-Type,直接解析 URL 查询参数。
POST:必须检查请求头 Content-TypeContent-Length,按需解析 Body。


三、GET 请求的解包处理

1. 提取 URL 查询参数

• 从 URL 中截取 ? 后的查询字符串:
http GET /api?name=Alice&age=20 HTTP/1.1
代码示例
cpp size_t pos = url.find('?'); if (pos != std::string::npos) { std::string query = url.substr(pos + 1); auto params = parse_url_encoded(query); // 解析键值对 }

2. URL 编码解码

• 对 %20 等特殊字符进行解码:
cpp std::string decoded = url_decode("Alice%26Bob"); // "Alice&Bob"

3. 缓存优化

• 根据 Cache-ControlExpires 头决定是否缓存响应。


四、POST 请求的解包处理

1. 确定 Body 数据类型

• 从 Content-Type 头选择解析方式:
http Content-Type: application/x-www-form-urlencoded Content-Type: multipart/form-data; boundary=xxx Content-Type: application/json

2. 解析 Body 数据

场景 1:application/x-www-form-urlencoded
cpp // 伪代码:解析键值对(类似 GET 查询参数) std::string body = read_body(content_length); auto params = parse_url_encoded(body);

场景 2:multipart/form-data(文件上传)
cpp // 伪代码:解析多部分数据 std::string boundary = extract_boundary(content_type); auto parts = parse_multipart_body(body, boundary); for (auto& part : parts) { if (part.is_file) save_file(part.filename, part.data); else process_field(part.name, part.value); }

场景 3:application/json
cpp // 伪代码:解析 JSON json data = json::parse(body); std::string username = data["username"];

3. 处理大数据与流式上传

• 分块读取 Body 数据,避免内存溢出:
cpp while (bytes_read < content_length) { int n = read(fd, buffer, buffer_size); buffer.append(buffer, n); bytes_read += n; }


五、安全与防御措施

1. GET 潜在风险

URL 参数泄露:避免在 GET 中传递敏感信息(如密码)。
XSS 攻击:对输出到 HTML 的参数进行转义。

2. POST 潜在风险

CSRF 攻击:校验 Referer 或添加 CSRF Token。
文件上传漏洞:限制文件类型、大小,重命名存储路径。

3. 通用防御

输入验证:对所有参数进行白名单过滤。
缓冲区溢出:限制最大请求长度(Header + Body)。


六、工程实践建议

1. 框架选择

• 优先使用成熟的 HTTP 解析库(如 llhttphttp-parser)。
• 避免重复造轮子,尤其是处理 multipart/form-data 边界和编码。

2. 性能优化

零拷贝技术:直接引用接收缓冲区解析数据,减少内存复制。
流式解析:在数据到达时逐步解析,而非等待完整 Body。

3. 调试工具

• 使用 Wiresharktcpdump 抓包验证协议解析正确性。
• 通过 Postmancurl 构造复杂请求测试边界条件。


4. 一般 Reactor 模式用得更多,那你知道 Proactor 和 Reactor 模式有什么区别吗

Proactor 与 Reactor 模式对比详解

一、核心概念对比
特性Reactor 模式Proactor 模式
I/O 模型同步非阻塞(Non-blocking Sync)异步(Asynchronous)
事件驱动核心监听 I/O 就绪事件(可读/可写)监听 I/O 完成事件(读写完成)
主动权归属应用层主动读写数据操作系统/框架完成 I/O,应用层处理结果
编程复杂度较低(需自行管理读写缓冲区)较高(需处理异步回调逻辑)
操作系统依赖依赖 epoll/kqueue依赖 IOCP(Windows)、io_uring(Linux)

二、工作流程对比
1. Reactor 模式流程
1. 主线程注册事件(可读/可写)到多路复用器(如 epoll)。
2. 多路复用器监听事件,当 socket 可读时通知应用层。
3. 工作线程从 socket **同步非阻塞读取数据**,处理业务逻辑。
4. 工作线程将响应数据**同步写入** socket。
2. Proactor 模式流程
1. 应用层发起异步 I/O 操作(如 `aio_read`)。
2. 操作系统负责将数据从内核缓冲区读取到用户缓冲区(无需应用层参与)。
3. I/O 完成后,操作系统通知应用层(如通过回调函数)。
4. 应用层直接处理已准备好的数据(如解析 HTTP 请求)。

三、架构角色对比
1. Reactor 模式组件

Handles(句柄):Socket 文件描述符。
Synchronous Event Demultiplexerepoll/kqueue 等待事件就绪。
Event Handler:定义 on_read()on_write() 回调接口。
Concrete Event Handler:实现业务逻辑(如 HTTP 解析)。

2. Proactor 模式组件

Proactor:管理所有异步 I/O 操作,接收完成通知。
Asynchronous Operation Processor:内核或框架异步执行 I/O。
Completion Handler:定义 on_completion() 回调接口。
Future/Promise:封装异步操作结果(如 C++ std::future)。


四、性能与资源消耗对比
指标ReactorProactor
CPU 利用率高(需频繁切换上下文处理读写)更高(I/O 与计算完全解耦)
内存占用需维护读写缓冲区通常由系统管理缓冲区
吞吐量受限于应用层读写效率更高(零拷贝优化潜力大)
适用场景10K~100K 并发连接100K+ 并发连接(如高频交易)

五、代码示例对比
1. Reactor 伪代码(epoll 实现)
// 主线程事件循环
while (true) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) {
        if (events[i].events & EPOLLIN) {
            // 同步非阻塞读取数据
            char buf[1024];
            int len = read(fd, buf, sizeof(buf));
            if (len > 0) process_data(buf, len);
        }
    }
}
2. Proactor 伪代码(io_uring 实现)
// 提交异步读请求
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buf, size, 0);
io_uring_submit(ring);

// 处理完成事件
struct io_uring_cqe *cqe;
io_uring_wait_cqe(ring, &cqe);
if (cqe->res > 0) process_data(buf, cqe->res);

六、优缺点总结
Reactor 模式

优点
• 跨平台支持好(Linux/BSD 均支持 epoll/kqueue)。
• 编程模型简单,适合快速开发。
缺点
• 高频小数据包场景可能产生“惊群效应”。
• 需自行管理读写边界(如 HTTP 分帧)。

Proactor 模式

优点
• 最大化利用系统异步 I/O 能力,吞吐量极高。
• 天然支持零拷贝(如 sendfile 直接传输文件)。
缺点
• Linux 生态支持较弱(io_uring 较新,需内核 ≥5.1)。
• 回调地狱(Callback Hell)增加代码维护成本。


七、选型建议
场景推荐模式理由
高并发 Web 服务(Nginx)Reactor成熟稳定,适合 Linux 环境
高频交易系统(Windows 服务)Proactor利用 IOCP 最大化 Windows 性能
文件传输/大数据处理Proactor异步 + 零拷贝显著提升吞吐量
嵌入式/IoT 设备Reactor资源消耗低,适配实时操作系统(RTOS)

八、混合模式(Hybrid)实践

现代高性能框架(如 Boost.Asiolibuv)通常提供统一接口,底层自动选择最优模式:
Linux:默认使用 epoll(Reactor),io_uring 可用时切换至 Proactor。
Windows:直接使用 IOCP(Proactor)。

// Boost.Asio 统一接口示例
asio::async_read(socket, asio::buffer(buf), [](error_code ec, size_t len) {
    if (!ec) process_data(buf, len); // 回调由框架自动适配 Reactor/Proactor
});

总结

Reactor 与 Proactor 本质是两种不同的 I/O 事件处理哲学
Reactor“等你有数据了我来读” → 同步非阻塞,主动权在应用层。
Proactor“你读好数据了通知我” → 完全异步,主动权在系统层。

选择时需权衡 性能需求、平台特性、开发成本,在高并发场景下,Reactor 因通用性强仍是主流,而 Proactor 在特定环境(如 Windows 或 Linux io_uring)中可发挥极致性能。

5.select、poll、epoll,我接着询问这三个 IO 复用函数的使用场景和优缺点,该同学答对后,我问了下 epoll 模型的水平和边缘触发模式的区别。

一、select、poll、epoll 对比

特性selectpollepoll
跨平台所有平台支持多数系统(Linux/BSD)Linux 特有
最大连接数受 FD_SETSIZE 限制(默认 1024)无硬性限制(基于链表)仅受系统内存限制
时间复杂度O(n) 线性遍历O(n) 线性遍历O(1) 事件驱动,仅处理活跃 fd
触发模式水平触发(LT)水平触发(LT)支持 LT 和边缘触发(ET)
内存开销固定大小 fd_set动态数组(pollfd 结构体)红黑树存储 fd,高效管理
适用场景低并发、兼容性要求高中低并发、需更多连接高并发(如 10K+ 连接)
核心区别

select/poll:每次调用需传递全部 fd 集合,内核遍历所有 fd 检查状态。
epoll:通过 epoll_ctl 注册 fd,内核维护就绪队列,仅返回活跃 fd。


二、epoll 的 LT(Level-Triggered)与 ET(Edge-Triggered)模式

1. 触发机制对比
模式触发条件数据未读完时的行为
LT只要 fd 可读/可写,持续通知下次调用 epoll_wait 会再次通知
ET仅当 fd 状态变化(如空→非空)时通知一次必须一次处理完,否则丢失通知
2. 代码实现差异
LT 模式示例:
// 水平触发:只要缓冲区有数据就会触发
if (events[i].events & EPOLLIN) {
    char buf[1024];
    int len = read(fd, buf, sizeof(buf)); // 可能未读完所有数据
    // 未读完时,下次 epoll_wait 会再次触发
}
ET 模式示例:
// 边缘触发:必须一次性读完所有数据
if (events[i].events & EPOLLIN) {
    while (true) {
        char buf[1024];
        int len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno == EAGAIN) break; // 无数据可读
        if (len <= 0) { /* 处理关闭或错误 */ break; }
        process_data(buf, len);
    }
}
3. 关键注意事项

ET 模式必须搭配非阻塞 fd

// 设置非阻塞模式(ET 必须)
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

ET 模式优势
• 减少事件触发次数,提高吞吐量(如处理突发流量)。
• 避免 LT 模式下“惊群效应”(多个线程/进程被唤醒)。
ET 模式缺点
• 编程复杂度高,需处理一次性读取全部数据。
• 可能因未及时处理数据导致业务延迟。


三、使用场景选择

模式适用场景典型案例
LT简单业务逻辑、低并发、需兼容性传统 Web 服务、代理服务器
ET高性能要求、高并发、需精细控制事件触发高频交易系统、实时游戏服务器
LT 与 ET 性能对比(假设 10K 并发连接):
指标LTET
事件触发次数高(每次数据到达均触发)低(仅状态变化时触发)
CPU 占用率较高较低
吞吐量

四、生产环境最佳实践

  1. ET 模式 + 非阻塞 IO
    • 避免因未读完数据导致事件丢失。
    • 结合 read/write 循环直到 EAGAIN 错误。

  2. LT 模式下的优化
    • 在数据可读时立即处理,避免多次触发。
    • 使用 EPOLLONESHOT 标志避免重复触发(需重新注册 fd)。

  3. 混合使用 LT 和 ET
    • 对写操作使用 ET(减少触发次数),对读操作使用 LT(简化处理逻辑)。


五、常见问题解答

Q1:ET 模式是否一定比 LT 快?

不一定。ET 减少事件触发次数,但处理不当可能因多次系统调用(如循环 read)抵消优势。需根据业务场景实测。

Q2:如何避免 ET 模式下的数据饥饿?

• 使用非阻塞 IO + 循环读取到 EAGAIN,确保单次事件处理完所有数据。

Q3:epoll 为什么高效?

• 内核通过红黑树管理 fd,仅返回就绪事件,避免无意义的遍历。


总结

LT 模式:编程简单,适合对吞吐量要求不高的场景。
ET 模式:性能更优,但需结合非阻塞 IO 和精细的数据处理逻辑。
• 实际开发中,推荐 ET 模式 应对高并发挑战,并通过严谨的代码设计规避潜在问题。

6.假设我某个客户端 socket 绑定到 epollfd 上后使用边缘触发模式,现在该客户端发来了 100 个字节,是否会触发读事件;服务端收了 50 个字节,读事件会在下一轮中继续触发吗?假设接着客户端又发了 10 字节,此时服务端会触发读事件吗?

分步解释与答案:

  1. 客户端发送 100 字节时
    触发条件:客户端首次发送数据,socket 接收缓冲区从空变为非空(状态变化)。
    结果触发一次读事件epoll_wait 返回该 fd 的 EPOLLIN 事件。

  2. 服务端读取 50 字节后
    缓冲区状态:仍有 50 字节未读。
    触发条件:ET 模式仅在状态变化时触发。由于缓冲区始终有数据(从 100 → 50,但未变空),无新状态变化
    结果下一轮 epoll_wait 不会触发读事件,剩余 50 字节将滞留在缓冲区,直到下次数据到达或应用主动读取。

  3. 客户端再次发送 10 字节时
    缓冲区状态:从 50 字节 → 60 字节(未发生空 → 非空的状态变化)。
    触发条件:ET 模式只关注“边缘”(如空 → 非空),不关心数据量增加。无新状态变化
    结果不会触发读事件,除非满足以下条件:
    ◦ 服务端将缓冲区读空(返回 EAGAIN)。
    ◦ 客户端再次发送数据,导致缓冲区从空 → 非空。


关键结论:

ET 模式的核心逻辑:仅在 fd 状态发生边缘变化(如空→非空、非空→空)时触发事件。
必须一次性读取所有数据:应用需循环读取直到 read 返回 EAGAIN,否则后续数据可能无法及时处理。
数据滞留风险:若未读完数据,即使新数据到达,只要缓冲区未变空,ET 模式不会重复通知。


处理 ET 模式的正确姿势:

// 边缘触发模式下的读操作(非阻塞 fd)
while (true) {
    char buf[1024];
    int len = read(fd, buf, sizeof(buf));
    if (len == -1) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break; // 数据已读完,退出循环
        }
        // 处理其他错误(如连接关闭)
        close(fd);
        break;
    } else if (len == 0) {
        // 对端关闭连接
        close(fd);
        break;
    }
    process_data(buf, len); // 处理数据
}

最终答案:

第一次发 100 字节:触发读事件 ✅
读取 50 字节后:下一轮不触发 ❌
再发 10 字节:不触发 ❌(除非缓冲区被读空后再次发数据)


http://www.kler.cn/a/598604.html

相关文章:

  • Mybatis——04
  • 计算机网络高频(二)TCP/IP基础
  • 深度学习框架PyTorch——从入门到精通(6.2)自动微分机制
  • git 基础操作
  • 机器学习结合盘古模型与RAMS实现多尺度气象分析与降尺度的程序结构与流程
  • 母婴电商企业案例:日事清驱动项目管理执行与OKR目标管理的流程自动化实践
  • 串口自动化断电测试
  • 神聖的綫性代數速成例題19. 最小二乘法在線性代數中的應用、線性空間的直和分解及相關性質、矩陣的特徵值分解的拓展應用
  • nginx配置https域名后,代理后端服务器流式接口变慢
  • Sqoop 常用命令
  • LeetCode(27):移除元素
  • mybatis操作数据库报错Cause: Cannot find class: ${com.mysql.cj.jdbc.Driver}
  • sgpt 终端使用指南
  • python每日十题(6)
  • 贪心算法(9)(java)最优除法
  • 深入解析 Redis 实现分布式锁的最佳实践
  • 基于Spring Boot的二手物品交易管理系统的设计与实现(LW+源码+讲解)
  • 无人机动平衡-如何在螺旋桨上添加或移除材料
  • 2025年优化算法:龙卷风优化算法(Tornado optimizer with Coriolis force,TOC)
  • 常见中间件漏洞攻略-Jboss篇