【C++项目】从零搭建 C++ 在线判题系统:OJ 完整开发实战
目录
1.3 项目定位:轻量化可落地,而非复刻商业级 LeetCode
三、HTTP 接口标准化设计:浏览器与 C++ 服务通信全流程
四、MySQL 持久化与 Repository 分层:规避 SQL 注入,保障数据一致性
4.2 Repository 分层:预编译语句杜绝 SQL 注入
六、核心判题沙箱:Linux 系统编程核心实现(项目技术难点)
9.2 故障二:Nginx 访问出现 502 Bad Gateway
前言
很多学习 C++、Linux 服务器开发的同学都会陷入一个典型困境:语法、系统调用、数据库、网络编程分开学都能看懂,单独写几十行小 Demo 也毫无压力,但一旦要落地一套完整、可上线、具备业务闭环的工程项目,立刻无从下手。不知道如何拆分模块、如何做分层解耦、如何处理安全边界、如何兼顾功能与稳定性,更不知道怎么把零散知识点串联成一套可用系统。
我在系统学习完 C++17、Linux 进程 / 文件 / 管道系统编程、Socket 网络基础、MySQL 数据库操作后,选择从零自研一套轻量化在线判题 OJ 平台作为综合实战项目。整套系统覆盖前端交互页面、C++ HTTP 后端服务、分层业务架构、MySQL 数据持久化、Cookie+Session 权限鉴权、Linux 底层代码沙箱判题、自动化单元测试、Nginx 反向代理服务器部署全流程。它不是简化的玩具 Demo,而是拥有完整业务闭环、真实评测能力、完善异常处理、可直接部署在云服务器的工程项目,既能夯实底层系统编程能力,也可以作为 C++ 后端、服务器开发、Linux 系统方向面试核心项目履历。
本文完整记录项目从需求规划、架构设计、分层编码、核心沙箱实现、安全设计、测试编写到线上部署排障的全部过程,同时梳理开发过程中踩过的各类技术坑、设计取舍与工程化思考。
一、为什么选择自研一套 OJ 判题平台:技术价值与学习收益
1.1 单一知识点学习的局限性
初学编程阶段,我们接触的代码大多是碎片化代码片段:写一段排序算法、用 socket 实现简单客户端服务端通信、写 MySQL 增删改查、调用 fork 创建子进程。这些练习只能掌握单一技术点的基础用法,但缺失工程化思维:
- 不会基于业务需求拆分独立模块,代码高度耦合,一处改动牵动全局;
- 不懂分层隔离思想,网络处理、业务逻辑、数据库操作、底层系统调用全部堆在同一个文件,难以维护、难以测试;
- 缺少安全意识,不知道 SQL 注入、XSS、CSRF、越权访问、恶意代码执行等线上风险如何规避;
- 没有完整上线经验,不清楚服务后台常驻、反向代理、进程重启、日志排查、服务器故障定位等运维知识。
市面上的 CRUD 后台管理系统大多只涉及数据库与 HTTP 接口,几乎不触碰 Linux 底层系统调用;而单纯的网络通信 Demo 又没有完整业务和数据持久化逻辑。在线判题平台 OJ 恰好是少有的能同时串联网络编程、数据库、业务架构、操作系统底层、安全开发、服务部署六大核心方向的综合性项目,是打通前后端、应用层、系统层的绝佳实战载体。
1.2 OJ 平台完整业务链路覆盖
一套标准 OJ 平台天然分为两大角色、多层业务流程,每个环节对应不同技术考点: 普通用户侧流程:注册账号→登录鉴权→浏览题库列表→查看单题完整题面、输入输出样例→在线编写 C++ 源码提交评测→等待后端编译执行→查看评测结果(通过 / 答案错误 / 编译失败 / 超时 / 内存超限等); 管理员侧流程:登录后台→新增 / 编辑 / 删除题目→配置多组测试用例(公开样例 + 隐藏评测数据)→管理所有用户账号; 后端服务核心链路:接收前端 HTTP 请求→身份权限校验→数据库读写持久化数据→调用沙箱模块编译隔离执行用户不可信代码→资源限制与输出比对→封装统一 JSON 结果返回前端; 底层沙箱核心能力:隔离恶意用户代码、限制 CPU 运行时长、限制内存占用、限制输出字符长度、捕获程序崩溃、防止子进程逃逸、自动清理临时编译文件。
1.3 项目定位:轻量化可落地,而非复刻商业级 LeetCode
在项目立项之初,我就明确了边界,避免无限堆砌功能导致工期失控:本项目目标不是复刻 LeetCode、洛谷等商业 OJ 平台完整生态,不做排行榜、积分、题解社区、在线代码高亮插件、多语言批量评测、分布式判题集群等高复杂度功能。 核心目标是搭建一套最小可用、逻辑完整、模块可拆解、每一段代码都能对应面试考点的单机版 C++ OJ,满足三点核心标准:
- 真实可运行:用户提交源码后,真实调用系统 g++ 编译器编译,通过 Linux 子进程隔离执行,依据程序输出精准给出各类评测状态;
- 逻辑可解释:从前端请求到底层系统调用的每一层代码都有明确设计目的,每一个模块都能对应后端面试高频问题;
- 架构可扩展:分层架构保证后续新增功能、更换存储、拓展多语言评测时,无需大规模重构核心代码。
完成这套项目后,既能熟练掌握 C++ 高性能后端开发规范,又能吃透 Linux 进程、资源限制、管道通信等系统编程核心知识,同时积累 Web 服务、数据库安全、线上部署排障完整工程经验。
二、需求拆解与分层架构设计:先落地最小业务闭环
2.1 避坑:拒绝前期堆砌非核心功能
很多新手做项目最容易犯的致命错误:项目刚启动,就规划大量锦上添花的附加功能,比如用户积分、提交记录分页、评论区、深色模式、代码语法高亮、排行榜、消息通知等。大量非核心需求会分散开发精力,导致核心判题逻辑迟迟无法完成,项目极易半途而废。
正确的开发思路是先搭建最小业务闭环,只保留支撑平台运转的核心流程,闭环跑通后,再迭代新增拓展功能。本项目最小闭环仅包含 4 个核心流程:
- 管理员登录后台,创建题目,录入多组测试用例与预期输出;
- 普通用户完成注册、登录,无权限访问管理员后台接口;
- 用户在线查看题目,提交自定义 C++ 源代码;
- 后端完成源码编译、沙箱隔离运行、输出比对,返回标准化评测结果。
围绕这套闭环,完成基础版本开发、全流程测试、服务器部署后,再逐步迭代前端美化、提交记录、分页查询、日志打印等附加功能,保证项目始终有可演示、可运行的版本,不会长期停留在半成品状态。
2.2 五层分层架构:实现模块解耦与灵活拓展
为了隔离不同功能的代码,降低模块之间的耦合度,方便后续迭代替换底层依赖,我将整套系统自上而下划分为五层架构,每层职责严格划分,禁止跨层直接调用:
- 前端展示层:原生 HTML+CSS+JavaScript 页面,负责渲染页面、收集用户输入、发起 HTTP 请求、解析后端返回 JSON 并展示结果;
- HTTP Handler 请求处理层:基于 cpp-httplib 实现接口路由,负责解析 HTTP 请求、Cookie、JSON 请求体,完成基础参数校验,调用对应业务层接口,统一封装响应数据;
- Service 业务逻辑层:承载全部业务语义,封装 “创建题目”“用户登录校验”“提交代码评测” 等完整业务流程,不直接操作数据库、不调用底层编译执行命令;
- Repository 数据访问层:封装 MySQL 数据库所有操作,使用预编译语句执行 SQL,对外提供增删改查接口,屏蔽底层数据库细节;
- Judge&Runner 判题沙箱层:项目最核心底层模块,负责源码写入临时文件、g++ 编译、fork 创建隔离子进程、管道 IO 通信、setrlimit 资源限制、程序输出比对、临时文件清理。
分层架构带来三大核心优势,也是面试中可以重点阐述的设计亮点: 第一,底层依赖可无缝替换。如果后续需要把 MySQL 更换为 SQLite、PostgreSQL,仅需要修改 Repository 层代码,上层 Service、Handler、前端完全不需要改动;如果后续拓展支持 Python、Java 代码评测,仅需要新增 Runner 执行器,题库、用户、鉴权逻辑全部复用; 第二,单元测试更简单。分层隔离后,可以单独对 Repository 层做数据库测试、单独对 Judge 沙箱做执行测试、单独对 Service 做业务逻辑测试,不需要启动完整 HTTP 服务; 第三,代码可读性更高。每一层代码职责单一,Handler 只处理网络请求,Service 只处理业务规则,沙箱只处理程序执行,不会出现一个文件同时包含网络、数据库、系统调用的混乱代码。
2.3 完整技术栈选型说明
整套项目全部基于 C++17 标准开发,配套工具、第三方库、中间件、前端技术选型兼顾轻量化与实用性,避免引入过重框架增加学习成本:
- 编译构建工具:CMake,跨平台统一构建脚本,支持 Windows 本地调试、Linux 服务器编译;
- HTTP 服务框架:cpp-httplib,轻量级单头文件 C++ HTTP 服务库,无需复杂编译配置,快速搭建 RESTful 接口;
- JSON 处理库:nlohmann/json,零依赖 JSON 解析序列化库,前后端数据交互统一使用 JSON 格式;
- 数据库:MySQL 8.0,搭配 MySQL C API 作为底层驱动,持久化所有业务数据;
- 单元测试框架:Google Test(GTest),覆盖正常流程、异常边界、权限拦截、沙箱执行全场景自动化测试;
- 前端技术:原生 HTML5 + CSS3 + 原生 JavaScript,不引入 Vue、React 等前端框架。前期开发重心放在后端底层逻辑,原生 JS 可以直观理解浏览器 Cookie、AJAX 请求、跨域等基础 Web 原理;
- 服务器部署:Nginx 反向代理、Linux systemd 服务、nohup 临时后台启动工具。
整套技术栈没有复杂重型中间件,全部是后端开发基础常用工具,既能快速上手开发,也完全贴合校招、社招 C++ 后端岗位技术考察范围。
三、HTTP 接口标准化设计:浏览器与 C++ 服务通信全流程
3.1 核心 RESTful 接口规划
浏览器无法直接调用 C++ 函数,前后端所有交互都依靠标准 HTTP 请求完成,遵循 RESTful 设计风格,区分 GET 查询、POST 提交修改两类请求,核心接口清单如下:
GET /api/problems:无参接口,查询全部题目列表,返回题目 ID、标题、难度标签;GET /api/problems/{id}:路径参数 id 为题目编号,查询单题完整信息,包含题面、输入输出描述、样例输入输出;POST /api/register:用户注册接口,请求体携带用户名、明文密码,后端加盐哈希存储密码;POST /api/login:用户登录接口,校验账号密码,登录成功下发登录 Cookie;POST /api/logout:退出登录接口,销毁服务端 Session,清除浏览器登录 Cookie;POST /api/problems/{id}/submit:代码提交评测接口,请求体携带用户编写的 C++ 源码,返回完整评测结果;- 管理员专属接口:
POST /api/admin/problem/add、PUT /api/admin/problem/{id}、DELETE /api/admin/problem/{id},仅管理员账号可访问,用于题库维护。
3.2 Handler 层职责与代码规范
Handler 层是网络请求与业务逻辑的中间桥梁,严格限定职责边界,只允许完成四类操作:
- 解析 HTTP 基础信息:请求方法、请求路径、请求头 Cookie、POST 请求 JSON 请求体;
- 参数合法性基础校验:判断用户名是否为空、源码是否过长、题目 ID 是否为数字、未登录用户拦截提交请求;
- 调用对应 Service 业务接口,传递解析完成后的参数;
- 统一封装 JSON 响应,设置 HTTP 状态码,返回给前端。
强制规范:Handler 层禁止直接拼接 SQL 语句操作数据库、禁止直接调用 g++ 命令编译代码、禁止调用 fork/exec 等底层系统调用。所有底层操作全部下沉至 Repository 和 Judge 层,彻底隔离网络逻辑与底层系统逻辑。
这种隔离规范的工程价值十分明显:当我们需要对沙箱编译逻辑重构、或者更换数据库时,不需要修改任何接口 Handler 代码,不会影响前端请求链路;同时单独测试 HTTP 接口时,可以通过 Mock 模拟 Service 返回结果,不用启动完整数据库与沙箱环境。
3.3 统一 JSON 返回结构与分层错误设计
所有接口无论成功失败,统一返回相同 JSON 数据结构,前端可以编写一套通用逻辑解析所有接口返回,无需针对不同接口单独处理数据:
{
"code": 0,
"data": {
"judge_status": "Accepted",
"pass_case": 3,
"total_case": 3
},
"message": "评测完成"
}
字段含义说明:
- code:业务状态码,0 代表业务执行成功,非 0 数字代表各类业务异常(1001 未登录、1002 用户名已存在、1003 题目不存在、2001 编译失败等);
- data:业务返回数据,无返回数据时为空对象;
- message:可读中文提示信息,用于前端弹窗展示错误原因。
同时区分两层错误体系,避免业务错误与协议错误混淆,这是面试高频加分设计点:
- HTTP 标准状态码:负责表达网络、协议层面异常,401 未登录、400 请求参数非法、404 接口 / 资源不存在、409 用户名冲突、403 无管理员权限;
- JSON 内部 code+message:承载细化业务场景错误,比如编译语法错误、程序运行超时、内存超限等业务独有的异常信息。
两者各司其职,互不替代。例如普通用户访问管理员新增题目接口,HTTP 返回 403,JSON code 返回 1004,message 提示 “当前账号无管理员操作权限”,前端可以根据 HTTP 状态码做统一拦截弹窗,再根据 message 展示精准错误描述。
四、MySQL 持久化与 Repository 分层:规避 SQL 注入,保障数据一致性
4.1 核心数据表结构设计与关联关系
平台所有业务数据全部持久化存储在 MySQL 中,设计四张核心数据表,通过外键建立关联,利用数据库原生约束保障数据一致性:
-
problems 题目表 存储字段:自增主键 id、题目标题 title、难度 difficulty、完整题面 content、输入描述 input_desc、输出描述 output_desc、创建时间 create_time。 存储所有题目基础信息,是整个题库系统的核心主表。
-
test_cases 测试用例表 存储字段:自增主键 id、关联题目 id problem_id、输入数据 input、预期输出 expect_output、是否公开样例 is_sample、用例排序序号 sort。 通过 problem_id 外键关联 problems.id,设置级联删除规则
ON DELETE CASCADE。该规则作用为:管理员删除某道题目时,MySQL 会自动删除该题目全部测试用例,不会产生无归属的孤儿测试数据,将数据一致性交给数据库底层约束,减少业务层额外判断逻辑。 -
users 用户表 存储字段:自增主键 id、用户名 username、加盐密码哈希 password_hash、用户角色 role(0 普通用户,1 管理员)、注册时间 register_time。 绝不存储明文密码,用户注册时后端生成随机盐值,使用哈希算法加密后存入数据库,登录时仅比对哈希值,即使数据库泄露也无法还原用户原始密码。前端提交注册表单时,禁止传递 role 字段,用户角色完全由后端代码控制,杜绝前端篡改权限。
-
sessions 登录会话表 存储字段:自增主键 id、随机会话 token、关联用户 id user_id、会话过期时间 expire_time。 每一次用户登录都会生成 32 位高随机度 Token 写入该表,退出登录或会话过期后删除对应记录,用于实现服务端 Session 登录鉴权。
4.2 Repository 分层:预编译语句杜绝 SQL 注入
如果直接在 Service 层拼接 SQL 字符串,会产生严重的 SQL 注入安全漏洞,同时字符串拼接会频繁处理引号、换行、特殊符号,极易出现语法错误。因此项目单独封装 Repository 数据访问层,统一管理所有数据库操作。
Repository 层两大核心设计: 第一,全程使用 MySQL 预编译语句(Prepared Statement),将 SQL 模板与业务数据完全分离。SQL 语句固定不变,用户输入的用户名、源码、测试数据全部作为绑定参数传入,数据库底层自动转义特殊字符,从根源杜绝 SQL 注入攻击,同时预编译语句可复用,提升频繁查询数据库的性能。 第二,对外仅提供语义化接口,屏蔽底层 SQL 细节。例如 Repository 提供 add_problem()、get_problem_by_id()、delete_problem() 等函数,上层 Service 调用时无需关心 SQL 如何编写,未来切换其他数据库存储,仅重写 Repository 层接口即可,上层业务代码完全不用修改。
4.3 题库数据维护与数据库运维经验
为方便项目演示与测试,我在数据库中预置 10 道入门经典算法题,覆盖基础输入输出、循环、数学、字符串、数组、二分查找等知识点:A+B 问题、三数求最大值、奇偶数字判断、连续整数求和、最大公约数计算、阶乘运算、字符串反转、回文字符串校验、数组升序排序、二分查找。每道题目至少配置 1 组公开样例用例、2-3 组隐藏评测用例,保证判题结果具备区分度。
开发过程中积累三条数据库运维实战经验,适合写入项目总结:
- 批量导入、修改测试数据前,必须先备份数据表,避免误操作丢失题库;
- 批量删除数据时限定 id 范围,禁止无限制清空整张表,防止误删正式业务数据;
- 批量操作完成后,通过
SELECT COUNT(*)统计数据总量,同时调用前端接口查询,双重校验数据是否符合预期,避免 SQL 执行成功但数据逻辑出错。
五、登录鉴权与权限管控:前端展示不能替代后端安全校验
5.1 用户注册与密码安全处理
用户注册流程存在多处安全控制点,全程由后端校验,不相信前端任何输入:
- 前端表单仅能提交用户名、明文密码两个字段,接口直接丢弃传入的 role 角色参数,所有新注册账号默认设置为普通用户;
- 后端校验用户名长度、密码复杂度,过滤空格、特殊危险字符;
- 生成随机盐值,将盐值与用户密码拼接后做哈希运算,哈希结果存入 users 表,原始密码、盐值不会长期保存在内存中;
- 查询 users 表判断用户名是否重复,重复则返回 409 冲突错误,阻止重复注册。
整套流程杜绝两种高危漏洞:前端篡改角色直接注册管理员、数据库明文密码泄露。
5.2 Session Cookie 鉴权流程与安全属性
用户输入正确账号密码登录后,后端生成 32 位高随机性 Session Token,写入 sessions 会话表,同时通过 HTTP 响应头下发登录 Cookie:
Set-Cookie: OJ_SESSION=随机Token字符串; Path=/; HttpOnly; SameSite=Lax
三个关键安全属性作用是面试高频考点:
- HttpOnly:禁止页面 JavaScript 脚本读取 Cookie 内容。即使网站存在 XSS 跨站脚本漏洞,攻击者也无法通过 JS 窃取用户登录凭证,大幅降低账号被盗风险;
- SameSite=Lax:限制跨站请求自动携带 Cookie,第三方网站发起 AJAX 请求时不会附带登录 Cookie,有效防御 CSRF 跨站请求伪造攻击;
- Path=/:Cookie 作用域为网站全部路径,访问题库、提交代码、管理后台均可自动携带登录凭证。
用户后续所有页面跳转、接口请求,浏览器会自动携带 OJ_SESSION Cookie,后端从请求头提取 Token,查询 sessions 表匹配有效会话,获取当前登录用户 ID 与角色。
5.3 双层鉴权函数:区分普通登录与管理员权限
项目封装两套独立鉴权工具函数,在对应接口入口强制调用,没有通过鉴权直接终止接口执行并返回错误:
require_current_user():仅校验存在有效未过期 Session,确认用户已登录。题库浏览、代码提交等普通用户接口使用;require_admin():双重校验,先判断用户已登录,再查询 users 表确认角色为管理员。新增、修改、删除题目等后台接口强制调用。
这里有一条非常关键的安全设计理念:前端页面隐藏管理后台入口,仅仅是优化用户交互体验,不属于安全防护手段。恶意用户可以手动在浏览器地址栏输入/admin.html,或者使用 Postman、curl 直接调用管理员专属 API,仅靠前端隐藏按钮完全无法阻挡越权操作。
所有权限判断逻辑必须放在后端服务中,无论前端如何伪造请求,接口入口的鉴权函数都会拦截无权限访问,返回 403 无权限错误,从服务底层杜绝越权漏洞。
5.4 退出登录完整逻辑
简单跳转首页无法彻底销毁登录状态,完善的登出流程分为两步:
- 后端接收登出请求,根据当前 Cookie 中的 Token,删除 sessions 表内对应会话记录,该 Token 永久失效;
- 返回新的 Set-Cookie 响应头,设置
Max-Age=0,覆盖浏览器本地缓存的登录 Cookie,浏览器本地 Cookie 立即失效。
双重销毁机制保证:即使浏览器缓存旧 Cookie,再次发起请求时,服务端会话表已无对应记录,鉴权直接失败,不存在缓存 Cookie 非法登录的问题。
六、核心判题沙箱:Linux 系统编程核心实现(项目技术难点)
用户提交的 C++ 源码属于不可信高危代码,存在大量恶意风险:死循环占用服务器 CPU、无限循环输出填满磁盘、超大内存申请耗尽服务器内存、fork 创建多层子进程逃逸管控、调用系统删除文件、网络请求攻击服务器等。绝对不能在 OJ 主服务进程中直接编译、运行用户代码,必须通过独立隔离子进程搭建轻量化沙箱。判题沙箱是本项目技术含金量最高、面试最有内容可讲的模块。
6.1 完整判题执行流程
单次代码评测标准化流程,每一步都依赖 Linux 系统调用:
- 接收前端传递的 C++ 源码字符串,在服务器临时目录创建唯一文件夹,写入 main.cpp 源码文件;
- 调用 g++ 编译器,以子进程形式编译源码,捕获编译标准错误输出,若编译报错直接返回 Compile Error;
- 编译成功生成可执行文件后,调用 fork () 创建全新子进程,父进程负责监控、计时、IO 通信、资源回收,子进程专门运行用户程序;
- 父进程创建管道,向子进程标准输入写入单组测试用例输入数据,同时读取子进程标准输出、标准错误;
- 子进程调用 setrlimit 设置 CPU、内存资源硬限制,再调用 exec () 替换自身地址空间,执行用户编译后的可执行程序;
- 父进程使用 waitpid 阻塞等待子进程结束,获取程序退出状态、运行耗时,判断是否触发超时、内存超限;
- 将子进程输出与测试用例预期输出比对,剔除末尾空白换行符,判定 AC/WA;
- 全部测试用例评测完成后,递归删除临时源码、可执行文件,释放服务器磁盘空间。
6.2 核心系统调用底层原理讲解
- fork ():复制当前进程,生成父子两个独立进程,父子进程拥有独立地址空间,用户代码运行在子进程,不会污染主服务进程内存、资源;
- exec 系列函数:完全替换子进程内存中的程序,销毁原有代码、变量,只保留文件描述符,专门用于执行第三方用户程序;
- pipe 管道:父子进程之间单向通信通道,实现父进程向子进程下发输入、读取输出,无需读写磁盘临时文件,提升评测效率;
- waitpid:阻塞等待指定子进程结束,获取退出码、信号终止状态,区分程序正常结束、崩溃、被系统杀死;
- setrlimit:设置进程资源限制,是轻量化沙箱核心,管控 CPU 时长、虚拟内存上限,防止恶意程序耗尽服务器资源。
6.3 资源限制与恶意程序拦截方案
通过 setrlimit 配置两类核心资源上限,同时限制管道输出长度:
- RLIMIT_CPU:限制进程最大 CPU 运行时间,超过阈值系统自动发送终止信号,判定为 TLE 时间超限;
- RLIMIT_AS:限制进程虚拟内存最大占用,用户代码申请内存超过上限会触发分配失败,判定为 MLE 内存超限;
- 输出管道读取设置字节上限,程序无限打印内容时管道截断输出,判定为 OLE 输出超限。
针对用户程序 fork 子进程逃逸管控的解决方案:创建子进程后设置进程组,超时后调用 kill 杀死整个进程组,而非仅杀死直接子进程。如果只销毁一级子进程,用户代码 fork 出的孙进程会脱离父进程管控,持续占用服务器资源,进程组可以一次性销毁所有衍生子进程。
6.4 输出比对规则与评测状态区分
项目支持 7 种标准 OJ 评测结果,覆盖所有常见场景:
- Compile Error CE:g++ 编译源码出现语法错误、头文件缺失、函数未定义;
- Runtime Error RE:程序运行中崩溃,空指针、数组越界、除零错误、段错误;
- Time Limit Exceeded TLE:程序运行超过 CPU 时间限制,死循环、低效暴力算法;
- Memory Limit Exceeded MLE:动态申请内存超出设定上限;
- Output Limit Exceeded OLE:程序打印输出字符总量超限;
- Wrong Answer WA:程序正常运行结束,但输出与预期结果不一致;
- Accepted AC:全部测试用例输出与预期匹配,无资源超限、无崩溃。
输出比对统一剔除每行末尾换行符、空格,例如10、10\n、10\n\n会判定为相同输出,贴合主流 OJ 评判规则;但字符串中间的空格、制表符严格区分,题目要求严格匹配格式时不会误判通过,规则可根据题目需求灵活调整。
6.5 技术边界说明(面试加分项)
需要客观说明:本项目 fork+exec+setrlimit 实现的沙箱仅为课程、单机项目级隔离方案,不满足线上生产环境安全标准。生产级代码沙箱还需要叠加多层防护:Linux Namespace 隔离 PID、挂载、网络、用户空间;seccomp 系统调用过滤,禁止用户程序调用危险系统函数;cgroup 精细化资源管控;只读文件系统,禁止用户程序读写服务器磁盘;使用低权限 nobody 用户执行代码;全量操作审计日志;容器化隔离等。
主动说明现有方案局限性与生产环境优化方向,远比宣称 “沙箱绝对安全” 更能体现严谨的工程思维,面试时面试官会重点认可这种技术取舍意识。
七、前端页面开发:从基础交互到完整平台界面
7.1 页面迭代规划
前端开发分为两个阶段:第一阶段仅实现基础功能页面,完成接口联调,保证业务闭环;第二阶段优化页面布局、交互反馈、样式美化,提升使用体验。最终完整页面包含:网站首页、题库列表页、题目详情 & 代码提交一体化页面、登录注册弹窗、管理员后台控制台。
页面布局参考主流 OJ 信息架构,布局逻辑清晰:顶部全局导航栏,区分题库、登录 / 注册、管理后台入口;左侧题目列表,展示标题、难度标签;右侧占据主要区域,分为 C++ 代码编辑器、提交按钮、评测结果展示面板,用户无需切换页面即可完成查看题目、编写代码、提交评测全流程。
7.2 前后端交互逻辑
题目详情一体化页面是前端核心页面,交互流程:
- 页面加载时通过 AJAX 请求
GET /api/problems/{id},拉取题面、样例数据渲染页面; - 用户在文本框编写 C++ 源码,点击提交按钮触发 AJAX POST 请求,携带源码提交评测;
- 后端同步返回评测结果,前端渲染通过用例数量、评测状态、错误提示;
- 若用户未登录,后端接口返回 401 状态码,前端弹窗提示跳转登录,不会伪造虚假 AC 结果。
所有前端交互完全依赖后端接口校验,不存在前端本地判断权限、本地模拟评测的逻辑,保证前后端逻辑统一。
7.3 前端开发踩坑:CSS 样式优先级问题
开发登录注册弹窗时遇到典型 UI Bug:登录弹窗与注册弹窗同时显示,隐藏样式失效。根因是样式加载顺序与 CSS 选择器权重冲突,控制弹窗隐藏的.is-hidden类设置display:none,但后续布局 grid 样式选择器权重更高,覆盖了隐藏规则。
标准解决方案:提升状态专用样式的选择器权重,精准限定作用 DOM 节点,代码示例:
.lc-auth-form.is-hidden {
display: none !important;
}
这个小问题带来工程思考:页面 UI 显示、弹窗显隐本质也是程序状态,和后端业务状态一样需要严谨设计、充分测试,不能随意编写临时样式,否则会出现不可预期的界面异常。
八、自动化测试:Google Test 覆盖全链路异常场景
只依靠手动测试无法保证系统稳定性,项目引入 Google Test 搭建自动化测试用例,覆盖从底层工具函数、数据库、业务逻辑、HTTP 接口到沙箱判题全链路,同时重点覆盖各类异常边界场景,提前发现隐藏 Bug。
8.1 分层测试策略
按照代码分层设计四类测试用例,兼顾执行速度与覆盖范围:
- 纯工具单元测试:无外部依赖,运行速度最快。测试密码哈希、输出空白符比对、字符串处理等纯逻辑函数;
- Repository 数据库测试:启动测试专用 MySQL 库,验证数据表增删改查、外键级联删除、用户名唯一约束;
- HTTP 接口集成测试:启动轻量 HTTP 服务,模拟 AJAX 请求,校验路由匹配、JSON 格式、Cookie 鉴权、权限拦截;
- Judge 沙箱专项测试:单独调用判题执行模块,分别提交正常代码、语法错误代码、死循环代码、超大内存申请代码,校验 AC/CE/TLE/MLE 等状态是否符合预期。
8.2 重点覆盖异常边界用例
测试的核心价值不是验证 “正常流程能跑通”,而是保证各类异常场景不会导致程序崩溃、逻辑错乱,核心测试场景清单:
- 用户模块:重复用户名注册、错误密码登录、空 Cookie 访问需要登录的接口、普通用户调用管理员接口;
- 题库模块:查询不存在的题目 ID、删除不存在的题目、测试用例为空提交评测;
- 沙箱判题模块:语法错误源码(预期 CE)、无限循环死循环(预期 TLE)、递归申请超大数组(预期 MLE)、除零崩溃程序(预期 RE)、输出超长字符串(预期 OLE)、正常 A+B 代码(预期 AC)。
自动化测试可以在每次修改代码后一键执行,快速定位修改引入的 Bug,是工程化项目必不可少的环节,面试时可以完整阐述分层测试思路,体现质量意识。
九、Linux 服务器部署与线上故障排查
本地开发调试完成后,需要部署至云 Linux 服务器对外提供访问,部署过程中遇到三类高频典型故障,整理标准化排查流程与解决方案。
9.1 故障一:重新编译代码,接口功能无更新
现象:修改后端代码,执行cmake --build build重新编译,刷新页面调用接口,功能、返回结果完全没有变化。 根因:cmake 编译仅生成新二进制可执行文件 build/oj_server,但服务器后台已经运行旧版本 oj_server 进程,端口被旧进程占用,新编译的程序并未启动,请求依旧由旧进程处理。 解决方案:先查询进程 PID,kill 终止原有服务进程,再重新启动二进制文件,新代码才会生效。
9.2 故障二:Nginx 访问出现 502 Bad Gateway
502 不代表后端程序崩溃,仅代表 Nginx 反向代理无法连接后端 HTTP 服务,标准化排查顺序:
- 使用 ps 命令查看 oj_server 进程是否正常运行;
- 通过
ss -ltnp查看端口监听状态,确认后端服务监听 8080 端口; - 在服务器本机执行
curl http://127.0.0.1:8080/api/problems,测试后端接口本地是否可访问; - 打开 Nginx 配置文件,核对 proxy_pass 转发地址、端口是否与后端一致;
- 查看 Nginx 错误日志,定位连接拒绝、端口错误等底层原因。
常见诱因:后端服务意外退出、Nginx 配置转发到错误端口、服务器防火墙拦截本地端口通信。
9.3 故障三:SSH 关闭终端,OJ 服务直接停止
直接在 SSH 终端运行./oj_server,断开 SSH 连接后进程被 SIGHUP 信号终止,服务下线。 临时解决方案:使用 nohup 后台启动,将日志输出至文件,关闭终端进程不会退出; 长期稳定方案:编写 systemd 系统服务配置文件,设置工作目录、自动启动命令、异常重启策略、日志持久化、启动依赖 MySQL 服务。配置完成后服务器重启可自动拉起 OJ 服务,运维操作标准化,避免手工启停服务带来的不稳定性。
十、项目整体收获、面试口述模板与后续迭代方向
10.1 项目开发核心能力提升
整套 OJ 项目完整走完从需求设计、编码开发、测试、线上部署全流程,彻底跳出碎片化学习模式,建立完整工程思维,核心收获分为四大维度:
- 架构设计能力:掌握分层解耦思想,能够根据业务需求拆分多层模块,隔离网络、业务、数据库、底层系统逻辑;
- Web 安全开发能力:吃透 Cookie+Session 鉴权、HttpOnly/SameSite 安全属性、SQL 预编译防注入、前后端权限分离、越权访问防护等安全机制;
- Linux 系统编程实战:熟练运用 fork、exec、pipe、waitpid、setrlimit 等核心系统调用,理解进程资源隔离、父子进程通信原理;
- 线上运维排障能力:积累服务重启、Nginx 反向代理、进程端口排查、后台常驻服务、日志定位问题等服务器运维实操经验。
不再局限于背诵 API、写零散 Demo,能够独立从零搭建一套具备完整业务闭环、可上线、可解释的后端工程项目。
10.2 面试标准口述模板(可直接背诵)
我独立从零开发了一套单机版 C++ 在线判题 OJ 平台,后端基于 cpp-httplib 搭建 RESTful HTTP 接口,整体采用 Handler-Service-Repository 三层业务分层 + 底层 Judge 沙箱五层架构,使用 MySQL 持久化存储题目、测试用例、用户账号与登录会话。 身份认证体系采用 HttpOnly Cookie 搭配服务端 Session 实现登录鉴权,区分普通用户与管理员双角色,所有后台修改操作均在后端做权限拦截,杜绝前端越权漏洞。 项目核心难点是 Linux 轻量化代码沙箱判题模块:接收用户 C++ 源码后写入临时文件,调用 g++ 编译,通过 fork 创建隔离子进程执行用户程序,利用管道完成父子进程 IO 交互,借助 setrlimit 限制 CPU、内存、输出资源,通过进程组杀死防止恶意子进程逃逸,能够精准区分 AC、WA、CE、RE、TLE、MLE、OLE 七种评测结果。 项目使用 Google Test 完成全链路自动化测试,覆盖正常流程与各类异常边界场景,最终通过 Nginx 反向代理部署至 Linux 云服务器,支持后台常驻自动重启。同时我也清晰区分了当前轻量化沙箱与生产级安全沙箱的技术差距,清楚 Namespace、seccomp、cgroup 等生产环境加固方案。
10.3 后续迭代拓展规划
当前基础版本功能完整,后续可按优先级迭代拓展功能,每新增功能都遵循 “梳理数据存储→设计权限校验→补充异常处理→新增自动化测试→适配部署流程” 的开发规范,不盲目堆砌功能:
- 业务拓展:用户提交记录存储、个人中心、题目标签分类、题库分页检索;
- 沙箱升级:基于 Linux Namespace、cgroup、seccomp 实现生产级安全隔离;
- 运维优化:完善 systemd 常驻服务、全链路分级日志、接口访问监控、异常告警;
- 功能拓展:支持多语言评测(C、Python)、在线代码语法高亮、批量导入题库。
结语
很多人学习 C++ 服务器开发,总觉得底层系统编程晦涩难懂、没有落地场景,这套自研 OJ 平台恰好打通了应用层 Web 开发与操作系统底层的壁垒。整套项目不依赖复杂第三方框架,所有核心逻辑均自主实现,每一段代码都对应后端开发、系统编程面试高频考点。
做项目的本质不是堆砌功能,而是在落地过程中不断思考架构取舍、安全边界、异常处理、线上稳定性。从只会写代码片段,到独立完成一套可部署、可维护、可讲解的完整工程项目,才是真正从入门走向工程开发的核心标志。

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)