当前位置: 首页 > news >正文

从零实现在线OJ平台

从零实现在线OJ平台

什么是OJ?

OJ(Online Judge)简介

OJ(在线判题系统) 是一种自动化编程评测平台,用户可通过网页提交程序源代码(如C/C++、Java、Python等),系统自动编译、执行代码,并基于预设测试用例验证程序的正确性、效率及资源消耗。其核心功能是为编程训练、竞赛和教学提供实时、公正的自动化评测服务


核心功能与特点

  1. 自动化评测
    • 多维度评判:系统对提交的代码进行编译、运行,检测输出是否匹配预期结果,并统计运行时间、内存占用等指标。
    • 即时反馈:返回结果包括 Accepted(通过)Wrong Answer(答案错误)Time Limit Exceed(超时)Memory Limit Exceed(超内存) 等状态,帮助用户快速定位问题
  2. 多语言支持
    • 支持主流编程语言(如C/C++、Java、Python),部分平台还涵盖Shell、SQL等专项语言
  3. 资源与社区功能
    • 题库分类:题目按难度(入门→竞赛级)、知识点(算法、数据结构)分类,便于针对性训练。
    • 竞赛与排名:支持在线举办编程比赛,实时更新用户排名,激发竞争动力
    • 讨论区:用户可交流解题思路,分享代码优化方案

发展起源与应用场景

  • 起源:诞生于 ACM-ICPC 国际大学生程序设计竞赛信息学奥林匹克竞赛(OI),用于自动化评审计分

  • 应用扩展:

    • 教育领域:高校(如浙大ZOJ、北大POJ)将OJ融入程序设计课程,辅助学生练习与作业评测
    • 技术招聘:企业(如LeetCode、HackerRank)通过OJ筛选候选人,考察算法与编码能力
    • 竞赛训练:Codeforces、TopCoder等平台定期举办全球性编程赛事,培养顶尖选手

知名OJ平台推荐

类型代表平台特点
国际综合UVA(西班牙)、Codeforces(俄)题量庞大,覆盖算法难题,高手云集
国内高校浙大ZOJ、北大POJ、杭电HDU题目丰富,贴近竞赛需求,适合新手进阶@ref)
面试向LeetCode、HackerRank聚焦企业笔试真题,提供面试模拟环境
竞赛专项USACO(美国奥赛)、洛谷(NOIP)分阶段训练,支持查看测试数据

本文所实现的OJ的宏观结构

image-20250701160336831

  1. 公共模块:负责存放工具类
  2. 编译运行模块:负责处理服务模块传来的编译请求并返回结果
  3. OJ服务模块:负责将用户提交的请求,负载均衡式地发送给编译服务模块

项目设计

  1. 编写编译运行模块
  2. 编写服务模块
  3. 编写前端页面

