跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 在线判题系统(OJ)设计与实现

基于 C++ 开发的轻量级在线判题系统(OJ),采用分层架构设计。系统包含 compile_server 编译运行服务、oj_server 业务服务及 comm 公共模块。oj_server 基于 MVC 结构,Model 层管理题库数据,View 层负责网页渲染,Control 层整合业务逻辑与负载均衡。支持代码提交、编译、运行、资源限制(CPU/内存)及故障主机自动下线恢复。实现了从题目管理到判题反馈的完整链路,适用于编程教学或小型竞赛场景。

魔尊发布于 2026/3/21更新于 2026/9/1769 浏览
C++ 在线判题系统(OJ)设计与实现

1. 项目说明与整体框架

1.1 项目说明

本项目是模仿牛客平台的 OJ 判题系统,主要实现三个模块:

  • comm:公共模块
  • compile_server:编译运行模块
  • oj_server:获取题目列表、查看题目、编写题目界面、负载均衡及其他功能

1.2 项目宏观结构图

(图片已移除)

1.3 实现思路

  1. 先实现 compile_server
  2. 实现 oj_server
  3. 基于文件版本的在线 OJ
  4. 前端页面的设计

2. compile_server 服务设计

提供的服务:编译并运行代码,得到格式化的相关结果。

在正式编写编译功能之前,我们先来编写一下 comm 公共模块,因为后面其他模块可能都需要用到其中的方法,所以我们先来实现公共模块中的一些需求。

公共模块包含两个部分:简化版日志、实用工具部分。

我们先来看简化版日志的设计:

简化版日志代码(log.hpp)

#pragma once
#include <iostream>
#include <string>
#include "util.hpp"
namespace ns_log {
using namespace ns_util;
//枚举日志等级
enum { INFO, DEBUG, WARNING, ERROR, FATAL };
inline std::ostream &Log(const std::string &level, const std::string &file_name, int line) {
    //添加日志等级
    std::string message = "[";
    message += level;
    message += "]";
    //添加报错文件名称
    message += "[";
    message += file_name;
    message += "]";
    //添加报错行
    message += ;
    message += std::(line);
    message += ;
    
    message += ;
    message += TimeUtil::();
    message += ;
    std::cout << message; 
     std::cout;
}


}
"["
to_string
"]"
//添加时间戳
"["
GetTimeStamp
"]"
//这里不要刷新缓冲区,cout 本质内部是包含缓冲区的
return
//LOG(level)<<"日志信息 1"<<"日志信息 2"<<"\n";
#define LOG(level) Log(#level, __FILE__, __LINE__) //开放式日志

这里我们定义了一个命名空间 ns_log,这样做可以防止与其他代码发生冲突;

在设计 Log 函数的时候,将其设计为内联函数,因为这个函数在后面会被我们频繁调用,且它整体代码长度也不长,这样可以减少函数调用的栈帧开销,提高效率。

在函数体中,我们直接构造 message,实现格式化日志输出,返回 std::ostream&支持链式调用,这里注意我们在代码中没有主动刷新缓冲区。

在最后用到了宏定义,使用了 __FILE__和__LINE__这两个 C++ 预定义宏,分别自动获取当前源文件名和行号,用于定位日志产生的位置(调试关键信息),宏中的#level是预处理器的字符串化操作,将宏参数level直接转换为字符串字面量,这样用户就不需要手动传入字符串了,直接使用宏,传入日志等级即可输出日志,开放式日志支持用户输入多条日志消息(通过<<操作符)。

实用工具代码(util.hpp)

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <atomic>
#include <fstream>
#include <boost/algorithm/string.hpp>
namespace ns_util {
class TimeUtil {
public:
    static std::string GetTimeStamp() {
        struct timeval _time;
        gettimeofday(&_time, nullptr);
        return std::to_string(_time.tv_sec);
    }
    // 获得毫秒级时间戳
    static std::string GetTimeMS() {
        struct timeval _time;
        gettimeofday(&_time, nullptr);
        return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
    }
};
const std::string temp_path = "./temp/";
class PathUtil {
public:
    static std::string Addsuffix(const std::string &filename, const std::string &suffix) {
        std::string pathname = temp_path;
        pathname += filename;
        pathname += suffix;
        return pathname;
    }
    // 编译时需要的临时文件
    static std::string Src(const std::string &filename) { return Addsuffix(filename, ".cpp"); }
    static std::string Exe(const std::string &filename) { return Addsuffix(filename, ".exe"); }
    static std::string CompilerError(const std::string &filename) { return Addsuffix(filename, ".compile_error"); }
    // 运行时需要的临时文件
    static std::string Stdin(const std::string &filename) { return Addsuffix(filename, ".stdin"); }
    static std::string Stdout(const std::string &filename) { return Addsuffix(filename, ".stdout"); }
    static std::string Stderr(const std::string &filename) { return Addsuffix(filename, ".stderr"); }
};
class Fileutil {
public:
    static bool IsFileExists(const std::string &path_name) {
        struct stat st;
        if (stat(path_name.c_str(), &st) == 0) return true;
        return false;
    }
    static std::string UniqFileName() {
        // 毫秒级时间戳 + 原子性递增唯一值:保证原子性
        static std::atomic_uint id(0);
        id++;
        std::string ms = TimeUtil::GetTimeMS();
        std::string uniq_id = std::to_string(id);
        return ms + "_" + uniq_id;
    }
    static bool WriteFile(const std::string &target, const std::string &content) {
        std::ofstream out(target);
        if (!out.is_open()) { return false; }
        out.write(content.c_str(), content.size());
        out.close();
        return true;
    }
    static bool ReadFile(const std::string &target, std::string *content, bool keep = false) {
        (*content).clear(); //避免数据污染,保证每次读到的都是一个文件的内容
        std::ifstream in(target);
        if (!in.is_open()) { return false; }
        std::string line;
        while (std::getline(in, line)) {
            (*content) += line;
            (*content) += (keep ? "\n" : "");
        }
        in.close();
        return true;
    }
};
class StringUtil {
public:
    // 切分字符串
    // str:输入型参数,要切分的目标字符串
    // target:输出型参数,保存切分完毕的结果
    // sep:指定的分割符
    static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string sep) {
        boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on);
    }
};
}

本段代码涉及到四个功能类,分别为 TimeUtil、PathUtil、FileUtil、StringUtil;

下面我们来一一介绍每一个类的功能;

一、TimeUtil 类:时间戳生成工具,聚焦多精度时间获取

实现思路:

  1. 提供了两个接口,分别是获得秒级时间戳和获得毫秒级时间戳,使用 gettimeofday(来自<sys/time.h>);
  2. 两个函数均为 static 函数,包括后面其他工具类中的函数也都是静态的,无需实例化对象就可以直接调用,简化了使用方式。

