C++在线OJ负载均衡项目

1.演示项目

项目源码链接:

2.项目所用技术和开发环境

所用技术

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

开发环境

Ubuntu 云服务器
vscode
Mysql Workbench

**3.**项目宏观结构

核心模块

  1. common: 共享模块(日志、访问文件等方法)

  2. compile_server: 编译与运行模块

  3. oj_server: 获得题目列表等其他功能模块

项目宏观结构

编写思路

  1. 先编写 compile_server
  2. 再编写 oj_server
  3. 基于文件版的在线 OJ
  4. 前端的页面设计
  5. 基于 MySQL 版的在线 OJ

4.共享模块代码

LOG日志设计

cpp 复制代码
#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 += "[";
        message += file_name;//所在文件名
        message += "]";

        message += "[";
        message += std::to_string(line);//所在行数
        message += "]";

        message += "[";
        message += timeutil::GetTimeStamp();//时间
        message += "]";

        std::cout<<message;
        return std::cout;
    }
    #define LOG(level) LOG(#level,__FILE__,__LINE__)
}

Util工具设计

cpp 复制代码
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#include <fstream>
#include <atomic>
#include <boost/algorithm/string.hpp>

namespace ns_util
{
    const std::string temp_path = "./temp/";

    //获取时间
    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);
        }
    };
    //组装临时文件所在路径+后缀名
    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 compileError(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 IsFileExits(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修饰 所有这个类的对象共用同一个
            static std::atomic_uint id(0);
            std::string ms = timeutil::GetTimeMs();//获取微妙级时间戳
            std::string uniq_id = std::to_string(id);
            id++;
            return ms + "_" + uniq_id;
        }

        //形成src文件
        static bool WriteFile(const std::string &target,const std::string &code)
        {
            std::ofstream out(target);
            if(!out.is_open())
            {
                return false;
            }
            out.write(code.c_str(),code.size());
            out.close();
            return true;
        }

        static bool ReadFile(const std::string &target,std::string *code,bool keep = false)
        {
            (*code).clear();
            std::ifstream in(target);
            if(!in.is_open())
            {
                return false;
            }
            std::string line;
            //getline不保存行分隔符
            while(getline(in,line))
            {
                (*code) += line;
                (*code) += (keep ? "\n":"");//是否保留\n
            }
            in.close();
            return true;
        }
    };

    //boost库进行字符串分割
    class stringutil
    {
        public:
        //str 要分割的字符串
        //target 切分完毕的结果
        //sep 指定的分割符
        static void splitstring(const std::string &str,std::vector<std::string> *target,std::string sep)
        {
            boost::split((*target),str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
        }
    };
}

5..compiler 模块设计

compile编译功能

只进行编译,成功则生成可执行程序,否则错误返回

cpp 复制代码
#pragma once
#include<iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>

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

namespace ns_compile
{
    using namespace ns_util;
    using namespace ns_log;

    //只负责编译功能
    class Compiler{
        public:
        Compiler()
        {}
        ~Compiler()
        {}
        //输入参数:编译的文件名
        //file_name: abc
        //1abc -> ./temp/abc.cpp
        //abc -> ./temp/abc.exe
        static bool Compile(const std::string &file_name)
        {
            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(ERROR)<<"创建子进程失败!"<<std::endl;
                return false;
            }
            else if(pid == 0)
            {
                //设置文件权限限制
                umask(0);

                //重定向
                int compilError = open(PathUtil::compileError(file_name).c_str(),O_CREAT|O_WRONLY,0644);
                if(compilError < 0){
                    LOG(WARNING)<<"打开compileError文件失败!"<<std::endl;
                    exit(1);
                }
                
                //进程替换不影响文件描述符表
                dup2(compilError,2);//重定向

                //子进程 调用g++进行编译 进程替换
                //g++ -o target src -std=c++11
                

                execlp("g++","g++","-o",PathUtil::exe(file_name).c_str(),PathUtil::src(file_name).c_str(),
                "-std=c++11","-D","COMPILER_ONLINE",nullptr);
                LOG(ERROR)<<"启动g++编译器失败!,可能是参数错误"<<std::endl;
                exit(2);//g++错误
            }
            else if(pid > 0)
            {
                waitpid(pid,nullptr,0);
                //判断编译是否成功
                //有没有形成可执行程序
                if(fileutil::IsFileExits(PathUtil::exe(file_name)))
                {
                    LOG(INFO)<<PathUtil::src(file_name)<<"编译成功!"<<std::endl;
                    return true;//编译成功
                }
            }
            LOG(ERROR)<<"编译失败!,没有形成可执行程序"<<std::endl;
            return false;
        } 

    };
}

