C++ 从零实现负载均衡式在线 OJ 判题系统|分布式沙箱判题实战

一、项目前言

平时我们在 LeetCode、牛客、洛谷刷题时,最核心的体验就是:在线写代码、一键提交、即时判题。很多同学只知其用,不知其底层原理:服务器如何安全编译用户代码?如何防止恶意代码死循环、爆内存?高并发刷题场景下如何扛住压力?

为此,我基于 C++ + Linux 系统编程 + httplib + ctemplate + jsoncpp 从零手写实现了一套分布式负载均衡在线 OJ 判题系统。区别于传统单机 OJ,本项目实现了服务解耦、进程沙箱隔离、资源限制、分布式负载均衡、故障自动容灾等企业级核心能力,完整复刻了主流刷题平台的底层判题逻辑。

本文严格按照从零开发项目的编码顺序讲解代码,从底层工具逐步搭建到完整分布式服务,全程循序渐进,方便跟着源码复盘。

二、项目整体介绍

2.1 核心功能

  1. 题库页面渲染:动态展示全部题目列表、单题详情(题号、标题、难度、题目描述、代码模板)
  2. 在线代码判题:支持用户提交 C++ 代码,自动编译、运行、结果校验
  3. 安全沙箱隔离:进程级资源限制,杜绝死循环、内存溢出、恶意系统调用
  4. 分布式负载均衡:多台编译服务集群部署,自动选择空闲节点分发判题任务
  5. 故障自动容灾:编译节点故障自动下线,服务接收信号一键恢复所有节点
  6. 全流程日志监控:分级日志记录系统运行状态,快速定位编译、运行、网络异常

2.2 技术栈

  • 核心语言:C++11
  • 网络服务:cpp-httplib(轻量级 HTTP 服务,快速搭建接口)
  • 页面渲染:ctemplate(谷歌模板引擎,服务端动态渲染 HTML)
  • 数据交互:jsoncpp(前后端 JSON 序列化与解析)
  • 系统编程:Linux fork/exec、dup2 重定向、setrlimit 资源限制、信号处理
  • 工具依赖:Boost 字符串分割、原子变量、文件 IO、多线程互斥锁
  • 架构设计:MVC 分层架构 + 分布式微服务集群

2.3 整体架构

整套系统分为两大核心服务、多层业务模块,完全解耦,支持横向扩展:

  1. OJ 主服务(Web 服务):负责页面展示、路由分发、业务调度、负载均衡,对接前端与编译集群
  2. 编译运行子服务(集群节点):独立部署多实例,专注代码编译、沙箱运行、结果返回

开发编码顺序:公共工具层 → Model 数据层 → View 视图层 → Control 业务 & 负载均衡 → Web 主服务 → 编译沙箱 Compiler/Runner → CompileAndRun 总控 → 编译子服务入口

三、核心源码逐行深度讲解(按从零开发顺序)

3.1 第一步:开发公共底层工具 comm(所有模块依赖,最先写)

项目所有文件都会用到日志、文件、路径、字符串、时间工具,因此最先封装通用工具库,避免重复编码。

3.1.1 log.hpp 分级日志模块

日志用于全局打印运行信息、错误告警,封装宏简化调用,自动携带文件名、行号、时间戳。

#pragma once
#include <iostream>    
#include <string>      
#include "util.hpp"  

namespace ns_log
{
    using namespace ns_util;
    enum
    {
        INFO,    // 普通运行信息
        DEBUG,   // 开发调试信息
        WARNING, // 轻微警告,不阻断程序
        ERROR,   // 功能异常,但服务可继续运行
        FATAL    // 致命错误,程序无法正常工作
    };

    inline std::ostream &Log(const std::string &level, const std::string &file_name, int line)
    {
        std::string message = "[";
        message += level;       
        message += "][";
        message += file_name;   
        message += "][";
        message += std::to_string(line);  
        message += "][";
        message += TimeUtil::GetTimeStamp();
        message += "]";
        std::cout << message;
        return std::cout;
    }

    // 语法糖宏,简化日志调用 LOG(INFO) << "xxx";
    #define LOG(level) Log(#level, __FILE__, __LINE__)
}

