一个OJ系统的诞生(九)Router-Server-main.cc

发布时间:2026/6/28 6:27:13
一个OJ系统的诞生(九)Router-Server-main.cc 前面 8 篇我们讲了每一个模块的细节现在这篇是后端的大结局——看 main.cc 如何像总指挥一样把 Config、Logger、ConnectionPool、AuthService、ProblemService、ExecutorService、Router 全部串起来启动一个能处理 HTTP 请求的服务器。1今天要将哪几个文件project-cpp-oj-vibecoding/ ├── src/ │ ├── main.cc ← ★ 程序入口67 行 │ ├── server/ │ │ ├── server.cc ← ★ 路由注册28 行 │ │ └── router.h ← ★ 路由声明7 行 │ ├── handler/ ← 已讲 │ ├── service/ ← 已讲 │ ├── model/ ← 已讲 │ ├── db/ ← 已讲 │ └── utils/ ← 已讲这三个文件负责的东西文件行数作用比喻router.h7 行声明register_routes()函数施工图纸—— 说要建什么server.cc28 行把所有 URL 和 Handler 绑定施工队—— 按图纸建好main.cc67 行整个程序的入口和启动流程总指挥—— 指挥所有人干活2router.h——最简单的头文件// // 文件名: router.h // 作用: 声明路由注册函数 // 这是接口——告诉别人有 register_routes 这个函数可以用 // #pragma once // 前向声明告诉编译器 httplib::Server 是一个类 // 不需要包含 httplib.h 的全部内容减少编译时间 namespace httplib { class Server; } // 声明一个函数注册所有 API 路由 // 具体的实现在 server.cc 中 void register_routes(httplib::Server server);3server.cc——注册路由表// // 文件名: server.cc // 作用: 注册所有 API 路由 // 一个函数28 行把 15 个 API 接口全部绑定到对应的 Handler // // 每个 API 的格式 // server.方法(路径, 处理函数); // 方法: Get / Post / Put / Delete // 路径: /api/problems/:id:id 是 URL 参数 // 处理函数: handle_get_problem // #include router.h #include ../handler/auth_handler.hpp #include ../handler/problem_handler.hpp #include ../handler/submit_handler.hpp #include ../handler/admin_handler.hpp #include httplib.h void register_routes(httplib::Server server) { // ═══════════════ 认证 API4 个═══════════════ server.Post(/api/register, handle_register); // 注册 server.Post(/api/login, handle_login); // 登录 server.Post(/api/logout, handle_logout); // 登出 server.Get(/api/me, handle_me); // 获取当前用户 // ═══════════════ 题目 API5 个═══════════════ server.Get(/api/problems, handle_get_problems); // 题目列表 server.Get(/api/problems/:id, handle_get_problem); // 题目详情 server.Post(/api/problems, handle_create_problem); // 创建题目 server.Put(/api/problems/:id, handle_update_problem); // 更新题目 server.Delete(/api/problems/:id, handle_delete_problem); // 删除题目 // ═══════════════ 提交 API3 个═══════════════ server.Post(/api/submit, handle_submit); // 提交代码 server.Get(/api/submissions/:id, handle_get_submission); // 查询判题结果 server.Get(/api/submissions, handle_get_submissions); // 提交历史 // ═══════════════ 管理 API3 个═══════════════ server.Get(/api/problems/:id/testcases, handle_get_testcases); // 获取测试用例 server.Post(/api/problems/:id/testcases, handle_add_testcase); // 添加测试用例 server.Delete(/api/problems/:id/testcases/:tc_id, handle_delete_testcase); // 删除测试用例 }路由匹配原理httplib怎么工作的步骤 1浏览器发送 HTTP 请求 GET /api/problems/42 HTTP/1.1 Host: localhost:8080 步骤 2httplib::Server 收到请求 步骤 3路由匹配按注册顺序 GET /api/problems ← 不匹配路径不同 GET /api/problems/:id ← ★ 匹配 ↓ :id 42 → 存到 req.path_params[id] 步骤 4调用对应的 Handler 函数 handle_get_problem(req, res); → 函数内部通过 req.path_params.at(id) 拿到 424main.cc——程序总指挥这是整个项目的起点。当你运行 ./oj_backend 时第一个执行的就是 main() 函数。// // 文件名: main.cc // 作用: 程序入口按顺序初始化所有模块启动服务器 // // 启动顺序重要不能乱 // 1. 加载配置Config // 2. 初始化日志Logger // 3. 初始化数据库连接池ConnectionPool // 4. 初始化各项服务AuthService / ProblemService / ExecutorService // 5. 创建 HTTP 服务器 // 6. 注册路由 // 7. 挂载静态文件目录 // 8. 启动监听 // // 关闭顺序反过来 // 8. 停止监听 // 7. 关闭 ExecutorService停止 Worker 线程 // 6. 关闭 AuthService停止 Session 清理线程 // 5. 关闭数据库连接池 // #include server/router.h #include service/auth_service.hpp #include service/problem_service.hpp #include service/executor_service.hpp #include db/connection_pool.hpp #include utils/config.hpp #include utils/logger.hpp #include httplib.h #include iostream #include csignal // 全局指针指向 HTTP 服务器用于信号处理 static httplib::Server* g_server nullptr; // // 信号处理函数 // 当用户按下 CtrlC 或系统发来 SIGTERM 时调用 // 作用优雅关闭服务器而不是强制 kill // void signal_handler(int) { if (g_server) g_server-stop(); } // ═══════════════════════════════════════════════════════════ // ★ 主函数一切从这里开始 // ═══════════════════════════════════════════════════════════ int main(int argc, char* argv[]) { // ── 第 1 步加载配置 ── // 默认加载 config/config.json // 也可以通过命令行参数指定./oj_backend my_config.json std::string config_path config/config.json; if (argc 1) config_path argv[1]; if (!Config::instance().load(config_path)) { std::cerr Failed to load config: config_path std::endl; return 1; // 配置加载失败 → 程序退出 } // ── 第 2 步初始化日志 ── Logger::instance().init(oj_backend.log); // 输出到 oj_backend.log 文件默认级别 INFO // ── 第 3 步初始化数据库连接池 ── auto db_cfg Config::instance().database(); if (!ConnectionPool::instance().init(db_cfg)) { Logger::instance().error(Failed to initialize database connection pool); return 1; // 数据库连不上 → 程序退出 } // ── 第 4 步初始化所有 Service ── AuthService::instance().init(); // 认证服务创建 Session 目录 启动清理线程 ProblemService::instance().init(); // 题目服务简单记录日志 ExecutorService::instance().init(); // 判题服务创建沙箱目录 启动 Worker 线程 // ── 第 5 步创建 HTTP 服务器 ── httplib::Server server; g_server server; // 保存全局指针供信号处理用 // ── 第 6 步注册信号处理 ── // SIGINT CtrlC // SIGTERM kill 命令 signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); // ── 第 7 步挂载静态文件目录 ── // 这样用户访问 http://localhost:8080/index.html // 就能返回 public/index.html 文件 auto static_dir Config::instance().server().static_dir; if (!server.set_mount_point(/, static_dir)) { Logger::instance().warn(Static directory not found: static_dir); // 静态文件目录不存在 → 只是警告不影响启动 // 没有前端页面API 仍然可用 } // ── 第 8 步注册 API 路由 ── // 把 server.cc 中定义的所有 API 绑定到服务器 register_routes(server); // ── 第 9 步启动服务器 ── auto port Config::instance().server().port; Logger::instance().info(Starting server on port std::to_string(port)); // listen() 会阻塞程序直到服务器停止 if (!server.listen(0.0.0.0, port)) { Logger::instance().error(Failed to start server); return 1; } // ════════════════════════════════════════════ // 服务器已停止收到信号或出错 // ════════════════════════════════════════════ Logger::instance().info(Server stopped); // ── 清理按初始化的反顺序关闭 ── ExecutorService::instance().shutdown(); // 停止判题 Worker 线程 AuthService::instance().shutdown(); // 停止 Session 清理线程 ConnectionPool::instance().close_all(); // 关闭所有数据库连接 return 0; // 程序正常退出 }1启动流程用户执行 ./oj_backend │ ▼ main() 开始 │ ├── 1. Config::load(config/config.json) │ └── 读取端口、数据库密码、判题参数等 │ ├── 2. Logger::init(oj_backend.log) │ └── 打开日志文件 │ ├── 3. ConnectionPool::init(db_cfg) │ └── 创建 4 个 MySQL 连接 │ ├── 4. AuthService::init() │ ├── 创建 /var/oj/sessions/ 目录 │ └── 启动后台清理线程 │ ├── 4. ProblemService::init() │ └── 记录日志没啥好初始化的 │ ├── 4. ExecutorService::init() │ ├── 创建 /tmp/oj_sandbox/ 目录 │ └── 启动 2 个 Worker 线程 │ ├── 5. 创建 httplib::Server │ ├── 6. 注册信号处理CtrlC / kill │ ├── 7. 挂载 public/ 为静态文件目录 │ ├── 8. register_routes(server) │ ├── POST /api/register │ ├── POST /api/login │ ├── GET /api/problems │ └── ...共 15 个 API │ ├── 9. server.listen(0.0.0.0, 8080) │ └── ★ 服务器开始运行 │ └── [等待请求...]2关闭流程用户按下 CtrlC │ ▼ 信号处理函数 signal_handler() │ └── g_server-stop() — 通知 httplib 停止监听 │ ▼ server.listen() 返回 │ ▼ Logger::info(Server stopped) │ ├── ExecutorService::shutdown() │ └── 停止 Worker 线程 │ ├── AuthService::shutdown() │ └── 停止 Session 清理线程 │ └── ConnectionPool::close_all() └── 关闭 4 个数据库连接 │ ▼ return 0 — 程序正常退出3为什么初始化那么重要// 错误顺序先初始化 Service再初始化连接池 AuthService::instance().init(); // Service 需要操作数据库 // → 但连接池还没初始化拿不到连接 → 崩溃 ConnectionPool::instance().init(cfg); // 太晚了 // 正确顺序先初始化依赖项 Config::instance().load(...); // 1. 配置最优先别的都要读配置 ConnectionPool::instance().init(cfg); // 2. 数据库连接池Service 依赖它 AuthService::instance().init(); // 3. Service依赖数据库 // → √ 一切正常4依赖关系图Config (谁都不依赖) ↑ Logger (谁都不依赖) ↑ ConnectionPool (依赖 Config) ↑ AuthService / ProblemService / ExecutorService (依赖 ConnectionPool Config) ↑ register_routes (依赖所有 Handler) ↑ server.listen (依赖 Route Static File)5信号处理——优雅退出// 全局指针httplib::Server 的快捷键 static httplib::Server* g_server nullptr; // 信号处理函数 void signal_handler(int) { if (g_server) g_server-stop(); // 告诉服务器停止监听 } int main() { httplib::Server server; g_server server; // 保存地址 // 注册信号处理 signal(SIGINT, signal_handler); // CtrlC signal(SIGTERM, signal_handler); // kill 命令 server.listen(0.0.0.0, port); // listen() 会阻塞在这里 // 直到 g_server-stop() 被调用 // 程序来到这里时开始清理 Logger::instance().info(Server stopped); // ...清理资源... return 0; }为什么需要信号处理用户按下 CtrlC → 进程被强制杀死 → 数据库连接没关闭 → MySQL 那边会保留僵尸连接 → Worker 线程正在判题 → 子进程变成孤儿进程 → Session 文件可能损坏 用户按下 CtrlC → signal_handler 被调用 → server.stop() → listen() 返回 → 关闭 Worker 线程等当前判题结束 → 关闭数据库连接 → 程序正常退出6静态文件服务// 挂载静态文件目录 auto static_dir Config::instance().server().static_dir; // public if (!server.set_mount_point(/, static_dir)) { Logger::instance().warn(Static directory not found: static_dir); }这段代码作用浏览器访问 服务器返回 ────────────────────────────────────────────── http://localhost:8080/ → public/index.html http://localhost:8080/login.html → public/login.html http://localhost:8080/js/api.js → public/js/api.js http://localhost:8080/css/style.css → public/css/style.css 如果 public/ 目录不存在 → 只是警告API 仍然可用 你可以用 curl 调 API只是没有前端页面这样前端 HTML/JS/CSS 文件就和后端在同一个端口服务了不需要 Nginx 或另一个服务器。7整个系统的完整启动日志当你运行 ./oj_backend 时oj_backend.log 文件中会看到2026-06-25 21:00:00 [INFO] AuthService initialized, session dir: /var/oj/sessions2026-06-25 21:00:00 [INFO] ProblemService initialized2026-06-25 21:00:00 [INFO] ExecutorService initialized with 2 workers2026-06-25 21:00:00 [INFO] Starting server on port 8080每一行对应main.cc中的一个init()调用每当有请求进来会看到2026-06-25 21:01:00 [INFO] User registered: alice (id5)2026-06-25 21:02:00 [INFO] User login: alice2026-06-25 21:03:00 [INFO] Problem created: id32026-06-25 21:04:00 [INFO] Submission 42 → JUDGING2026-06-25 21:04:01 [INFO] Submission 42 → AC按下CtrlC可以看到2026-06-25 21:05:00 [INFO] Server stopped8CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(oj_backend LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) # C17 标准 set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 第三方库header-only不需要编译 set(THIRD_PARTY_DIR ${CMAKE_SOURCE_DIR}/third_party) include_directories(${THIRD_PARTY_DIR}) # 查找系统库 find_package(OpenSSL REQUIRED) # 密码哈希需要 find_package(PkgConfig REQUIRED) pkg_check_modules(MYSQL REQUIRED mysqlclient) # MySQL pkg_check_modules(SECCOMP REQUIRED libseccomp) # 沙箱 include_directories(${CMAKE_SOURCE_DIR}/src) # 添加源码目录 # 编译所有 .cc 文件 file(GLOB_RECURSE SOURCES src/*.cc) # 生成可执行文件 add_executable(oj_backend ${SOURCES}) # 链接库 target_link_libraries(oj_backend PRIVATE ${MYSQL_LIBRARIES} # MySQL 客户端库 ${SECCOMP_LIBRARIES} # seccomp 沙箱库 OpenSSL::SSL # OpenSSL OpenSSL::Crypto pthread # 多线程 dl # 动态加载 crypt # 密码哈希 ) # 编译后自动复制 public/ 到可执行文件目录 add_custom_command(TARGET oj_backend POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/public $TARGET_FILE_DIR:oj_backend/public COMMENT Copying static files... )编译和运行# 1. 创建构建目录 mkdir build cd build # 2. 生成 Makefile cmake .. # 3. 编译 make -j$(nproc) # 4. 运行 cd build ./oj_backend # 或者用 start.sh 一步完成 bash scripts/start.sh9完整项目结构回顾整个 OJ 系统15 个 API 5 个前端页面 │ ▼ ┌────────Config────────┐ │ config.json 读取 │ │ 端口 / 数据库 / 判题 │ └────────┬─────────────┘ │ ┌────────Logger────────┐ │ 日志输出到文件 │ └────────┬─────────────┘ │ ┌────ConnectionPool────┐ │ MySQL 连接池4 个 │ └────────┬─────────────┘ │ ┌────┴────┐ │ Service │ │ 层 │ ├─ AuthService ───── 注册/登录/鉴权/限流 ├─ ProblemService ── 题目 CRUD 测试用例 └─ ExecutorService ─ 判题引擎2 个 Worker │ ▼ ┌────┴────┐ │ Handler │ │ 层 │ ├─ auth_handler ──── 4 个认证 API ├─ problem_handler ─ 5 个题目 API ├─ submit_handler ── 3 个提交 API └─ admin_handler ─── 3 个管理 API │ ▼ ┌────┴────┐ │ Router │ │ │ └────┬────┘ │ ▼ ┌────┴────┐ │ Server │ ← httplib 监听 8080 端口 │ │ ├─ API 路由 ──── 15 个 REST 接口 └─ 静态文件 ──── public/ 目录 │ ▼ 浏览器 ←→ HTTP ←→ 服务器10总结技术点在 main.cc 中的体现程序入口int main(int argc, char* argv[])初始化顺序依赖项优先被依赖项在后信号处理signal(SIGINT/SIGTERM, handler)优雅关闭资源清理反向初始化顺序关闭RAII 思想全局指针static Server*让信号处理函数能访问 server静态文件服务set_mount_point()一个函数搞定前端文件服务CMake 构建GLOB_RECURSE自动收集源文件POST_BUILD复制静态文件