公共模块的设计

  1. 日志类
    • 这个日志系统实现得非常简洁高效,下面我详细解析其设计和实现:

    • 整体设计理念

      1. 轻量化:只包含核心功能,没有冗余依赖
      2. 即时输出:日志直接输出到控制台
      3. 流式操作:采用 << 流式操作符输出日志内容
      4. 信息丰富:包含关键元数据(日志等级、文件名、行号等)
      5. 零配置:开箱即用,无需初始化
    • 核心组件解析

      1. 日志等级系统
      enum {INFO,     // 普通信息DEBUG,    // 调试信息WARNING,  // 警告信息ERROR,    // 错误信息FATAL     // 严重错误
      };
      

      特点:

      • 使用简单整数代替枚举类,减少类型转换开销
      • 级别从低到高排列 (0-4)
      • 支持快速扩展新等级
      1. 核心日志函数
      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;      // 返回输出流
      }
      

      关键特性:

      1. 构造日志头
        • 包含四部分元数据:
          • 日志等级(如 “[INFO]”)
          • 文件名(如 “[main.cpp]”)
          • 行号(如 “[42]”)
          • 时间戳(如 “[1633023456]”)
        • 格式示例:[INFO][main.cpp][42][1633023456]
      2. 行内优化
        • 使用 inline 关键字消除函数调用开销
        • 直接操作字符串避免多次I/O操作
      3. 流式返回
        • 返回 std::cout 使得能链式输出内容
        • 支持任意类型的数据输出(通过 operator<<
    • 核心日志宏

    #define LOG(level) Log(#level, __FILE__, __LINE__)
    
    宏技巧解析:
    1. 字符串化操作
      • #level 将日志等级转为字符串(INFO → “INFO”)
      • 避免手动输入字符串导致错误
    2. 预定义宏
      • __FILE__:获取当前源文件名
      • __LINE__:获取当前代码行号
      • 自动捕获代码位置信息
    3. 用户友好接口
      • 简化调用: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
    

    技术亮点

    1. 性能优化
      • 内存操作:使用字符串拼接代替多次I/O
      • 缓冲控制:不强制刷新(std::endl),由用户控制
      • 内联函数:消除调用开销
    2. 元数据自动捕获
      • 通过预处理器宏自动获取文件名和行号
      • 时间戳通过工具类动态获取
      • 日志等级自动转为字符串
    3. 扩展性
      • 轻松添加新日志等级(只需在枚举添加)
      • 支持与其他流输出结合使用
    4. 跨平台
      • 基于标准C++实现
      • 依赖极少的系统功能
  2. 工具类
    • 整体结构

      代码位于 ns_util 命名空间下,包含四个主要工具类:

      • TimeUtil - 时间处理工具
      • PathUtil - 文件路径处理工具
      • FileUtil - 文件操作工具
      • StringUtil - 字符串处理工具
    1. 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);}
    };
    

    功能说明:

    1. GetTimeStamp():
      • 使用 gettimeofday 系统调用获取当前时间
      • 返回从1970年1月1日0时至今的秒数(字符串格式)
    2. GetTimeMs():
      • 同样基于 gettimeofday 系统调用
      • 返回从1970年1月1日0时至今的毫秒数(字符串格式)
      • 计算方式:(秒数 × 1000) + (微秒数 ÷ 1000)

    使用场景:

    • 日志时间戳
    • 性能测量
    • 唯一ID生成基准
    1. 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");}
    };
    

    核心设计:

    1. 统一的临时文件目录:./temp/
    2. 基于文件基本名 + 后缀的统一路径构建
    3. 使用点语法简化各类文件的路径获取

    文件类型说明:

    方法名后缀用途
    Src.cppC++源代码文件
    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"
    
    1. 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)

    1. 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生效)
    

    系统依赖说明

    1. Linux系统调用:
      • gettimeofday (获取高精度时间)
      • stat (文件状态检查)
    2. 第三方依赖:
      • Boost库 (仅用于字符串分割)

    整体设计特点

    1. 实用主义:每个类聚焦解决特定问题
    2. 静态方法:所有功能无需实例化即可使用
    3. 原子操作:唯一文件名生成保证线程安全
    4. 路径抽象:统一管理临时文件位置
    5. 可选参数:提供灵活控制(如换行符保留)

    典型使用场景

    1. 编译系统
    // 生成唯一文件名
    std::string filename = FileUtil::UniqFileName();// 写入源码
    FileUtil::WriteFile(PathUtil::Src(filename), user_code);// 检查可执行文件是否存在
    if (FileUtil::IsFileExists(PathUtil::Exe(filename))) {// 编译成功处理
    }
    
    1. 日志处理
    // 带时间戳的日志
    std::string log_entry = "[" + TimeUtil::GetTimeMs() + "] " + message;// 错误信息分割
    std::vector<std::string> error_lines;
    StringUtil::SplitString(compiler_output, &error_lines, "\n");
    
    1. 运行环境隔离
    // 重定向运行环境
    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模板引擎来动态渲染题目数据。以下是该模板的详细分析:

整体结构设计

image-20250701183222002

核心功能区域
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>