代码解析

  1. 枚举区分 5 种日志等级,线上可按需屏蔽低等级日志;
  2. 内联函数自动拼接日志前缀:等级、文件、行号、时间戳,排错直观;
  3. __FILE__/__LINE__是编译器内置宏,无需手动传参,开发效率极高。
3.1.2 util.hpp 通用工具集(时间 / 路径 / 文件 / 字符串)
#pragma once
#include <iostream>
#include <string>       
#include <vector>      
#include <atomic>      
#include <fstream>     
#include <sys/time.h>   
#include <boost/algorithm/string.hpp>  

namespace ns_util
{
    // 时间工具:生成时间戳,用于临时文件名
    class TimeUtil
    {
    public:
        static std::string GetTimeStamp()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec);
        }
        static std::string GetTimeMs()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
        }
    };

    // 统一管理临时文件路径(编译运行产生的源码、exe、日志、标准流文件)
    const std::string temp_path = "./temp/";
    class PathUtil
    {
    public:
        static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
        {
            std::string path_name = temp_path;
            path_name += file_name;
            path_name += suffix;
            return path_name;
        }
        static std::string Src(const std::string &file_name) { return AddSuffix(file_name, ".cpp"); }
        static std::string Exe(const std::string &file_name) { return AddSuffix(file_name, ".exe"); }
        static std::string CompilerError(const std::string &file_name) { return AddSuffix(file_name, ".compile_error"); }
        static std::string Stdin(const std::string &file_name) { return AddSuffix(file_name, ".stdin"); }
        static std::string Stdout(const std::string &file_name) { return AddSuffix(file_name, ".stdout"); }
        static std::string Stderr(const std::string &file_name) { return AddSuffix(file_name, ".stderr"); }
    };

    // 文件工具:文件读写、判存在、生成唯一文件名
    class FileUtil
    {
    public:
        static bool IsFileExists(const std::string &path_name)
        {
            struct stat st;
            if (stat(path_name.c_str(), &st) == 0) return true;
            return false;
        }
        // 原子变量+毫秒时间戳生成全局唯一文件名,多用户并发不会覆盖文件
        static std::string UniqFileName()
        {
            static std::atomic_uint id(0);
            id++;
            std::string ms = TimeUtil::GetTimeMs();
            std::string uniq_id = std::to_string(id);
            return ms + "_" + uniq_id;
        }
        static bool WriteFile(const std::string &target, const std::string &content)
        {
            std::ofstream out(target);
            if (!out.is_open()) return false;
            out.write(content.c_str(), content.size());
            out.close();
            return true;
        }
        static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
        {
            (*content).clear();
            std::ifstream in(target);
            if (!in.is_open()) return false;
            std::string line;
            while (std::getline(in, line))
            {
                (*content) += line;
                if (keep) (*content) += "\n";
            }
            in.close();
            return true;
        }
    };

    // 字符串分割工具,解析配置文件
    class StringUtil
    {
    public:
        static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string &sep)
        {
            boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on);
        }
    };
}

代码解析

  1. TimeUtil:毫秒时间戳用于生成唯一临时文件,解决并发文件冲突;
  2. PathUtil:统一管理所有临时文件路径,后续修改存储目录只需改一处;
  3. FileUtil:封装文件读写、唯一性文件名,原子变量保证多线程自增无冲突;
  4. StringUtil:boost 分割字符串,自动合并连续分隔符,解析题库、集群配置文件容错性更强。

3.2 第二步:开发 Model 数据层 oj_model(数据源,工具写完后开发)

底层工具就绪后,搭建题库数据管理层,负责加载本地题目文件、内存存储题目,为上层业务提供数据。

#pragma once
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include <iostream>      
#include <string>      
#include <vector>        
#include <unordered_map>  
#include <fstream>       
#include <cstdlib>     
#include <cassert>     

namespace ns_model
{
    using namespace std;
    using namespace ns_log;    
    using namespace ns_util;  