runner运行功能

只运行可执行程序,不判断对错

cpp 复制代码
#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"../common/log.hpp"
#include"../common/util.hpp"

namespace ns_run{
    using namespace ns_log;
    using namespace ns_util;
    class runner{
        public:
        runner()
        {}
        ~runner()
        {}
        public:
        //对资源进行控制 cpu使用时间 占用空间大小(KB)
        static void SerProcLimit(int cpu_limit,int mem_limit)
        {
            //设置CPU时长
            rlimit cpu_rlimit;
            cpu_rlimit.rlim_max = RLIM_INFINITY;// 硬限制,软限制不能超过硬限制
            cpu_rlimit.rlim_cur = cpu_limit;// 软限制,进程达到此限制时会收到信号
            setrlimit(RLIMIT_CPU,&cpu_rlimit);
            //设置内存大小
            rlimit mem_rlimit;
            mem_rlimit.rlim_max = RLIM_INFINITY;
            mem_rlimit.rlim_cur = mem_limit * 1024;//转换为KB
            setrlimit(RLIMIT_AS,&mem_rlimit);
        }
        
        static int run(const std::string &file_name,int cpu_limit,int mem_limit)
        {
            /*********************************************
            * 程序运行:
            * 1. 代码跑完,结果正确 = 0
            * 2. 代码跑完,结果不正确 > 0
            * 3. 代码没跑完,异常了 < 0
            * Run只考虑:是否正确运行完毕
            * 
            * 我们必须知道可执行程序是谁?
            * 一个程序在默认启动的时候
            * 标准输入: 不处理
            * 标准输出: 程序运行完成,输出结果是什么
            * 标准错误: 运行时错误信息
            * *******************************************/
            std::string _execute = PathUtil::src(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_WRONLY,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)<<"打开标准文件失败!"<<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;//代表创建子进程失败
            }
            else if(pid == 0)
            {
                SerProcLimit(cpu_limit,mem_limit);
                //重定向
                dup2(_stderr_fd,0);
                dup2(_stdout_fd,1);
                dup2(_stderr_fd,2);
                execl(PathUtil::exe(file_name).c_str(),PathUtil::exe(file_name).c_str(),nullptr);
                exit(1);
            }
            else if(pid > 0)
            {
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                int status = 0;
                waitpid(pid,&status,0);
                // 程序运行异常,一定是因为因为收到了信号!
                LOG(INFO) << "运行完毕, info: " << (status & 0x7F) << "\n";
                return status & 0x7f;
            }
            return 0;
        }
    };
}

compileandrun功能

进行编译和运行

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

#include"compile.hpp"
#include"runner.hpp"
#include "../common/util.hpp"   
#include "../common/log.hpp"
#include<signal.h>


namespace ns_compile_and_run
{
    using namespace ns_compile;
    using namespace ns_log;
    using namespace ns_run;
    using namespace ns_util;
    class CompileAndRun
    {
        public:
        //清理文件 删除硬链接
        static void RemoveTempFile(const std::string &file_name)
        {
            std::string src = PathUtil::src(file_name);
            if(fileutil::IsFileExits(src)) unlink(src.c_str());

            std::string excute = PathUtil::exe(file_name);
            if(fileutil::IsFileExits(excute)) unlink(excute.c_str());

            std::string compile = PathUtil::compileError(file_name);
            if(fileutil::IsFileExits(compile)) unlink(compile.c_str());

            std::string stdin = PathUtil::stdin(file_name);
            if(fileutil::IsFileExits(stdin)) unlink(stdin.c_str());

            std::string stdout = PathUtil::stdout(file_name);
            if(fileutil::IsFileExits(stdout)) unlink(stdout.c_str());

            std::string stderr = PathUtil::stderr(file_name);
            if(fileutil::IsFileExits(stderr)) unlink(stderr.c_str());
        }