二、PathUtil 类:路径生成工具,聚焦临时文件路径标准化

实现思路:

1. 基于'基础路径 + 文件名 + 后缀'的统一规则

设置了全局常量 temp_path 作为所有临时文件的根目录,再通过核心函数 Addsuffix 来完成路径拼接。

2. 针对不同文件类型封装专用接口

在 Addsuffix 的基础上,封装出了各种文件的专用函数,避免用户手动拼接后缀。

编译相关:Src(.cpp 源文件)、Exe(.exe 可执行文件)、CompilerError(.compiler_error 编译错误文件);

运行相关:Stdin(.stdin 标准输入文件)、Stdout(.stdout 文件)、Stderr(.stderr 标准错误文件)

三、FileUtil 类:文件操作工具,聚焦文件读写与唯一性保证

实现思路:

1. 文件存在性检查

通过系统调用 stat 来检查文件元数据,若调用成功则返回 0,说明文件存在,否则不存在,stat 更轻量适合快速检查。

2. 唯一文件名生成(多线程安全)

核心逻辑:毫秒级时间戳 + 原子性递增 ID

毫秒级时间戳:通过调用上面时间类中的 GetTimeMS 方法获得。

静态原子计数器:通过 id++(原子操作),来获得唯一序号。

最后将时间戳和序号进行拼接,就可以得到一个唯一的文件名。

3. 文件读写封装

WriteFile:使用 std::ofstream 打开目标文件,写入完成后关闭,返回值为 bool 类型表示是否写入成功。

ReadFile:使用 std::ifstream 读取文件,这里注意将 content 内容进行清空操作,保证每次读取的都是一个文件的内容,不会存在几个文件内容追加的现象,使用 getline 函数从打开的文件流中进行读取,同时支持支持通过 keep 参数控制是否保留原文件的换行符,返回类型同样为 bool 类型表示是否读取成功。

四、StringFile:字符串处理工具,负责字符串切分工作

实现思路:

1. 复用成熟库函数

直接复用 boost 库中的 split 函数,快速可靠地完成字符串切分任务。

2. 封装为简洁接口

对外提供 SplitString 函数接口,这里的参数需要注意一下,str 的输入型参数,即要切分的原字符串,target 为输出型参数,它的类型是 vector,即函数最后会将切分好的结果存入到 target 中,sep 为指定分隔符。

2.1 compiler(编译功能)

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
// 只负责代码的编译
namespace ns_complier {
using namespace ns_util;
using namespace ns_log;
class Complier {
public:
    Complier() { }
    ~Complier() { }
    static bool Compile(const std::string &filename) {
        pid_t res = fork();
        if (res < 0) {
            LOG(ERROR) << "创建子进程失败" << "\n";
            return false;
        } else if (res == 0) {
            umask(0);
            int _stderr = open(PathUtil::CompilerError(filename).c_str(), O_CREAT | O_WRONLY, 0644);
            if (_stderr < 0) {
                LOG(WARNING) << "没有成功形成 stderr 文件" << "\n";
                exit(1);
            }
            // 重定向标准错误到_stderr
            dup2(_stderr, 2);
            // 子进程:调用编译器,完成代码编译工作
            execlp("g++","g++", "-o", PathUtil::Exe(filename).c_str(), PathUtil::Src(filename).c_str(), "-std=c++11", "-D","COMPILER_ONLINE",nullptr);
            LOG(ERROR) << "启动编译器 g++失败,可能是参数错误" << "\n";
            exit(2);
        } else {
            waitpid(res, nullptr, 0); // 判断编译是否成功,即判断是否形成了对应了可执行程序
            if (Fileutil::IsFileExists(PathUtil::Exe(filename))) {
                LOG(INFO) << PathUtil::Exe(filename) << "编译成功!" << "\n";
                return true;
            }
        }
        LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";
        return false;
    }
};
}

核心功能:通过调用 g++ 将源文件编译成为可执行程序,并对编译过程中的错误和结果进行管理。

实现思路:

1. 创建子进程,隔离编译操作

通过 fork() 创建子进程:父进程负责等待和判断结果,子进程负责执行编译,如果创建子进程失败了,程序回直接返回,并且打印错误日志。

2. 子进程:配置编译环境并执行编译器

调整文件权限掩码,确保后续创建的错误文件权限正确。

以读写的方式打开编译错误文件,用于存储 g++ 的错误输出。

重定向标准错误到上面打开的错误文件中,使 g++ 的错误信息存入到错误文件中。

利用进程替换,调用 execlp 执行 g++ 命令,参数为'g++ -o 可执行程序 源文件 -std=c++11',这里需要指定 c++11 标准,避免语法兼容问题。

如果 execlp 执行失败,那么输出一条日志信息,然后退出子进程。

3. 父进程:等待编译完成并判断结果

通过 waitpid 等待子进程结束(确保编译过程完成)。

利用之前文件类中功能函数检查是否存在可执行文件,存在则说明编译成功,输出日志;否则说明编译失败,同样输出日志,并返回 false。

2.2 run(运行功能)

这里我们首先要有一个概念,就是运行模块只考虑代码有没有运行结束,不考虑最终结果的正确与否,结果是否正确是由测试用例来决定的,本模块只负责将程序运行完成。

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
namespace ns_runner {
using namespace ns_util;
using namespace ns_log;
class Runner {
public:
    Runner() {}
    ~Runner() {}
public:
    static void SetProcLimit(int _cpu_limit, int _mem_limit) {
        // 设置 CPU 时长
        struct rlimit cpu_rlimit;
        cpu_rlimit.rlim_max = RLIM_INFINITY;
        cpu_rlimit.rlim_cur = _cpu_limit;
        setrlimit(RLIMIT_CPU, &cpu_rlimit);
        // 设置内存大小
        struct rlimit mem_rlimit;
        mem_rlimit.rlim_max = RLIM_INFINITY;
        mem_rlimit.rlim_cur = _mem_limit * 1024; // 转化为 KB
        setrlimit(RLIMIT_AS, &mem_rlimit);
    }
    // 返回值 > 0,证明运行异常了,进程收到了系统的信号,返回值就是信号编号
    // 返回值 == 0,证明程序正常运行完毕,结果保存在临时文件中
    // 返回值 < 0,内部错误
    // cpu_limit:该程序运行的时候,可以使用的最大 cpu 资源上限
    // mem_limit:该程序运行的时候,可以使用的内存大小(KB)
    static int Run(const std::string &file_name, int cpu_limit, int mem_limit) {
        // 标准输入、标准输出、标准错误
        std::string _execute = PathUtil::Exe(file_name);
        std::string _stdin = PathUtil::Stdin(file_name);
        std::string _stdout = PathUtil::Stdout(file_name);
        std::string _stderr = PathUtil::Stderr(file_name);
        umask(0);
        int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);
        int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
        int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
        if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0) {
            LOG(ERROR) << "运行时打开标准文件失败" << "\n";
            return -1;
        }
        pid_t pid = fork();
        if (pid < 0) {
            LOG(ERROR) << "运行时创建子进程失败" << "\n";
            close(_stdin_fd);
            close(_stdout_fd);
            close(_stderr_fd);
            return -2; // 代表创建子进程失败
        } else if (pid == 0) {
            dup2(_stdin_fd, 0);
            dup2(_stdout_fd, 1);
            dup2(_stderr_fd, 2);
            SetProcLimit(cpu_limit, mem_limit);
            execl(_execute.c_str(), _execute.c_str(), nullptr);
            exit(1);
        } else {
            close(_stdin_fd);
            close(_stdout_fd);
            close(_stderr_fd);
            int status = 0;
            waitpid(pid, &status, 0);
            LOG(INFO) << "运行完毕" << (status & 0x7f) << "\n";
            return status & 0x7f;
        }
    }
};
}