    // 单个题目数据结构体,存储一道题完整信息
    struct Question
    {
        std::string number; // 题号
        std::string title;  // 标题
        std::string star;   // 难度
        int cpu_limit;      // CPU最大运行时间
        int mem_limit;      // 最大内存限制
        std::string desc;   // 题目描述
        std::string header; // 用户代码模板
        std::string tail;   // 后台测试用例代码
    };

    const std::string questins_list = "./questions/questions.list";
    const std::string questins_path = "./questions/";

    class Model
    {
    private:
        unordered_map<string, Question> questions; // 题号映射题目,O(1)查询
    public:
        // 构造函数启动自动加载全部题库
        Model()
        {
            assert(LoadQuestionList(questins_list));
        }
        ~Model(){}

        // 读取配置文件,加载所有题目到内存
        bool LoadQuestionList(const string &question_list)
        {
            ifstream in(question_list);
            if(!in.is_open()) 
            {
                LOG(FATAL) << " 加载题库失败,请检查是否存在题库文件" << "\n";
                return false;
            }
            string line;
            while(getline(in, line)) 
            {
                vector<string> tokens;
                StringUtil::SplitString(line, &tokens, " ");
                if(tokens.size() != 5)
                {
                    LOG(WARNING) << "加载部分题目失败, 请检查文件格式" << "\n";
                    continue; 
                }
                Question q;
                q.number = tokens[0];
                q.title = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = atoi(tokens[3].c_str());
                q.mem_limit = atoi(tokens[4].c_str());

                // 读取当前题目文件夹内的描述、模板、测试代码
                string path = 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) << "加载题库...成功!" << "\n";
            in.close();
            return true;
        }

        // 获取全部题目列表
        bool GetAllQuestions(vector<Question> *out)
        {
            if(questions.size() == 0)
            {
                LOG(ERROR) << "用户获取题库失败" << "\n";
                return false;
            }
            for(const auto &q : questions) out->push_back(q.second); 
            return true;
        }

        // 根据题号查询单道题目
        bool GetOneQuestion(const std::string &number, Question *q)
        {
            const auto& iter = questions.find(number);
            if(iter == questions.end()) 
            {
                LOG(ERROR) << "用户获取题目失败, 题目编号: " << number << "\n";
                return false;
            }
            (*q) = iter->second;
            return true;
        }
    };
}

代码解析

  1. Question 结构体统一封装题目所有属性,规范数据格式;
  2. unordered_map 存储题目,通过题号快速查询,相比 vector 遍历效率更高;
  3. 构造函数自动加载题库,服务启动一次性读取磁盘,后续请求直接读内存,性能更高;
  4. 分离配置文件questions.list和题目详情文件,增删题目无需修改代码。

3.3 第三步:开发 View 视图层 oj_view(页面渲染,数据层完成后开发)

有了题目数据,接下来实现页面渲染模块,基于 ctemplate 模板引擎,将数据填充 HTML 模板生成网页。

#pragma once
#include <iostream>       
#include <string>       
#include <ctemplate/template.h> 
#include "oj_model.hpp"

namespace ns_view
{
    using namespace ns_model;
    const std::string template_path = "./template_html/";

    class View
    {
    public:
        View(){}  
        ~View(){}
    public:
        // 渲染全部题目列表页面
        void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
        {
            std::string src_html = template_path + "all_questions.html";
            ctemplate::TemplateDictionary root("all_questions");
            // 循环填充列表数据
            for (const auto& q : questions)
            {
                ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
                sub->SetValue("number", q.number);
                sub->SetValue("title", q.title);
                sub->SetValue("star", q.star);
            }
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
            tpl->Expand(html, &root);
        }
        // 渲染单题详情页面
        void OneExpandHtml(const struct Question &q, std::string *html)
        {
            std::string src_html = template_path + "one_questions.html";
            ctemplate::TemplateDictionary root("one_questions");
            root.SetValue("number", q.number);   
            root.SetValue("title", q.title);    
            root.SetValue("star", q.star);     
            root.SetValue("desc", q.desc);      
            root.SetValue("pre_code", q.header); 
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
            tpl->Expand(html, &root);
        }
    };
}

