分布式在线评测系统

news2024/11/27 17:19:54

OnlineJudge

  • 前言
    • 所用技术
    • 开发环境
  • 1. 需求分析
  • 2. 项目宏观结构
  • 3. compile_server服务设计
    • 3.1 compiler服务设计
    • 3.2 runner服务设计
    • 3.3 compile_run
    • 3.4 compile_server.cpp
  • 4. oj_server服务设计
    • 4.1 model设计
    • 4.2 view设计
    • 4.3 control设计
      • 4.3.1 获取题目列表功能
      • 4.3.2 获取单个题目详情页
      • 4.3.3 判题功能
    • oj_server.cpp
  • 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

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

#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

#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. 运行成功

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

#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第三方库来完成

输入格式:

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

输出格式

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

compile_run

#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第三方库

#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平台,最重要的数据就是题目

题目的设计:

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代码

#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设计

#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 获取题目列表功能

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 获取单个题目详情页

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 判题功能

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

主机类:

//这个主机类要注意,在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是不允许拷贝的,后序有涉及到主机的拷贝,所以存储锁的指针,而不是锁本身

负载均衡类

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;
};

判题功能:

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

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

#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. 功能上更完善一下,判断一道题目正确之后,自动下一道题目

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2248543.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

政安晨【零基础玩转各类开源AI项目】探索Cursor-AI Coder的应用实例

目录 Cusor的主要特点 Cusor实操 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; Cursor 是 Visual Studio Code 的一个分支。这使我们能够…

第三届航空航天与控制工程国际 (ICoACE 2024)

重要信息 会议官网&#xff1a;www.icoace.com 线下召开&#xff1a;2024年11月29日-12月1日 会议地点&#xff1a;陕西西安理工大学金花校区 &#xff08;西安市金花南路5号&#xff09; 大会简介 2024年第三届航空航天与控制工程国际学术会议&#xff08;ICoACE 2024&a…

Vue前端开发2.3.5 条件渲染指令

本文介绍了Vue中两种条件渲染指令&#xff1a;v-if和v-show。v-if通过布尔值控制元素的DOM树存在&#xff0c;适用于不频繁切换显示状态的场景&#xff1b;v-show则通过CSS的display属性控制显示&#xff0c;适合频繁切换。通过创建单文件组件示例&#xff0c;演示了如何使用这…

初级数据结构——二叉树题库(c++)