实现思路:

1. SetProLimit:资源限制的底层实现

目标:限制进程的 CPU 时间和内存空间,类似于 OJ 中空间、时间限制

实现逻辑:

使用 struct rlimit 结构体配置 CPU 限制和内存限制,硬限制设为 RLIM_INFINITY(无限),我们传入的参数是用来设置 rlim_cur(软限制)的,最后通过 setrlimit 将配置应用到进程。

2. Run:程序运行的全流程管理

目标:启动可执行程序,管理其输入输出,限制资源,并返回运行状态。

实现逻辑:

  1. 路径与文件准备

通过工具类中的路径类来生成运行时需要的标准文件,然后用 open 来打开并创建对应文件,获取对应的文件描述符,如果有任意一个文件打开失败,那么输出日志消息,退出程序。

  1. 进程创建与隔离

子进程逻辑:

IO 重定向:通过调用 dup2() 将标准输入、标准输出、标准错误全部重定向到对应的标准文件中,实现程序 IO 与文件的联系。

资源限制:调用上面实现好的接口,直接将传入的时间、空间限制参数传给实现好的功能函数,完成资源限制操作。

执行程序:通过 execl() 执行可执行文件,如果 execl 返回则说明执行失败,此时就会立即退出子进程。

父进程逻辑:

关闭不再需要的文件描述符,调用 waitpid 阻塞等待子进程运行结束,获取其退出状态 status,并且通过位运算来判断程序是'正常退出'还是'因异常信号终止'(如段错误、超时等)。如果为 0 说明程序正常运行完毕了,如果大于 0,说明子进程接收到了系统的信号,那么这个值就是信号的编号。

2.3 compile_run(编译运行功能)

#include "compiler.hpp"
#include "run.hpp"
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include <jsoncpp/json/json.h>
#include <signal.h>
#include <unistd.h>
namespace ns_compile_and_run {
using namespace ns_util;
using namespace ns_log;
using namespace ns_complier;
using namespace ns_runner;
class CompileAndRun {
public:
    // 清理临时文件
    static void RemoveTempFile(const std::string &file_name) {
        std::string _src = PathUtil::Src(file_name);
        if (Fileutil::IsFileExists(_src)) unlink(_src.c_str());
        std::string _compile_error = PathUtil::CompilerError(file_name);
        if (Fileutil::IsFileExists(_compile_error)) unlink(_compile_error.c_str());
        std::string _execute = PathUtil::Exe(file_name);
        if (Fileutil::IsFileExists(_execute)) unlink(_execute.c_str());
        std::string _stdin = PathUtil::Stdin(file_name);
        if (Fileutil::IsFileExists(_stdin)) unlink(_stdin.c_str());
        std::string _stdout = PathUtil::Stdout(file_name);
        if (Fileutil::IsFileExists(_stdout)) unlink(_stdout.c_str());
        std::string _stderr = PathUtil::Stderr(file_name);
        if (Fileutil::IsFileExists(_stderr)) unlink(_stderr.c_str());
    }
    // code>0:进程收到信号,导致异常崩溃
    // code<0:整个过程非运行报错
    // code==0:整个过程 OK
    static std::string CodeToDesc(int code, const std::string &file_name) {
        std::string desc;
        switch (code) {
        case 0: desc = "编译运行成功"; break;
        case -1: desc = "用户提交的代码为空"; break;
        case -2: desc = "未知错误"; break;
        case -3: // desc = "编译错误";
            Fileutil::ReadFile(PathUtil::CompilerError(file_name), &desc, true); break;
        case SIGABRT: // 6
            desc = "内存超出范围"; break;
        case SIGXCPU: // 24
            desc = "CPU 运行超时"; break;
        case SIGFPE: desc = "浮点数溢出"; break;
        default: desc = "未知" + std::to_string(code); break;
        }
        return desc;
    }
    static void Start(const std::string in_json, std::string *out_json) {
        Json::Value in_value;
        Json::Reader reader;
        reader.parse(in_json, in_value);
        std::string code = in_value["code"].asString();
        std::string input = in_value["input"].asString();
        int cpu_limit = in_value["cpu_limit"].asInt();
        int mem_limit = in_value["mem_limit"].asInt();
        int status_code = 0;
        Json::Value out_value;
        int run_result = 0;
        std::string file_name;
        // 需要内部形成的唯一文件名
        if (code.size() == 0) {
            status_code = -1;
            goto END;
        }
        file_name = Fileutil::UniqFileName(); // 形成的文件名只具有唯一性,没有路径与后缀
        // 形成了临时 src 文件
        if (!Fileutil::WriteFile(PathUtil::Src(file_name), code)) {
            status_code = -2;
            goto END;
        }
        if (!Complier::Compile(file_name)) {
            status_code = -3;
            goto END;
        }
        run_result = Runner::Run(file_name, cpu_limit, mem_limit);
        if (run_result < 0) {
            status_code = -2;
            goto END;
        } else if (run_result > 0) {
            status_code = run_result;
        } else {
            status_code = 0;
        }
END:
        out_value["status"] = status_code;
        out_value["reason"] = CodeToDesc(status_code, file_name);
        if (status_code == 0) {
            // 整个过程全部 OK
            std::string _stdout;
            Fileutil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);
            out_value["stdout"] = _stdout;
            std::string _stderr;
            Fileutil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
            out_value["stderr"] = _stderr;
        }
        // 实现序列化
        Json::StreamWriterBuilder writer_builder;
        writer_builder["emitUTF8"] = true;
        writer_builder["indentation"] = " ";
        std::unique_ptr<Json::StreamWriter> writer(writer_builder.newStreamWriter());
        std::ostringstream oss;
        writer->write(out_value, &oss);
        *out_json = oss.str();
        RemoveTempFile(file_name);
    }
};
}