代码解析

  1. 分离模板文件与业务代码,修改页面样式无需改动 C++;
  2. AddSectionDictionary实现模板循环渲染,自动生成多条题目条目,不用手动拼接字符串;
  3. 对外提供两个接口,分别对应题库列表、单题详情,职责单一。

3.4 第四步:开发 Control 控制层(负载均衡 + 业务调度,M/V 完成后开发)

MVC 三层最后一层 Control,串联 Model、View,同时实现分布式负载均衡模块,是整个系统业务中枢。

3.4.1 Machine 机器负载类(负载均衡基础单元)

cpp

class Machine
{
public:
    std::string ip;        // 编译服务器IP
    int port;              // 端口
    uint64_t load;         // 当前任务负载
    std::mutex *mtx;       // 单机互斥锁
public:
    Machine() : ip(""), port(0), load(0), mtx(nullptr){}
    ~Machine(){}
    // 任务增加,负载+1
    void IncLoad()
    {
        if (mtx) mtx->lock();  
        ++load;                
        if (mtx) mtx->unlock();
    }
    // 任务完成,负载-1
    void DecLoad()
    {
        if (mtx) mtx->lock();  
        --load;                
        if (mtx) mtx->unlock();
    }
    // 重置负载(机器恢复上线使用)
    void ResetLoad()
    {
        if(mtx) mtx->lock();   
        load = 0;              
        if(mtx) mtx->unlock(); 
    }
    // 线程安全获取当前负载
    uint64_t Load()
    {
        uint64_t _load = 0;
        if (mtx) mtx->lock();  
        _load = load;         
        if (mtx) mtx->unlock();
        return _load;
    }
};

解析:每台编译机器独立互斥锁,多线程并发修改负载计数无竞争,记录当前正在处理的判题任务数量。

3.4.2 LoadBlance 负载均衡器

cpp

const std::string service_machine = "./conf/service_machine.conf";
class LoadBlance
{
private:
    std::vector<Machine> machines;  // 所有编译机器数组
    std::vector<int> online;        // 在线机器下标
    std::vector<int> offline;       // 故障离线机器下标
    std::mutex mtx;                 // 集群全局锁
public:
    LoadBlance()
    {
        assert(LoadConf(service_machine));
        LOG(INFO) << "加载 " << service_machine << " 成功" << "\n";
    }
    ~LoadBlance(){}

    // 读取集群配置文件,初始化所有机器
    bool LoadConf(const std::string &machine_conf)
    {
        std::ifstream in(machine_conf); 
        if (!in.is_open())              
        {
            LOG(FATAL) << " 加载: " << machine_conf << " 失败" << "\n";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            std::vector<std::string> tokens;
            StringUtil::SplitString(line, &tokens, ":");
            if (tokens.size() != 2) 
            {
                LOG(WARNING) << " 切分 " << line << " 失败" << "\n";
                continue;
            }
            Machine m;
            m.ip = tokens[0];
            m.port = atoi(tokens[1].c_str());
            m.load = 0;
            m.mtx = new std::mutex();
            online.push_back(machines.size());
            machines.push_back(m);
        }
        in.close(); 
        return true;
    }

    // 核心:选出当前负载最低的在线机器
    bool SmartChoice(int *id, Machine **m)
    {
        mtx.lock(); 
        int online_num = online.size();
        if (online_num == 0) 
        {
            mtx.unlock();
            LOG(FATAL) << " 所有的后端编译主机已经离线" << "\n";
            return false;
        }
        *id = online[0];
        *m = &machines[online[0]];
        uint64_t min_load = machines[online[0]].Load();
        for (int i = 1; i < online_num; i++)
        {
            uint64_t curr_load = machines[online[i]].Load();
            if (min_load > curr_load) 
            {
                min_load = curr_load;
                *id = online[i];
                *m = &machines[online[i]];
            }
        }
        mtx.unlock(); 
        return true;
    }

    // 机器故障,标记离线
    void OfflineMachine(int which)
    {
        mtx.lock();
        for(auto iter = online.begin(); iter != online.end(); iter++)
        {
            if(*iter == which)
            {
                machines[which].ResetLoad(); 
                online.erase(iter);          
                offline.push_back(which);    
                break;                      
            }
        }
        mtx.unlock(); 
    }

