CHAPTER 22
Server 函数手册
依据源码:include/Server.h、src/Server.cpp。
本模块是项目主干,本文特别按你要求区分两种运行模式:
- 线程池版本:
accept -> ThreadPool::addTask(handleClient)(历史/常规) - 协程版本:
accept -> Scheduler::submit(handleClient)(当前源码默认)
0. 双版本切换点(先看)
在 Server::run() 主循环中:
- 线程池投递代码被注释:
threadpool_.addTask(&Server::handleClient, this, clientSocket, &clientAddress);
- 当前启用协程调度投递:
Scheduler::instance().submit(&Server::handleClient, this, clientSocket, &clientAddress);
因此现状是协程版本在跑,但代码结构保留了线程池版本,面试可讲“可回切”。
1. 顶层自由函数与静态变量
1.1 std::string mpToJson(const std::map<std::string, std::string>& mp)
- 将简单字符串 map 拼成 JSON 文本。
- 逻辑较朴素:手工拼接,最后
pop_back去掉尾逗号。 - 风险:未做 JSON 转义(value 若含引号/换行可能不合法)。
1.2 Server* Server::instance_
- 单例指针,用于信号回调中访问当前
Server。
1.3 匿名命名空间常量(接收策略)
RECV_CHUNK_SIZE = 64KBMAX_HEADER_SIZE = 1MBMAX_BODY_SIZE = 100MB
1.4 static size_t parseContentLengthFromHeaders(const std::string& headerPart)
- 小写化头文本后查找
content-length: - 解析连续数字,不合法返回 0
- 用于“先收头后收体”的两阶段收包
2. Server 生命周期函数
2.1 Server::Server(int port, bool printParams)
作用
初始化服务实例,接入崩溃处理与信号处理。
关键步骤
- 初始化成员:端口、socket=-1、running=false、日志参数
- 初始化
threadpool_(hardware_concurrency或 1) instance_ = thisCrashHandler::instance().init([this]{ cleanup(); })registerSignalHandlers()
2.2 Server::~Server()
- 输出
"Shutting down server...",调用stop()
2.3 void Server::cleanup()
用于崩溃路径与 run 退出收尾。
- 加
mutex_ running_ = false- 关闭
serverSocket_ Scheduler::instance().stop()- 输出清理日志
注意:打印了
"Thread pool shutdown",但实际调用的是Scheduler::stop();这是文案与实现不一致点。
2.4 void Server::stop()
正常停服入口:
- 若已停止则返回
running_ = falseScheduler::instance().stop()- 关闭
serverSocket_ - Windows 下
WSACleanup()
3. 信号处理相关
3.1 void Server::registerSignalHandlers()
- 注册
SIGINT(Ctrl+C) - Windows 额外
SIGBREAK - Linux 额外
SIGTERM - 回调中仅将
instance_->running_ = false
3.2 void Server::signalHandler(int)
- 当前空实现(保留接口)。
4. 路由注册与查找
4.1 基础注册函数
get/post/put/del(带/不带 middleware 两组重载)- 最终都落到:
addRoute(method, path, handler, middlewares)
4.2 void addRoute(...)
- 加锁
routesMutex_ routes_[method][path] = RouteEntry{handler, middlewares}
4.3 void use(Middleware middleware)
- 全局 middleware 追加到
middlewares_
4.4 const RouteEntry* findRoute(const std::string& method, const std::string& path) const
- 精确匹配:method map -> path map
- 未命中返回
nullptr
5. RouterGroup 函数族
5.1 构造
RouterGroup(Server& app, std::string prefix, std::vector<Middleware> middlewares)
5.2 group(prefix)
- 前缀拼接(处理
/重复) - 继承上级 middlewares
5.3 use(middleware)
- 追加组级 middleware
5.4 resolvePath(path)
- 处理前缀与子路径拼接规则:
a/ + /b -> a/ba + b -> a/b
5.5 mergeMiddlewares(routeMiddlewares)
- 组级在前,路由级在后,生成最终链
5.6 get/post/put/del(两组重载)
- 调
app_.addRoute(HTTP_METHOD, resolvePath(path), handler, mergeMiddlewares(...))
6. 运行主流程 run()(双版本核心)
6.1 公共启动阶段
- Windows 下
WSAStartup - 创建/绑定 IPv4 socket
- 尝试创建/绑定 IPv6 socket(失败不阻断 IPv4)
listen(serverSocket_, 10)- 打印路由与地址信息
- 启动协程调度器:
configureTimeSlice(5ms)start(hardware/2)submit(doTaskRegular, 30min)
6.2 主 accept 循环
while (running_) {
clientSocket = accept(...)
if error -> continue
dispatch(handleClient)
}
cleanup()
6.3 分派差异(面试重点)
A) 线程池版本(可回切)
threadpool_.addTask(&Server::handleClient, this, clientSocket, &clientAddress)- 特点:
- 每连接一个线程池任务
- 阻塞 recv/send 会占 worker 线程
- 代码心智简单
B) 协程版本(当前启用)
Scheduler::instance().submit(&Server::handleClient, this, clientSocket, &clientAddress)- 特点:
- 任务调度在协程框架中运行
handleClient内多处调用safe_point()主动让出- 仍基于阻塞 socket,但可通过协程安全点减少单任务长期独占
关键说明:当前并非完整非阻塞 I/O 协程,网络调用仍是阻塞
recv/send。
7. 客户端处理函数(HTTP 主链)
7.1 void handleClient(int clientSocket, const sockaddr_in* clientAddress)
阶段一:收请求头
- 循环
recv - 查找
\r\n\r\n或\n\n - 超过
MAX_HEADER_SIZE直接关闭 - 协程版本在循环尾部
safe_point()
阶段二:收请求体
parseContentLengthFromHeaders- 若 body 超过
MAX_BODY_SIZE关闭连接 - 继续读到
headerEnd + contentLength - 每轮
safe_point()
阶段三:解析与分发
parseRequest(requestStr)- 构建
Response - 若
OPTIONS直接 200 - 执行全局 middlewares(可短路)
findRoute- 执行路由 middlewares(可短路)
- 执行 handler
- 异常 ->
response.error(500, ...) - 未命中 -> 404
阶段四:回包与日志
responseStr = buildResponse(response)- 发送采用“循环 send 直到 remaining==0”
- 每轮 send 后
safe_point() - 若
response.streamWriter,执行流式发送 - 打印访问日志(IP/method/path/code/bodyLen/cost)
- 关闭客户端 socket
7.2 void handleClientIPv6(int clientSocket, const sockaddr_in6* clientAddress)
- 逻辑与 IPv4 版本基本一致
- 当前发送仍是一次
send(不像 IPv4 版本有 remaining 循环) - 日志 IP 使用
getClientIPv6 - 在
listenIPv6中通过threadpool_.addTask分发(注意与主 accept 的协程分发不同)
这意味着当前 IPv6 分支仍偏线程池路径,属于双模型并存状态。
7.3 void listenIPv6(int serverSocketIPv6)
- 循环
acceptIPv6 - 成功后
threadpool_.addTask(handleClientIPv6, ...) - 退出后关闭 IPv6 socket
8. 请求/响应编解码函数
8.1 Request parseRequest(const std::string& requestStr)
- 分离 header/body
- 解析请求行:method/path
- 解析 query:
parseQueryParams - 解析 headers(trim)
- 根据
Content-Length截取 body request.parseBody()(解析 form/json/multipart 等)
8.2 std::string buildResponse(const Response& response)
- 写状态行(200/201/400/404/500)
- 注入 CORS 头
- 写自定义 headers
- 若未设置
Content-Length自动补齐 - 空行分隔
- 非流式时追加 body
8.3 std::map<std::string,std::string> parseQueryParams(const std::string& query)
- 按
&切分,再按=切分 - value 用
urlDecode
8.4 std::string urlDecode(const std::string& value)
+ -> 空格%XX十六进制解码- 非法
%编码保留原字符
9. 网络与工具函数
9.1 std::string getLanIpv4()
- 创建 UDP socket,connect 到
8.8.8.8:53 - 通过
getsockname获取本机出站 IP - 失败回退
127.0.0.1
9.2 std::string getLanIpv6()
- 类似 IPv4 逻辑,目标
2001:4860:4860::8888 - 过滤 link-local 与 loopback,失败回退
::1
9.3 std::string getClientIP(...) / getClientIPv6(...)
inet_ntop/InetNtopA转字符串
9.4 void printRegisteredRoutes() const
- 按
GET/POST/PUT/DELETE分组打印路由表
9.5 std::string getFormattedDate()
- 输出形如
%d/%b/%Y:%H:%M:%S +0800
10. 定时任务函数
void doTaskRegular(long long during) const
- 参数必须 > 0
- 使用
steady_clock做间隔调度 - 每到间隔调用
task() - 捕获异常避免循环崩溃
- 动态 sleep(interval/10 或剩余时间)
该函数由
Scheduler::submit启动,和主请求处理并行运行。
11. Request / Response 内联函数要点(Server.h)
11.1 Request 相关
query_param/body_param/json_param/fileparseBody按 content-type 分发:- urlencoded / json / multipart / text / xml / autoDetect
parseMultipartFormData与parseMultipartPart实现了简化文件上传解析
11.2 Response 相关
json/text/success/errorfile:整文件读取到 bodyfileStream:通过streamWriter分块发送大文件getFileNameWithExt
12. 双版本面试答法(可直接复述)
- 线程池版本:模型稳定、实现直观,但阻塞 I/O 会占住 worker。
- 协程版本:通过
Scheduler::submit + safe_point提升任务切换能力,减少长任务独占。 - 当前状态:是“协程调度 + 阻塞 socket”的过渡架构,不是完整 event-loop 非阻塞架构。
- 下一步演进:将 socket 改为非阻塞并绑定 epoll/IOCP 事件,再把可等待点映射到协程挂起/恢复。
下一模块(顺序)
Threadpool → docs/modules/detail/Threadpool_FUNCTIONS.md