        //status_code > 0 进程收到了信号
        //status_code < 0 产生了错误,非运行报错
        //status_code = 0 运行成功 答案对错并不关心
        static std::string CodeToDesc(int status_code,const std::string &file_name)
        {
            std::string desc;
            switch(status_code)
            {
                case 0:
                desc = "编译运行成功";
                break;
                case -1:
                desc = "代码为空";
                break;
                case -2:
                desc = "未知错误";
                break;
                case -3:
                fileutil::ReadFile(PathUtil::compileError(file_name),&desc,true);
                break;
                case SIGABRT: // 6
                desc = "内存超过范围";
                break;
                case SIGXCPU: // 24
                desc = "CPU使用超时";
                case SIGFPE: // 8
                desc = "浮点数溢出";
                break;
                default:
                desc = "未知!code :" + std::to_string(status_code);
                break;
            }
            return desc;
        }
        /***************************************
        * 输入:
        * code: 用户提交的代码
        * input: 用户给自己提交的代码对应的输入,不做处理
        * cpu_limit: 时间要求
        * mem_limit: 空间要求
        *
        * 输出:
        * 必填
        * status: 状态码
        * reason: 请求结果
        * 选填:
        * stdout: 程序运行完的结果
        * stderr: 程序运行完的错误结果
        *
        * 参数:
        * in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
        * out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
        * ************************************/
       static void Stat(const std::string &in_json,std::string *out_json)
       {
            //反序列化
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json,in_value);

            //序列化
            Json::Value out_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;//返回的状态码
            std::string file_name;//唯一文件名
            int run_result;//run返回值

            if(code.size() == 0)
            {
                status_code = -1;//代码为空
                goto END;
            }

            //形成唯一文件名
            //毫秒级时间戳+原子性递增确定唯一性
            file_name = fileutil::UniqFileName();

            //形成临时src文件
            if(!fileutil::WriteFile(PathUtil::src(file_name),code))
            {
                status_code = -2;//未知错误 没有形成src文件
                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;//未知错误
                goto END;
            }
            else if(run_result > 0)
            {
                status_code = run_result;//信号
                goto END;
            }
            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);//删除临时文件
       }
    };
}

compile_server.cc功能

将上面的功能利用httplib库打包成一个网络服务

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

using namespace ns_compile_and_run;
using namespace httplib;

void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}
// ./compile prot
int main(int argc,char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return-1;
    }
    Server ser;
    
    //注册服务
    ser.Post("/compile_and_run",[](const Request &req,Response &resp){
        std::string in_json = req.body;//body中存储的是json数据
        std::string out_json;
        if(!in_json.empty())
        {
            CompileAndRun::Stat(in_json,&out_json);
            resp.set_content(out_json,"application/json;charset=utf-8");
        }
    });

    ser.listen("0.0.0.0",atoi(argv[1]));//启动服务
    return 0;
}

makefile

php 复制代码
compile_server:compile_server.cc
	g++ -o compile_server compile_server.cc -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
	rm -rf compile_server

6.基于MVC结构的oj服务设计

本质就是建议一个小型网站

  1. 获取首页,用题目列表充当
  2. 编辑区域页面
  3. 提交判题功能 ( 编译并运行 )
    M : Model , 通常是和数据交互的模块,比如,对题库进行增删改查( MySQL )
    V : view , 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的 ( 浏览器 )
    C : control , 控制器,就是我们的核心业务逻辑

oj_server.cc 用户请求的服务路由功能

php 复制代码
//M: Model,通常是和数据交互的模块,比如,对题库进行增删改查
//V: view, 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
//C: control, 控制器,就是我们的核心业务逻辑

#include<iostream>
#include<signal.h>
#include"../common/httplib.h"
#include"./oj_control.hpp"

using namespace httplib;
using namespace ns_control;

static control *con_ptr  = nullptr;

void Recovery(int signo)
{
    con_ptr->RecoveryMachine();//上线所有主机
}