下面我们来逐个分析每个模块的具体实现;

一、RemoveTempFile:临时文件的全生命周期回收

功能:清理所有临时文件,避免磁盘资源泄漏。

实现思路:

**1. 定位所有临时文件路径:**通过 PathUtil 分别获取源文件、编译错误文件、标准输入文件、标准输出文件、标准错误文件的路径。

**2. 存在性检查与删除:**对于每个路径,利用 FileUtil 中的方法检查其是否存在,如果存在,就调用 unlink 系统调用接口删除文件。

二、CodeToDesc:状态码到可读描述的映射

功能:将抽象的'状态码'转化为可读性较高的'错误/成功描述',以便问题排查和结果展示。

实现思路:

分类处理状态码:通过 switch-case 语句覆盖各种可能的状态类型;

流程性错误:如状态码为 -1,表示用户代码为空;状态码为 -2,表示未知错误。

编译错误:状态码为 -3 的时候,读取编译错误的文件作为描述。

运行时信号异常:如果代码运行过程中接收到了系统信号,那么我们转化为对应的描述即可。

默认分支:对未定义的状态码,返回'未知 + 编号'的兜底描述。

三、Start:编译 - 运行全流程串联与结果封装

功能:整合'编译、运行、JSON 处理、资源清理'全流程,完成'用户代码→编译→运行→结果返回'的端到端逻辑。

实现思路:

**1. 输入 JSON 解析:**调用第三方库,用 Jsoncpp 的 Json::Reader 解析输入字符串 in_json,提取 code(用户代码)、input(程序输入)、cpu_limit(CPU 时间限制)、mem_limit(内存限制)等核心参数。

**2. 生成唯一临时文件名:**调用 FileUtil 中的 UniFileName 方法生成全局唯一的文件名。

**3. 将用户代码写入临时源文件:**调用 FileUtil 中的 WriteFile 方法将用户代码写入对应生成的源文件中,如果写入失败,设置状态码为 -2,然后跳转到结果构造阶段。

**4. 调用编译模块:**调用 Compiler 类中的 Compile 方法对代码进行编译处理,如果编译失败,设置状态码为 -3,然后跳转到结果构造阶段。

**5. 调用运行模块:**编译成功后,调用运行功能,根据运行结果分三种情况;

run_result<0:运行过程中内部出现错误,设置状态码为 -2。

run_result>0:程序运行收到了系统的信号,设置状态码为 run_result(信号编号)。

run_result=0:程序正常运行完毕,设置状态码为 0。

**6. 输出 JSON 构造:**在 END 标签处,用 out_value 构造输出结构:

必选字段:'status'状态码、'reason'状态描述。

可选字段:如果状态码为 0,那么说明整个编译运行的过程都是没有问题的。这时候我们可以再添加两个字段,分别读取程序标准输出和标准错误的内容,设置到'stdout'和'stderr'字段。

**7. JSON 序列化和资源清理:**将 out_value 序列化为格式化字符串,这里的 out_value 是一个输出型参数,我们最终将 out_value 返回即可,最后再清理编译运行过程中产生的所有临时文件,保证磁盘资源回收。

下面我们来进行一下简单的测试,测试一下编译运行模块有没有什么问题;

这里编写了一段测试代码:

#include "compile_run.hpp"
using namespace ns_compile_and_run;
int main() {
    std::string in_json;
    Json::Value in_value;
    // R"()", raw string
    in_value["code"] = R"(
#include <iostream>
int main() {
    std::cout << "测试成功哈哈哈" << std::endl;
    return 0;
}
)";
    in_value["input"] = "";
    in_value["cpu_limit"] = 1;
    in_value["mem_limit"] = 10240 * 3;
    Json::FastWriter writer;
    in_json = writer.write(in_value);
    std::cout << in_json << std::endl; // 这个是将来给客户端返回的 json 串
    std::string out_json;
    CompileAndRun::Start(in_json, &out_json);
    std::cout << out_json << std::endl;
    return 0;
}

(图片已移除)

这里大家可以看到,我们的代码成功完成了编译并运行的功能,证明我们前面的代码是 OK 的。

2.4 将编译运行功能打包成一个网络服务

#include "compile_run.hpp"
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;
void Usage(std::string proc) {
    std::cerr << "Usage:" << "\n\t" << proc << "port" << std::endl;
}
int main(int argc, char *argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        return 1;
    }
    Server svr;
    svr.Post("/compile_and_run", [](const Request &req, Response &resp) {
        std::string in_json=req.body;
        std::string out_json;
        std::cout << "compile run request " << std::endl;
        if(!in_json.empty()) {
            std::cout << "!in_json.empty() " << std::endl;
            CompileAndRun::Start(in_json,&out_json);
            resp.set_content(out_json,"application/json;charset=utf-8");
        }
    });
    svr.listen("0.0.0.0", atoi(argv[1]));
    return 0;
}

这里我们需要引入 cpp-httplib 第三方库,使用其中的方法就可以将我们的编译运行功能打包成一个网络服务,下面我们用 postman 来测试一下我们的网络服务:

(图片已移除)

(图片已移除)

这里我们可以看到,应答字符串按照我们要求的格式进行了输出,这就说明我们成功地将我们的编译运行功能打包成了一个网络服务。

3. 基于 MVC 结构的 oj_server 服务设计

在设计 oj_server 之前,先来介绍一下本模块需要完成的功能以及什么是 MVC;

本模块实现的功能如下:

  1. 获取首页,用题目列表充当。
  2. 编辑区域页面。
  3. 提交判题功能 (编译并运行)。

M: Model,通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL)。

V: view,通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的 (浏览器)。

C: control,控制器,就是我们的核心业务逻辑。

3.1 model 模块(提供对数据的操作)