表格设计特点:

  1. 简洁表头
    • 编号、标题、难度三列
    • 清晰标识信息类型
  2. 动态行渲染
    • 使用CTemplate模板语法
    • {{#question_list}}标记循环开始
    • {{number}}{{title}}{{star}}占位符填充数据
  3. 题目链接
    • 标题可点击跳转到题目详情
    • 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}}变量替换题目难度占位符
动态渲染流程

image-20250701183443356

问题详情模板

这个模板是OJ系统的题目详情页面,集成了题目展示、代码编辑和提交功能,使用了ACE代码编辑器提供专业的编程体验。下面我将详细解析这个模板的设计和实现:

整体结构设计

image-20250701183729911

核心功能区域
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
});

核心功能:

  1. 主题设置:Monokai暗色主题
  2. 语言支持:C/C++语法高亮
  3. 编辑体验
    • 字体大小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);}});
}

流程说明:

  1. 获取编辑器中的代码内容
  2. 提取题目编号
  3. 构造JSON格式请求
  4. 发送到判题接口 /judge/题目编号
  5. 处理返回结果
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 …”
数据渲染流程

image-20250701183848798

编译运行模块的设计

编译模块结构

编译服务流程

image-20250701160046548

编译类的设计

这个编译类(Compiler)是一个静态工具类,用于将C++源代码编译成可执行文件。下面我将详细分析其设计、实现和使用方式。

类概述
基本特性
  • 位于 ns_compiler 命名空间
  • 工具类设计(所有方法为静态)
  • 核心方法:static bool Compile(const std::string &file_name)
  • 无状态类(构造函数和析构函数为空)
依赖组件
  1. 路径工具ns_util::PathUtil
  2. 文件工具ns_util::FileUtil
  3. 日志系统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);
}
  • 关键步骤说明:
  1. umask(0)

    • 重置文件权限掩码
    • 确保新创建文件获得0644权限(rw-r–r–)
  2. 错误文件创建

    • 使用 open() 系统调用创建错误日志文件
    • 路径:PathUtil::CompilerError(file_name)
    • 标志:O_CREAT | O_WRONLY(创建+只写)
    • 权限:0644(rw-r–r–)
  3. 错误重定向

    • dup2(_stderr, 2) 将标准错误(fd 2)重定向到错误文件
    • 编译器输出的所有错误信息都会被写入该文件
  4. 编译器调用

    • execlp("g++", ...) 执行g++编译器

    关键参数

    • -o [output]:指定输出文件路径(PathUtil::Exe(file_name))

    • [input]:输入源文件路径(PathUtil::Src(file_name))

    • -D COMPILER_ONLINE:定义编译器在线标志(用户代码中可用)

    • -std=c++11:使用C++11标准

    • 必须参数nullptr 结束参数列表

  5. 错误处理

    • 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;
  • 关键步骤说明:
  1. 进程等待
    • waitpid(pid, nullptr, 0) 阻塞等待子进程结束
    • 不关心子进程退出状态(第二参数为 nullptr
  2. 结果验证
    • 使用 FileUtil::IsFileExists() 检查可执行文件是否存在
    • 路径:PathUtil::Exe(file_name)
  3. 日志反馈
    • 成功:记录包含源文件名的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);
参数解析:
  1. -o [output]

    • 指定输出文件名(可执行文件)
    • 示例:./temp/1234.exe
  2. [input]

    • 源文件路径
    • 示例:./temp/1234.cpp
  3. -D COMPILER_ONLINE

    • 定义预处理器宏 COMPILER_ONLINE

    使用场景

    #ifdef COMPILER_ONLINE
    // 仅在线编译环境执行的代码
    #else
    // 本地开发环境执行的代码
    #endif
    
    • 可增强安全性(限制特定功能)
  4. -std=c++11

    • 强制使用C++11标准
    • 确保语法兼容性
安全设计考虑
  1. 进程隔离
    • 子进程执行编译操作
    • 防止编译器崩溃影响主进程
  2. 错误隔离
    • 标准错误重定向到独立文件
    • 不会污染主进程输出
  3. 权限控制
    • umask(0) 确保文件正确权限
    • 错误文件仅开放写权限
  4. 资源限制
    • 父进程等待时间不限
    • 实际使用中可能需要添加超时控制