这里写目录标题 前言[1.——965. 单值二叉树](https://leetcode.cn/problems/univalued-binary-tree/)[2.——222. 完全二叉树的节点个数](https://leetcode.cn/problems/count-complete-tree-nodes/)[3.——144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-…

Java基础——(一)Java概述

Java特性 简单性&#xff1a;Java与C很相似&#xff0c;但剔除了C中许多比较复杂并且很少使用的功能&#xff0c;比如头文件、指针运算、结构、联合、操作符重载、虚基类等&#xff0c;从而使Java更易于上手、学习。面向对象&#xff1a;Java是一门面向对象语言&#xff0c;具…

打造智能化在线教育平台详解:教培网校APP的架构设计与实现

本篇文章&#xff0c;小编将以教培网校APP的架构设计与实现为核心&#xff0c;深入探讨如何打造一套智能化的在线教育平台&#xff0c;为企业和教育机构提供落地参考。 一、在线教育平台的核心功能需求 构建一个高效的教培网校APP&#xff0c;首先需要明确其核心功能需求。一…

Java学习笔记--继承的介绍,基本使用,成员变量和成员方法访问特点

目录 一&#xff0c;继承 1.什么是继承 2.怎么去继承: 3.注意: 4.继承怎么学 二&#xff0c;继承基本使用 三&#xff0c;成员变量和成员方法访问特点 1.成员变量访问特点 1&#xff0c;子类和父类中的成员变量不重名: 总结: 2&#xff0c;子类和父类中的成员变量重…

IDEA2024创建一个spingboot项目

以下是创建一个基本的 Spring Boot 项目的步骤和示例&#xff1a; 初始化一个springboot工程其实有许多方法&#xff0c;笔者这里挑了一个最快捷的方式搭建一个项目。我们直接通过官方平台&#xff08;start.spring.io&#xff09;进行配置&#xff0c;然后下载压缩包就可以获取…

【Leetcode】3206.交替组1

题目描述&#xff1a; https://leetcode.cn/problems/alternating-groups-i/description/?envTypedaily-question&envId2024-11-26 题目示例&#xff1a; 解题思路 思路一&#xff1a; 1.如果color.size()小于等于2&#xff0c;则构不成环&#xff0c;直接返回结果…

利用Docker容器技术部署发布web应用程序

Docker是什么&#xff1f; docker 是一个开源的应用容器引擎&#xff0c;可以帮助开发者打包应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux机器上&#xff0c;也可以实现虚拟化&#xff0c;容器是完全使用沙箱机制&#xff0c;相互之间不会有任何…

问题:smart监控没有能流的问题? smart200与维纶屏

目前路由器网口连接一台电脑&#xff0c;一个伺服&#xff0c;一个smart200从站&#xff0c;一个smart200主站&#xff0c;还有一个mt9106iq维纶屏。 用smart主站监控&#xff0c;发现没有能流。拔掉维纶屏网线&#xff0c;就可以正常显示能流。请问各位老师傅&#xff0c;这个…

微软Ignite 2024:建立一个Agentic世界!

在今年的Microsoft Ignite 2024上&#xff0c;AI Agent无疑成为本次大会的重点&#xff0c;已经有十万家企业通过Copilot Studio创建智能体了。微软更是宣布&#xff1a;企业可以在智能体中&#xff0c;使用Azure目录中1800个LLM中的任何一个模型了&#xff01; 建立一个Agent…

【Ubuntu】E: Unable to locate package xxx

报错描述 在 Ubuntu 上 执行 apt install xxx 出现下面的报错&#xff1a; 即无法定位到该 Package&#xff0c;一般形式如下&#xff1a; # apt install xxx Reading package lists... Done Building dependency tree... Done Reading state information... Done E: Unable …

go语言逆向-基础basic

文章目录 go 编译命令 ldflags -w -s的作用和问题使用 file 命令查看文件类型 go 语言逆向参考go ID版本GOROOT和GOPATHGOROOTGOPATHGOROOT和GOPATH的关系示例 go build和 go modpclntab &#xff08;Program Counter Line Table 程序计数器行数映射表&#xff09;Moduledata程…

RL78/G15 Fast Prototyping Board Arduino IDE 平台开发过程

这是一篇基于RL78/G15 Fast Prototyping Board的Arduino IDE开发记录 RL78/G15 Fast Prototyping Board硬件简介&#xff08;背景&#xff09;基础测试&#xff08;方法说明/操作说明&#xff09;开发环境搭建&#xff08;方法说明/操作说明代码结果&#xff09;Arduino IDE RL…

Servlet细节

目录 1 Servlet 是否符合线程安全&#xff1f; 2 Servlet对象的创建时间&#xff1f; 3 Servlet 绑定url 的写法 3.1 一个Servlet 可以绑定多个url 3.2 在web.xml 配置文件中 url-pattern写法 1 Servlet 是否符合线程安全&#xff1f; 答案&#xff1a;不安全 判断一个线程…

使用UE5.5的Animator Kit变形器

UE5.5版本更新了AnimatorKit内置插件&#xff0c;其中包含了一些内置变形器&#xff0c;可以辅助我们的动画制作。 操作步骤 首先打开UE5.5&#xff0c;新建第三人称模板场景以便测试&#xff0c;并开启AnimatorKit组件。 新建Sequence&#xff0c;放入测试角色 点击角色右…

【前端】ES6基础

1.开发工具 vscode地址 :https://code.visualstudio.com/download, 下载对应系统的版本windows一般都是64位的 安装可以自选目录&#xff0c;也可以使用默认目录 插件&#xff1a; 输入 Chinese&#xff0c;中文插件 安装&#xff1a; open in browser&#xff0c;直接右键文件…

蓝桥杯模拟题不知名题目

题目:p是一个质数&#xff0c;但p是n的约数。将p称为是n的质因数。求2024最大质因数。 #include<iostream> #include<algorithm> using namespace std; bool fun(int x) {for(int i 2 ; i * i < x ; i){if(x % i 0)return false;}return true; } int main() …

Android 13 编译Android Studio版本的Launcher3

Android 13 Aosp源码 源码版本 Android Studio版本 Launcher3QuickStepLib (主要代码) Launcher3ResLib(主要资源) Launcher3IconLoaderLib(图片加载&#xff0c;冲突资源单独新建) 需要值得注意的是&#xff1a; SystemUISharedLib.jar 有kotlin和java下的&#xff0c;在 Lau…