#pragma once
// 主要用来进行数据交互,对外提供访问数据的接口
#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <cstdlib>
#include <vector>
#include <fstream>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_model {
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question {
    string number; // 题目编号,唯一
    string title; // 题目标题
    string star; // 题目难度
    int cpu_limit; // 时间限制
    int mem_limit; // 空间限制
    string desc; // 题目描述
    string header; // 题目预设给用户的在线编辑器代码
    string tail; // 测试用例
};
const string questions_list = "./questions/questions.list";
const string questions_path = "./questions/";
class Model {
private:
    unordered_map<string, Question> questions;//题号 : 题目细节,查询 O(1)
public:
    Model() {
        assert(LoadQuestionList(questions_list));
    }
    bool LoadQuestionList(const string &question_list) {
        //加载配置文件:questions/questions.list + 题目编号文件
        ifstream in(question_list);
        if (!in.is_open()) {
            LOG(FATAL)<<"加载题库失败,请检查是否存在配置文件"<<"\n";
            return false;
        }
        string line;
        while (getline(in, line)) {
            vector<string> tokens;
            StringUtil::SplitString(line, &tokens, " ");
            if (tokens.size() != 5) {
                LOG(WARNING)<<"加载部分题目失败,请检查文件格式"<<"\n";
                continue;
            }
            Question q;
            q.number = tokens[0];
            q.title = tokens[1];
            q.star = tokens[2];
            q.cpu_limit = atoi(tokens[3].c_str());
            q.mem_limit = atoi(tokens[4].c_str());
            string path = questions_path;
            path += q.number;
            path += "/";
            Fileutil::ReadFile(path + "/desc.txt", &(q.desc), true);
            Fileutil::ReadFile(path + "/header.cpp", &(q.header), true);
            Fileutil::ReadFile(path + "/tail.cpp", &(q.tail), true);
            questions.insert({q.number,q});
        }
        LOG(INFO)<<"加载题目成功"<<"\n";
        in.close();
        return true;
    }
    bool GetAllQuestions(vector<Question> *out) {
        if (questions.size() == 0) {
            LOG(ERROR)<<"用户获取题目失败"<<"\n";
            return false;
        }
        for (const auto &q : questions) {
            out->push_back(q.second);
        }
        return true;
    }
    bool GetOneQuestion(const string &number, Question *q) {
        const auto &iter = questions.find(number);
        if (iter == questions.end()) {
            LOG(ERROR)<<"用户获取题目失败,题目编号:"<<number<<"\n";
            return false;
        }
        (*q) = iter->second;
        return true;
    }
    ~Model() { }
};
}

一、数据载体:Question 结构体

核心功能:封装单道题目的所有属性,作为数据传输和存储的载体。

实现思路:

  1. 聚合数据所有信息:包含题目的各种信息,包括'标识类'(编号、标题、难度)、'资源限制类'(CPU 限制、内存限制)、'代码/描述类'(题目描述、预设代码、测试用例)。
  2. 所有属性均为内置类型或者 string 类型,结构清晰,便于管理。

二、核心 Model 类

1. Model 构造函数

核心功能:初始化题库,确保程序启动前题库加载完毕。

实现思路:

利用 assert 直接对加载结果进行断言,如果加载失败程序将直接终止。确保后续对题目进行操作的接口不会对空数据进行操作。

2. 核心加载逻辑:LoadQuestionList

核心功能:从文件系统读取并解析题库数据,填充到内存存储中。

实现思路:

  1. 打开配置文件:读取 question.list 文件,如果读取失败,那么输出 FATAL 级别日志,然后直接返回 false,表示加载失败了。
  2. 逐行解析配置

每行对应一道题,此时就需要用到我们前面在工具类中封装好的 StringUtil::SplitString 方法来对字符串进行切分,提取'编号 标题 难度 CPU 限制 内存限制'5 个核心字段。

如果切分后的格式错误(不是 5 部分),那么输出日志,跳过当前行,继续切分下一行,这样就不会影响其他题目加载。

  1. 读取题目详情文件

拼接题目详细文件路径。

调用 Fileutil::ReadFile 分别读取题目描述、预设代码、测试用例,填充到 Question 对象中。

  1. 内存存储:将 Question 对象加载到哈希表中,完成一道题目的加载。

  2. 收尾:加载成功,输出日志,关闭文件。

3. 题库查询接口

GetAllQuestions

核心功能:获取全部题目列表,适用于前端展示题库清单。

实现思路:

遍历哈希表,将数据逐一存储到 out 数组中,这里的 out 是输出型参数,最后所有题目的信息将存放在这个数组中并返回给调用层。

如果题库为空,那就返回 false,否则返回 true,明确告诉调用获取数据是否成功。

GetOneQuestion

核心功能:获取某一道题目,适用于用户选择某题后加载详情。

实现思路:

利用 find 函数直接按照题号查找,时间复杂度为 O(1),效率较高。

4. ~Model 析构函数

核心功能:默认析构、无需额外操作。

实现思路:

存储的 unordered_map 和 Question 对象均为栈上或标准库容器管理的资源,无需手动释放资源。

3.2 view 模块(网页渲染)

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <ctemplate/template.h>
#include "oj_model.hpp"
namespace ns_view {
using namespace ns_model;
const std::string template_path = "./template_html/";
class View {
public:
    View() { }
    void AllExpandHtml(const vector<struct Question> &questions, std::string *html) {
        // 1.形成路径
        std::string src_html = template_path + "all_questions.html";
        // 2.形成数据字典
        ctemplate::TemplateDictionary root("all_questions");
        for (const auto &q : questions) {
            ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
            sub->SetValue("number", q.number);
            sub->SetValue("title", q.title);
            sub->SetValue("star", q.star);
        }
        // 3.获取被渲染的 html
        ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
        // 4.开始进行渲染
        tpl->Expand(html, &root);
    }
    void OneExpandHtml(const struct Question &q, std::string *html) {
        // 1.形成路径
        std::string src_html = template_path + "one_question.html";
        // 2.形成数据字典
        ctemplate::TemplateDictionary root("one_question");
        root.SetValue("number", q.number);
        root.SetValue("title", q.title);
        root.SetValue("star", q.star);
        root.SetValue("desc", q.desc);
        root.SetValue("pre_code", q.header);
        // 3.获取被渲染的 html
        ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
        // 4.开始进行渲染
        tpl->Expand(html, &root);
    }
};
}

本模块核心功能:依赖 ctemplate 模板库,完成'数据→HTML 页面'的渲染工作,具体功能分为两类:

1. 批量渲染题目列表

将所有题目的核心信息(编号、标题、难度)填充到列表模板,生成题库首页 HTML(供用户浏览所有题目);

AllExpandHtml 函数

拼接模板路径->构建列表型数据字典->加载 HTML 模板->数据填充+HTML 生成。

2. 精准渲染单题详情

将单道题的完整信息(编号、标题、难度、描述、预设代码)填充到详情模板,生成做题页面 HTML(供用户查看题目要求并编写代码)。

OneExpandHtml 函数

与上面类似,第二步是构建单条数据字典。

3.3 control 模块(核心逻辑控制)

