C++ 从零实现负载均衡式在线 OJ 判题系统|分布式沙箱判题实战
一、项目前言平时我们在 LeetCode、牛客、洛谷刷题时最核心的体验就是在线写代码、一键提交、即时判题。很多同学只知其用不知其底层原理服务器如何安全编译用户代码如何防止恶意代码死循环、爆内存高并发刷题场景下如何扛住压力为此我基于C Linux 系统编程 httplib ctemplate jsoncpp从零手写实现了一套分布式负载均衡在线 OJ 判题系统。区别于传统单机 OJ本项目实现了服务解耦、进程沙箱隔离、资源限制、分布式负载均衡、故障自动容灾等企业级核心能力完整复刻了主流刷题平台的底层判题逻辑。本文严格按照从零开发项目的编码顺序讲解代码从底层工具逐步搭建到完整分布式服务全程循序渐进方便跟着源码复盘。二、项目整体介绍2.1 核心功能题库页面渲染动态展示全部题目列表、单题详情题号、标题、难度、题目描述、代码模板在线代码判题支持用户提交 C 代码自动编译、运行、结果校验安全沙箱隔离进程级资源限制杜绝死循环、内存溢出、恶意系统调用分布式负载均衡多台编译服务集群部署自动选择空闲节点分发判题任务故障自动容灾编译节点故障自动下线服务接收信号一键恢复所有节点全流程日志监控分级日志记录系统运行状态快速定位编译、运行、网络异常2.2 技术栈核心语言C11网络服务cpp-httplib轻量级 HTTP 服务快速搭建接口页面渲染ctemplate谷歌模板引擎服务端动态渲染 HTML数据交互jsoncpp前后端 JSON 序列化与解析系统编程Linux fork/exec、dup2 重定向、setrlimit 资源限制、信号处理工具依赖Boost 字符串分割、原子变量、文件 IO、多线程互斥锁架构设计MVC 分层架构 分布式微服务集群2.3 整体架构整套系统分为两大核心服务、多层业务模块完全解耦支持横向扩展OJ 主服务Web 服务负责页面展示、路由分发、业务调度、负载均衡对接前端与编译集群编译运行子服务集群节点独立部署多实例专注代码编译、沙箱运行、结果返回开发编码顺序公共工具层 → Model 数据层 → View 视图层 → Control 业务 负载均衡 → Web 主服务 → 编译沙箱 Compiler/Runner → CompileAndRun 总控 → 编译子服务入口三、核心源码逐行深度讲解按从零开发顺序3.1 第一步开发公共底层工具 comm所有模块依赖最先写项目所有文件都会用到日志、文件、路径、字符串、时间工具因此最先封装通用工具库避免重复编码。3.1.1 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 file_name; message ][; message std::to_string(line); message ][; message TimeUtil::GetTimeStamp(); message ]; std::cout message; return std::cout; } // 语法糖宏简化日志调用 LOG(INFO) xxx; #define LOG(level) Log(#level, __FILE__, __LINE__) }代码解析枚举区分 5 种日志等级线上可按需屏蔽低等级日志内联函数自动拼接日志前缀等级、文件、行号、时间戳排错直观宏__FILE__/__LINE__是编译器内置宏无需手动传参开发效率极高。3.1.2 util.hpp 通用工具集时间 / 路径 / 文件 / 字符串#pragma once #include iostream #include string #include vector #include atomic #include fstream #include sys/time.h #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); } }; // 统一管理临时文件路径编译运行产生的源码、exe、日志、标准流文件 const std::string temp_path ./temp/; class PathUtil { public: static std::string AddSuffix(const std::string file_name, const std::string suffix) { std::string path_name temp_path; path_name file_name; path_name suffix; return path_name; } static std::string Src(const std::string file_name) { return AddSuffix(file_name, .cpp); } static std::string Exe(const std::string file_name) { return AddSuffix(file_name, .exe); } static std::string CompilerError(const std::string file_name) { return AddSuffix(file_name, .compile_error); } static std::string Stdin(const std::string file_name) { return AddSuffix(file_name, .stdin); } static std::string Stdout(const std::string file_name) { return AddSuffix(file_name, .stdout); } static std::string Stderr(const std::string file_name) { return AddSuffix(file_name, .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; if (keep) (*content) \n; } in.close(); return true; } }; // 字符串分割工具解析配置文件 class StringUtil { public: static void SplitString(const std::string str, std::vectorstd::string *target, const std::string sep) { boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on); } }; }代码解析TimeUtil毫秒时间戳用于生成唯一临时文件解决并发文件冲突PathUtil统一管理所有临时文件路径后续修改存储目录只需改一处FileUtil封装文件读写、唯一性文件名原子变量保证多线程自增无冲突StringUtilboost 分割字符串自动合并连续分隔符解析题库、集群配置文件容错性更强。3.2 第二步开发 Model 数据层 oj_model数据源工具写完后开发底层工具就绪后搭建题库数据管理层负责加载本地题目文件、内存存储题目为上层业务提供数据。#pragma once #include ../comm/util.hpp #include ../comm/log.hpp #include iostream #include string #include vector #include unordered_map #include fstream #include cstdlib #include cassert namespace ns_model { using namespace std; using namespace ns_log; using namespace ns_util; // 单个题目数据结构体存储一道题完整信息 struct Question { std::string number; // 题号 std::string title; // 标题 std::string star; // 难度 int cpu_limit; // CPU最大运行时间 int mem_limit; // 最大内存限制 std::string desc; // 题目描述 std::string header; // 用户代码模板 std::string tail; // 后台测试用例代码 }; const std::string questins_list ./questions/questions.list; const std::string questins_path ./questions/; class Model { private: unordered_mapstring, Question questions; // 题号映射题目O(1)查询 public: // 构造函数启动自动加载全部题库 Model() { assert(LoadQuestionList(questins_list)); } ~Model(){} // 读取配置文件加载所有题目到内存 bool LoadQuestionList(const string question_list) { ifstream in(question_list); if(!in.is_open()) { LOG(FATAL) 加载题库失败,请检查是否存在题库文件 \n; return false; } string line; while(getline(in, line)) { vectorstring 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 questins_path q.number /; FileUtil::ReadFile(pathdesc.txt, (q.desc), true); FileUtil::ReadFile(pathheader.cpp, (q.header), true); FileUtil::ReadFile(pathtail.cpp, (q.tail), true); questions.insert({q.number, q}); } LOG(INFO) 加载题库...成功! \n; in.close(); return true; } // 获取全部题目列表 bool GetAllQuestions(vectorQuestion *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 std::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; } }; }代码解析Question 结构体统一封装题目所有属性规范数据格式unordered_map 存储题目通过题号快速查询相比 vector 遍历效率更高构造函数自动加载题库服务启动一次性读取磁盘后续请求直接读内存性能更高分离配置文件questions.list和题目详情文件增删题目无需修改代码。3.3 第三步开发 View 视图层 oj_view页面渲染数据层完成后开发有了题目数据接下来实现页面渲染模块基于 ctemplate 模板引擎将数据填充 HTML 模板生成网页。#pragma once #include iostream #include string #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(){} ~View(){} public: // 渲染全部题目列表页面 void AllExpandHtml(const vectorstruct Question questions, std::string *html) { std::string src_html template_path all_questions.html; 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); } ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP); tpl-Expand(html, root); } // 渲染单题详情页面 void OneExpandHtml(const struct Question q, std::string *html) { std::string src_html template_path one_questions.html; ctemplate::TemplateDictionary root(one_questions); 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); ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP); tpl-Expand(html, root); } }; }代码解析分离模板文件与业务代码修改页面样式无需改动 CAddSectionDictionary实现模板循环渲染自动生成多条题目条目不用手动拼接字符串对外提供两个接口分别对应题库列表、单题详情职责单一。3.4 第四步开发 Control 控制层负载均衡 业务调度M/V 完成后开发MVC 三层最后一层 Control串联 Model、View同时实现分布式负载均衡模块是整个系统业务中枢。3.4.1 Machine 机器负载类负载均衡基础单元cppclass Machine { public: std::string ip; // 编译服务器IP int port; // 端口 uint64_t load; // 当前任务负载 std::mutex *mtx; // 单机互斥锁 public: Machine() : ip(), port(0), load(0), mtx(nullptr){} ~Machine(){} // 任务增加负载1 void IncLoad() { if (mtx) mtx-lock(); load; if (mtx) mtx-unlock(); } // 任务完成负载-1 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 0; if (mtx) mtx-lock(); _load load; if (mtx) mtx-unlock(); return _load; } };解析每台编译机器独立互斥锁多线程并发修改负载计数无竞争记录当前正在处理的判题任务数量。3.4.2 LoadBlance 负载均衡器cppconst std::string service_machine ./conf/service_machine.conf; class LoadBlance { private: std::vectorMachine machines; // 所有编译机器数组 std::vectorint online; // 在线机器下标 std::vectorint offline; // 故障离线机器下标 std::mutex mtx; // 集群全局锁 public: LoadBlance() { assert(LoadConf(service_machine)); LOG(INFO) 加载 service_machine 成功 \n; } ~LoadBlance(){} // 读取集群配置文件初始化所有机器 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 (std::getline(in, line)) { std::vectorstd::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 curr_load machines[online[i]].Load(); if (min_load curr_load) { min_load curr_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; } void ShowMachines() { mtx.lock(); std::cout 当前在线主机列表: ; for(auto id : online) std::cout id ; std::cout \n离线主机列表: ; for(auto id : offline) std::cout id ; std::cout std::endl; mtx.unlock(); } };3.4.3 Control 业务控制器整合 M/V/ 负载均衡cppclass Control { private: Model model_; // 数据层 View view_; // 视图层 LoadBlance load_blance_;// 负载均衡集群 public: Control() {} ~Control() {} public: // 信号回调恢复所有离线编译机器 void RecoveryMachine() { load_blance_.OnlineMachine(); } // 获取全部题目渲染页面 bool AllQuestions(string *html) { bool ret true; vectorstruct Question all; if (model_.GetAllQuestions(all)) { // 题号升序排序 sort(all.begin(), all.end(), [](const Question q1, const Question q2){ return atoi(q1.number.c_str()) atoi(q2.number.c_str()); }); view_.AllExpandHtml(all, html); } else { *html 获取题目失败, 形成题目列表失败; ret false; } return ret; } // 获取单题渲染详情页 bool Question(const string number, string *html) { bool ret true; struct Question q; if (model_.GetOneQuestion(number, q)) { view_.OneExpandHtml(q, html); } else { *html 指定题目: number 不存在!; ret false; } return ret; } // 核心判题业务接收前端代码分发到编译集群返回结果 void Judge(const std::string number, const std::string in_json, std::string *out_json) { struct Question q; model_.GetOneQuestion(number, q); Json::Reader reader; Json::Value in_value; reader.parse(in_json, in_value); std::string code in_value[code].asString(); // 组装发送给编译节点的JSON Json::Value compile_value; compile_value[input] in_value[input].asString(); compile_value[code] code \n 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); // 循环选择可用机器直到分发成功 while(true) { int id 0; Machine *m nullptr; if(!load_blance_.SmartChoice(id, m)) break; 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;charsetutf-8)) { if(res-status 200) { *out_json res-body; m-DecLoad(); LOG(INFO) 请求编译和运行服务成功... \n; break; } m-DecLoad(); } else { LOG(ERROR) 主机id: id 离线 \n; load_blance_.OfflineMachine(id); load_blance_.ShowMachines(); } } } };整体解析页面业务调用 Model 拿数据排序后交给 View 渲染 HTML判题业务拼接用户代码 后台测试用例通过负载均衡分发任务故障机制机器请求失败自动下线避免重复请求故障节点容灾接口RecoveryMachine 提供机器恢复能力给信号函数调用。3.5 第五步Web 主服务 main.cchttplib 路由Control 写完后开发搭建 HTTP 服务注册页面、判题路由绑定信号处理函数。#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; // SIGQUIT信号触发恢复所有离线编译机器 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) { std::string html; ctrl.AllQuestions(html); resp.set_content(html, text/html; charsetutf-8); }); // 单题详情页正则匹配题号 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; charsetutf-8); }); // 提交代码判题接口 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;charsetutf-8); }); svr.set_base_dir(./wwwroot); // 静态资源目录 svr.listen(0.0.0.0, 8080); return 0; }解析信号绑定收到 SIGQUIT 信号一键恢复所有编译集群节点三类路由区分页面访问与代码提交返回不同 Content-Type正则路由自动捕获题号接口规范简洁。3.6 第六步开发编译沙箱底层模块Compiler Runner主服务完成后开发分布式子服务前面是 Web 主服务现在开发独立编译运行节点底层能力先写编译器、运行器两个基础模块。3.6.1 compiler.hpp 代码编译模块#pragma once #include iostream #include unistd.h #include sys/wait.h #include sys/types.h #include sys/stat.h #include fcntl.h #includestring #include ../comm/util.hpp #include ../comm/log.hpp namespace ns_compiler { using namespace ns_util; using namespace ns_log; class Compiler { public: Compiler(){} ~Compiler(){} // 子进程调用g编译源码 static bool Compile(const std::string file_name) { pid_t pid fork(); if(pid 0) { LOG(ERROR) 创建子进程失败 \n; return false; } else if (pid 0) { umask(0); int _stderr open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644); if(_stderr 0){ LOG(WARNING) 编译错误日志文件创建失败 \n; exit(1); } dup2(_stderr, 2); // 替换程序执行g编译 execlp(g, g, -o, PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(),-stdc11, nullptr); LOG(ERROR) 编译器启动失败 \n; exit(2); } else{ waitpid(pid, nullptr, 0); if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){ LOG(INFO) 代码编译成功 \n; return true; } } LOG(ERROR) 代码编译失败未生成可执行程序 \n; return false; } }; }解析fork 子进程隔离编译操作重定向 stderr 到文件保存编译报错execlp 调用系统 g 完成编译。3.6.2 runner.hpp 沙箱运行模块安全核心#pragma once #include iostream #include string #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h #include sys/wait.h #include sys/time.h #include sys/resource.h #include ../comm/log.hpp #include ../comm/util.hpp namespace ns_runner { using namespace ns_util; using namespace ns_log; class Runner { public: Runner() {} ~Runner() {} // 设置进程CPU、内存资源限制 static void SetProcLimit(int _cpu_limit, int _mem_limit) { 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; setrlimit(RLIMIT_AS, mem_rlimit); } // 执行编译后的可执行程序 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; } } }; }解析setrlimit 限制子进程 CPU 时间、虚拟内存防御恶意死循环、内存溢出dup2 重定向标准输入输出到临时文件隔离用户程序 IO返回进程终止信号用于区分超时、内存超限、运行崩溃等错误。3.7 第七步CompileAndRun 编译运行总控模块整合 CompilerRunner整合编译、运行、错误转换、临时文件清理对外提供统一判题入口。#pragma once #include compiler.hpp #include runner.hpp #include ../comm/log.hpp #include ../comm/util.hpp #include signal.h #include unistd.h #include jsoncpp/json/json.h namespace ns_compile_and_run { using namespace ns_log; using namespace ns_util; using namespace ns_compiler; 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 _compiler_error PathUtil::CompilerError(file_name); if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_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()); } // 将系统错误码/信号转为中文提示 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: FileUtil::ReadFile(PathUtil::CompilerError(file_name), desc, true); break; case SIGABRT: desc 内存超过范围; break; case SIGXCPU: 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(); if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) { status_code -2; goto END; } if (!Compiler::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; 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) { 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::StyledWriter writer; *out_json writer.write(out_value); RemoveTempFile(file_name); } }; }解析统一入口 Start接收 JSON 参数串联写文件→编译→运行CodeToDesc 翻译底层系统信号为用户可读中文报错任务结束自动删除所有临时文件防止磁盘堆积。3.8 第八步编译集群子服务入口 compile_run/main.cc最后开发独立 HTTP 服务部署多台机器形成集群只提供判题接口接收主服务转发的任务。#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: \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; if(!in_json.empty()){ CompileAndRun::Start(in_json, out_json); resp.set_content(out_json, application/json;charsetutf-8); } }); svr.listen(0.0.0.0, atoi(argv[1])); return 0; }解析轻量化独立服务仅暴露判题接口可多端口多机器部署实现分布式横向扩容。四、核心业务全流程复盘4.1 用户访问题库页面流程浏览器 GET/all_questionshttplib 路由调用 Control::AllQuestionsModel 读取全量题库并排序View 调用 ctemplate 填充模板生成 HTMLHTTP 返回页面给浏览器渲染4.2 用户提交代码判题全流程前端 POST/judge/题号携带 JSON 代码与输入用例Control::Judge 接收请求读取题目资源限制、后台测试代码拼接用户代码 测试代码组装判题 JSON负载均衡筛选当前最空闲编译节点主服务 HTTP 客户端转发任务到编译子服务子服务执行生成唯一临时文件→写入代码→编译→受限沙箱运行捕获运行状态、输出、错误封装 JSON 回传给 Web 主服务主服务更新节点负载将判题结果返回前端子服务自动清理所有临时文件一次判题流程结束五、项目核心技术亮点极致安全Linux 进程沙箱隔离fork 子进程 setrlimit 资源限制杜绝恶意代码攻击服务器分布式负载均衡自研最小负载调度算法故障节点自动下线信号一键恢复集群支持高并发扩容MVC 分层解耦底层工具→Model→View→Control 层层解耦修改单一模块不影响全局微服务拆分Web 展示服务、编译判题服务完全分离各司其职可独立部署扩容全自动化运维自动加载题库、自动清理临时文件、自动统计节点负载、分级日志快速定位异六、项目总结这套负载均衡式在线 OJ 系统完整覆盖 C 后端开发、Linux 系统编程、网络编程、分布式架构设计核心知识点。文章严格按照从零开发的编码顺序讲解全部源码从底层工具库逐步搭建到分布式集群服务逻辑连贯适合新手复盘、课程设计、毕设、面试项目展示。不同于简单 CRUD 项目本项目亲手实现进程隔离、资源管控、负载分发、服务容灾、动态页面渲染等底层核心能力完整还原在线判题平台底层运行逻辑代码分层清晰、规范度高具备极高实战含金量。