int main()
{
    signal(SIGINT,Recovery);

    Server ser;
    control con;
    //获取所有题目列表
    ser.Get("/all_question",[&con](const Request &req,Response &resp){
        std::string html;
        con.allquestion(&html);
        resp.set_content(html,"text/html;charset=utf-8");
    });


    //获取单个题目的信息
    ser.Get(R"(/question/(\d+))",[&con](const Request &req,Response &resp){
        std::string number = req.matches[1];
        std::string html;
        con.onequestion(number,&html);
        resp.set_content(html,"text/html;charset=utf-8");
    });


    //提交代码 判断对错
    ser.Post(R"(/judge/(\d+))",[&con](const Request &req,Response &resp){
        std::string number = req.matches[1];
        std::string out_json;
        con.Judge(number,req.body,&out_json);
        resp.set_content(out_json,"application/json;charset=utf-8");
    });


    ser.set_base_dir("./wwwroot");
    ser.listen("0.0.0.0",8080);
    return 0;
}

model.hpp 提供对数据的操作功能

cpp 复制代码
#pragma once
// mysql版本
#include "../common/util.hpp"
#include "../common/log.hpp"
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include <cstdlib>
#include <cassert>
#include <algorithm>
#include "include/mysql.h"

// model: 主要用来和数据进行交互,对外提供访问数据的接口
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;      // 题目的时间要求(S)
        int mem_limit;      // 题目的空间要去(KB)
        std::string desc;   // 题目的描述
        std::string header; // 题目预设给用户在线编辑器的代码
        std::string tail;   // 题目的测试用例,需要和header拼接,形成完整代码
    };

    const std::string questions = "questions";
    const std::string host = "127.0.0.1";
    const std::string user = "oj_client";
    const std::string passwd = "123456";
    const std::string db = "oj";
    const int port = 3306;
    class Model
    {
    private:
    public:
        Model()
        {
        }
        ~Model()
        {
        }

    public:
        bool QueryMysql(const std::string &sql, vector<Question> *out)
        {
            // 创建mysql句柄
            MYSQL *my = mysql_init(nullptr);
            // 连接数据库
            if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(),
                                              passwd.c_str(), db.c_str(), port, nullptr, 0))
            {
                LOG(FATAL) << "连接数据库失败!" << "\n";
                return false;
            }
            // 一定要设置该链接的编码格式, 要不然会出现乱码问题
            mysql_set_character_set(my, "utf8");
            LOG(INFO) << "连接数据库成功!" << "\n";
            // 执行sql语句
            if (0 != mysql_query(my, sql.c_str()))
            {
                LOG(WARNING) << sql << " execute error!" << "\n";
                return false;
            }
            // 提取结果
            MYSQL_RES *res = mysql_store_result(my);
            // 分析结果
            int rows = mysql_num_rows(res);   // 获得行数量
            int cols = mysql_num_fields(res); // 获得列数量
            Question q;
            for (int i = 0; i < rows; i++)
            {
                MYSQL_ROW row = mysql_fetch_row(res);
                q.number = row[0];
                q.title = row[1];
                q.star = row[2];
                q.desc = row[3];
                q.header = row[4];
                q.tail = row[5];
                q.cpu_limit = atoi(row[6]);
                q.mem_limit = atoi(row[7]);
                out->push_back(q);
            }

            // 释放结果空间
            mysql_free_result(res);
            //free(res);

            // 关闭mysql连接
            mysql_close(my);
            return true;
        }

        // 拿到所有题目
        bool GetAllQuestions(vector<Question> *out)
        {
            std::string sql = "select * from ";
            sql += questions;
            return QueryMysql(sql, out);
        }

        // 拿到一道题的题目细节
        bool GetOneQuestion(const std::string &number, Question *q)
        {
            std::string sql = "select * from ";
            sql += questions;
            sql += " where id=";
            sql += number;
            vector<Question> result;    
            if (QueryMysql(sql, &result))
            {
                if (result.size() == 1)
                {
                    *q = result[0];
                    return true;
                }
            }
            return false;
        }
    };
}