    // 信号触发,恢复所有离线机器
    void OnlineMachine()
    {
        mtx.lock(); 
        online.insert(online.end(), offline.begin(), offline.end());
        offline.erase(offline.begin(), offline.end()); 
        mtx.unlock();
        LOG(INFO) << "所有的主机都上线了!" << "\n";
    }
    void ShowMachines()
    {
         mtx.lock(); 
         std::cout << "当前在线主机列表: ";
         for(auto &id : online) std::cout << id << " ";
         std::cout << "\n离线主机列表: ";
         for(auto &id : offline) std::cout << id << " ";
         std::cout << std::endl;
         mtx.unlock();
    }
};
3.4.3 Control 业务控制器(整合 M/V/ 负载均衡)

cpp

class Control
{
private:
    Model model_;           // 数据层
    View view_;             // 视图层
    LoadBlance load_blance_;// 负载均衡集群
public:
    Control() {}
    ~Control() {}
public:
    // 信号回调:恢复所有离线编译机器
    void RecoveryMachine()
    {
        load_blance_.OnlineMachine();
    }

    // 获取全部题目,渲染页面
    bool AllQuestions(string *html)
    {
        bool ret = true;
        vector<struct Question> all;
        if (model_.GetAllQuestions(&all))
        {
            // 题号升序排序
            sort(all.begin(), all.end(), [](const Question &q1, const Question &q2){
                return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
            });
            view_.AllExpandHtml(all, html);
        }
        else 
        {
            *html = "获取题目失败, 形成题目列表失败";
            ret = false;
        }
        return ret;
    }

    // 获取单题,渲染详情页
    bool Question(const string &number, string *html)
    {
        bool ret = true;
        struct Question q;
        if (model_.GetOneQuestion(number, &q))
        {
            view_.OneExpandHtml(q, html);
        }
        else 
        {
            *html = "指定题目: " + number + " 不存在!";
            ret = false;
        }
        return ret;
    }

    // 核心判题业务:接收前端代码,分发到编译集群,返回结果
    void Judge(const std::string &number, const std::string in_json, std::string *out_json)
    {
        struct Question q;
        model_.GetOneQuestion(number, &q);
        Json::Reader reader;
        Json::Value in_value;
        reader.parse(in_json, in_value);
        std::string code = in_value["code"].asString(); 

        // 组装发送给编译节点的JSON
        Json::Value compile_value;
        compile_value["input"] = in_value["input"].asString();
        compile_value["code"] = code + "\n" + q.tail;       
        compile_value["cpu_limit"] = q.cpu_limit;            
        compile_value["mem_limit"] = q.mem_limit;           
        Json::FastWriter writer;
        std::string compile_string = writer.write(compile_value);

        // 循环选择可用机器,直到分发成功
        while(true)
        {
            int id = 0;
            Machine *m = nullptr;
            if(!load_blance_.SmartChoice(&id, &m)) break; 

            Client cli(m->ip, m->port);
            m->IncLoad();
            LOG(INFO) << " 选择主机成功, id: " << id << " " << m->ip << ":" << m->port << " 负载: " << m->Load() << "\n";

            if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
            {
                if(res->status == 200)
                {
                    *out_json = res->body;
                    m->DecLoad();
                    LOG(INFO) << "请求编译和运行服务成功..." << "\n";
                    break;
                }
                m->DecLoad();
            }
            else
            {
                LOG(ERROR) << "主机id: " << id << " 离线"<< "\n";
                load_blance_.OfflineMachine(id);
                load_blance_.ShowMachines();
            }
        }
    }
};

整体解析

  1. 页面业务:调用 Model 拿数据,排序后交给 View 渲染 HTML;
  2. 判题业务:拼接用户代码 + 后台测试用例,通过负载均衡分发任务;
  3. 故障机制:机器请求失败自动下线,避免重复请求故障节点;
  4. 容灾接口:RecoveryMachine 提供机器恢复能力,给信号函数调用。

3.5 第五步:Web 主服务 main.cc(httplib 路由,Control 写完后开发)

搭建 HTTP 服务,注册页面、判题路由,绑定信号处理函数。