本模块包含三个核心类,职责层层递进:Machine(主机数据载体)->LoadBalance(负载均衡管理)->Control(业务逻辑整合),形成'资源管理->调度->业务落地'的完整链路。

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <cassert>
#include <fstream>
#include <algorithm>
#include <jsoncpp/json/json.h>
#include "oj_model.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include "../comm/httplib.h"
#include "oj_view.hpp"
namespace ns_control {
using namespace std;
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
using namespace httplib;
// 提供服务的主机
class Machine {
public:
    std::string ip;
    int port;
    uint64_t load; // 编译服务的负载
    std::mutex *mtx;
public:
    Machine() : ip(""), port(0), load(0), mtx(nullptr) { }
    ~Machine() { }
public:
    // 递增负载
    void IncLoad() {
        if (mtx) mtx->lock();
        ++load;
        if (mtx) mtx->unlock();
    }
    // 递减负载
    void DecLoad() {
        if (mtx) mtx->lock();
        --load;
        if (mtx) mtx->unlock();
    }
    // 重置负载
    void ResetLoad() {
        if (mtx) mtx->lock();
        load = 0;
        if (mtx) mtx->unlock();
    }
    // 获取主机负载
    uint64_t Load() {
        uint64_t _load = load;
        if (mtx) mtx->lock();
        _load = load;
        if (mtx) mtx->unlock();
        return _load;
    }
};
const std::string service_machine = "./conf/service_machine.conf";
// 负载均衡模块
class LoadBalance {
private:
    // 可以给我们提供编译服务的所有主机
    // 每台主机的下标充当主机 id
    std::vector<Machine> machines;
    // 所有在线主机的 id
    std::vector<int> online;
    // 所有离线主机的 id
    std::vector<int> offline;
    // 保证 LoadBalance 的数据安全
    std::mutex mtx;
public:
    LoadBalance() {
        assert(LoadConf(service_machine));
        LOG(INFO) << "加载" << service_machine << "成功" << "\n";
    }
    ~LoadBalance() { }
public:
    bool LoadConf(const std::string &machine_conf) {
        std::ifstream in(machine_conf);
        if (!in.is_open()) {
            LOG(FATAL) << "加载" << machine_conf << "失败" << "\n";
            return false;
        }
        std::string line;
        while (getline(in, line)) {
            std::vector<std::string> tokens;
            StringUtil::SplitString(line, &tokens, ":");
            if (tokens.size() != 2) {
                LOG(WARNING) << "切分" << line << "失败" << "\n";
                continue;
            }
            Machine m;
            m.ip = tokens[0];
            m.port = atoi(tokens[1].c_str());
            m.load = 0;
            m.mtx = new std::mutex();
            online.push_back(machines.size());
            machines.push_back(m);
        }
        in.close();
        return true;
    }
    bool SmartChoice(int *id, Machine **m) {
        mtx.lock();
        int online_num = online.size();
        if (online_num == 0) {
            mtx.unlock();
            LOG(FATAL) << "所有后端编译主机全部离线,请尽快检查" << "\n";
            return false;
        }
        // 通过遍历选择负载最小的机器
        *id = online[0];
        *m = &machines[online[0]];
        uint64_t min_load = machines[online[0]].Load();
        for (int i = 1; i < online_num; i++) {
            uint64_t cur_load = machines[online[i]].Load();
            if (cur_load < min_load) {
                min_load = cur_load;
                *id = online[i];
                *m = &machines[online[i]];
            }
        }
        mtx.unlock();
        return true;
    }
    void OfflineMachine(int which) {
        mtx.lock();
        for (auto iter = online.begin(); iter != online.end(); iter++) {
            if (*iter == which) {
                machines[which].ResetLoad(); // 要离线的主机找到了
                online.erase(iter);
                offline.push_back(which);
                break;
            }
        }
        mtx.unlock();
    }
    void OnlineMachine() {
        // 当所有主机都离线后,再统一上线
        mtx.lock();
        online.insert(online.end(), offline.begin(), offline.end());
        offline.erase(offline.begin(), offline.end());
        mtx.unlock();
        LOG(INFO) << "所有的主机有上线啦!" << "\n";
    }
};
// 核心业务逻辑控制器
class Control {
private:
    Model _model; // 提供后台数据
    View _view; // 提供 html 渲染功能
    LoadBalance _load_balance; // 核心负载均衡器
public:
    Control() { }
    ~Control() { }
public:
    void RecoveryMachine() {
        _load_balance.OnlineMachine();
    }
    // 根据题目数据构建网页
    bool AllQuestions(string *html) {
        vector<struct Question> all;
        if (_model.GetAllQuestions(&all)) {
            sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2) {
                return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
            });
            _view.AllExpandHtml(all, html); // 获取题目成功,构建网页
        } else {
            *html = "获取题目失败,形成题目列表失败";
            return false;
        }
        return true;
    }
    bool Question(const string &number, string *html) {
        struct Question q;
        if (_model.GetOneQuestion(number, &q)) {
            _view.OneExpandHtml(q, html);
        } else {
            *html = "指定题目" + number + "不存在!";
            return false;
        }
        return true;
    }
    void Judge(const std::string &number, const std::string &in_json, std::string *out_json) {
        struct Question q;
        _model.GetOneQuestion(number, &q);
        // 1.对 in_json 进行反序列化,得到题目的 id,用户提交的源代码
        Json::Value in_value;
        Json::Reader reader;
        reader.parse(in_json, in_value);
        // 把 in_json 反序列化为 in_value
        std::string code = in_value["code"].asString();
        // 2.重新拼接用户代码 + 测试用例代码,形成新的代码
        Json::Value compile_value;
        compile_value["input"] = in_value["input"].asString();
        compile_value["code"] = code + q.tail;
        compile_value["cpu_limit"] = q.cpu_limit;
        compile_value["mem_limit"] = q.mem_limit;
        Json::FastWriter writer;
        std::string compile_string = writer.write(compile_value);
        // 将拼接好的代码进行序列化,准备发给后台的编译服务
        // 3.选择负载最低的主机
        while (true) {
            int id = 0;
            Machine *m = nullptr;
            if (!_load_balance.SmartChoice(&id, &m)) break;
            LOG(INFO) << "选择主机成功,主机 id:" << id << "详情:" << m->ip << ":" << m->port << "\n";
            // 4.然后发起 http 请求,得到结果
            Client cli(m->ip, m->port);
            m->IncLoad();
            LOG(INFO) << "选择主机成功,主机 id: " << id << " 详情:" << m->ip << ":" << m->port << " 当前主机的负载是:" << m->Load() << "\n";
            if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")) {
                if (res->status == 200) {
                    *out_json = res->body; // 将结果赋值给 out_json
                    m->DecLoad();
                    LOG(INFO) << "请求成功" << "\n";
                    break;
                }
                m->DecLoad();
            } else {
                // 请求失败
                LOG(ERROR) << "当前请求主机的 id:" << id << "详情:" << m->ip << ":" << m->port << "可能已经离线" << "\n";
                _load_balance.OfflineMachine(id);
            }
        }
    }
};
}

