前面 8 篇我们讲了每一个模块的细节,现在这篇是后端的大结局------看 main.cc 如何像"总指挥"一样,把 Config、Logger、ConnectionPool、AuthService、ProblemService、ExecutorService、Router 全部串起来,启动一个能处理 HTTP 请求的服务器。
1:今天要将哪几个文件
cpp
project-cpp-oj-vibecoding/
├── src/
│ ├── main.cc ← ★ 程序入口(67 行)
│ ├── server/
│ │ ├── server.cc ← ★ 路由注册(28 行)
│ │ └── router.h ← ★ 路由声明(7 行)
│ ├── handler/ ← (已讲)
│ ├── service/ ← (已讲)
│ ├── model/ ← (已讲)
│ ├── db/ ← (已讲)
│ └── utils/ ← (已讲)
这三个文件负责的东西
| 文件 | 行数 | 作用 | 比喻 |
|---|---|---|---|
| router.h | 7 行 | 声明 register_routes() 函数 |
"施工图纸"------ 说要建什么 |
| server.cc | 28 行 | 把所有 URL 和 Handler 绑定 | "施工队"------ 按图纸建好 |
| main.cc | 67 行 | 整个程序的入口和启动流程 | "总指挥"------ 指挥所有人干活 |
2:router.h------最简单的头文件
cpp
// ============================================================
// 文件名: router.h
// 作用: 声明路由注册函数
// 这是"接口"------告诉别人有 register_routes 这个函数可以用
// ============================================================
#pragma once
// 前向声明:告诉编译器 httplib::Server 是一个类
// 不需要包含 httplib.h 的全部内容,减少编译时间
namespace httplib {
class Server;
}
// 声明一个函数:注册所有 API 路由
// 具体的实现在 server.cc 中
void register_routes(httplib::Server& server);
3:server.cc------注册路由表
cpp
// ============================================================
// 文件名: 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) {
// ═══════════════ 认证 API(4 个)═══════════════
server.Post("/api/register", handle_register); // 注册
server.Post("/api/login", handle_login); // 登录
server.Post("/api/logout", handle_logout); // 登出
server.Get("/api/me", handle_me); // 获取当前用户
// ═══════════════ 题目 API(5 个)═══════════════
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); // 删除题目
// ═══════════════ 提交 API(3 个)═══════════════
server.Post("/api/submit", handle_submit); // 提交代码
server.Get("/api/submissions/:id", handle_get_submission); // 查询判题结果
server.Get("/api/submissions", handle_get_submissions); // 提交历史
// ═══════════════ 管理 API(3 个)═══════════════
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怎么工作的)
cpp
步骤 1:浏览器发送 HTTP 请求
GET /api/problems/42 HTTP/1.1
Host: localhost:8080
步骤 2:httplib::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") 拿到 "42"
4:main.cc------程序总指挥
这是整个项目的起点。当你运行 `./oj_backend` 时,第一个执行的就是 main() 函数。
cpp
// ============================================================
// 文件名: 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;
// ============================================================
// 信号处理函数
// 当用户按下 Ctrl+C 或系统发来 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 = Ctrl+C
// 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:启动流程
cpp
用户执行 ./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. 注册信号处理(Ctrl+C / 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:关闭流程
cpp
用户按下 Ctrl+C
│
▼
信号处理函数 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:为什么初始化那么重要
cpp
// 错误顺序:先初始化 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:依赖关系图
cpp
Config (谁都不依赖)
↑
Logger (谁都不依赖)
↑
ConnectionPool (依赖 Config)
↑
AuthService / ProblemService / ExecutorService (依赖 ConnectionPool + Config)
↑
register_routes (依赖所有 Handler)
↑
server.listen (依赖 Route + Static File)
5:信号处理------优雅退出
cpp
// 全局指针: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); // Ctrl+C
signal(SIGTERM, signal_handler); // kill 命令
server.listen("0.0.0.0", port);
// listen() 会阻塞在这里
// 直到 g_server->stop() 被调用
// 程序来到这里时,开始清理
Logger::instance().info("Server stopped");
// ...清理资源...
return 0;
}
为什么需要信号处理
cpp
用户按下 Ctrl+C → 进程被强制杀死
→ 数据库连接没关闭 → MySQL 那边会保留"僵尸连接"
→ Worker 线程正在判题 → 子进程变成"孤儿进程"
→ Session 文件可能损坏
用户按下 Ctrl+C → signal_handler 被调用
→ server.stop() → listen() 返回
→ 关闭 Worker 线程(等当前判题结束)
→ 关闭数据库连接
→ 程序正常退出
6:静态文件服务
cpp
// 挂载静态文件目录
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);
}
这段代码作用:
bash
浏览器访问 服务器返回
──────────────────────────────────────────────
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/sessions
2026-06-25 21:00:00 INFO ProblemService initialized
2026-06-25 21:00:00 INFO ExecutorService initialized with 2 workers
2026-06-25 21:00:00 INFO Starting server on port 8080
每一行对应main.cc中的一个init()调用
每当有请求进来,会看到:
2026-06-25 21:01:00 INFO User registered: alice (id=5)
2026-06-25 21:02:00 INFO User login: alice
2026-06-25 21:03:00 INFO Problem created: id=3
2026-06-25 21:04:00 INFO Submission 42 → JUDGING
2026-06-25 21:04:01 INFO Submission 42 → AC
按下Ctrl+C可以看到:
2026-06-25 21:05:00 INFO Server stopped
8:CMakeLists.txt
bash
cmake_minimum_required(VERSION 3.16)
project(oj_backend LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) # C++17 标准
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..."
)
编译和运行
bash
# 1. 创建构建目录
mkdir build && cd build
# 2. 生成 Makefile
cmake ..
# 3. 编译
make -j$(nproc)
# 4. 运行
cd build
./oj_backend
# 或者用 start.sh 一步完成
bash scripts/start.sh
9:完整项目结构回顾
bash
整个 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 复制静态文件 |