#include <iostream>
#include <signal.h>
#include "../comm/httplib.h"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_control;
static Control *ctrl_ptr = nullptr;

// SIGQUIT信号触发,恢复所有离线编译机器
void Recovery(int signo)
{
    ctrl_ptr->RecoveryMachine();
}

int main()
{
    signal(SIGQUIT, Recovery);
    Server svr;     
    Control ctrl;      
    ctrl_ptr = &ctrl;  

    // 全部题目页面
    svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp) {
        std::string html;
        ctrl.AllQuestions(&html);
        resp.set_content(html, "text/html; charset=utf-8");
    });

    // 单题详情页,正则匹配题号
    svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp) {
        std::string number = req.matches[1];
        std::string html;
        ctrl.Question(number, &html);
        resp.set_content(html, "text/html; charset=utf-8");
    });

    // 提交代码判题接口
    svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp) {
        std::string number = req.matches[1];
        std::string result_json;
        ctrl.Judge(number, req.body, &result_json);
        resp.set_content(result_json, "application/json;charset=utf-8");
    });

    svr.set_base_dir("./wwwroot"); // 静态资源目录
    svr.listen("0.0.0.0", 8080);
    return 0;
}

解析

  1. 信号绑定:收到 SIGQUIT 信号一键恢复所有编译集群节点;
  2. 三类路由区分页面访问与代码提交,返回不同 Content-Type;
  3. 正则路由自动捕获题号,接口规范简洁。

3.6 第六步:开发编译沙箱底层模块(Compiler + Runner,主服务完成后开发分布式子服务)

前面是 Web 主服务,现在开发独立编译运行节点底层能力,先写编译器、运行器两个基础模块。

3.6.1 compiler.hpp 代码编译模块
#pragma once
#include <iostream>    
#include <unistd.h>    
#include <sys/wait.h>  
#include <sys/types.h> 
#include <sys/stat.h>  
#include <fcntl.h>      
#include<string>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
namespace ns_compiler
{
    using namespace ns_util;
    using namespace ns_log;
    class Compiler
    {
    public:
        Compiler(){}
        ~Compiler(){}
        // 子进程调用g++编译源码
        static bool Compile(const std::string &file_name)
        {
            pid_t pid = fork();
            if(pid < 0)  
            {
                LOG(ERROR) << "创建子进程失败" << "\n";
                return false;
            }
            else if (pid == 0) 
            {
                umask(0);
                int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if(_stderr < 0){ 
                    LOG(WARNING) << "编译错误日志文件创建失败" << "\n";
                    exit(1); 
                }
                dup2(_stderr, 2);
                // 替换程序,执行g++编译
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),
                PathUtil::Src(file_name).c_str(),"-std=c++11",  nullptr);
                LOG(ERROR) << "编译器启动失败" << "\n";
                exit(2);
            }
            else{  
                waitpid(pid, nullptr, 0);
                if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){
                    LOG(INFO) << "代码编译成功!" << "\n";
                    return true;
                }
            }
            LOG(ERROR) << "代码编译失败,未生成可执行程序" << "\n";
            return false;
        }
    };
}

解析:fork 子进程隔离编译操作,重定向 stderr 到文件保存编译报错,execlp 调用系统 g++ 完成编译。

3.6.2 runner.hpp 沙箱运行模块(安全核心)
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"