下面我们来一一分析每一个类的实现:

一、Machine 类

核心功能:封装单台编译服务主机的核心信息(IP、端口号)和运行状态(负载),提供线程安全的负载操作接口,为负载均衡提供基础数据支持。

**设计初衷:**编译服务是多进程并发调用的,load 作为共享资源,必须通过锁机制保证操作安全。

实现思路:

1. 属性设计

基础信息:IP+端口,定位编译服务主机。

状态信息:load,当前负载,用于负载均衡决策。

线程安全:mtx,互斥锁,保护 load 变量的并发修改。

2. 核心接口(线程安全)

InLoad()/DeLoad():负载递增/递减,加锁保证多线程下负载操作的原子性。

ResetLoad():负载重置,主机离线时调用,避免故障后负载残留。

Load():获取当前负载,加锁保证读取到的是最新值(避免内存可见性问题)。

二、LoadBalance 类

核心功能:管理所有编译主机的生命周期(在线 / 离线),实现'最小负载选择'的负载均衡算法,确保编译任务高效、稳定分配。

设计初衷

  1. 负载均衡:避免单台主机过载,提升系统整体并发能力;
  2. 容错机制:故障主机自动下线,保证业务不中断;批量上线机制简化故障恢复流程。

实现思路:

1. 初始化:加载配置文件(LoadConf)

读取 service_machine.conf 配置文件,格式:IP:端口,逐行解析主机信息。

每台主机对应一个 Machine 对象,初始化负载为 0,分配互斥锁,这里有一个细节,我们将 m 对象填充好后,先进行操作 online 数组,我们把每台主机的下标当作主机 id,所以在第一次调用时,此时 machines 数组中还没有元素,所以这里插入的就是 0,表示 0 号主机已经在线了。

如果解析失败(字段数!=2),那么就跳过当前行,这样做不影响其他主机加载,提升鲁棒性。

2. 核心调度:最小负载选择(SmartChoice)

加锁保护主机状态,避免选择过程中主机状态发生变化。

遍历 online 数组,筛选出负载最小的主机,返回其 ID 和指针,这里 idh 和 m 都是输出型参数,并且这里 m 是一个二级指针,这样做是为了在我们找到负载最小的主机后,我们返回其地址,这样就可以直接访问这台主机,就不需要在去遍历寻找了。

如果没有在线主机,输出 FATAL 日志并返回 false,提示运维检查。

3. 主机状态管理(OfflineMachine/OnlineMachine)

OfflineMachine:将故障主机(请求失败)从 online 数组移至 offline 数组,重置其负载,避免后续任务分配到故障主机。

OnlineMachine:故障恢复后,将 offline 数组中的所有主机批量地移回 online,快速恢复服务。

三、Control 类

核心功能:作为这个系统是'中枢神经',整合 Model(数据)、View(视图)、LoadBalance(负载均衡),对外提供三大核心业务接口。

设计初衷

  1. 业务整合:将分散的模块(数据、视图、负载均衡)串联成完整业务流程,对外提供统一接口;
  2. 容错重试:判题时自动重试离线主机,提升服务可用性;
  3. 解耦设计:Control 不关心模块内部实现(如 Model 如何加载题库、LoadBalance 如何选主机),仅调用接口,便于后续替换模块。

1. AllQuestions:渲染所有题目列表(题库首页)

数据获取:调用 GetAllQuestions,从题库中获取所有题目数据;

用户体验优化:按题号进行升序排序,方便用户按序号浏览;

视图渲染:调用 AllExpandHtml,将排序后的题目数据填充到 HTML 模板中,生成题库首页;

错误处理:获取题目失败,返回明确的错误提示 HTML。

2. Question:渲染单题详情(做题页面)

精准查询:按用户传入的题号,调用 GetOneQuestion 获取单题完整数据(描述、预设代码、测试用例);

视图渲染:调用 OneExpandHtml,生成包含题目要求、代码编辑区的做题页面;

错误处理:当题号不存在时,返回错误提示 HTML;

3. Judge:代码判题(核心业务)

解析输入 JSON:通过 jsoncpp 解析前端传入的 in_json,提取用户提交的代码(code)和输入(input);

拼接完整代码:将用户代码与题目自带的测试用例进行拼接,形成'用户代码 + 测试逻辑'的完整可编译代码;

构造编译请求参数:封装拼接后的代码、用户输入、题目要求的资源限制(CPU、内存),序列化为 JSON 字符串;

选择可用编译主机:循环调用 SmartChoice,直到选中可用主机;

跨服务通信:通过 httplib 发起 POST 请求,调用后台的编译运行服务;

处理响应结果:

响应成功(状态码 200):将编译运行后的结果赋值给 out_json,返回给前端;

响应失败:将该主机标记为离线,继续选择下一台主机。

返回结果:将最终判题结果(成功 / 失败、输出 / 错误信息)序列化后返回。

4. 顶层代码设计

#include <iostream>
#include <signal.h>
#include "../comm/httplib.h"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_control;
static Control *ctrl_ptr = nullptr;
void Recovery(int signo) {
    ctrl_ptr->RecoveryMachine();
}
int main() {
    signal(SIGQUIT, Recovery); //用户请求的服务路由功能
    Server svr;
    Control ctrl;
    ctrl_ptr = &ctrl;
    // 获取所有的题目列表
    svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){
        //返回一张包含有所有题目的 html 网页
        std::string html;
        ctrl.AllQuestions(&html);
        //用户看到的是:网页数据 + 拼上了题目相关的数据
        resp.set_content(html, "text/html; charset=utf-8");
    });
    // 用户要根据题目编号,获取题目的内容
    // /question/100 -> 正则匹配
    // R"()", 原始字符串 raw string,保持字符串内容的原貌,不用做相关的转义
    svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){
        std::string number = req.matches[1];
        std::string html;
        ctrl.Question(number, &html);
        resp.set_content(html, "text/html; charset=utf-8");
    });
    // 用户提交代码,使用我们的判题功能 (1. 每道题的测试用例 2. compile_and_run)
    svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){
        std::string number = req.matches[1];
        std::string result_json;
        ctrl.Judge(number, req.body, &result_json);
        resp.set_content(result_json, "application/json;charset=utf-8");
    });
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0", 8080);
    return 0;
}

本模块将前后端全部整合在一起,提供标准化 HTTP 接口,对接前端三大核心场景(浏览题库、查看单题、提交代码判题),同时支持静态资源托管和故障恢复。

一、整体初始化与服务搭建

实现思路:

1. 故障恢复信号注册

注册 SIGNQUIT 信号(Ctrl+\),绑定 Recovery 函数;

