C++基础:session实现和http server类最终组装

2025/12/4:

实现一下http框架中的session部分

将所有部件组装为server

系列文章:

数据库连接池

代码详解http请求报文解析流程

http框架路由匹配算法实现

参考:kama httpserver

复现:TinyHttpServer

文章目录

  • [一、在实践中查看cookie & session](#一、在实践中查看cookie & session)
    • [二、session 面向对象分析](#二、session 面向对象分析)
  • 三、模块概览
  • 四、测试与bug梳理
    • [4.1 socket异常](#4.1 socket异常)
    • [4.2 客户端阻塞在recv函数位置](#4.2 客户端阻塞在recv函数位置)
    • [4.3 功能测试结果](#4.3 功能测试结果)
    • [4.4 性能测试](#4.4 性能测试)

一、在实践中查看cookie & session

Cookie与Session深度解析:原理、区别

对于没有web技术栈的人来说稍微了解一下就行。

这两个东西网上资料连篇累牍,不再赘述。不过看原理总是不够的,在编写这一模块之前,我们需要对两者有着更深入的理解。

首先随便打开一个网站,按 F12 后刷新,在筛选器处点击文档并选取相应部分:

这里我们可以看到,响应标头上带了个 set-cookie,与此同时,请求表头上有cookie:

点进cookie的详情部分,能看到请求和响应包里面的session id是一样的,这就是我们要找的部分:

看完相关的 blog 和这里的实践,应该能对 cookie 和 session 有一个大概的理解了。

二、session 面向对象分析

再分析一下解析 session 的流程。

首先,session 必须从 request 中提取。在前面,我们已经写过请求头的相关代码。请求头中的每个字段会被放入一个 unordered_map 的映射中,通过 request.getHead() 可以获取到字段 cookie 对应的一大段字符串。通过解析这段字符串,可以获取 request 所携带的 sessionId。根据Id,可以在 session_storage 中查询是否存在该sessionId。

在代码的实际使用过程中,我们将 session 模块拆分成三个子模块:Session SessionManager 以及 SessionStorage。

Manager的内容如下:

cpp 复制代码
    class SessionManager
    {
    public:
        explicit SessionManager(std::unique_ptr<SessionStorage> storage);

        // 从请求中获取或创建会话
        std::shared_ptr<Session> getSession(const HttpRequest& req, HttpResponse* resp);

        // 销毁会话
        void destroySession(const std::string& sessionId);

        // 清理过期会话
        void cleanExpiredSessions();

        // 更新会话
        void updateSession(std::shared_ptr<Session> session)
        {
            storage_->save(session);
        }
    private:
        // 生成唯一的会话标识符
        std::string generateSessionId();
        // 解析请求中的Cookie以获取会话ID
        static std::string getSessionIdFromCookie(const HttpRequest& req);
        // 在响应中设置会话Cookie
        static void setSessionCookie(const std::string& sessionId, HttpResponse* resp);

        std::unique_ptr<SessionStorage> storage_;
        std::mt19937 rng_; // 用于生成随机会话id
    };

分析一下设计思路:

在 Manager中,直接控制的子对象是 storage_ ,Manager 中通过调用 storage_ 的方法来间接操作存储的 session,这样操作有一个好处:storage 是一个基类,可以通过继承这个基类来实现多种不同的 storage,Manager 只需要操作 storage 给出的接口,无需关心底层具体的对 session 的操作,因此可以定制多种不同的 session 存储方式,如放在数据库中或者直接放在内存中等等。

下面的代码是放在内存中的模式:

cpp 复制代码
    class SessionStorage
    {
    public:
        virtual ~SessionStorage() = default;
        virtual void save(std::shared_ptr<Session> session) = 0;
        virtual std::shared_ptr<Session> load(const std::string& sessionId) = 0;
        virtual void remove(const std::string& sessionId) = 0;
    };

    // 基于内存的会话存储实现
    class MemorySessionStorage : public SessionStorage
    {
    public:
        void save(std::shared_ptr<Session> session) override;
        std::shared_ptr<Session> load(const std::string& sessionId) override;
        void remove(const std::string& sessionId) override;
    private:
        std::unordered_map<std::string, std::shared_ptr<Session>> sessions_;
    };

具体的 session 实现便不再赘述:

cpp 复制代码
    class Session : public std::enable_shared_from_this<Session>
    {
    public:
        Session(const std::string& sessionId, SessionManager* sessionManager, int maxAge = 3600); // 默认1小时过期

        const std::string& getId() const
        { return sessionId_; }

        bool isExpired() const;
        void refresh(); // 刷新过期时间

        void setManager(SessionManager* sessionManager)
        { sessionManager_ = sessionManager; }

        SessionManager* getManager() const
        { return sessionManager_; }

        // 数据存取
        void setValue(const std::string&key, const std::string&value);
        std::string getValue(const std::string&key) const;
        void remove(const std::string&key);
        void clear();
    private:
        std::string                                  sessionId_;
        std::unordered_map<std::string, std::string> data_;
        std::chrono::system_clock::time_point        expiryTime_;
        int                                          maxAge_; // 过期时间(秒)
        SessionManager*                              sessionManager_;
    };

三、模块概览

当前我们已经实现了多个模块,在组装成 http server 之前,这里先做一下梳理:

  • context(request)

    存放已连接客户端发来数据包的连接信息和数据信息等上下文。context是持久的,封装一个request和对其进行解析的函数,request是不持久的。

  • response

    用于组装为发送给客户端的数据,body返回客户端需要的信息。

  • middleware

    中间件,在收到请求前 or 发送请求后执行。

  • session

    标识不同客户端的手段,用于持久化连接信息。

  • router

    根据路由构建不同的回调函数,当解析完用户发送的请求后可以调用相应的函数执行任务。

  • sqlConnectionPool

    连接池,用于减少连接开销。

server暴露给用户的接口:router、中间件的注册方法以及sessionManager的相关返回等。

一次请求的具体处理流程:

    1. 解析请求 context->parseRequest(buf, receiveTime);
    1. 解析正常,执行请求,在该函数中构建response并发送
    • 根据连接情况初始化response对象
    • 执行核心处理流程 前置中间件 -> 路由函数调用 -> 后置中间件
      • middlewareChain_.handleRequest(req);
      • router_.route(req, &response);
      • middlewareChain_.handleResponse(response);
    1. 动作执行结束,重置上下文

具体代码见kama github。

四、测试与bug梳理

写了一个比较简单的测试文件,并没有用上gtest,每过五秒客户端会发送一个get请求:

cpp 复制代码
#include "include/http/HttpServer.h"
#include "include/http/HttpRequest.h"
#include "include/http/HttpResponse.h"
#include "include/router/Router.h"
#include "./gtest/gtest.h"
#include <sys/socket.h>
#include <thread>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace tinyHttp;

class HttpClient
{
public:
    HttpClient() = default;

    ~HttpClient() = default;

    void connect(const std::string& host, int port)
    {
        socket_ = socket(AF_INET, SOCK_STREAM, 0);
        if (socket_ < 0)
        {
            throw std::runtime_error("Failed to create socket");
        }
        sockaddr_in addr{};
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr(host.c_str());
        if (::connect(socket_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
        {
            throw std::runtime_error("Failed to connect");
        }
        std::cout << "Connected to " << host << ":" << port << std::endl;
    }

    void get(const std::string& path) const
    {
        std::cout << "发送 get 请求" << std::endl;
        std::string request =
            "GET " + path + " HTTP/1.1\r\n"
            "Host: localhost\r\n"
            "Connection: close\r\n"
            "\r\n";

        char buffer[1024];
        send(socket_, request.c_str(), request.length(), 0);
        int bytesReceived = recv(socket_, buffer, sizeof(buffer) - 1, 0);
        while (bytesReceived > 0)
        {
            buffer[bytesReceived] = '\0';
            std::cout << "recv: " << buffer << std::endl;
            bytesReceived = recv(socket_, buffer, sizeof(buffer) - 1, 0);
        }
    }

private:
    int socket_;
};

int main()
{
    int port = 80;
    std::cout << "Starting server on port " << port << "...\n";
    HttpServer server(port, "TestServer");
    std::cout << "注册路由 /hello\n";
    server.Get("/hello", [](const HttpRequest& req, HttpResponse* resp) {
        resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);
        resp->setStatusMessage("OK");
        resp->setBody("Hello, World!");
        std::cout << "收到请求,返回响应\n";
    });
    server.setThreadNum(4);

    {
        // 建立线程,每过五秒发送一次请求
        std::thread clientThread([]() {
            std::this_thread::sleep_for(std::chrono::seconds(5));
            try
            {
                HttpClient client{};
                client.connect("127.0.0.1", 80);
                while (true)
                {
                    client.get("/hello");
                    std::this_thread::sleep_for(std::chrono::seconds(5));
                }
            }
            catch (const std::exception& e)
            {
                std::cerr << "Client error: " << e.what() << std::endl;
            }
        });
        clientThread.detach();
    }


    server.start();
    std::cout << "服务器已启动,监听端口 " << port << std::endl;

}

4.1 socket异常

20251209 07:51:42.306071Z 51817 FATAL Address family not supported by protocol (errno=97) sockets::createNonblockingOrDie - SocketsOps.cc:91

调试出现的第一个bug一开始有点难以理解------绑定的端口以及ip协议的设置存在问题。排查过后发现是server内对象初始化顺序的问题(server_变量通过listenAddr_进行构造),具体来说:

cpp 复制代码
HttpServer::HttpServer(int port,
                       const std::string &name,
                       bool useSSL,
                       muduo::net::TcpServer::Option option)
    : listenAddr_(port)
    , server_(&mainLoop_, listenAddr_, name, option)
    , useSSL_(useSSL)
    , httpCallback_(std::bind(&HttpServer::handleRequest, this, std::placeholders::_1, std::placeholders::_2))
{
    initialize();
}


	  // 正确排列
    muduo::net::InetAddress                      listenAddr_; // 监听地址
    muduo::net::TcpServer                        server_; 
    // 错误排列
    muduo::net::TcpServer                        server_; 
    muduo::net::InetAddress                      listenAddr_; // 监听地址

当在初始化列表中进行初始化时,成员变量初始化的顺序与在初始化列表中排列的顺序无关,只与在对象内部声明的顺序有关。错误排列会导致server_在初始化的时候listenAddr_还没被初始化,导致错误。

4.2 客户端阻塞在recv函数位置

排查后发现粗心没把server最后发送包给客户端的逻辑加上。。。

4.3 功能测试结果

排查所有问题之后代码运行正常:

后续会进行更多样和复杂的测试

4.4 性能测试

今天加一下对tinyhttp的性能测试,不得不说这个项目其实还有很多小缺陷需要排查,这两天又解决了一些bug。

测试代码:

cpp 复制代码
// cpp

#include "./gtest/gtest.h"
#include "include/http/HttpServer.h"
#include "include/http/HttpRequest.h"
#include "include/http/HttpResponse.h"
#include "include/router/Router.h"
#include <future>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#include <thread>
#include <vector>
#include <atomic>
#include <chrono>
#include <iostream>
#include <cstring>
#include <string>

using namespace tinyHttp;

static bool sendAll(int sock, const char* data, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t n = ::send(sock, data + sent, static_cast<int>(len - sent), 0);
        if (n > 0) { sent += static_cast<size_t>(n); continue; }
        if (n == 0) return false;
        if (errno == EINTR) continue;
        return false;
    }
    return true;
}

static std::string requestOnce(const std::string& host, uint16_t port, const std::string& req) {
    int sock = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) return std::string();
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(host.c_str());
    if (::connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
        ::close(sock);
        return std::string();
    }

    if (!sendAll(sock, req.data(), req.size())) {
        ::close(sock);
        return std::string();
    }

    std::string resp;
    std::vector<char> buf(4096);
    for (;;) {
        ssize_t n = ::recv(sock, buf.data(), static_cast<int>(buf.size()), 0);
        if (n > 0) {
            resp.append(buf.data(), static_cast<size_t>(n));
            continue;
        } else if (n == 0) {
            // EOF: peer closed -> response complete
            break;
        } else {
            if (errno == EINTR) continue;
            break;
        }
    }
    ::close(sock);
    return resp;
}

static std::string makeGet(const std::string& path, const std::string& host = "localhost") {
    return std::string("GET ") + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
}

TEST(HttpServerPerf, Throughput) {
    const std::string host = "127.0.0.1";
    const uint16_t port = 8080;

    // 用 promise 通知主线程 server 已经构造并即将 start
    std::promise<void> serverReady;
    std::future<void> readyFuture = serverReady.get_future();

    std::thread serverThread([port, &serverReady]() {
        // 在服务器线程内构造 HttpServer,确保 EventLoop 在同一线程创建
        HttpServer server(port, "PerfTestServer");
        server.Get("/hello", [](const HttpRequest& req, HttpResponse* resp) {
            resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);
            resp->setStatusMessage("OK");
            resp->setBody("Hello, World!");
        });
        server.Post("/hello/pst", [](const HttpRequest& req, HttpResponse* resp) {
            std::string name = req.getHeader("name");
            resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);
            resp->setStatusMessage("OK");
            resp->setBody("Hello, " + name + "!");
        });
        server.setThreadNum(4);

        // 通知主线程 server 已构造完毕(即将 start)
        serverReady.set_value();

        // start() 通常会阻塞并在当前线程运行事件循环
        server.start();
    });

    // 等待服务器线程完成构造并准备 start(避免 EventLoop 跨线程)
    readyFuture.get();
    // 额外小等待确保 bind/listen 完成(根据实际 server 实现可调整/移除)
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    // 并发请求测试逻辑(与原实现相同)
    const int threads = 16;
    const int reqsPerThread = 200;
    std::atomic<int> successCnt{0};
    std::atomic<int> totalCnt{0};

    auto worker = [&](int id){
        for (int i = 0; i < reqsPerThread; ++i) {
            std::string req = makeGet("/hello?t=" + std::to_string(id) + "_" + std::to_string(i), "localhost");
            std::string resp = requestOnce(host, port, req);
            totalCnt.fetch_add(1, std::memory_order_relaxed);
            if (!resp.empty() && (resp.find("200") != std::string::npos || resp.find("Hello, World!") != std::string::npos)) {
                successCnt.fetch_add(1, std::memory_order_relaxed);
            }
        }
    };

    auto start = std::chrono::steady_clock::now();
    std::vector<std::thread> pool;
    pool.reserve(threads);
    for (int t = 0; t < threads; ++t) pool.emplace_back(worker, t);
    for (auto &th : pool) if (th.joinable()) th.join();
    auto end = std::chrono::steady_clock::now();

    double elapsed = std::chrono::duration_cast<std::chrono::duration<double>>(end - start).count();
    double qps = totalCnt.load() / elapsed;
    std::cout << "Perf: threads=" << threads
              << " total_requests=" << totalCnt.load()
              << " success=" << successCnt.load()
              << " elapsed(s)=" << elapsed
              << " QPS=" << qps << std::endl;

    ASSERT_GT(successCnt.load(), 0);

    // 如果没有优雅 stop 接口,分离线程以便测试进程结束时退出
    if (serverThread.joinable()) serverThread.detach();
}


int main(int argc, char **argv)
{
    // 不输出日志
	  muduo::Logger::setOutput([](const char*, int){});
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

贴一下结果:

Perf: threads=16 total_requests=3200 success=3200 elapsed(s)=3.31284 QPS=965.937

并发线程:16

总请求:3200(成功 3200,成功率 100%)

总耗时:3.31284 s

报告 QPS(每s处理请求数量):965.937

Perf: threads=16 total_requests=16000 success=16000 elapsed(s)=10.5133 QPS=1521.88

这里增加了连接数量,结果也还不错
Perf: threads=500 total_requests=50000 success=50000 elapsed(s)=30.9471 QPS=1615.66

这里增加了并发线程数量,可以看到对性能其实有比较大的影响

这里测试的主要是大量短连接

相关推荐
仰泳的熊猫2 小时前
1116 Come on! Let‘s C
数据结构·c++·算法·pat考试
倔强的小石头_2 小时前
Python 从入门到实战(六):字典(关联数据的 “高效管家”)
java·服务器·python
千疑千寻~3 小时前
【QML】C++访问QML控件
c++·qml
@YDWLCloud3 小时前
出海 APP 如何降低延迟?腾讯云国际版 GME 音视频深度评测
大数据·服务器·云计算·音视频·腾讯云
九河云3 小时前
华为云 ModelArts 赋能 AI 开发:从模型训练到边缘部署的全流程优化实践
服务器·人工智能·华为云·云计算
June`3 小时前
C++11(四):特殊类与单例模式设计精要
开发语言·c++
gaize12133 小时前
服务器全套知识科普
服务器
wadesir3 小时前
Linux文件系统创建(从零开始构建你的存储空间)
linux·运维·服务器
明月别枝惊鹊丶3 小时前
【C++】GESP 三级手册
java·开发语言·c++