namespace ns_runner
{
    using namespace ns_util;
    using namespace ns_log;
    class Runner
    {
    public:
        Runner() {}
        ~Runner() {}
        // 设置进程CPU、内存资源限制
        static void SetProcLimit(int _cpu_limit, int _mem_limit)
        {
            struct rlimit cpu_rlimit;
            cpu_rlimit.rlim_max = RLIM_INFINITY;
            cpu_rlimit.rlim_cur = _cpu_limit;
            setrlimit(RLIMIT_CPU, &cpu_rlimit);

            struct rlimit mem_rlimit;
            mem_rlimit.rlim_max = RLIM_INFINITY; 
            mem_rlimit.rlim_cur = _mem_limit * 1024;
            setrlimit(RLIMIT_AS, &mem_rlimit);
        }
        // 执行编译后的可执行程序
        static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
        {
            std::string _execute = PathUtil::Exe(file_name);  
            std::string _stdin   = PathUtil::Stdin(file_name);
            std::string _stdout  = PathUtil::Stdout(file_name);
            std::string _stderr  = PathUtil::Stderr(file_name);

            umask(0);
            int _stdin_fd = open(_stdin.c_str(), O_CREAT|O_RDONLY, 0644);
            int _stdout_fd = open(_stdout.c_str(), O_CREAT|O_WRONLY, 0644);
            int _stderr_fd = open(_stderr.c_str(), O_CREAT|O_WRONLY, 0644);
            if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){
                LOG(ERROR) << "运行时打开标准文件失败" << "\n";
                return -1;
            }            

            pid_t pid = fork(); 
            if (pid < 0) 
            {
                LOG(ERROR) << "运行时创建子进程失败" << "\n";
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                return -2; 
            }
            else if (pid == 0) 
            {
                dup2(_stdin_fd, 0);  
                dup2(_stdout_fd, 1);
                dup2(_stderr_fd, 2); 
                SetProcLimit(cpu_limit, mem_limit);
                execl(_execute.c_str(), _execute.c_str(), nullptr);
                exit(1); 
            }
            else 
            {
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                int status = 0; 
                waitpid(pid, &status, 0); 
                LOG(INFO) << "运行完毕, 终止信号: " << (status & 0x7F) << "\n"; 
                return status & 0x7F;
            }
        }
    };
}

解析

  1. setrlimit 限制子进程 CPU 时间、虚拟内存,防御恶意死循环、内存溢出;
  2. dup2 重定向标准输入输出到临时文件,隔离用户程序 IO;
  3. 返回进程终止信号,用于区分超时、内存超限、运行崩溃等错误。

3.7 第七步:CompileAndRun 编译运行总控模块(整合 Compiler+Runner)

整合编译、运行、错误转换、临时文件清理,对外提供统一判题入口。

#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>
namespace ns_compile_and_run
{
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_compiler;
    using namespace ns_runner;

    class CompileAndRun
    {
    public:
        // 任务结束清理所有临时文件
        static void RemoveTempFile(const std::string &file_name)
        {
            std::string _src = PathUtil::Src(file_name);
            if(FileUtil::IsFileExists(_src)) unlink(_src.c_str());
            std::string _compiler_error = PathUtil::CompilerError(file_name);
            if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());
            std::string _execute = PathUtil::Exe(file_name);
            if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());
            std::string _stdin = PathUtil::Stdin(file_name);
            if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());
            std::string _stdout = PathUtil::Stdout(file_name);
            if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());
            std::string _stderr = PathUtil::Stderr(file_name);
            if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());
        }

        // 将系统错误码/信号转为中文提示
        static std::string CodeToDesc(int code, const std::string &file_name)
        {
            std::string desc;
            switch (code)
            {
            case 0:
                desc = "编译运行成功";
                break;
            case -1:
                desc = "提交的代码是空";
                break;
            case -2:
                desc = "未知错误";
                break;
            case -3:
                FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);
                break;
            case SIGABRT:
                desc = "内存超过范围";
                break;
            case SIGXCPU:
                desc = "CPU使用超时";
                break;
            case SIGFPE:
                desc = "浮点数溢出";
                break;
            default:
                desc = "未知错误: " + std::to_string(code);
                break;
            }
            return desc;
        }

        // 完整编译运行流水线入口
        static void Start(const std::string &in_json, std::string *out_json)
        {
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value);
            std::string code = in_value["code"].asString();       
            std::string input = in_value["input"].asString();     
            int cpu_limit = in_value["cpu_limit"].asInt();        
            int mem_limit = in_value["mem_limit"].asInt();        

            int status_code = 0;
            Json::Value out_value;
            int run_result = 0;
            std::string file_name;

            if (code.size() == 0)
            {
                status_code = -1;
                goto END;         
            }
            file_name = FileUtil::UniqFileName();
            if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
            {
                status_code = -2; 
                goto END;
            }
            if (!Compiler::Compile(file_name))
            {
                status_code = -3; 
                goto END;
            }
            run_result = Runner::Run(file_name, cpu_limit, mem_limit);
            if (run_result < 0) status_code = -2;
            else if (run_result > 0) status_code = run_result;
            else status_code = 0;

        END:
            out_value["status"] = status_code;                
            out_value["reason"] = CodeToDesc(status_code, file_name); 
            if (status_code == 0)
            {
                std::string _stdout;
                FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);
                out_value["stdout"] = _stdout;
                std::string _stderr;
                FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
                out_value["stderr"] = _stderr;
            }
            Json::StyledWriter writer;
            *out_json = writer.write(out_value);
            RemoveTempFile(file_name);
        }
    };
}