定义全局指针 ctrl_ptr 指向核心业务对象 Control(信号处理函数无法直接访问局部变量,需通过全局指针间接调用)。

功能:当所有编译主机离线后,我们可以通过 Ctrl+\让所有主机重新上线,这样就可以快速恢复服务。

2. 核心业务对象初始化

创建 Control 类实例 ctrl,该对象融合了'题库管理、页面渲染、负载均衡、判题逻辑'所有核心业务功能。

路由回调函数通过引用捕获 ctrl,避免重复创建对象。

3. HTTP 服务启动

调用 svr.listen 监听所有网卡的 8080 接口,允许外部通过 IP+Port 进行访问。

服务启动后,持续接收前端 HTTP 请求,分发到对应路由进行处理。

二、三大核心路由

路由是'前端请求'与'后端业务'之间的桥梁,这里注册了三个核心路由;

1. 路由/all_questions(GET 请求):浏览题库列表

功能:返回所有题目信息的 HTML 页面(前端题库首页)。

实现思路:

请求处理:前端发起 GET 请求,触发回调函数;

业务调用:通过调用 AllQuestions 函数获取渲染后的题目列表 HTML(包含题号、标题、难度);

响应封装:设置响应的 Content-Type 为 text/html,charset 为 utf-8,告诉浏览器这是 html 页面,并且

支持中文编码,避免乱码问题,将 HTML 返回给前端。

2. 路由/question/(\d+)(GET 请求,正则匹配):查看单题详情

功能:根据题号返回单道题的详情 HTML 页面(前端做题页面)。

实现思路:

路由匹配:使用正则表达式精准匹配题号,例如/question/2 会匹配成功,反之如果是/question/abc 则会匹配失败;

参数提取:通过 req.matchs[1] 获取正则匹配到的题号;

业务调用:通过调用 Question 函数,传入题号,获取某一题详情 HTML(包含题目描述、代码编辑区、预设代码);

响应封装:同样设置响应的 Content-Type 为 text/html,charset 为 utf-8 编码,返回个性化的做题页面。

3. 路由/judge/(\d+)(POST 请求,正则匹配):提交代码判题

功能:接收用户提交的代码,返回编译运行结果(前端判题交互)。

实现思路:

路由匹配:正则 R"(/judge/(\d+))"匹配题号,确保判题请求与具体题目绑定;

参数提取:通过 req.matchs[1] 获取正则匹配到的题号,req.body 获取前端 POST 提交的 JSON 数据(包含用户代码、输入);

业务调用:调用 Judge 函数,传入题号和请求体,触发判题流程。

响应封装:设置 Content-Type 为 application/json;charset=utf-8(告知前端返回的是 JSON 数据),将**判题结果(状态码、描述、输出 / 错误信息)**返回给前端,供前端解析展示。

三、静态资源托管

功能:提供前端页面所需的静态资源(CSS、JS、图片、静态 HTML 等)。

实现思路:

通过 svr.set_base_dir("./wwwroot") 设置静态资源根目录,httplib 会自动托管该目录下的所有文件;

优势:简化部署流程,无需单独搭建 Nginx 等静态资源服务器,降低运维成本。

顶层 Makefile——使项目具有发布功能

.PHONY: all
all:
	@cd compile_server;
	make;
	cd -;
	cd oj_server;
	make;
	cd -;
.PHONY:output
output:
	@mkdir -p output/compile_server;
	mkdir -p output/oj_server;
	cp -rf compile_server/compile_server output/compile_server;
	cp -rf compile_server/temp output/compile_server;
	cp -rf oj_server/conf output/oj_server/;
	cp -rf oj_server/questions output/oj_server/;
	cp -rf oj_server/template_html output/oj_server/;
	cp -rf oj_server/wwwroot output/oj_server/;
	cp -rf oj_server/oj_server output/oj_server/;
.PHONY:clean
clean:
	@cd compile_server;
	make clean;
	cd -;
	cd oj_server;
	make clean;
	cd -;
	rm -rf output;

5. 总结

本项目是一款轻量级、模块化、高可用的在线判题系统(Online Judge, OJ),基于 C++ 开发,依托 Linux 系统调用和开源库,实现了'题目管理、代码提交、编译运行、结果反馈'的端到端完整链路。系统采用分层设计思想,模块职责清晰、耦合度低,支持多编译节点负载均衡和故障快速恢复,适用于编程教学、小型竞赛或团队代码评测场景。

目录

  1. 1. 项目说明与整体框架
  2. 1.1 项目说明
  3. 1.2 项目宏观结构图
  4. 1.3 实现思路
  5. 2. compile_server 服务设计
  6. 2.1 compiler(编译功能)
  7. 2.2 run(运行功能)
  8. 2.3 compile_run(编译运行功能)
  9. 2.4 将编译运行功能打包成一个网络服务
  10. 3. 基于 MVC 结构的 oj_server 服务设计
  11. 3.1 model 模块(提供对数据的操作)
  12. 3.2 view 模块(网页渲染)
  13. 3.3 control 模块(核心逻辑控制)
  14. 4. 顶层代码设计
  15. 5. 总结

更多推荐文章

查看全部
  • HTML 核心语法和常用标签
  • GraphRAG 结合 Ollama 实现本地化部署完整教程
  • Python 兼职接单渠道与防骗技术指南
  • 豆包、文心一言、DeepSeek、元宝四大 AI 模型特点与选择指南
  • Java 模拟算法实战:LeetCode 经典题解
  • 基于 Amazon SageMaker 的 AIGC 模型训练与 Web 应用部署实践
  • OpenClaw 本地 AI 智能体入门与实战指南
  • PyTorch 文本引导图像生成与 Stable Diffusion 实践
  • ClawX:OpenClaw 可视化桌面客户端,零配置玩转 AI 智能体
  • VS Code 远程模式下 GitHub Copilot 无法使用的解决方案
  • 2026 年值得关注的开源低代码与零代码平台推荐
  • VSCode Copilot 终极魔改:以智谱 GLM-5.1 为例,一文搞定任意大模型接入
  • C++ 基于正倒排索引的搜索引擎核心实现与代码详解
  • C++11 核心特性:列表初始化与右值引用移动语义详解
  • Qwen-Image-2512 V2 模型 ComfyUI 与 WebUI 整合包使用指南
  • Windows 部署 Qwen2.5-Coder-7B-Instruct 模型:环境配置与 Web 界面搭建
  • 《Agent Runtime 工程化》第五章 上下文工程:5.1 token budget 是产品预算
  • Supabase 实战教程:数据库、SDK 与本地部署
  • MySQL 8.0.43 免安装版配置与安装指南
  • Python 数据科学工具链实战:NumPy、Pandas 与 Matplotlib 入门

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online