oj_view.hpp 对网页进行渲染

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include<string>
#include<mutex>
#include<assert.h>
#include<fstream>
// #include"./oj_model.hpp"
#include"./oj_model2.hpp"

#include"../common/log.hpp"
#include"../common/util.hpp"
#include<ctemplate/template.h>

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

    class View
    {
    public:
        View()
        {}
        ~View()
        {}
    public:
    // struct Question
    // {
    //     std::string number; // 题目编号,唯一
    //     std::string title;  // 题目的标题
    //     std::string star;   // 难度: 简单 中等 困难
    //     int cpu_limit;      // 题目的时间要求(S)
    //     int mem_limit;      // 题目的空间要去(KB)
    //     std::string desc;   // 题目的描述
    //     std::string header; // 题目预设给用户在线编辑器的代码
    //     std::string tail;   // 题目的测试用例,需要和header拼接,形成完整代码
    // };
        const std::string template_path = "./template_html/";
        bool AllExpandHtml(const vector<Question> &allq,std::string *html)
        {
            //题目编号 题目标题 题目难度
            //1.获取题目路径
            std::string src = template_path + "allquestion.html";

            //2.形成数据字典
            ctemplate::TemplateDictionary root("allquestion");
            for(const auto &q : allq)
            {
                ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("questions_list");
                sub->SetValue("number",q.number);
                sub->SetValue("title",q.title);
                sub->SetValue("star",q.star);
            }
            //3.获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src,ctemplate::DO_NOT_STRIP);
            //4.将渲染的html返回给上层  
            tpl->Expand(html,&root);

            return true;
        }
        bool OneExpandHtml(const Question &q,std::string *html)
        {
            //1.获取题目路径
            std::string src = template_path + "onequestion.html";

            //2.形成数据字典
            ctemplate::TemplateDictionary root("onequestion");

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

            //3.获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src,ctemplate::DO_NOT_STRIP);
            //4.将渲染的html返回给上层  
            tpl->Expand(html,&root);
            return true;
        }

    };
}

control.hpp 逻辑控制功能

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <jsoncpp/json/json.h>

#include "../common/log.hpp"
#include "../common/util.hpp"
#include "../common/httplib.h"
// #include "./oj_model.hpp"
#include"./oj_model2.hpp"