使用示例
#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: 执行程序并监控资源使用
  • 无状态设计(构造函数和析构函数为空)
关键依赖
  1. 路径工具ns_util::PathUtil
  2. 文件工具ns_util::FileUtil
  3. 日志系统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);
}
功能详解:
  1. CPU限制

    • 使用 RLIMIT_CPU 资源限制类型
    • rlim_cur: 软限制(实际限制值)
    • rlim_max: 硬限制(设为无限)
    • 单位:秒(进程允许的最大CPU时间)
  2. 内存限制

    • 使用 RLIMIT_AS 限制进程地址空间大小
    • _mem_limit * 1024: 将KB转换为字节
    • 限制整个进程的虚拟内存
  3. 资源限制类型

    限制类型描述
    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);
}

关键操作:

  1. IO重定向
    • 使用 dup2 重定向标准输入/输出/错误
    • 文件描述符说明:
      • 0: 标准输入
      • 1: 标准输出
      • 2: 标准错误
  2. 资源限制
    • 调用 SetProcLimit 设置CPU和内存限制
  3. 程序执行
    • 使用 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;
}

关键操作:

  1. 资源清理

    • 父进程关闭不再需要的文件描述符
  2. 进程等待

    • waitpid 等待子进程结束
    • status: 存储子进程的退出状态
  3. 状态解码

    • status & 0x7F: 提取低7位信号编号

    • Linux进程状态编码规则:

      status (int) = [退出状态 (8位) | 信号编号 (7位) | coredump标志 (1位)]
      
状态码解释
返回值意义典型场景
>0进程被信号终止 (信号编号)SIGSEGV(11), SIGALRM(14)
0正常终止程序运行结束
-1文件打开失败权限问题/路径错误
-2创建子进程失败系统资源耗尽
异常处理机制
CPU超限处理
  • 当程序超过CPU时间限制时:
    1. 内核向进程发送 SIGXCPU 信号
    2. 如果进程不捕获该信号,将被终止
    3. 父进程收到信号编号 SIGXCPU (24)
内存超限处理
  • 当程序超过内存限制时:
    1. 内存分配操作失败
    2. 进程可能因非法内存访问收到 SIGSEGV (11)
    3. 或因堆栈溢出收到 SIGSTKFLT (16)
其他信号处理
信号编号原因
SIGFPE8算术异常
SIGABRT6程序主动退出
SIGKILL9强制终止
SIGTERM15终止请求
使用示例
#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)等场景。

核心功能
  1. 流程整合:串联编译和运行过程
  2. 资源管理:自动清理临时文件
  3. 结果处理:转换状态码为可读描述
  4. 数据交换:JSON格式输入输出
类依赖关系

image-20250701170117460

核心方法详解
临时文件清理 (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": "错误输出"   // 可选
}
详细步骤:
  1. 解析输入

    Json::Reader reader;
    reader.parse(in_json, in_value);
    std::string code = in_value["code"].asString();
    
  2. 基础检查

    • 代码为空立即返回错误

      if (code.size() == 0) {status_code = -1;goto END;
      }
      
  3. 文件准备

    • 生成唯一文件名:FileUtil::UniqFileName()
    • 写入源码文件:FileUtil::WriteFile(PathUtil::Src(file_name), code)
  4. 编译阶段

    if (!Compiler::Compile(file_name)) {status_code = -3; // 编译错误goto END;
    }
    
  5. 运行阶段

    run_result = Runner::Run(file_name, cpu_limit, mem_limit);
    
  6. 结果处理

    • 成功时收集输出:

      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);
      
  7. 资源清理

    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库实现,主要功能是接收客户端提交的代码,执行编译运行操作,并将结果返回给客户端。

整体架构

image-20250701171035296

核心功能
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");
}
  1. 获取请求体:HTTP POST 请求的正文包含 JSON 数据

  2. 调用服务:交给 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)的核心控制器,负责处理用户请求、路由到对应的处理逻辑,并管理整个系统的运行状态。以下是该系统的详细设计分析:

整体架构

image-20250701174749027

核心组件设计
信号处理与恢复机制
static Control *ctrl_ptr = nullptr;void Recovery(int signo) {ctrl_ptr->RecoveryMachine();
}int main() {signal(SIGQUIT, Recovery); // 注册信号处理函数// ...
}
  • 信号机制:使用 SIGQUIT 触发系统恢复

  • 设计目的:在系统异常时恢复机器状态

实现方式

  1. 注册信号处理函数
  2. 保存控制类实例指针
  3. 调用控制类的恢复方法
控制中心 (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();
};
路由设计
  1. 获取所有题目列表 (/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内容
  • 包含题目编号、标题、难度等信息
  1. 获取单个题目详情 (/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
    • 提供代码编辑器区域
  1. 判题服务 (/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数据
  • 设计特点:
    • 与编译服务模块协同工作
    • 返回详细的评测信息
请求处理流程
用户访问题目列表

image-20250701175135562

用户查看题目详情

image-20250701175159340

用户提交代码

image-20250701175213760

异常恢复机制
恢复流程

image-20250701175358377

设计特点:
  1. 手动触发:管理员通过发送信号主动恢复
  2. 状态重建:重置判题服务组件
  3. 资源清理:释放可能泄漏的资源
  4. 服务恢复:重启失败的服务进程
安全设计
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)的数据核心。下面是对该设计的详细解析:

整体架构设计

image-20250701175857461

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. 题库查询接口

  1. 获取所有题目列表
bool GetAllQuestions(vector<Question> *out) {if(questions.empty()) {LOG(ERROR) << "题库为空,获取题目列表失败";return false;}for(const auto &pair : questions) {out->push_back(pair.second);}return true;
}
  1. 获取单个题目详情
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
编译节点2
编译节点N
编译运行
结果返回
核心组件详解
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(); // 获取负载
};

设计特点:

  1. 负载计数:原子操作记录节点当前任务数
  2. 线程安全:通过mutex保护负载状态变更
  3. 状态隔离:每个节点独立维护自身状态
  4. 轻量化:仅存储必要网络标识符
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(智能选择)
  1. 锁定资源:加锁保护节点状态
  2. 检查可用性:确认存在在线节点
  3. 寻找最小负载节点
    *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]];}
    }
    
  4. 返回最优节点
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 方法)
User Control Model LoadBalancer CompileNode CompileService 提交代码和输入(JSON) 获取题目详情(number) 返回题目数据(q) 拼接代码: user_code + q.tail 请求可用节点(SmartChoice) 返回节点ID及地址 通过HTTP提交编译请求(Post) 执行编译运行 返回结果 返回结果JSON 减少节点负载(DecLoad) 离线故障节点(OfflineMachine) alt [请求成功] [请求失败] loop [节点选择] 返回评测结果 User Control Model LoadBalancer CompileNode CompileService
详细步骤:
  1. 获取题目信息

    struct Question q;
    model_.GetOneQuestion(number, &q);
    
  2. 构造编译请求

    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;
    
  3. 智能选择节点

    int id = 0;
    Machine *m = nullptr;
    load_blance_.SmartChoice(&id, &m);
    
  4. 发送编译请求

    Client cli(m->ip, m->port);
    m->IncLoad();
    auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8");
    
  5. 结果处理

    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. 健壮性设计
  1. 错误隔离

    • 单节点故障不影响系统整体
    • 请求失败自动切换节点
  2. 线程安全

    • 节点负载变更使用互斥锁
    • 节点状态变更加锁保护
  3. 故障恢复

    • 管理员触发的节点恢复
    • 日志记录辅助故障排查
3. 资源管理
  1. 精准调度

    • 选择负载最低节点
    • 动态调整节点权重
  2. 资源释放

    • 任务完成释放节点资源
    • 超时任务强制回收资源
配置文件格式

service_machine.conf 示例:

