从零实现在线OJ平台
从零实现在线OJ平台
什么是OJ?
OJ(Online Judge)简介
OJ(在线判题系统) 是一种自动化编程评测平台,用户可通过网页提交程序源代码(如C/C++、Java、Python等),系统自动编译、执行代码,并基于预设测试用例验证程序的正确性、效率及资源消耗。其核心功能是为编程训练、竞赛和教学提供实时、公正的自动化评测服务
核心功能与特点
- 自动化评测
- 多维度评判:系统对提交的代码进行编译、运行,检测输出是否匹配预期结果,并统计运行时间、内存占用等指标。
- 即时反馈:返回结果包括
Accepted(通过)
、Wrong Answer(答案错误)
、Time Limit Exceed(超时)
、Memory Limit Exceed(超内存)
等状态,帮助用户快速定位问题
- 多语言支持
- 支持主流编程语言(如C/C++、Java、Python),部分平台还涵盖Shell、SQL等专项语言
- 资源与社区功能
- 题库分类:题目按难度(入门→竞赛级)、知识点(算法、数据结构)分类,便于针对性训练。
- 竞赛与排名:支持在线举办编程比赛,实时更新用户排名,激发竞争动力
- 讨论区:用户可交流解题思路,分享代码优化方案
发展起源与应用场景
-
起源:诞生于 ACM-ICPC 国际大学生程序设计竞赛 和 信息学奥林匹克竞赛(OI),用于自动化评审计分
-
应用扩展:
- 教育领域:高校(如浙大ZOJ、北大POJ)将OJ融入程序设计课程,辅助学生练习与作业评测
- 技术招聘:企业(如LeetCode、HackerRank)通过OJ筛选候选人,考察算法与编码能力
- 竞赛训练:Codeforces、TopCoder等平台定期举办全球性编程赛事,培养顶尖选手
知名OJ平台推荐
类型 | 代表平台 | 特点 |
---|---|---|
国际综合 | UVA(西班牙)、Codeforces(俄) | 题量庞大,覆盖算法难题,高手云集 |
国内高校 | 浙大ZOJ、北大POJ、杭电HDU | 题目丰富,贴近竞赛需求,适合新手进阶@ref) |
面试向 | LeetCode、HackerRank | 聚焦企业笔试真题,提供面试模拟环境 |
竞赛专项 | USACO(美国奥赛)、洛谷(NOIP) | 分阶段训练,支持查看测试数据 |
本文所实现的OJ的宏观结构
- 公共模块:负责存放工具类
- 编译运行模块:负责处理服务模块传来的编译请求并返回结果
- OJ服务模块:负责将用户提交的请求,负载均衡式地发送给编译服务模块
项目设计
- 编写编译运行模块
- 编写服务模块
- 编写前端页面
公共模块的设计
-
日志类
-
这个日志系统实现得非常简洁高效,下面我详细解析其设计和实现:
-
整体设计理念
- 轻量化:只包含核心功能,没有冗余依赖
- 即时输出:日志直接输出到控制台
- 流式操作:采用
<<
流式操作符输出日志内容 - 信息丰富:包含关键元数据(日志等级、文件名、行号等)
- 零配置:开箱即用,无需初始化
-
核心组件解析
- 日志等级系统
enum {INFO, // 普通信息DEBUG, // 调试信息WARNING, // 警告信息ERROR, // 错误信息FATAL // 严重错误 };
特点:
- 使用简单整数代替枚举类,减少类型转换开销
- 级别从低到高排列 (0-4)
- 支持快速扩展新等级
- 核心日志函数
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; // 返回输出流 }
关键特性:
- 构造日志头:
- 包含四部分元数据:
- 日志等级(如 “[INFO]”)
- 文件名(如 “[main.cpp]”)
- 行号(如 “[42]”)
- 时间戳(如 “[1633023456]”)
- 格式示例:
[INFO][main.cpp][42][1633023456]
- 包含四部分元数据:
- 行内优化:
- 使用
inline
关键字消除函数调用开销 - 直接操作字符串避免多次I/O操作
- 使用
- 流式返回:
- 返回
std::cout
使得能链式输出内容 - 支持任意类型的数据输出(通过
operator<<
)
- 返回
-
核心日志宏
#define LOG(level) Log(#level, __FILE__, __LINE__)
宏技巧解析:
- 字符串化操作:
#level
将日志等级转为字符串(INFO → “INFO”)- 避免手动输入字符串导致错误
- 预定义宏:
__FILE__
:获取当前源文件名__LINE__
:获取当前代码行号- 自动捕获代码位置信息
- 用户友好接口:
- 简化调用:
LOG(INFO)
替代完整函数调用 - 类型安全:编译器检查日志等级
- 简化调用:
使用示例
// 普通日志 LOG(INFO) << "系统启动成功" << "\n";// 调试信息 LOG(DEBUG) << "收到请求,ID=" << request_id << "\n";// 错误日志 if(error_code) {LOG(ERROR) << "操作失败,错误码: " << error_code << "\n"; }
输出示例:
[INFO][server.cpp][35][1633023456] 系统启动成功 [DEBUG][request_handler.cpp][78][1633023457] 收到请求,ID=1001 [ERROR][database.cpp][122][1633023458] 操作失败,错误码: 503
技术亮点
- 性能优化:
- 内存操作:使用字符串拼接代替多次I/O
- 缓冲控制:不强制刷新(
std::endl
),由用户控制 - 内联函数:消除调用开销
- 元数据自动捕获:
- 通过预处理器宏自动获取文件名和行号
- 时间戳通过工具类动态获取
- 日志等级自动转为字符串
- 扩展性:
- 轻松添加新日志等级(只需在枚举添加)
- 支持与其他流输出结合使用
- 跨平台:
- 基于标准C++实现
- 依赖极少的系统功能
-
-
工具类
-
整体结构
代码位于
ns_util
命名空间下,包含四个主要工具类:TimeUtil
- 时间处理工具PathUtil
- 文件路径处理工具FileUtil
- 文件操作工具StringUtil
- 字符串处理工具
- TimeUtil 类 (时间处理工具)
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);} };
功能说明:
GetTimeStamp()
:- 使用
gettimeofday
系统调用获取当前时间 - 返回从1970年1月1日0时至今的秒数(字符串格式)
- 使用
GetTimeMs()
:- 同样基于
gettimeofday
系统调用 - 返回从1970年1月1日0时至今的毫秒数(字符串格式)
- 计算方式:
(秒数 × 1000) + (微秒数 ÷ 1000)
- 同样基于
使用场景:
- 日志时间戳
- 性能测量
- 唯一ID生成基准
- PathUtil 类 (文件路径处理工具)
const std::string temp_path = "./temp/";class PathUtil { public:// 基础路径构建方法static std::string AddSuffix(const std::string &file_name, const std::string &suffix) {return temp_path + file_name + suffix;}// 各种文件类型路径生成器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");} };
核心设计:
- 统一的临时文件目录:
./temp/
- 基于文件基本名 + 后缀的统一路径构建
- 使用点语法简化各类文件的路径获取
文件类型说明:
方法名 后缀 用途 Src
.cpp
C++源代码文件 Exe
.exe
可执行程序 CompilerError
.compile_error
编译器错误信息 Stdin
.stdin
程序输入重定向文件 Stdout
.stdout
程序输出重定向文件 Stderr
.stderr
程序错误输出重定向文件 示例:
PathUtil::Src("1234") // -> "./temp/1234.cpp" PathUtil::Exe("1234") // -> "./temp/1234.exe" PathUtil::Stderr("1234") // -> "./temp/1234.stderr"
- FileUtil 类 (文件操作工具)
class FileUtil { public:// 检查文件是否存在static bool IsFileExists(const std::string &path_name) {struct stat st;return stat(path_name.c_str(), &st) == 0;}// 生成唯一文件名static std::string UniqFileName() {static std::atomic_uint id(0); // 原子计数器id++;return TimeUtil::GetTimeMs() + "_" + std::to_string(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;} };
核心功能详解:
A. 文件存在检查 (
IsFileExists
)- 使用
stat
系统调用 - 返回
true
仅当文件存在且能获取状态信息 - 不区分文件类型(目录也会返回 true)
B. 唯一文件名生成 (
UniqFileName
)- 使用原子计数器
std::atomic_uint
保证线程安全 - 组合元素:毫秒时间戳 + 递增ID
- 生成示例:
"1633023456789_1"
,"1633023456790_2"
- 并发安全:适合多线程/多进程环境
C. 文件写入 (
WriteFile
)- 简单覆盖式写入
- 二进制安全:直接写入原始内容
- 返回布尔值表示成功与否
D. 文件读取 (
ReadFile
)-
可选参数
keep
控制是否保留换行符
keep=false
(默认):按行读取并丢弃换行符keep=true
:读取后添加\n
保持原始格式
-
兼容各种换行符格式(Unix/LF, Windows/CRLF)
- StringUtil 类 (字符串处理工具)
class StringUtil { public:static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string &sep) {// 使用Boost进行分割boost::split(*target, str, boost::is_any_of(sep), boost::algorithm::token_compress_on);} };
功能说明:
- 基于 Boost 库的字符串分割功能
- 参数说明:
str
: 待分割的输入字符串target
: 输出分割结果(字符串向量)sep
: 分隔符集合(可以是多个字符)
token_compress_on
: 压缩连续分隔符(避免空元素)
示例:
std::vector<std::string> parts; StringUtil::SplitString("a,b,c,,d", &parts, ","); // 结果: {"a", "b", "c", "d"} (token_compress_on生效)
系统依赖说明
- Linux系统调用:
gettimeofday
(获取高精度时间)stat
(文件状态检查)
- 第三方依赖:
- Boost库 (仅用于字符串分割)
整体设计特点
- 实用主义:每个类聚焦解决特定问题
- 静态方法:所有功能无需实例化即可使用
- 原子操作:唯一文件名生成保证线程安全
- 路径抽象:统一管理临时文件位置
- 可选参数:提供灵活控制(如换行符保留)
典型使用场景
- 编译系统:
// 生成唯一文件名 std::string filename = FileUtil::UniqFileName();// 写入源码 FileUtil::WriteFile(PathUtil::Src(filename), user_code);// 检查可执行文件是否存在 if (FileUtil::IsFileExists(PathUtil::Exe(filename))) {// 编译成功处理 }
- 日志处理:
// 带时间戳的日志 std::string log_entry = "[" + TimeUtil::GetTimeMs() + "] " + message;// 错误信息分割 std::vector<std::string> error_lines; StringUtil::SplitString(compiler_output, &error_lines, "\n");
- 运行环境隔离:
// 重定向运行环境 std::string stdin_path = PathUtil::Stdin(filename); std::string stdout_path = PathUtil::Stdout(filename); std::string stderr_path = PathUtil::Stderr(filename);
-
前端模块设计
首页
这个HTML文件是一个在线判题系统(OJ)的首页,提供了简洁而功能明确的用户界面。下面我将从结构、样式和功能三个维度详细解析这个首页设计:
页面结构分析
1. 整体布局 (container
)
<div class="container"><div class="navbar">...</div><div class="content">...</div>
</div>
- container:顶层容器,包裹所有页面内容
- navbar:导航栏区域
- content:主要内容区域
2. 导航栏结构 (navbar
)
<div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a>
</div>
- 6个导航链接,包括:
- 首页:当前页面
- 题库:跳转到题目列表
- 竞赛:预留功能
- 讨论:预留功能
- 求职:预留功能
- 登录:用户登录入口
3. 主内容区结构 (content
)
<div class="content"><h1 class="font_">欢迎来到我的OnlineJudge平台</h1><p class="font_">这个我个人独立开发的一个在线OJ平台</p><a class="font_" href="/all_questions">点击我开始编程啦!</a>
</div>
- 主标题(h1):平台欢迎语
- 副标题(p):平台简介
- 行动按钮(a):核心功能入口
视觉设计特色
1. 整体视觉风格
- 简约现代:没有多余的装饰元素
- 高对比度:黑白主色调+绿色点睛色
- 空间留白:200px的上边距创造舒适空间
- 统一字体:使用系统默认无衬线字体
2. 导航栏设计
.navbar {background-color: black;overflow: hidden;
}
- 黑色背景:突出导航区域
- 隐藏溢出:确保浮动元素不破坏布局
- 悬停效果:绿色背景增加交互反馈
导航项样式
.navbar a {display: inline-block;width: 80px;color: white;line-height: 50px;text-align: center;
}
- 固定宽度:每个导航项80px
- 垂直居中:行高=容器高度(50px)
- 文字居中:视觉平衡
3. 主内容区设计
.content {width: 800px;margin: 0 auto;margin-top: 200px;text-align: center;
}
- 居中布局:800px宽 + auto margin
- 垂直呼吸空间:200px顶部间距
- 居中对齐:所有内容居中显示
文本样式
.font_ {display: block;margin-top: 20px;text-decoration: none;
}
- 块级显示:每个元素独占一行
- 统一间距:20px垂直间距
- 无装饰链接:去下划线更简洁
用户体验设计
1. 导航体验
- 当前页面指示:未实现,可添加active类
- 悬停反馈:颜色变化提示可点击
- 功能分组:平台功能在左,用户功能在右
2. 核心功能入口
<a class="font_" href="/all_questions">点击我开始编程啦!</a>
- 行动召唤(CTA):显眼的文字引导
- 直达核心功能:跳转到题库页面
- 无障碍设计:语义化标签+清晰链接文本
3. 响应式考虑
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- 视口配置:移动友好设计基础
- 暂缺媒体查询:未实现完整响应式
全部问题模板
这个题目列表模板是一个动态生成题目列表的HTML页面,使用了CTemplate模板引擎来动态渲染题目数据。以下是该模板的详细分析:
整体结构设计
核心功能区域
1. 导航栏
<div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a>
</div>
- 导航项目:首页、题库、竞赛、讨论、求职
- 登录入口:固定在导航栏右侧
- 视觉反馈:鼠标悬停时背景变绿
2. 标题区域
<div class="question_list"><h1>OnlineJuge题目列表</h1><!-- ... -->
</div>
- 使用绿色主题色强调平台名称
- 居中布局增强视觉焦点
3. 题目列表表格
<table><tr><th class="item">编号</th><th class="item">标题</th><th class="item">难度</th></tr>{{#question_list}}<tr><td class="item">{{number}}</td><td class="item"><a href="/question/{{number}}">{{title}}</a></td><td class="item">{{star}}</td></tr>{{/question_list}}
</table>
表格设计特点:
- 简洁表头:
- 编号、标题、难度三列
- 清晰标识信息类型
- 动态行渲染:
- 使用CTemplate模板语法
{{#question_list}}
标记循环开始{{number}}
、{{title}}
、{{star}}
占位符填充数据
- 题目链接:
- 标题可点击跳转到题目详情
- URL格式:
/question/题目编号
- 悬停效果:蓝色+下划线
4. 页脚区域
<div class="footer"><h4>CSDN:ZZJM</h4>
</div>
- 简单版权信息
- 作者署名及来源标识
视觉设计亮点
1. 色彩方案
元素 | 背景色 | 文字色 | 悬停色 |
---|---|---|---|
导航栏 | #000000 | #FFFFFF | #008000 |
表格标题行 | rgb(243, 248, 246) | #000000 | - |
题目行链接 | - | #000000 | #0000FF |
2. 排版细节
.question_list table {font-size: large;font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;margin-top: 50px;
}
- 字体选择:清晰易读的无衬线字体集
- 字号设置:large级别确保可读性
- 间距控制:50px顶部间距创造舒适呼吸空间
3. 表格样式优化
.item {width: 100px;height: 40px;font-size: large;font-family:'Times New Roman', Times, serif;
}
- 单元格尺寸:固定高度40px确保行高一致
- 字体切换:内容区使用衬线字体提升可读性
- 背景层次:浅蓝绿色背景提高识别度
响应式设计考虑
1. 基础响应式
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2. 自适应布局
.question_list {width: 800px;margin: 0px auto;
}
- 固定宽度居中布局
- 适合主流屏幕尺寸
3. 待增强的响应式
/* 可添加媒体查询增强小屏体验 */
@media (max-width: 800px) {.question_list {width: 95%;padding-top: 20px;}.item {width: auto;padding: 0 10px;}.navbar a {width: auto;padding: 0 10px;font-size: medium;}
}
交互设计细节
1. 导航体验
.navbar a:hover {background-color: green;
}
- 悬停提示当前选项
- 颜色变化提供视觉反馈
2. 题目链接交互
.item a {text-decoration: none;color: black;
}
.item a:hover {color: blue;text-decoration:underline;
}
- 默认状态:无下划线、黑色文字
- 悬停状态:蓝色文字+下划线
- 点击预期:跳转到题目详情页
模板引擎应用
CTemplate语法解析
语法 | 说明 | 作用 |
---|---|---|
{{#question_list}} | 列表区块开始 | 标记题目循环开始 |
{{/question_list}} | 列表区块结束 | 标记题目循环结束 |
{{number}} | 变量替换 | 题目编号占位符 |
{{title}} | 变量替换 | 题目标题占位符 |
{{star}} | 变量替换 | 题目难度占位符 |
动态渲染流程
问题详情模板
这个模板是OJ系统的题目详情页面,集成了题目展示、代码编辑和提交功能,使用了ACE代码编辑器提供专业的编程体验。下面我将详细解析这个模板的设计和实现:
整体结构设计
核心功能区域
1. 导航栏 (navbar)
<div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a>
</div>
- 功能链接:首页、题库、竞赛、讨论、求职
- 用户入口:登录按钮
- 视觉反馈:悬停时绿色背景变化
2. 题目描述区 (left_desc)
<div class="left_desc"><h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3><pre>{{desc}}</pre>
</div>
- 题目信息:
- 编号:
{{number}}
- 标题:
{{title}}
- 难度:
{{star}}
- 编号:
- 题目描述:
- 使用
<pre>
标签保留格式 - 支持多行文本和代码片段
- 使用
3. 代码编辑区 (right_code)
<div class="right_code"><pre id="code" class="ace_editor"><textarea class="ace_text-input">{{pre_code}}</textarea></pre>
</div>
- 集成ACE编辑器:
- 专业代码编辑功能
- 语法高亮支持
- 预设代码填充
4. 提交与结果区 (part2)
<div class="part2"><div class="result"></div><button class="btn-submit" onclick="submit()">提交代码</button>
</div>
- 提交按钮:触发判题流程
- 结果容器:动态显示评测结果
技术亮点分析
1. ACE编辑器集成
// 初始化ACE编辑器
editor = ace.edit("code");// 配置编辑器
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/c_cpp");
editor.setFontSize(16);
editor.getSession().setTabSize(4);// 启用智能提示
editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true
});
核心功能:
- 主题设置:Monokai暗色主题
- 语言支持:C/C++语法高亮
- 编辑体验:
- 字体大小16px
- Tab缩进4空格
- 实时自动补全
- 代码片段支持
2. AJAX判题流程
function submit(){// 获取代码和题号var code = editor.getSession().getValue();var number = $("#number").text();// 构建请求$.ajax({method: 'Post',url: '/judge/' + number,dataType: 'json',contentType: 'application/json;charset=utf-8',data: JSON.stringify({'code': code,'input': ''}),success: function(data){show_result(data);}});
}
流程说明:
- 获取编辑器中的代码内容
- 提取题目编号
- 构造JSON格式请求
- 发送到判题接口
/judge/题目编号
- 处理返回结果
3. 结果展示逻辑
function show_result(data) {var result_div = $(".result");result_div.empty();// 显示状态信息$("<p>", {text: data.reason}).appendTo(result_div);if(data.status == 0) {// 显示标准输出和错误$("<pre>", {text: data.stdout}).appendTo(result_div);$("<pre>", {text: data.stderr}).appendTo(result_div);}
}
结果显示:
- 状态信息:编译/运行结果描述
- 标准输出:程序输出内容
- 标准错误:错误信息(如有)
视觉设计特点
1. 分屏布局
.part1 {width: 100%;height: 600px;overflow: hidden;
}.left_desc, .right_code {height: 600px;
}
- 左右分屏:题目描述 + 代码编辑
- 固定高度:600px确保可视区域
- 独立滚动:题目描述区可滚动
2. 代码编辑区
.ace_editor {height: 600px;
}
- 全高度显示:最大化编辑区域
- 专业配色:Monokai主题
- 清晰字体:16px字号
3. 提交按钮设计
.btn-submit {width: 120px;height: 50px;font-size: large;background-color: #26bb9c;color: #FFF;border: 0px;
}
.btn-submit:hover {color: green;
}
- 醒目位置:右下角浮动
- 色彩对比:绿色背景+白色文字
- 悬停反馈:文字变绿提示
动态数据绑定
模板变量说明
变量名 | 说明 | 示例 |
---|---|---|
{{number}} | 题目编号 | “1001” |
{{title}} | 题目标题 | “两数之和” |
{{star}} | 题目难度 | “中等” |
{{desc}} | 题目描述 | 包含HTML格式的题目描述 |
{{pre_code}} | 预设代码 | “#include …” |
数据渲染流程
编译运行模块的设计
编译模块结构
编译服务流程
编译类的设计
这个编译类(Compiler)是一个静态工具类,用于将C++源代码编译成可执行文件。下面我将详细分析其设计、实现和使用方式。
类概述
基本特性
- 位于
ns_compiler
命名空间 - 工具类设计(所有方法为静态)
- 核心方法:
static bool Compile(const std::string &file_name)
- 无状态类(构造函数和析构函数为空)
依赖组件
- 路径工具:
ns_util::PathUtil
- 文件工具:
ns_util::FileUtil
- 日志系统:
ns_log::LOG
核心方法:Compile()
static bool Compile(const std::string &file_name)
输入输出
- 输入:文件名(不含扩展名),如
1234
- 输出:
- 成功时:返回
true
- 失败时:返回
false
- 附带详细日志输出
- 成功时:返回
文件映射
- 编译过程中处理三类文件:
文件名: 1234源文件: ./temp/1234.cpp
可执行文件: ./temp/1234.exe
错误文件: ./temp/1234.stderr
编译流程详解
创建子进程
pid_t pid = fork();
if(pid < 0) {LOG(ERROR) << "内部错误,创建子进程失败" << "\n";return false;
}
- 使用
fork()
创建子进程 - 失败记录错误日志并返回
子进程处理(编译器进程)
else if (pid == 0) {// 1. 设置文件权限掩码umask(0);// 2. 创建错误输出文件int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if(_stderr < 0){LOG(WARNING) << "没有成功形成stderr文件" << "\n";exit(1);}// 3. 重定向标准错误dup2(_stderr, 2);// 4. 调用g++编译器execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE","-std=c++11", nullptr);// 5. 执行失败处理LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";exit(2);
}
- 关键步骤说明:
-
umask(0)
- 重置文件权限掩码
- 确保新创建文件获得0644权限(rw-r–r–)
-
错误文件创建
- 使用
open()
系统调用创建错误日志文件 - 路径:
PathUtil::CompilerError(file_name)
- 标志:
O_CREAT | O_WRONLY
(创建+只写) - 权限:0644(rw-r–r–)
- 使用
-
错误重定向
dup2(_stderr, 2)
将标准错误(fd 2)重定向到错误文件- 编译器输出的所有错误信息都会被写入该文件
-
编译器调用
-
execlp("g++", ...)
执行g++编译器
关键参数
:
-
-o [output]
:指定输出文件路径(PathUtil::Exe(file_name)) -
[input]
:输入源文件路径(PathUtil::Src(file_name)) -
-D COMPILER_ONLINE
:定义编译器在线标志(用户代码中可用) -
-std=c++11
:使用C++11标准 -
必须参数:
nullptr
结束参数列表
-
-
错误处理
execlp()
成功时不返回- 如果执行到后续代码,说明调用失败
- 记录错误日志后退出子进程
父进程处理
else {// 1. 等待子进程结束waitpid(pid, nullptr, 0);// 2. 检查编译结果if(FileUtil::IsFileExists(PathUtil::Exe(file_name)) {LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";return true;}
}
LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";
return false;
- 关键步骤说明:
- 进程等待
waitpid(pid, nullptr, 0)
阻塞等待子进程结束- 不关心子进程退出状态(第二参数为
nullptr
)
- 结果验证
- 使用
FileUtil::IsFileExists()
检查可执行文件是否存在 - 路径:
PathUtil::Exe(file_name)
- 使用
- 日志反馈
- 成功:记录包含源文件名的INFO日志
- 失败:记录ERROR日志
编译器参数详解
execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE","-std=c++11", nullptr);
参数解析:
-
-o [output]
- 指定输出文件名(可执行文件)
- 示例:
./temp/1234.exe
-
[input]
- 源文件路径
- 示例:
./temp/1234.cpp
-
-D COMPILER_ONLINE
-
定义预处理器宏
COMPILER_ONLINE
使用场景
:
#ifdef COMPILER_ONLINE // 仅在线编译环境执行的代码 #else // 本地开发环境执行的代码 #endif
- 可增强安全性(限制特定功能)
-
-
-std=c++11
- 强制使用C++11标准
- 确保语法兼容性
安全设计考虑
- 进程隔离
- 子进程执行编译操作
- 防止编译器崩溃影响主进程
- 错误隔离
- 标准错误重定向到独立文件
- 不会污染主进程输出
- 权限控制
umask(0)
确保文件正确权限- 错误文件仅开放写权限
- 资源限制
- 父进程等待时间不限
- 实际使用中可能需要添加超时控制
使用示例
#include "compiler.hpp"int main() {std::string file_id = "1234";if (ns_compiler::Compiler::Compile(file_id)) {std::cout << "编译成功" << std::endl;} else {std::cerr << "编译失败" << std::endl;// 读取错误信息std::string error_msg;ns_util::FileUtil::ReadFile(ns_util::PathUtil::CompilerError(file_id), &error_msg);std::cerr << "错误信息:\n" << error_msg << std::endl;}return 0;
}
运行类的设计
这个运行类(Runner)是负责在受控环境中执行程序的组件,特别适用于需要限制程序资源使用(如在线判题系统)的场景。下面我将详细分析其设计和实现:
类概述
基本特性
- 位于
ns_runner
命名空间 - 工具类设计(所有方法为静态)
- 核心方法:
SetProcLimit
: 设置进程资源限制Run
: 执行程序并监控资源使用
- 无状态设计(构造函数和析构函数为空)
关键依赖
- 路径工具:
ns_util::PathUtil
- 文件工具:
ns_util::FileUtil
- 日志系统:
ns_log::LOG
资源限制方法:SetProcLimit
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; // 转换为KBsetrlimit(RLIMIT_AS, &mem_rlimit);
}
功能详解:
-
CPU限制:
- 使用
RLIMIT_CPU
资源限制类型 rlim_cur
: 软限制(实际限制值)rlim_max
: 硬限制(设为无限)- 单位:秒(进程允许的最大CPU时间)
- 使用
-
内存限制:
- 使用
RLIMIT_AS
限制进程地址空间大小 _mem_limit * 1024
: 将KB转换为字节- 限制整个进程的虚拟内存
- 使用
-
资源限制类型:
限制类型 描述 RLIMIT_CPU
最大CPU时间(秒) RLIMIT_AS
进程地址空间大小(字节) RLIMIT_FSIZE
文件大小限制 RLIMIT_STACK
栈大小限制
核心方法:Run
static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
输入输出
- 输入:
file_name
: 文件名(不含扩展名),如1234
cpu_limit
: CPU时间限制(秒)mem_limit
: 内存限制(KB)
- 输出:
> 0
: 程序异常终止(信号编号)0
: 程序正常终止< 0
: 内部错误
运行流程详解
文件准备阶段
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);
- 使用
PathUtil
生成各种文件路径 - 创建和打开文件(使用
umask(0)
确保权限) - 文件说明:
stdin
: 程序输入重定向文件stdout
: 程序输出重定向文件stderr
: 错误输出重定向文件
创建子进程
pid_t pid = fork();
if (pid < 0) {LOG(ERROR) << "运行时创建子进程失败" << "\n";// 关闭文件描述符return -2; // 创建子进程失败
}
子进程处理(执行程序)
else if (pid == 0) {// 重定向标准IOdup2(_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);
}
关键操作:
- IO重定向:
- 使用
dup2
重定向标准输入/输出/错误 - 文件描述符说明:
- 0: 标准输入
- 1: 标准输出
- 2: 标准错误
- 使用
- 资源限制:
- 调用
SetProcLimit
设置CPU和内存限制
- 调用
- 程序执行:
- 使用
execl
执行可执行文件 - 参数说明:
- 第一参数:可执行文件路径
- 第二参数:程序名(与第一参数相同)
nullptr
: 参数列表结束
- 使用
父进程处理(监控)
else {// 关闭不需要的文件描述符close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);// 等待子进程结束int status = 0;waitpid(pid, &status, 0);// 提取退出状态LOG(INFO) << "运行完毕, info: " << (status & 0x7F) << "\n"; return status & 0x7F;
}
关键操作:
-
资源清理:
- 父进程关闭不再需要的文件描述符
-
进程等待:
waitpid
等待子进程结束status
: 存储子进程的退出状态
-
状态解码:
-
status & 0x7F
: 提取低7位信号编号 -
Linux进程状态编码规则:
status (int) = [退出状态 (8位) | 信号编号 (7位) | coredump标志 (1位)]
-
状态码解释
返回值 | 意义 | 典型场景 |
---|---|---|
>0 | 进程被信号终止 (信号编号) | SIGSEGV(11), SIGALRM(14) |
0 | 正常终止 | 程序运行结束 |
-1 | 文件打开失败 | 权限问题/路径错误 |
-2 | 创建子进程失败 | 系统资源耗尽 |
异常处理机制
CPU超限处理
- 当程序超过CPU时间限制时:
- 内核向进程发送
SIGXCPU
信号 - 如果进程不捕获该信号,将被终止
- 父进程收到信号编号
SIGXCPU
(24)
- 内核向进程发送
内存超限处理
- 当程序超过内存限制时:
- 内存分配操作失败
- 进程可能因非法内存访问收到
SIGSEGV
(11) - 或因堆栈溢出收到
SIGSTKFLT
(16)
其他信号处理
信号 | 编号 | 原因 |
---|---|---|
SIGFPE | 8 | 算术异常 |
SIGABRT | 6 | 程序主动退出 |
SIGKILL | 9 | 强制终止 |
SIGTERM | 15 | 终止请求 |
使用示例
#include "runner.hpp"int main() {std::string file_id = "1234";int cpu_limit = 1; // 1秒CPU时间int mem_limit = 65536; // 64MB内存int result = ns_runner::Runner::Run(file_id, cpu_limit, mem_limit);if(result == 0) {std::cout << "程序正常结束" << std::endl;} else if(result > 0) {std::cerr << "程序被信号终止: " << result << std::endl;} else {std::cerr << "内部错误: " << result << std::endl;}return 0;
}
编译运行类的设计
这个编译运行类是一个高级封装层,负责整合编译和运行过程,处理整个代码提交到结果返回的完整生命周期。它使用JSON格式进行输入输出,非常适用于在线评测系统(Online Judge)等场景。
核心功能
- 流程整合:串联编译和运行过程
- 资源管理:自动清理临时文件
- 结果处理:转换状态码为可读描述
- 数据交换:JSON格式输入输出
类依赖关系
核心方法详解
临时文件清理 (RemoveTempFile)
static void RemoveTempFile(const std::string &file_name)
-
功能:清除误重定向文件 (.stderr)
-
实现特点:
-
使用
FileUtil::IsFileExists
检查文件是否存在 -
使用
unlink
系统调用删除文件 -
防止临时文件堆积,避免资源泄漏
-
状态码转换 (CodeToDesc)
static std::string CodeToDesc(int code, const std::string &file_name)
- 状态码体系:
状态码 | 含义 | 处理方式 |
---|---|---|
0 | 编译运行成功 | 直接返回成功描述 |
-1 | 提交的代码为空 | 返回错误描述 |
-2 | 未知错误 | 返回错误描述 |
-3 | 编译错误 | 读取编译错误文件内容 |
>0 | 运行时信号(如 SIGSEGV) | 转换为对应的错误描述 |
特殊处理:
-
编译错误:直接读取编译错误文件内容作为描述
-
信号相关错误:提供用户友好的描述而非原始信号编号
case SIGABRT: // 6desc = "内存超过范围";break; case SIGXCPU: // 24desc = "CPU使用超时";break; case SIGFPE: // 8desc = "浮点数溢出";break;
核心流程 (Start)
static void Start(const std::string &in_json, std::string *out_json)
输入格式 (in_json):
{"code": "C++源代码","input": "输入数据(可选)","cpu_limit": 1, // CPU时间限制(秒)"mem_limit": 10240 // 内存限制(KB)
}
输出格式 (out_json):
{"status": 状态码,"reason": "状态描述","stdout": "程序输出", // 可选"stderr": "错误输出" // 可选
}
详细步骤:
-
解析输入
Json::Reader reader; reader.parse(in_json, in_value); std::string code = in_value["code"].asString();
-
基础检查
-
代码为空立即返回错误
if (code.size() == 0) {status_code = -1;goto END; }
-
-
文件准备
- 生成唯一文件名:
FileUtil::UniqFileName()
- 写入源码文件:
FileUtil::WriteFile(PathUtil::Src(file_name), code)
- 生成唯一文件名:
-
编译阶段
if (!Compiler::Compile(file_name)) {status_code = -3; // 编译错误goto END; }
-
运行阶段
run_result = Runner::Run(file_name, cpu_limit, mem_limit);
-
结果处理
-
成功时收集输出:
FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true); FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
-
生成结果JSON:
Json::StyledWriter writer; *out_json = writer.write(out_value);
-
-
资源清理
RemoveTempFile(file_name);
设计亮点
错误处理策略
- 多级错误分类:区分编译错误、运行时错误和系统错误
- 错误信息增强:编译错误时直接返回编译器输出的错误信息
- 安全异常处理:使用
goto END
确保资源清理总能执行
文件管理策略
-
唯一文件名:使用时间戳+原子计数器避免冲突
file_name = FileUtil::UniqFileName();
- 全生命周期管理:- 自动创建临时文件
- 自动清理所有关联文件
- 防止资源泄漏###### JSON接口设计- 输入/输出标准化:- 统一使用JSON格式交换数据
- 清晰的字段定义- 扩展性强:- 可轻松添加新字段而不破坏兼容性
- 适合网络传输###### 代码结构优化- **状态机式流程**:使用状态码驱动处理流程
- **资源获取即初始化(RAII)**:自动管理文件资源
- **模块化设计**:各功能模块边界清晰##### 使用示例###### 请求生成```c++
Json::Value req;
req["code"] = R"(#include <iostream>
int main() {std::cout << "Hello, World!";return 0;
})";
req["input"] = "";
req["cpu_limit"] = 1;
req["mem_limit"] = 1024 * 10; // 10MBJson::StyledWriter writer;
std::string in_json = writer.write(req);
处理调用
std::string out_json;
ns_compile_and_run::CompileAndRun::Start(in_json, &out_json);
响应解析
{"status": 0,"reason": "编译运行成功","stdout": "Hello, World!","stderr": ""
}
编译服务类设计
这个编译服务模块是一个基于HTTP的网络服务,使用cpp-httplib库实现,主要功能是接收客户端提交的代码,执行编译运行操作,并将结果返回给客户端。
整体架构
核心功能
1. HTTP服务器设置
Server svr;
svr.Post("/compile_and_run", [](const Request &req, Response &resp){// 请求处理逻辑
});
svr.listen("0.0.0.0", atoi(argv[1]));
- 监听地址:
0.0.0.0
表示监听所有网络接口 - 端口设置:通过命令行参数指定(如
./compile_server 8080
) - 路由设置:只处理
/compile_and_run
的 POST 请求
2. 请求处理流程
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;charset=utf-8");
}
-
获取请求体:HTTP POST 请求的正文包含 JSON 数据
-
调用服务:交给
CompileAndRun::Start
处理
设置响应
:
- 内容类型:
application/json;charset=utf-8
- 内容:处理结果的 JSON 数据
3. 客户端请求格式
{"code": "C++源代码","input": "输入数据","cpu_limit": 1, // 单位:秒"mem_limit": 10240 // 单位:KB
}
4. 服务端响应格式
{"status": 0, // 状态码"reason": "编译运行成功", // 状态描述"stdout": "程序输出", // 标准输出"stderr": "" // 错误输出
}
关键设计要点
1. 服务唯一性保障
// 通过文件名唯一性避免多个用户之间的影响
file_name = FileUtil::UniqFileName();
- 时间戳+原子计数器:确保文件名全局唯一
- 并发安全:使用
static std::atomic_uint
实现原子递增
2. 参数传递方式
- 命令行参数:服务端口通过命令行参数指定
- HTTP请求体:使用JSON作为数据传输格式
- HTTP响应:同样使用JSON返回结果
3. 错误处理机制
- 参数检查:验证端口号是否有效
- 空请求处理:跳过空请求不处理
- 资源清理:确保临时文件被清理
4. 服务部署特点
- 轻量级:基于单个可执行文件
- 跨平台:依赖少,易于部署
- 服务化:通过HTTP接口提供服务
启动与使用
1. 编译服务
# 编译服务
g++ -o compile_server compile_server.cpp -ljsoncpp -lpthread
2. 启动服务
# 在8080端口启动服务
./compile_server 8080
3. 客户端调用示例
import requestsurl = "http://localhost:8080/compile_and_run"
data = {"code": "#include <iostream>\nint main() { std::cout << \"Hello, World!\"; return 0; }","input": "","cpu_limit": 1,"mem_limit": 10240
}response = requests.post(url, json=data)
print(response.json())
4. 客户端响应
{"status": 0,"reason": "编译运行成功","stdout": "Hello, World!","stderr": ""
}
服务模块的设计
服务请求路由类设计
这个服务请求路由类是在线判题系统(OJ)的核心控制器,负责处理用户请求、路由到对应的处理逻辑,并管理整个系统的运行状态。以下是该系统的详细设计分析:
整体架构
核心组件设计
信号处理与恢复机制
static Control *ctrl_ptr = nullptr;void Recovery(int signo) {ctrl_ptr->RecoveryMachine();
}int main() {signal(SIGQUIT, Recovery); // 注册信号处理函数// ...
}
-
信号机制:使用
SIGQUIT
触发系统恢复 -
设计目的:在系统异常时恢复机器状态
实现方式
:
- 注册信号处理函数
- 保存控制类实例指针
- 调用控制类的恢复方法
控制中心 (Control Class)
class Control {
public:// 获取所有题目列表bool AllQuestions(std::string* html);// 获取单个题目详情bool Question(const std::string& number, std::string* html);// 判题服务bool Judge(const std::string& number, const std::string& in_json, std::string* out_json);// 恢复机器状态void RecoveryMachine();
};
路由设计
- 获取所有题目列表 (
/all_questions
)
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){std::string html;ctrl.AllQuestions(&html);resp.set_content(html, "text/html; charset=utf-8");
});
-
功能:显示所有可用题目的列表
-
响应格式:HTML页面
设计特点
:
- 动态生成HTML内容
- 包含题目编号、标题、难度等信息
- 获取单个题目详情 (
/question/\d+
)
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");
});
- URL模式:使用正则表达式匹配题目ID
- 功能:显示单个题目的详细内容
- 设计特点:
- 提取URL中的题目编号
- 动态生成包含题目描述的HTML
- 提供代码编辑器区域
- 判题服务 (
/judge/\d+
)
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");
});
- 请求方法:POST
- 功能:编译、运行并评测用户提交的代码
- 输入:
- URL中的题目编号
- 请求体中的JSON数据(用户代码)
- 输出:评测结果的JSON数据
- 设计特点:
- 与编译服务模块协同工作
- 返回详细的评测信息
请求处理流程
用户访问题目列表
用户查看题目详情
用户提交代码
异常恢复机制
恢复流程
设计特点:
- 手动触发:管理员通过发送信号主动恢复
- 状态重建:重置判题服务组件
- 资源清理:释放可能泄漏的资源
- 服务恢复:重启失败的服务进程
安全设计
1. 输入验证
// 题目编号验证
bool IsValidQuestionNumber(const std::string& num) {return !num.empty() && std::all_of(num.begin(), num.end(), ::isdigit);
}
2. 正则表达式防护
R"(/question/(\d+))" // 只匹配数字ID
3. 异常隔离
- 每个判题请求在独立环境中执行
- 编译运行失败不会影响主服务
4. 资源限制
- CPU时间限制
- 内存使用限制
- 防止恶意代码破坏系统
model类设计
这个Model类是一个高效的数据管理模块,专门用于加载、组织和提供题库信息,是在线判题系统(OJ)的数据核心。下面是对该设计的详细解析:
整体架构设计
Question结构体设计
struct Question {std::string number; // 唯一题号std::string title; // 题目标题std::string star; // 难度级别int cpu_limit; // 时间限制(秒)int mem_limit; // 内存限制(KB)std::string desc; // 题目描述std::string header; // 预设代码头部std::string tail; // 预设代码尾部
};
-
字段说明:
-
number:题目唯一标识符
-
title:简短描述(如"两数之和")
-
star:难度分级(简单/中等/困难)
-
cpu_limit/mem_limit:执行资源限制
-
desc:题目详细描述(Markdown格式)
-
header:预置代码头部(函数签名等)
-
tail:测试代码和main函数
-
具体实现
1. 文件路径定义
const std::string questins_list = "./questions/questions.list";
const std::string questins_path = "./questions/";
- questions.list:题目元数据索引文件
- questions/:题目内容存储目录
2. 核心方法实现
A. 构造函数与初始化
Model() {assert(LoadQuestionList(questins_list));
}
- 自动加载题目列表
- 使用断言确保初始化成功
B. 题库加载 (LoadQuestionList
)
bool LoadQuestionList(const string &question_list) {ifstream in(question_list);// 错误处理:文件检查if(!in.is_open()) {LOG(FATAL) << "加载题库失败,请检查是否存在题库文件";return false;}string line;while(getline(in, line)) {vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");// 格式校验:题号 标题 难度 CPU限制 内存限制if(tokens.size() != 5) {LOG(WARNING) << "加载部分题目失败,请检查文件格式";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(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) << "加载题库成功! 题目数量: " << questions.size();in.close();return true;
}
文件格式示例 (questions.list)
1 两数之和 简单 1 30000
2 两数相加 中等 1 50000
3 无重复字符的最长子串 中等 2 100000
C. 题库查询接口
- 获取所有题目列表
bool GetAllQuestions(vector<Question> *out) {if(questions.empty()) {LOG(ERROR) << "题库为空,获取题目列表失败";return false;}for(const auto &pair : questions) {out->push_back(pair.second);}return true;
}
- 获取单个题目详情
bool GetOneQuestion(const std::string &number, Question *q) {auto it = questions.find(number);if(it == questions.end()) {LOG(ERROR) << "题目不存在,编号: " << number;return false;}*q = it->second;return true;
}
题目列表页面
vector<Question> all_questions;
model.GetAllQuestions(&all_questions);// 生成HTML表格
for (const auto& q : all_questions) {cout << "<tr>"<< "<td>" << q.number << "</td>"<< "<td>" << q.title << "</td>"<< "<td>" << q.star << "</td>"<< "</tr>";
}
题目详情页面
Question q;
model.GetOneQuestion("1", &q);cout << "<h1>" << q.title << "</h1>"<< "<div class='difficulty'>难度: " << q.star << "</div>"<< "<pre>" << q.desc << "</pre>"<< "<div class='code-editor'>"<< "<pre>" << q.header << "</pre>"<< "<textarea id='user-code'></textarea>"<< "<pre>" << q.tail << "</pre>"<< "</div>";
判题系统集成
// 用户提交的代码
string user_code = GetUserSubmittedCode();// 构建完整代码
string full_code = q.header + user_code + q.tail;// 设置资源限制
int cpu_limit = q.cpu_limit;
int mem_limit = q.mem_limit;// 执行判题
RunJudge(full_code, cpu_limit, mem_limit);
control类的设计
Control 类是整个在线判题系统(OJ)的核心控制器,负责协调模型、视图和负载均衡,处理用户请求和调度编译资源。下面我将详细解析这个复杂而精妙的设计:
整体架构设计
核心组件详解
1. Machine 类(编译节点)
class Machine {
public:std::string ip; // 节点IP地址int port; // 节点端口uint64_t load; // 当前负载(任务数)std::mutex *mtx; // 线程安全锁void IncLoad(); // 增加负载void DecLoad(); // 减少负载void ResetLoad();// 重置负载uint64_t Load(); // 获取负载
};
设计特点:
- 负载计数:原子操作记录节点当前任务数
- 线程安全:通过mutex保护负载状态变更
- 状态隔离:每个节点独立维护自身状态
- 轻量化:仅存储必要网络标识符
2. LoadBlance 类(负载均衡器)
class LoadBlance {
private:std::vector<Machine> machines; // 所有可用节点std::vector<int> online; // 在线节点IDstd::vector<int> offline; // 离线节点IDstd::mutex mtx; // 节点状态锁public:bool LoadConf(const std::string &machine_conf); // 加载节点配置bool SmartChoice(int *id, Machine **m); // 智能选择节点void OfflineMachine(int which); // 标记节点离线void OnlineMachine(); // 恢复离线节点void ShowMachines(); // 调试接口
};
核心算法:SmartChoice(智能选择)
- 锁定资源:加锁保护节点状态
- 检查可用性:确认存在在线节点
- 寻找最小负载节点:
*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]];} }
- 返回最优节点
3. Control 类(系统控制器)
class Control {
private:Model model_; // 题目数据模型View view_; // 网页视图生成器LoadBlance load_blance_; // 负载均衡器public:void RecoveryMachine(); // 恢复所有离线节点bool AllQuestions(string *html); // 获取所有题目bool Question(const string &number, string *html); // 获取题目详情void Judge(const std::string &number, const std::string in_json, std::string *out_json); // 判题服务
};
关键流程分析
1. 判题流程 (Judge
方法)
详细步骤:
-
获取题目信息:
struct Question q; model_.GetOneQuestion(number, &q);
-
构造编译请求:
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;
-
智能选择节点:
int id = 0; Machine *m = nullptr; load_blance_.SmartChoice(&id, &m);
-
发送编译请求:
Client cli(m->ip, m->port); m->IncLoad(); auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8");
-
结果处理:
if(res->status == 200) {*out_json = res->body;m->DecLoad(); } else {load_blance_.OfflineMachine(id); }
2. 节点故障处理
故障检测:
- HTTP请求失败自动标记为故障
- 记录错误日志:
LOG(ERROR) << "当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线";
故障处理:
load_blance_.OfflineMachine(id);
状态恢复:
void Control::RecoveryMachine() {load_blance_.OnlineMachine();
}
3. 视图渲染流程
题目列表页面:
bool AllQuestions(string *html) {vector<struct Question> all;model_.GetAllQuestions(&all);sort(all.begin(), all.end(), [](auto &q1, auto &q2){return atoi(q1.number.c_str()) < atoi(q2.number.c_str());});view_.AllExpandHtml(all, html);
}
题目详情页面:
bool Question(const string &number, string *html) {struct Question q;model_.GetOneQuestion(number, &q);view_.OneExpandHtml(q, html);
}
设计亮点
1. 负载均衡策略
策略 | 优势 | 实现 |
---|---|---|
最小负载优先 | 最优资源利用 | 遍历找到负载最低节点 |
节点健康检查 | 自动故障隔离 | 失败请求触发下线机制 |
状态恢复 | 手动恢复服务 | RecoveryMachine全局恢复 |
2. 健壮性设计
-
错误隔离:
- 单节点故障不影响系统整体
- 请求失败自动切换节点
-
线程安全:
- 节点负载变更使用互斥锁
- 节点状态变更加锁保护
-
故障恢复:
- 管理员触发的节点恢复
- 日志记录辅助故障排查
3. 资源管理
-
精准调度:
- 选择负载最低节点
- 动态调整节点权重
-
资源释放:
- 任务完成释放节点资源
- 超时任务强制回收资源
配置文件格式
service_machine.conf
示例:
192.168.1.101:8080
192.168.1.102:8080
192.168.1.103:8080
典型使用场景
1. 正常请求处理
2. 故障处理场景
3. 恢复机制
view类的设计
View类负责将数据模型渲染成HTML页面,是MVC架构中的视图层核心组件。它使用Google的CTemplate库实现了高效的模板渲染机制。
类设计概览
核心特性:
- 模板驱动:基于HTML模板实现渲染
- 数据绑定:动态填充题目数据到模板
- 模板复用:相同模板结构重复使用
- 路径配置:集中管理模板位置
实现细节
1. 模板路径配置
const std::string template_path = "./template_html/";
- 模板统一存放于
./template_html/
目录 - 使用相对路径便于部署
2. 题目列表页渲染
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");// 3. 填充题目数据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);}// 4. 加载模板ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 5. 渲染HTMLtpl->Expand(html, &root);
}
模板示例 (all_questions.html):
<!DOCTYPE html>
<html>
<head><title>题目列表</title>
</head>
<body><h1>在线题库</h1><table><tr><th>编号</th><th>标题</th><th>难度</th></tr>{{#question_list}}<tr><td>{{number}}</td><td><a href="/question/{{number}}">{{title}}</a></td><td>{{star}}</td></tr>{{/question_list}}</table>
</body>
</html>
3. 题目详情页渲染
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");// 3. 填充题目详情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);// 4. 加载模板ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 5. 渲染HTMLtpl->Expand(html, &root);
}
模板示例 (one_question.html):
<!DOCTYPE html>
<html>
<head><title>{{number}}.{{title}}</title><style>.header { background-color: #f0f0f0; padding: 10px; }.description { margin: 20px 0; white-space: pre-wrap; }.code-editor { border: 1px solid #ccc; padding: 10px; }</style>
</head>
<body><div class="header"><h2>{{number}}. {{title}}</h2><div class="difficulty">难度: {{star}}</div></div><div class="description">{{desc}}</div><div class="code-editor"><pre>{{pre_code}}</pre><textarea id="user-code" rows="20" cols="80"></textarea><button onclick="submitCode()">提交</button></div><script>function submitCode() {const code = document.getElementById('user-code').value;fetch('/judge/{{number}}', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ code: code })}).then(response => response.json()).then(data => {// 处理评测结果});}</script>
</body>
</html>
CTemplate渲染机制详解
1. 模板标记系统
语法 | 说明 | 示例 |
---|---|---|
{{variable}} | 变量替换 | {{title}} |
{{#section}} | 区块开始 | {{#question_list}} |
{{/section}} | 区块结束 | {{/question_list}} |
{{>include}} | 包含模板 | {{> header}} |
2. 模板字典(TemplateDictionary)
功能:
- 数据容器:存储要填充到模板的数据
- 作用域管理:支持嵌套的命名空间
- 区块控制:管理重复区域的渲染
核心方法:
-
值设置:
SetValue("key", "value"); // 设置普通值
-
区块管理:
TemplateDictionary* sub = AddSectionDictionary("section_name"); sub->SetValue("sub_key", "sub_value");
-
包含引用:
root.SetFilename("footer", "footer.html");
3. 模板加载选项
ctemplate::DO_NOT_STRIP
- 保留空白和注释,便于调试
- 生产环境建议使用
STRIP_BLANK_LINES
设计亮点
1. 表现与逻辑分离
- 前端开发者:只关心HTML/CSS模板
- 后端开发者:专注数据逻辑
- 协作方式:通过模板变量约定接口
2. 高效渲染机制
-
模板预编译:
Template::GetTemplate() // 加载时编译模板
-
内存缓存:
- 避免重复IO读取模板文件
- 自动缓存编译后的模板结构
-
快速渲染:
- 时间复杂度:O(n)
- 线性扫描模板结构填充数据
3. 错误安全机制
-
模板加载检查:
if (tpl) tpl->Expand(...); else /* 处理错误 */
-
自动转义:
SetValue("safe_content", value); // CTemplate默认自动转义HTML特殊字符
-
缺失值处理:
- 未设置的变量渲染为空
- 保留原始模板标记以便定位问题