解析

  1. 统一入口 Start,接收 JSON 参数,串联写文件→编译→运行;
  2. CodeToDesc 翻译底层系统信号为用户可读中文报错;
  3. 任务结束自动删除所有临时文件,防止磁盘堆积。

3.8 第八步:编译集群子服务入口 compile_run/main.cc(最后开发)

独立 HTTP 服务,部署多台机器形成集群,只提供判题接口,接收主服务转发的任务。

#include "compile_run.hpp"
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;

void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\t" << proc << " port" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2){
        Usage(argv[0]);
        return 1;
    }
    Server svr;
    svr.Post("/compile_and_run", [](const Request &req, Response &resp){
        std::string in_json = req.body; 
        std::string out_json;
        if(!in_json.empty()){
            CompileAndRun::Start(in_json, &out_json);
            resp.set_content(out_json, "application/json;charset=utf-8");
        }
    });
    svr.listen("0.0.0.0", atoi(argv[1]));
    return 0;
}

解析:轻量化独立服务,仅暴露判题接口,可多端口多机器部署,实现分布式横向扩容。

四、核心业务全流程复盘

4.1 用户访问题库页面流程

  1. 浏览器 GET /all_questions
  2. httplib 路由调用 Control::AllQuestions
  3. Model 读取全量题库并排序
  4. View 调用 ctemplate 填充模板生成 HTML
  5. HTTP 返回页面给浏览器渲染

4.2 用户提交代码判题全流程

  1. 前端 POST /judge/题号,携带 JSON 代码与输入用例
  2. Control::Judge 接收请求,读取题目资源限制、后台测试代码
  3. 拼接用户代码 + 测试代码,组装判题 JSON
  4. 负载均衡筛选当前最空闲编译节点
  5. 主服务 HTTP 客户端转发任务到编译子服务
  6. 子服务执行:生成唯一临时文件→写入代码→编译→受限沙箱运行
  7. 捕获运行状态、输出、错误,封装 JSON 回传给 Web 主服务
  8. 主服务更新节点负载,将判题结果返回前端
  9. 子服务自动清理所有临时文件,一次判题流程结束

五、项目核心技术亮点

  1. 极致安全:Linux 进程沙箱隔离,fork 子进程 + setrlimit 资源限制,杜绝恶意代码攻击服务器
  2. 分布式负载均衡:自研最小负载调度算法,故障节点自动下线,信号一键恢复集群,支持高并发扩容
  3. MVC 分层解耦:底层工具→Model→View→Control 层层解耦,修改单一模块不影响全局
  4. 微服务拆分:Web 展示服务、编译判题服务完全分离,各司其职,可独立部署扩容
  5. 全自动化运维:自动加载题库、自动清理临时文件、自动统计节点负载、分级日志快速定位异

六、项目总结

这套负载均衡式在线 OJ 系统,完整覆盖 C++ 后端开发、Linux 系统编程、网络编程、分布式架构设计核心知识点。文章严格按照从零开发的编码顺序讲解全部源码,从底层工具库逐步搭建到分布式集群服务,逻辑连贯,适合新手复盘、课程设计、毕设、面试项目展示。

不同于简单 CRUD 项目,本项目亲手实现进程隔离、资源管控、负载分发、服务容灾、动态页面渲染等底层核心能力,完整还原在线判题平台底层运行逻辑,代码分层清晰、规范度高,具备极高实战含金量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值