#include "./oj_view.hpp"

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

    // 提供服务的主机
    class Machine
    {
    public:
        std::string ip;   // 编译服务的ip
        std::string port; // 编译服务的port
        uint64_t load;    // 编译服务的负载
        std::mutex *mux;  // mutex禁止拷贝 使用指针
    public:
        Machine() : ip(""), port(""), load(0), mux(nullptr)
        {
        }
        ~Machine()
        {
        }

    public:
        // 提升主机负载
        void IncLoad()
        {
            if (mux)
                mux->lock();
            ++load;
            mux->unlock();
        }
        // 减小主机负载
        void DecLoad()
        {
            if (mux)
                mux->lock();
            --load;
            mux->unlock();
        }
        //清空主机负载
        void ReserLoad()
        {
            if (mux)
                mux->lock();
            load = 0;
             mux->unlock();
        }
        // 获得主机负载
        uint64_t Load()
        {
            uint64_t retload = 0;
            if (mux)
                mux->lock();
            retload = load;
            mux->unlock();
            return retload;
        }
    };

    const std::string server_machine_path = "./conf/server_machine.conf";
    // 负载均衡
    class LoadBlance
    {
    private:
        // 提供编译服务的所有主机 下标就是每个主机的id
        std::vector<Machine> machines;
        // 在线主机id
        std::vector<int> online;
        // 离线主机id
        std::vector<int> offline;
        std::mutex mux;

    public:
        LoadBlance()
        {
            assert(LoadConf(server_machine_path));
            LOG(INFO) << "加载配置文件" << server_machine_path << "成功!" << std::endl;
        }
        ~LoadBlance()
        {
        }

    public:
        // 加载配置文件
        bool LoadConf(const std::string &machine_conf)
        {
            std::ifstream in(machine_conf);
            if (!in.is_open())
            {
                LOG(FATAL) << "打开配置文件失败 :" << machine_conf << std::endl;
                return false;
            }
            std::string line;
            while (getline(in, line))
            {
                std::vector<std::string> tokens;
                stringutil::splitstring(line, &tokens, ":");
                if (tokens.size() != 2)
                {
                    LOG(WARNING) << "配置文件格式出错!" << std::endl;
                    continue;
                }
                Machine mach;
                mach.ip = tokens[0];
                mach.port = tokens[1];
                mach.load = 0;
                mach.mux = new mutex();

                online.push_back(machines.size());
                machines.push_back(mach);
            }
            in.close();
            return true;
        }

        // 选择负载最小的主机
        bool SmartChoice(int *d, Machine **m)
        {
            // 找到负载最小的主机
            mux.lock();
            if (online.size() == 0)
            {
                mux.unlock();
                LOG(FATAL) << "没有在线主机!!!" << std::endl;
                return false;
            }
            uint64_t min_load = 0;
            *d = online[0];
            *m = &machines[online[0]];
            for (int i = 0; i < online.size(); i++)
            {
                if (min_load > machines[online[i]].Load())
                {
                    min_load = machines[online[i]].Load();
                    *d = online[i];
                    *m = &machines[online[i]];
                }
            }
            mux.unlock();

            return true;
        }
        //当所有主机都下线时,统一上线所有主机
        void OnlineMachine()
        {
            mux.lock();
            online.insert(online.end(),offline.begin(),offline.end());
            offline.erase(offline.begin(),offline.end());
            mux.unlock();
        }

        //下线主机
        void OfflineMachine(int &id)
        {
            mux.lock();
            for (auto iter = online.begin(); iter != online.end(); iter++)
            {
                if (*iter == id)
                {
                    machines[id].ReserLoad();//将要离线的主机负载清零
                    iter = online.erase(iter);
                    offline.push_back(id);
                    break; // break的存在让我们不必担心迭代器失效的问题
                }
            }
            mux.unlock();
        }
        
        // for test
        void ShowMachines()
        {
            mux.lock();
            std::cout << "当前在线主机列表: ";
            for (auto &id : online)
            {
                std::cout << id << " ";
            }
            std::cout << std::endl;
            std::cout << "当前离线主机列表: ";
            for (auto &id : offline)
            {
                std::cout << id << " ";
            }
            std::cout << std::endl;
            mux.unlock();
        }
    };

    class control
    {
    private:
        Model _model;            // 进行数据交互
        View _view;              // 网页渲染
        LoadBlance _load_blance; // 负载均衡

    public:
        control()
        {
        }
        ~control()
        {
        }

    public:
        //恢复所有主机 让它们都上线
        void RecoveryMachine()
        {
            _load_blance.OnlineMachine();
        }
        // 获取所有题目列表
        bool allquestion(std::string *html)
        {
            bool ret = true;
            vector<Question> allq;
            if (_model.GetAllQuestions(&allq))
            {
                // 将数据转换成网页返回
                sort(allq.begin(),allq.end(),[](const Question &q1,const Question &q2){
                    return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
                });
                _view.AllExpandHtml(allq, html);
            }
            else
            {
                (*html) = "获取题目列表失败";
                ret = false;
            }
            return ret;
        }
        // 获取单个题目详细信息
        bool onequestion(const std::string &number, std::string *html)
        {
            bool ret = true;
            Question q;
            if (_model.GetOneQuestion(number, &q))
            {
                // 将数据转换成网页返回
                _view.OneExpandHtml(q, html);
            }
            else
            {
                (*html) = number + "题目不存在!";
                ret = false;
            }
            return ret;
        }

        // 提交代码 判断对错
        //  参数:
        //  in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
        //  out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
        void Judge(const std::string &number, const std::string &in_json, std::string *out_json)
        {
            // 0. 根据题目编号,直接拿到对应的题目细节
            Question q;
            _model.GetOneQuestion(number, &q);
            // 1. in_json进行反序列化,得到题目的id,得到用户提交源代码code,input
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value);
            // 2. 重新拼接用户代码+测试用例代码,形成新的代码
            Json::Value compile_value;
            compile_value["code"] = in_value["code"].asString() + q.tail;
            compile_value["input"] = in_value["input"].asString();
            compile_value["cpu_limit"] = q.cpu_limit;
            compile_value["mem_limit"] = q.mem_limit;
            // 将compile_value序列化
            Json::StyledWriter writer;
            std::string compile_json = writer.write(compile_value);
            // 3. 选择负载最低的主机(差错处理)
            while (true)
            {
                int id;
                Machine *m;
                if (!_load_blance.SmartChoice(&id, &m))
                {
                    LOG(FATAL) << "选择主机失败!!!" << std::endl;
                    break;
                }
                LOG(FATAL) << "选择主机成功,id :" << id << "ip:" << m->ip << ",port:" << m->port << std::endl;
                // 4. 然后发起http请求,得到结果
                Client cli(m->ip, atoi(m->port.c_str()));
                m->IncLoad();
                if (auto res = cli.Post("/compile_and_run", compile_json, "application/json;charset=utf-8"))
                {
                    if (res->status == 200)
                    {
                        // 5. 将结果赋值给out_json
                        *out_json = res->body;
                        m->DecLoad();
                        LOG(FATAL) << "请求编译和运行服务成功" << std::endl;
                        break;
                    }
                }
                else
                {
                    // 请求失败
                    LOG(FATAL) << "请求主机失败,id :" << id << "ip:" << m->ip << ",port:" << m->port << std::endl;
                    // 下线该主机
                    _load_blance.OfflineMachine(id);
                    _load_blance.ShowMachines();//用来调试代码
                }
            }
        }
    };
}

