2025/12/4:
实现一下http框架中的session部分
将所有部件组装为server
系列文章:
文章目录
- [一、在实践中查看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
对于没有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的相关返回等。
一次请求的具体处理流程:
-
- 解析请求 context->parseRequest(buf, receiveTime);
-
- 解析正常,执行请求,在该函数中构建response并发送
- 根据连接情况初始化response对象
- 执行核心处理流程 前置中间件 -> 路由函数调用 -> 后置中间件
- middlewareChain_.handleRequest(req);
- router_.route(req, &response);
- middlewareChain_.handleResponse(response);
-
- 动作执行结束,重置上下文
具体代码见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这里增加了并发线程数量,可以看到对性能其实有比较大的影响
这里测试的主要是大量短连接