分布式在线评测系统

OnlineJudge

  • 前言
  • [1. 需求分析](#1. 需求分析)
  • [2. 项目宏观结构](#2. 项目宏观结构)
  • [3. compile_server服务设计](#3. compile_server服务设计)
    • [3.1 compiler服务设计](#3.1 compiler服务设计)
    • [3.2 runner服务设计](#3.2 runner服务设计)
    • [3.3 compile_run](#3.3 compile_run)
    • [3.4 compile_server.cpp](#3.4 compile_server.cpp)
  • [4. oj_server服务设计](#4. oj_server服务设计)
    • [4.1 model设计](#4.1 model设计)
    • [4.2 view设计](#4.2 view设计)
    • [4.3 control设计](#4.3 control设计)
      • [4.3.1 获取题目列表功能](#4.3.1 获取题目列表功能)
      • [4.3.2 获取单个题目详情页](#4.3.2 获取单个题目详情页)
      • [4.3.3 判题功能](#4.3.3 判题功能)
    • oj_server.cpp
  • [5. 项目扩展](#5. 项目扩展)

前言

此项目是仿leetcode实现在线OJ功能的,只实现类似leetcode的题目列表+在线编程功能

主要聚焦于后端设计,前端仅仅实现其功能即可

所用技术

  • C++ STL 标准库
  • Boost 准标准库(字符串切割)
  • cpp-httplib 第三方开源网络库
  • ctemplate 第三方开源前端网页渲染库
  • jsoncpp 第三方开源序列化、反序列化库
  • 负载均衡设计
  • 多进程、多线程
  • MySQL C connect
  • Ace前端在线编辑器(了解)
  • html/css/js/jquery/ajax (了解)

开发环境

Ubuntu0.22.04.1 云服务器
vscode
Mysql Workbench

1. 需求分析

  1. 用户能够查看题目列表
  2. 用户能够看到题目详细信息
  3. 用户能够编写代码并提交测试,测试结果返回给用户

2. 项目宏观结构

我们的项目核心是三个模块

  1. comm : 公共模块
  2. compile_server : 编译与运行模块
  3. oj_server : 获取题目列表,查看题目编写题目界面,负载均衡,其他功能

采用BS模式,浏览器访问后端服务器

用户的请求将通过oj_server来进行处理,如果是访问题目的请求,会访问文件或者数据库,如果是编译与运行服务会下放到负责此功能的主机,实现功能解耦。

3. compile_server服务设计

compile_server模块分为:compilerrunnercompile_run
compiler:负责代码的编译服务
runner :负责代码的运行服务
compile_run:负责接收要处理的服务并将编译运行的结果处理成格式化结果返回

3.1 compiler服务设计

提供的服务:编译代码,得到编译的结果

  • 对于接收的代码,创建代码的临时文件,以供编译
  • 代码编译后,如果编译错误,将错误信息存入一个文件,如果编译正确,则会生成可执行文件
  • 可预见的:在运行时也需要生成很多临时文件存储标准输入、标准输出、标准错误

所以,我们需要一个临时目录来存放这些临时文件,且临时文件名不能重复

我们统一命名这些临时文件:时间戳_num.xxx

在创建临时文件时,需要先获取时间戳和num,num是一个原子性的计数器,线程安全,这样就能保证所有的文件都是不同名的

于是:

  1. 源文件名:时间戳_num.src
  2. 可执行文件名:时间戳_num.exe
  3. 编译错误文件名:时间戳_num.compile_err

在下一个模块中,也是同样的命名规范

cpp 复制代码
#pragma once

#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>

#include "../comm/util.hpp"
#include "../comm/log.hpp"

namespace ns_compiler
{
    using namespace ns_util;
    using namespace ns_log;

    class Compiler{
    public:
        //file_name: 不包含文件后缀和路径,只是文件名
        static bool Compile(const std::string& file_name)
        {
            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(ERROR) << "创建子进程失败" << std::endl;
                return false;
            }
            if(pid == 0)
            {
                //子进程
                std::string compile_err_name = PathUtil::CompileError(file_name);
                //设置权限mask,消除平台差异
                umask(0);
                //以写方式打开文件,若是新文件,权限为rw|r|r
                int file_compile_error = open(compile_err_name.c_str(),O_CREAT | O_WRONLY, 0644); 
                if(file_compile_error == -1)
                {
                    LOG(ERROR) << "打开compile_error文件失败" << std::endl;
                    exit(1);
                }
                
                //标准错误重定向到compile_err_name文件中
                //如果oldfd打开且合法,就不会出错
                dup2(file_compile_error,2);

                //子进程替换为g++,编译源文件
                execlp("g++","g++", "-o", PathUtil::Exec(file_name).c_str(),\
                PathUtil::Src(file_name).c_str(),"-D", "ONLINE_COMPILE" ,"--std=c++11", nullptr);
                
                LOG(ERROR) << "进程替换失败" << std::endl;
                exit(2);
            }
            else
            {
                //父进程
                waitpid(pid,nullptr,0);
                //如果没有形成可执行文件,表示编译出错
                if( !FileUtil::IsExistPathName(PathUtil::Exec(file_name)) )
                {
                    LOG(INFO) << "代码编译错误" << std::endl;
                    return false;
                }
            }
            LOG(INFO) << "代码编译成功" << std::endl;
            return true;
        }
    };
} // namespace ns_compile

开放式日志方法LOG

cpp 复制代码
#pragma once

#include <iostream>
#include <string>

#include "util.hpp"


namespace ns_log{
    using namespace ns_util;    
    enum{
        INFO, //提示信息
        DEBUG, //调试信息
        WARNING, //警告,不影响系统
        ERROR, //错误,影响系统但是系统依旧能提供服务
        FATAL // 致命错误,系统崩溃,无法提供服务
    };
    
    //开发式日志:[level][file][line]+ 其他信息
    inline std::ostream& Log(const std::string& level,const std::string& file_name,int line)
    {
        std::string message = "[";
        message += level;
        message += "]";
        
        message += "[";
        message += file_name;
        message += "]";

        message += "[";
        message += std::to_string(line);
        message += "]"; 

        message += "[";
        message += std::to_string(TimeUtil::GetTimeStamp());
        message += "]"; 
        
        std::cout << message;
        return std::cout;
    }
    //在预处理中,#是字符串化操作符,可以直接将宏参数转换为字符串字面量
    //__FILE__宏,在编译时直接替换为文件名
    //__LINE__宏,在编译时直接替换为代码行数
    #define LOG(level) Log(#level,__FILE__,__LINE__)
}

3.2 runner服务设计

提供的服务:运行编译好的可执行文件,得到程序的结果

临时文件:

  1. 标准输入文件名:时间戳_num.stdin
  2. 标准输出文件名:时间戳_num.stdout
  3. 标准错误文件名:时间戳_num.stderr

运行可执行文件有三种结果:

  1. 运行失败
  2. 运行成功,结果正确
  3. 运行成功,结果错误

对于runner模块,我们并不考虑程序结果正确与否,因为要达到功能解耦,我们只关心程序是否运行成功

所以运行是否成功也有三种情况:

  1. 运行失败,系统或者其他原因 -- 不需要让用户知道,例如:创建进程失败,代码为空等
  2. 运行失败,代码出错 -- 需要让用户知道,例如:野指针、时间复杂度过大等
  3. 运行成功

进程运行时崩溃一定是被信号杀掉的,所以我们获取进程退出的状态码中的信号标识位,可得到运行失败原因

cpp 复制代码
#pragma once

#include <string>
#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_log;
    using namespace ns_util;

    //只考虑程序能否运行成功,不考虑结果
    class Runner{
    public:
        /*****
         * 程序运行情况:
         * 1.系统自身发生错误---返回负数
         * 2.程序被信号杀掉了---返回信号
         * 3.程序运行成功--- 返回0
        *******/
		//设置时间空间限制
        static void SetProcLimit(int _cpu_limit,int _mem_limit)
        {
            struct rlimit cpu_limit;
            cpu_limit.rlim_cur = _cpu_limit;
            cpu_limit.rlim_max = RLIM_INFINITY;
            setrlimit(RLIMIT_CPU, &cpu_limit);

            struct rlimit mem_limit;
            mem_limit.rlim_max = RLIM_INFINITY;
            mem_limit.rlim_cur = _mem_limit*1024;
            setrlimit(RLIMIT_AS,&mem_limit);
        }
        //cpu单位:s, memory单位:kb
        static int Run(const std::string& file_name,int cpu,int memory)
        {
            std::string _exe_name = PathUtil::Exec(file_name);
            std::string _stdin_name = PathUtil::Stdin(file_name);
            std::string _stdout_name = PathUtil::Stdout(file_name);
            std::string _stderr_name = PathUtil::Stderr(file_name);
            
            umask(0);
            
            int _stdin_fd = open(_stdin_name.c_str(),O_CREAT | O_RDONLY,0644);
            int _stdout_fd = open(_stdout_name.c_str(),O_CREAT | O_WRONLY ,0644);
            int _stderr_fd = open(_stderr_name.c_str(),O_CREAT | O_WRONLY, 0644);
            if(_stdin_fd == -1 || _stdout_fd == -1 || _stderr_fd == -1)
            {
                LOG(ERROR) << "打开标准文件错误" << std::endl;
                return -1; //代表打开文件失败
            }


            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(ERROR) << "创建子进程失败" << std::endl;
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                return -2;
            }
            if(pid == 0)
            {
                //子进程
                dup2(_stdin_fd,0);
                dup2(_stdout_fd,1);
                dup2(_stderr_fd,2);
                
                SetProcLimit(cpu,memory);

                execl(_exe_name.c_str(),_exe_name.c_str(),nullptr);
                

                exit(1);
            }
            else{
                //父进程
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                int st;
                //程序运行异常一定是收到了信号
                waitpid(pid,&st,0);
                LOG(INFO) << "运行完毕, info: " << (st & 0x7F) << "\n"; 
                return 0x7f & st;
            }
        }
    };
}

3.3 compile_run

功能:获取输入,编译运行,提供格式化输出

此处输入输出需要序列化与反序列化,我们采用jsoncpp第三方库来完成

输入格式:

cpp 复制代码
{
	"code" : "", //需要编译运行代码
	"input" : "", //用户直接提供的测试代码
	"cpu_limit" : *, //时间限制,单位s
	"mem_limit" : * //空间限制,单位kb
}

输出格式

cpp 复制代码
{
	"reason" : "", //状态码对应的信息
	"status" : *, //状态码,0标识运行成功,>0代码运行异常,<0系统或者其他导致运行失败
	//如果状态码为0,运行成功才有stdout和stderr
	"stdout" : "", 
	"stderr" : 
}

compile_run

cpp 复制代码
#pragma once


#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"

#include <jsoncpp/json/json.h>

namespace compile_run{
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_compiler;
    using namespace ns_runner;    

    class CompileAndRun{
    public:

        static void RemoveFile(const std::string& file_name)
        {
            std::string exe_path = PathUtil::Exec(file_name);
            if(FileUtil::IsExistPathName(exe_path))
                unlink(exe_path.c_str());

            std::string src_path = PathUtil::Src(file_name);
            if(FileUtil::IsExistPathName(src_path))
                unlink(src_path.c_str());

            std::string compile_err_path = PathUtil::CompileError(file_name);
            if(FileUtil::IsExistPathName(compile_err_path))
                unlink(compile_err_path.c_str());
            
            std::string stdin_path = PathUtil::Stdin(file_name);
            if(FileUtil::IsExistPathName(stdin_path))
                unlink(stdin_path.c_str());
            
            std::string stderr_path = PathUtil::Stderr(file_name);
            if(FileUtil::IsExistPathName(stderr_path))
                unlink(stderr_path.c_str());

            std::string stdout_path = PathUtil::Stdout(file_name);
            if(FileUtil::IsExistPathName(stdout_path))
                unlink(stdout_path.c_str());
        }

        static std::string GetReason(int status,const std::string& file_name)
        {
            std::string message;
            switch(status)
            {
                case 0:
                message = "运行成功!";
                break;
                case -1:
                message = "代码为空";
                break;
                case -2:
                message = "未知错误";
                break;
                case -3:
                FileUtil::ReadFromFile(PathUtil::CompileError(file_name),&message);
                break;
                case SIGFPE:
                message = "浮点数溢出";
                break;
                case SIGXCPU:
                message = "运行超时";
                break;
                case SIGABRT:
                message = "内存超过范围";
                break;
                default:
                message = "未能识别此错误:[";
                message += std::to_string(status);
                message += ']';
                break;
            }
            return message;
        }
        /*************************
         * 接受的json的格式:
         * code:代码
         * input:输入
         * cpu_limit:cpu限制 s
         * mem_limit:内存限制 kb
         * 
         * 发送的json格式:
         * 必填:
         * status:状态码
         * reason:请求结果
         * 选填:
         * stdout:程序输出结果
         * stderr:程序运行完的错误信息
        */
        static void Start(const std::string& in_json,std::string* out_json)
        {
            Json::Value in_root;
            Json::Reader read;
            read.parse(in_json,in_root);

            //获取输入

            std::string code = in_root["code"].asString();
            std::string input = in_root["input"].asString();
            int cpu_limit = in_root["cpu_limit"].asInt();
            int mem_limit = in_root["mem_limit"].asInt();

            
            int status = 0;//运行编译的总状态码
            int run_st = 0; //程序运行返回的状态码
            
            std::string file_name;

            if(code.size() == 0)
            {
                status = -1; //代码为空
                goto END;
            }
            
            //得到一个唯一的文件名
            file_name = FileUtil::GetUniqeFileName();


            if(!FileUtil::WriteToFile(PathUtil::Src(file_name),code) 
            || !FileUtil::WriteToFile(PathUtil::Stdin(file_name),input))
            {
                status = -2; //未知错误
                goto END;
            }


            if(!Compiler::Compile(file_name))
            {
                status = -3; //编译错误
                goto END;
            }
            
            run_st = Runner::Run(file_name,cpu_limit,mem_limit);

            if(run_st < 0)
            {
                status = -2; //未知错误
            }
            else if(run_st > 0)
            {
                // 程序运行崩溃
                status = run_st;
            }
            else
            {
                //程序运行成功
                status = 0;
            }
            END:
            Json::Value out_root;
            std::string reason = GetReason(status,file_name);
            out_root["reason"] = reason;
            out_root["status"] = status;
            if(status == 0)
            {
                
                std::string stdout_mes;
                std::string stderr_mes;
                FileUtil::ReadFromFile(PathUtil::Stdout(file_name),&stdout_mes);
                FileUtil::ReadFromFile(PathUtil::Stderr(file_name),&stderr_mes);
                out_root["stdout"] = stdout_mes;
                out_root["stderr"] = stderr_mes;
            }
            Json::StyledWriter writer;
            if(out_json)
            {
                *out_json = writer.write(out_root);
            }
            //移除临时文件
            RemoveFile(file_name);
        }
    };
}

3.4 compile_server.cpp

主要用来提供网络服务,接收http请求,并响应结果返回

采用了cpp-httplib第三方库

cpp 复制代码
#include "compile_run.hpp"
#include "../comm/httplib.h"

using namespace compile_run;
using namespace httplib;

void Usage(const std::string& proc)
{
    std::cout << "Usage:" << proc << ' ' << "port" << std::endl;
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(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())
            return;
        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. oj_server服务设计

oj_server采用MVC模式,分为:modelviewcontrol

  • model:用来与底层数据交互
  • view:用来处理用户视图,即前端页面
  • control:统筹modelview实现业务逻辑

4.1 model设计

对于在线OJ平台,最重要的数据就是题目

题目的设计:

cpp 复制代码
struct Question{
    std::string _id; //题目编号
    std::string _title; //题目标题
    std::string _difficulty; //题目难度
    std::string _desc; //题目描述
    std::string _prev_code; //预设给用户的代码
    std::string _test_code; //测试用例
    int _cpu_limit; //时间限制
    int _mem_limit; //空间限制
};

对于题目的存储,我们可以采用文件版,也可以采用数据库版,这里我们就采用数据库版

采用第三方库mysql C API,需要去mysql官网下载

model2代码

cpp 复制代码
#pragma once

#include <string>
#include <vector>
#include <iostream>
#include <mysql/mysql.h>

#include "../comm/log.hpp"
#include "../comm/util.hpp"

namespace ns_model{
    using namespace ns_log;
    using namespace ns_util;

    struct Question{
        std::string _id;
        std::string _title;
        std::string _difficulty;
        std::string _desc;
        std::string _prev_code;
        std::string _test_code;
        int _cpu_limit;
        int _mem_limit;
    };

    std::string table_name = "题目表名";
    std::string ip = "mysql服务端ip";
    std::string user_name = "用户名";
    std::string password = "密码";
    std::string database = "数据库名字";
    uint32_t port = 3306;/*数据库端口号*/

    class Model{
    public:
        bool QueryMysql(const std::string& sql,std::vector<Question>* questions)
        {
            //创建与mysql的连接
            MYSQL* mysql = mysql_init(nullptr);

            if(!mysql_real_connect(mysql,ip.c_str(),user_name.c_str(),password.c_str(),database.c_str(),port,nullptr,0))
            {
                LOG(DEBUG) << "连接数据库失败" << "\n";
                return false;
            }
            
            mysql_set_character_set(mysql,"utf8");

            //执行sql语句
            if(mysql_query(mysql,sql.c_str()))
            {
                LOG(DEBUG) << "查询数据库失败" << "\n";
                mysql_close(mysql);
                return false;
            }

            //判断是否有结果
            MYSQL_RES* result = mysql_store_result(mysql);
            if(!result)
            {
                if (mysql_field_count(mysql) == 0) {
                    LOG(DEBUG) << "查询执行成功,但无返回结果(可能是非 SELECT 查询)" << "\n";
                } else {
                    LOG(DEBUG) << "查询数据库失败:" << mysql_error(mysql) << "\n";
                }
                mysql_close(mysql);
                return false;
            }
            
            //逐行获取结果
            MYSQL_ROW fields;
            while((fields = mysql_fetch_row(result)) != nullptr)
            {
                Question q;
                q._id = fields[0] ? fields[0] : "";
                q._title = fields[1] ? fields[1] : "";
                q._difficulty = fields[2] ? fields[2] : "";
                q._desc = fields[3] ? fields[3] : "";
                q._prev_code = fields[4] ? fields[4] : "";
                q._test_code = fields[5] ? fields[5] : "";
                q._cpu_limit = fields[6] ? atoi(fields[6]) : 0;
                q._mem_limit = fields[7] ? atoi(fields[7]) : 0;

                questions->push_back(q);
            }

            //记得关闭句柄和释放结果
            mysql_free_result(result);
            mysql_close(mysql);
            return true;
        }
        bool GetAllQuestions(std::vector<Question>* questions)
        {
            std::string sql = "select id,title,difficulty,`desc`,prev_code,test_code,cpu_limit,mem_limit from ";
            sql += table_name;
            if(!QueryMysql(sql,questions))
                return false;
            return true;
        }

        bool GetOneQuestion(const std::string& id,Question* quest)
        {
            std::string sql = "select id,title,difficulty,`desc`,prev_code,test_code,cpu_limit,mem_limit from ";
            sql += table_name;
            sql += " where id = ";
            sql += id;
            std::vector<Question> vq;
            if(!QueryMysql(sql,&vq) || vq.size() != 1)
                return false;
            *quest = vq[0];
            return true;
        }
    };
}

4.2 view设计

cpp 复制代码
#pragma once

#include <vector>
#include <ctemplate/template.h>

#include "oj_model2.hpp"

namespace ns_view{
    using namespace ns_model;
    using namespace ctemplate;

    static const std::string html_path = "./template_html/";
    static const std::string all_questions_html = "all_questions.html";
    static const std::string one_question_html = "one_question.html";
    class View{
    public:
        void ShowAllQuestion(const std::vector<Question>& questions,std::string* html)
        {
            if(!html) return;
            
            TemplateDictionary root("all_questions");

            for(const auto& q : questions)
            {
                TemplateDictionary* row_dict = root.AddSectionDictionary("question_list");
                row_dict->SetValue("id",q._id);
                row_dict->SetValue("title",q._title);
                row_dict->SetValue("difficulty",q._difficulty);
            }
            Template* tpl = Template::GetTemplate(html_path+all_questions_html,DO_NOT_STRIP);
            tpl->Expand(html,&root);
        }
        void ShowOneQuestion(const Question& quest,std::string* html)
        {
            if(!html) return;
            
            TemplateDictionary root("one_question");

            root.SetValue("id",quest._id);
            root.SetValue("prev_code",quest._prev_code);
            root.SetValue("title",quest._title);
            root.SetValue("difficulty",quest._difficulty);
            root.SetValue("desc",quest._desc);

            Template* tpl = Template::GetTemplate(html_path+one_question_html,DO_NOT_STRIP);
            tpl->Expand(html,&root);
        }
        
    };
}

总共分为三个页面呈现给用户:

  1. OJ主页
  2. 题目列表
  3. 题目详情页即代码编辑区

4.3 control设计

4.3.1 获取题目列表功能

cpp 复制代码
bool AllQuestions(std::string* html)
{
    if(!html) return false;
    
    std::vector<Question> questions;
    if(!_model.GetAllQuestions(&questions))
    {
        LOG(ERROR) << "用户读取所有题目失败" << '\n';
        return false;
    }
    
    std::sort(questions.begin(),questions.end(),[](const Question& q1,const Question& q2)
    {
        return stoi(q1._id) < stoi(q2._id);
    });

    _view.ShowAllQuestion(questions,html);
    
    return true;
}

4.3.2 获取单个题目详情页

cpp 复制代码
bool OneQuestion(const std::string& id,std::string* html)
{
    if(!html) return false;
    Question quest;
    if(!_model.GetOneQuestion(id,&quest))
    {
        LOG(WARNING) << "用户读取题目[" << id << "]失败" << '\n';
        return false;
    }
    _view.ShowOneQuestion(quest,html);
    return true;
}

4.3.3 判题功能

判题功能设计到的内容较多,包括负载均衡选择负载较少的主机、主机的下线等

主机类:

cpp 复制代码
//这个主机类要注意,在loadblance里会进行拷贝操作,如果实现了析构函数释放锁空间会出现问题,即二次释放
struct Machine{
    std::string _ip;
    int _port;
    uint64_t _load;
    std::mutex* _mtx;

    Machine(const std::string& ip,int port)
    :_ip(ip),
    _port(port),
    _load(0),
    _mtx(new std::mutex)
    {}

    void IncLoad()
    {
        _mtx->lock();
        _load++;
        _mtx->unlock();
    }
    void DecLoad()
    {
        _mtx->lock();
        _load--;
        _mtx->unlock();
    }

    size_t Load()
    {
        uint64_t load;
        _mtx->lock();
        load = _load;
        _mtx->unlock();
        return load;
    }
    void ResetLoad()
    {
        _mtx->lock();
        _load =0;
        _mtx->unlock();
    }

};

对于负载的修改,必须要保证线程安全

由于c++标准库中的mutex是不允许拷贝的,后序有涉及到主机的拷贝,所以存储锁的指针,而不是锁本身

负载均衡类

cpp 复制代码
const std::string machines_conf = "./cnf/service_machine.conf";

class LoadBlance{
    bool LoginMachines()
    {
        std::ifstream in(machines_conf);
        if(!in.is_open())
        {
            LOG(FATAL) << "未能读取判题服务器配置文件" << "\n";
            return false;
        }

        std::string line;
        while(getline(in,line))
        {
            std::vector<std::string> tokens;
            StringUtil::SplitString(line,&tokens,":");
            if(tokens.size() != 2)
            {
                LOG(WARNING) << "某个判题服务器配置出错" << "\n";
                continue;
            }
            Machine mac(tokens[0],stoi(tokens[1]));
            _onlines.push_back(_machines.size());
            _machines.push_back(mac);
        }
        in.close();
        return true;
    }
public:
    LoadBlance()
    {
        assert(LoginMachines());
        LOG(INFO) << "加载主机成功" << "\n";
    }

    //将下线主机全部上线策略
    void OnlineMachines()
    {
        _mtx.lock();
        _onlines.insert(_onlines.end(),_offlines.begin(),_offlines.end());
        _offlines.clear();
        _mtx.unlock();
        LOG(INFO) << "所有主机上线成功" << std::endl;
    }
    void OfflineMachine(int which)
    {
        _mtx.lock();
        std::vector<int>::iterator it = _onlines.begin();
        while(it != _onlines.end())
        {
            if(*it == which)
            {
                _machines[which].ResetLoad();
                _offlines.push_back(which);
                _onlines.erase(it);
                break;
            }
            it++;
        }
        _mtx.unlock();
    }
    bool SmartChoice(int* pnumber,Machine** ppmac)
    {
        //轮询+hash
        _mtx.lock();
        if(_onlines.size() == 0)
        {
            _mtx.unlock();
            LOG(FATAL) << "没有在线主机,请尽快修复" << "\n";
            return false;
        }
        
        std::vector<int>::iterator it = _onlines.begin();
        int min_machine_index = 0;
        while(it != _onlines.end())
        {
            if(_machines[*it].Load() < _machines[min_machine_index].Load())
            {
                min_machine_index = *it;
            }
            it++;
        }
        *pnumber = min_machine_index;
        *ppmac = &_machines[min_machine_index];
        _mtx.unlock();
        return true;
    }
    
    //仅仅为了调试
    void ShowMachines()
    {
         _mtx.lock();
         std::cout << "当前在线主机列表: ";
         for(auto &id : _onlines)
         {
             std::cout << id << " ";
         }
         std::cout << std::endl;
         std::cout << "当前离线主机列表: ";
         for(auto &id : _offlines)
         {
             std::cout << id << " ";
         }
         std::cout << std::endl;
         _mtx.unlock();
    }
private:
    std::vector<Machine> _machines;
    std::vector<int> _onlines;
    std::vector<int> _offlines;
    std::mutex _mtx;
};

判题功能:

cpp 复制代码
void Judge(const std::string& question_id,const std::string& in_json,std::string* out_json)
{
    if(!out_json) return;
    //先得到此题信息
    Question quest;
    if(!_model.GetOneQuestion(question_id,&quest)) return;
    Json::Reader reader;
    Json::Value root;
    reader.parse(in_json,root);
    std::string prev_code = root["code"].asString();
    std::string input = root["input"].asString();

    //构建编译运行的json串
    Json::Value compile_root;
    //一定要加\n,如果不加会导致test_code.cpp里的条件编译和prev_code.cpp的代码连在一起,以至于无法消除条件编译
    compile_root["code"] = prev_code + "\n" +quest._test_code;
    compile_root["input"] = input;
    compile_root["cpu_limit"] = quest._cpu_limit;
    compile_root["mem_limit"] = quest._mem_limit;

    Json::StyledWriter writer;
    std::string judge_json = writer.write(compile_root);
    
    //负载均衡的选择主机进行判题任务
    int id;
    Machine* m;
    while(true)
    {
        if(!_load_blance.SmartChoice(&id,&m))
        {
            break;
        }
        m->IncLoad();
        httplib::Client client(m->_ip,m->_port);
        LOG(INFO) << "选择主机成功,主机id: " << id << " 详情: " << m->_ip << ":" << m->_port << " 当前主机的负载是: " << m->Load() << "\n";
        if(auto res = client.Post("/compile_and_run",judge_json,"application/json;charset=utf-8"))
        {
            if(res->status = 200)
            {
                *out_json = res->body;
                LOG(INFO) << "请求编译运行服务成功" << '\n';    
                m->DecLoad();
                break;
            }
            m->DecLoad();
        }
        else
        {
            LOG(WARNING) << "请求主机[" << id << "]" << "可能已下线" << '\n';
            _load_blance.OfflineMachine(id);
            _load_blance.ShowMachines();
        }
    }
}

oj_server.cpp

主要完成网络服务,路由功能

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>

#include "../comm/httplib.h"
#include "oj_control.hpp"

using namespace httplib;

void Usage(const std::string& proc)
{
    std::cout << "Usage:" << proc << ' ' << "port" << std::endl;
}

using namespace ns_control;

Control* ptr_ctrl = nullptr;

void Recovery(int signo)
{
    ptr_ctrl->Recovery();    
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    //设置某个信号,当对oj_server发出这个信号时,负载均衡将重启所有主机
    signal(SIGQUIT, Recovery);
    Server svr;

    Control ctrl;

    ptr_ctrl = &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 html;
        std::string id = req.matches[1];
        ctrl.OneQuestion(id,&html);

        resp.set_content(html,"text/html;charset=utf-8");
    });

    svr.Post(R"(/judge/(\d+))", [&ctrl](const Request& req,Response& resp){
        std::string id = req.matches[1];
        std::string in_json = req.body;
        std::string out_json;
        ctrl.Judge(id,in_json,&out_json);
        resp.set_content(out_json,"application/json;charset=utf-8");
    });
    svr.set_base_dir("./wwwroot");

    svr.listen("0.0.0.0", atoi(argv[1]));


    return 0;
}

5. 项目扩展

  1. 基于注册和登陆的录题功能
  2. 业务扩展,自己写一个论坛,接入到在线OJ中
  3. 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
  4. 目前后端compiler的服务我们使用的是http方式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调用,推荐:rest_rpc,替换我们的httplib
  5. 功能上更完善一下,判断一道题目正确之后,自动下一道题目
相关推荐
爱吃羊的老虎13 分钟前
【WEB开发.js】getElementById :通过元素id属性获取HTML元素
前端·javascript·html
妙哉73629 分钟前
零基础学安全--HTML
前端·安全·html
夏天吃哈密瓜34 分钟前
用Scala来解决成绩排名的相关问题
开发语言·后端·scala
爱编程的小生34 分钟前
SpringBoot Task
java·spring boot·后端
咔叽布吉37 分钟前
【前端学习笔记】AJAX、axios、fetch、跨域
前端·笔记·学习
CoderJia程序员甲40 分钟前
重学SpringBoot3-异步编程完全指南
java·spring boot·后端·异步编程
IRevers44 分钟前
使用Python和Pybind11调用C++程序(CMake编译)
开发语言·c++·人工智能·python·深度学习
岁岁岁平安1 小时前
springboot实战(19)(条件分页查询、PageHelper、MYBATIS动态SQL、mapper映射配置文件、自定义类封装分页查询数据集)
java·spring boot·后端·mybatis·动态sql·pagehelper·条件分页查询
GISer_Jing1 小时前
Vue3常见Composition API详解(适用Vue2学习进入Vue3学习)
前端·javascript·vue.js
Dragon Wu1 小时前
TailwindCss 总结
前端·css·前端框架