makefile

html 复制代码
oj_server:oj_server.cc
	g++ -o oj_server oj_server.cc -std=c++11 -lpthread -lctemplate -ljsoncpp -lmysqlclient
.PHONY:clean
clean:
	rm -rf oj_server

7.前端网页设计

前端完整代码链接:C++在线OJ负载衡项目(前端网页代码)-CSDN博客

allquestion.html

html 复制代码
<body>
    <div class="container">
        <!-- 导航栏, 功能不实现-->
        <div class="navbar">
            <a href="/">首页</a>
            <a href="/all_questions">题库</a>
            <a href="#">竞赛</a>
            <a href="#">讨论</a>
            <a href="#">求职</a>
            <a class="login" href="#">登录</a>
        </div>
        <div class="question_list">
            <h1>OnlineJuge题目列表</h1>
            <table>
                <tr>
                    <th class="item">编号</th>
                    <th class="item">标题</th>
                    <th class="item">难度</th>
                </tr>
                {{#questions_list}}
                <tr>
                    <td class="item">{{number}}</td>
                    <td class="item"><a href="/question/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/questions_list}}
            </table>
        </div>
    </div>
</body>

onequestion.html

html 复制代码
<body>
    <div class="container">
        <!-- 导航栏, 功能不实现-->
        <div class="navbar">
            <a href="/">首页</a>
            <a href="/all_questions">题库</a>
            <a href="#">竞赛</a>
            <a href="#">讨论</a>
            <a href="#">求职</a>
            <a class="login" href="#">登录</a>
        </div>
        <!-- 左右呈现,题目描述和预设代码 -->
        <div class="part1">
            <div class="left_desc">
                <h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3>
                <pre>{{desc}}</pre>
            </div>
            <div class="right_code">
                <pre id="code" class="ace_editor"><textarea class="ace_text-input">
{{pre_code}}</textarea></pre>
            </div>
        </div>
        <!-- 提交并且得到结果,并显示 -->
        <div class="part2">
            <div class="result"></div>
            <button class="btn-submit" onclick="submit()">提交代码</button>
        </div>
    </div>
    <script>
        //初始化对象
        editor = ace.edit("code");
        //设置风格和语言(更多风格和语言,请到github上相应目录查看)
        // 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html
        editor.setTheme("ace/theme/monokai");
        editor.session.setMode("ace/mode/c_cpp");
        // 字体大小
        editor.setFontSize(16);
        // 设置默认制表符的大小:
        editor.getSession().setTabSize(4);
        // 设置只读(true时只读,用于展示代码)
        editor.setReadOnly(false);
        // 启用提示菜单
        ace.require("ace/ext/language_tools");
        editor.setOptions({
            enableBasicAutocompletion: true,
            enableSnippets: true,
            enableLiveAutocompletion: true
        });
        function submit() {
            // 1. 收集当前页面的有关数据, 1. 题号 2.代码
            var code = editor.getSession().getValue();
            // console.log(code);
            var number = $(".container .part1 .left_desc h3 #number").text();
            // console.log(number);
            var judge_url = "/judge/" + number;
            // console.log(judge_url);
            // 2. 构建json,并通过ajax向后台发起基于http的json请求
            $.ajax({
                method: 'Post', // 向后端发起请求的方式
                url: judge_url, // 向后端指定的url发起请求
                dataType: 'json', // 告知server,我需要什么格式
                contentType: 'application/json;charset=utf-8', // 告知server,我给你的是什么格式
                data: JSON.stringify({
                    'code': code,
                    'input': ''
                }),
                success: function (data) {
                    //成功得到结果
                    // console.log(data);
                    show_result(data);
                }
            });
            // 3. 得到结果,解析并显示到 result中
            function show_result(data) {
                // console.log(data.status);
                // console.log(data.reason);
                // 拿到result结果标签
                var result_div = $(".container .part2 .result");
                // 清空上一次的运行结果
                result_div.empty();
                // 首先拿到结果的状态码和原因结果
                var _status = data.status;
                var _reason = data.reason;
                var reason_lable = $("<p>", {
                    text: _reason
                });
                reason_lable.appendTo(result_div);
                if (status == 0) {
                    // 请求是成功的,编译运行过程没出问题,但是结果是否通过看测试用例的结果
                    var _stdout = data.stdout;
                    var _stderr = data.stderr;
                    var stdout_lable = $("<pre>", {
                        text: _stdout
                    });
                    var stderr_lable = $("<pre>", {
                        text: _stderr
                    })
                    stdout_lable.appendTo(result_div);
                    stderr_lable.appendTo(result_div);
                }
                else {
                    // 编译运行出错,do nothing
                }
            }
        }
    </script>
</body>

注:由于我对前端不是很熟悉,所以前端网页的样式代码是从网上找的.

8.备注及细节

mysql建表

php 复制代码
CREATE TABLE IF NOT EXISTS `questions`(
id int PRIMARY KEY AUTO_INCREMENT COMMENT '题目的ID',
title VARCHAR(64) NOT NULL COMMENT '题目的标题',
star VARCHAR(8) NOT NULL COMMENT '题目的难度',
question_desc TEXT NOT NULL COMMENT '题目描述',
header TEXT NOT NULL COMMENT '题目头部,给用户看的代码',
tail TEXT NOT NULL COMMENT '题目尾部,包含我们的测试用例',
time_limit int DEFAULT 1 COMMENT '题目的时间限制',
mem_limit int DEFAULT 5000000 COMMENT '题目的空间限制'
)ENGINE=INNODB DEFAULT CHARSET=utf8;
相关推荐
stevenzqzq7 分钟前
kotlin @JvmStatic的使用
android·开发语言·kotlin
reverie.Ly8 分钟前
递归算法(5)——深度优先遍历(4)决策树
c++·算法·决策树·深度优先
2345VOR9 分钟前
【C# 上位机UDP通讯】
开发语言·udp·c#
氦客14 分钟前
Kotlin知识体系(二) : Kotlin的七个关键特性
android·开发语言·kotlin·安卓·特性·data class·密封类
努力学习的小廉18 分钟前
【红黑树】—— 我与C++的不解之缘(二十五)
开发语言·数据结构·c++
李匠202436 分钟前
C++学习之QT实现取证小软件首页
c++·学习
Achou.Wang37 分钟前
go语言中空结构体
开发语言·后端·golang
拓端研究室TRL38 分钟前
【视频】文本挖掘专题:Python、R用LSTM情感语义分析实例合集|上市银行年报、微博评论、红楼梦、汽车口碑数据采集词云可视化
开发语言·python·r语言·汽车·lstm
程序媛小盐42 分钟前
Java基础编程练习第34题-正则表达式
java·开发语言·正则表达式
炬火初现43 分钟前
Go语言的基础类型
开发语言·后端·golang