192.168.1.101:8080
192.168.1.102:8080
192.168.1.103:8080
典型使用场景
1. 正常请求处理
用户请求判题
选择节点101
执行成功
返回结果
2. 故障处理场景
用户请求判题
选择节点101
请求失败?
标记101离线
选择节点102
执行成功
返回结果
3. 恢复机制
管理员发送恢复信号
恢复所有离线节点
节点101重新上线
节点...重新上线
view类的设计

View类负责将数据模型渲染成HTML页面,是MVC架构中的视图层核心组件。它使用Google的CTemplate库实现了高效的模板渲染机制。

类设计概览
View
- string template_path
+AllExpandHtml()
+OneExpandHtml()
核心特性:
  1. 模板驱动:基于HTML模板实现渲染
  2. 数据绑定:动态填充题目数据到模板
  3. 模板复用:相同模板结构重复使用
  4. 路径配置:集中管理模板位置
实现细节
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)

功能:

  • 数据容器:存储要填充到模板的数据
  • 作用域管理:支持嵌套的命名空间
  • 区块控制:管理重复区域的渲染

核心方法:

  1. 值设置

    SetValue("key", "value"); // 设置普通值
    
  2. 区块管理

    TemplateDictionary* sub = AddSectionDictionary("section_name");
    sub->SetValue("sub_key", "sub_value");
    
  3. 包含引用

    root.SetFilename("footer", "footer.html");
    
3. 模板加载选项
ctemplate::DO_NOT_STRIP
  • 保留空白和注释,便于调试
  • 生产环境建议使用STRIP_BLANK_LINES
设计亮点
1. 表现与逻辑分离

image-20250701182108449

  • 前端开发者:只关心HTML/CSS模板
  • 后端开发者:专注数据逻辑
  • 协作方式:通过模板变量约定接口
2. 高效渲染机制
  1. 模板预编译

    Template::GetTemplate() // 加载时编译模板
    
  2. 内存缓存

    • 避免重复IO读取模板文件
    • 自动缓存编译后的模板结构
  3. 快速渲染

    • 时间复杂度:O(n)
    • 线性扫描模板结构填充数据
3. 错误安全机制
  1. 模板加载检查

    if (tpl) tpl->Expand(...);
    else /* 处理错误 */
    
  2. 自动转义

    SetValue("safe_content", value);
    // CTemplate默认自动转义HTML特殊字符
    
  3. 缺失值处理

    • 未设置的变量渲染为空
    • 保留原始模板标记以便定位问题
http://www.lqws.cn/news/600589.html

相关文章:

  • Y-Combinator推导的Golang描述
  • Go语言的Map
  • 编写shell脚本扫描工具,扫描服务器开放了哪些端口(再尝试用python编写一个)
  • java web2(黑马)
  • 7.1_JAVA_其他
  • Excel
  • 【前端】vue工程环境配置
  • 洛谷P1379 八数码难题【A-star】
  • LangChain4j在Java企业应用中的实战指南-3
  • uniapp 中使用路由导航守卫,进行登录鉴权
  • css函数写个loading动画 | css预编译scss使用
  • MAC环境搭建SVN,并将TOMCAT集成到IDEA
  • 地震灾害的模拟
  • Springboot整合高德地图
  • filebeat收集日志到es
  • 大模型MCP技术之一句话安装Hadoop
  • 图神经网络(篇二)-基础知识
  • 安全左移(Shift Left Security):软件安全的演进之路
  • Badoo×亚矩云手机:社交约会革命的“云端心跳加速剂“
  • 计网学习笔记第1章 计算机网络体系结构(灰灰题库)
  • 微信小程序实现table表格
  • vue+three.js 加载模型,并让模型随航线飞行
  • 服务器种类与超融合
  • CSS 安装使用教程
  • mysql的自增id用完怎么办?
  • 【MobaXterm、Vim】使用合集1
  • 多容器应用与编排——AI教你学Docker
  • 单端输入转差分输出
  • ELK日志分析系统(filebeat+logstash+elasticsearch+kibana)
  • 学习字符串