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

前面 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 复制静态文件