负载均衡的在线OJ项目

负载均衡的在线OJ项目

本项目旨在实现一个负载均衡式的在线OJ系统。码云直达

所用技术与开发环境

所用技术

  • C++ STL标准库。
  • Boost标准库。
  • cpp-httplib开源轻量级网络库。
  • Ctemplate网页渲染库。
  • jsoncpp库对结构化数据进行序列化、反序列化。
  • 多进程、多线程。
  • MYSQL C++ connect
  • ace在线编辑器的简单使用(前端,简单使用,参考AI)。
  • html/css/js/jquery/ajax (前端,简单使用,使用AI生成)。

开发环境

  • ubuntu 24.04云服务器。
  • VScode
  • mysql workbench

项目的宏观结构

本项目主要分为三个核心模块:

  1. Comm公共模块:主要包含一些头文件,这些头文件中的方法是在很多模块中都会使用的。
  2. compile_server模块:提供编译运行的网络服务。
  3. oj_server模块:提供获取题库、获取单个题目列表、负载均衡的选择后端编译主机等功能,后续详细介绍。

我们的项目要实现的最重要的功能:

  • 主要的业务逻辑 :让用户能像leetcode等在线OJ平台一样查看题目列表、在线编写代码并提交代码,并获取结果的功能。

我们项目的整体结构:

项目编写思路

  1. 先完成compile_server编译运行服务的编写,编写完通过测试工具测试。
  2. 再编写OJ_Server模块的编写 ,完成MYSQL版本的OJ_Server
  3. 前端编写。
  4. 顶层makefile编写,实现一键编译项目。
  5. 结项与项目扩展思路。

compile_server模块

compiler模块设计与编写

compile.hpp

c++ 复制代码
#pragma once
#include<string>
#include<unistd.h>
#include <fcntl.h>
#include"../Comm/Util.hpp"
#include"../Comm/Log.hpp"
#include <sys/stat.h>
#include <sys/wait.h>
#include<iostream>
namespace Compile_ns
{
    using namespace util_ns;
    using namespace Log_ns;
    class Compiler
    {
        public:
            Compiler()
            {}
            ~Compiler()
            {}
            
            
            static bool Compile(const std::string& filename)
            {
                pid_t pid  = fork();
                
                if(pid < 0)
                {
                    //打印错误信息
                    LOG(ERROR) << "内部错误,进程创建失败" << "\n";
                    return false;
                }

                else  if(pid == 0)
                {
                    //子进程
                    //新建一个文件,用来存编译时的错误信息(如果有)
                    umask(0);
                    int _stderr = open(Path_Util::Compile_Error_Path(filename).c_str(),O_CREAT | O_WRONLY,0644);

                    if(_stderr < 0)
                    {
                        //文件打开失败了,无法继续执行
                        LOG(WARNNING) << "Compile_Error file 创建失败" << "\n";
                        exit(1);
                    }

                    //重定向标准错误流,让它将内容写到我们的文件中
                    dup2(_stderr,2);

                    //可以开始执行源代码了
                    //g++ -o target src -std-c++11
                    execlp("g++","g++","-o",Path_Util::Exe_Path(filename).c_str(),Path_Util::Src_Path(filename).c_str(),"-std=c++11","-D","COMPILER_ONLINE",nullptr);

                    //内部错误
                    LOG(ERROR) << "启动编译器失败,可能是参数传错了" << "\n";
                    exit(2);
                }

                else if(pid > 0)
                {
                    //父进程
                    waitpid(pid,nullptr,0);//阻塞式的等待子进程编译完代码

                    //判断是否编译成功,就看exe文件是否生成了
                    if(!File_Util::Is_Exist_File(Path_Util::Exe_Path(filename)))//编译失败
                    {
                        LOG(ERROR) << "编译失败,没有形成可执行文件" << "\n";
                        return false;
                    }
                }
                //走到这里一定编译成功了
                LOG(INFO) << "编译成功" << "\n";
                return true;
            }
    };
};

编译服务,程序流程图:

Runner模块设计与编写

该模块主要是负责运行编译成功后的可执行程序,所以如果用户提交的代码编译失败,是不会运行这个模块的相关函数的。

Runner.hpp

c++ 复制代码
#pragma once
#include<string>
#include<fcntl.h>
#include<unistd.h>
#include"../Comm/Util.hpp"
#include"../Comm/Log.hpp"
#include<sys/wait.h>
 #include <sys/resource.h>

namespace run_ns
{
    using namespace Log_ns;
    using namespace util_ns;
    class Runner
    {
    private:
        /* data */
    public:
        Runner(/* args */);
        ~Runner();

        static void SetProcLimit(int _cpu_rlim,int _mem_rlim)//_mem_rlim的单位是kB,_cpu_rlim是s
        {
            struct rlimit cpu_rlimit;
            cpu_rlimit.rlim_cur = _cpu_rlim;
            cpu_rlimit.rlim_max = RLIM_INFINITY;
            setrlimit(RLIMIT_CPU,&cpu_rlimit);

            struct rlimit mem_rlimit;
            mem_rlimit.rlim_cur = _mem_rlim*1024;//1KB
            mem_rlimit.rlim_max = RLIM_INFINITY;
            setrlimit(RLIMIT_AS,&mem_rlimit);       
        }
        static int Run(const std::string& file_name,int cpu_rlim,int mem_rlim)
        {
            //1.创建错误文件、输出文件、输入文件
            umask(0);
            int _stderr = open(Path_Util::Stderr_Path(file_name).c_str(),O_CREAT | O_WRONLY,0644);
            int _stdout = open(Path_Util::Stdout_Path(file_name).c_str(),O_CREAT | O_WRONLY,0644);
            int _stdin = open(Path_Util::Stdin_Path(file_name).c_str(),O_CREAT | O_RDONLY,0644);

            if(_stderr < 0 || _stdin < 0 || _stdout < 0)
            {
                LOG(ERROR) << "运行时打开标准文件失败" << "\n";
                return -1;
            }

            int pid = fork();

            if(pid < 0)
            {
                LOG(ERROR) << "创建进程失败" << "\n";
                ::close(_stderr);
                ::close(_stdin);
                ::close(_stdout);
                return -2;
            }

            if(pid == 0)
            {
                //设置当前进程的CPU时间和内存占用上限
                SetProcLimit(cpu_rlim,mem_rlim);

                //重定向标准输入、错误、输出到对应的文件中
                dup2(_stderr,2);
                dup2(_stdout,1);
                dup2(_stdin,0);
                //子进程
                execl(Path_Util::Exe_Path(file_name).c_str(),Path_Util::Exe_Path(file_name).c_str(),nullptr);

                //走到这里说明execl执行失败了
                LOG(ERROR) << "可执行程序执行失败,可能是参数传错了" << "\n";
                exit(1); 
            }

            else
            {
                //父进程
                int status;
                waitpid(pid,&status,0);
                ::close(_stderr);
                ::close(_stdin);
                ::close(_stdout);
                LOG(INFO) << "运行完成,INFO: " << (status & 0x7f) << "\n";
                return (status & 0x7f);//返回信号码
            }
        }
    };
    
}

细节

  1. 一般的在线oj平台给每道题都会设置超时时间和内存上限值,但是可能不会展示给用户,这两个功能我们的项目中也有体现,只需要调用系统调用setrlimit就可以设置程序执行的最大cpu时间和使用内存,如果一但超过这个值,OS就会给进程发送信号,终止它。

  2. 打开输入、输出、错误等临时文件放在主进程中,是为了给上层报告错误,如果放在子进程中,不好返回,放在主进程中,这样子进程就只会出现运行错误了,会返回信号码(一般程序替换不会执行失败,除非参数错误,这些我们不考虑,打印日志就行,不必返回给用户)。

setrlimit系统调用函数

c 复制代码
int setrlimit(int resource, const struct rlimit *rlim);//设置
int getrlimit(int resource, struct rlimit *rlim);//获取

struct rlimit {
    rlim_t rlim_cur;  // 软限制
    rlim_t rlim_max;  // 硬限制
};
  • 函数功能:用于设置每个进程资源限制。它允许你控制诸如最大文件大小、最大CPU时间、最大堆栈大小等资源的使用量。

  • 软限制与硬限制

    • rlim_max:硬限制只有超级用户才能够设置,这是所有进程的软限制可以被设置的最大值。
    • rlim_cur(软限制):这是当前生效的限制值。如果当前进程尝试分配超过此值的资源,则该分配操作将失败,还会收到相关的信号,导致进程被终止或操作失败(如malloc申请内存失败返回NULL)。
  • 函数参数

    • int resource:指定你要查询或修改的资源类型。

      • RLIMIT_CPU : 最大CPU时间。如果进程超过了这个限制,则会收到 SIGXCPU 信号。默认情况下,这会导致进程终止。可以通过设置更宽松的限制来改变这一行为,或者处理该信号以执行其他操作。
      • RLIMIT_FSIZE : 文件大小限制。若进程试图超过此限制,则会产生 SIGXFSZ 信号。通常,这也意味着相关操作失败,并可能记录错误信息。
      • RLIMIT_STACK , RLIMIT_AS , RLIMIT_CORE 等:对于这些限制,如果进程尝试超出其软限制,结果取决于具体的资源和操作。例如,增加栈空间失败可能导致程序崩溃;而对核心转储文件大小的限制不会直接产生信号,但会影响核心转储的行为。RLIMIT_AS则是对进程地址空间的大小做限制。
    • const struct rlimit *rlim:输入型参数,用于添加当前进程对某个资源的软限制、硬限制。我们通常将硬限制设置为 RLIM_INFINITY,这是一个宏表示无限大。防止默认的硬限制比我们的软限制还小,影响预期结果

程序流程图

Compile_and_run模块设计与编写

这个模块主要将我们之前写的编译、运行模块整合在一起,给外部提供一个简单的接口,让它可以不不关注我们这个编译运行模块的细节。

  • 外部只需要给我们提供用户提交来的完整代码(json串的形式),最终我们这个模块帮它执行编译运行后,也给外部返回一个特定格式的json串。

Compile_and_run.hpp:

c 复制代码
#pragma once
#include "Compile.hpp"
#include "Runner.hpp"
#include <jsoncpp/json/json.h>

namespace compile_and_run_ns
{
    using namespace Compile_ns;
    using namespace run_ns;

    class CompileAndRun
    {
    public:
        /**
         *去除此时编译运行形成的临时文件
         **/
        static void RemoveTempFile(const std::string &file_name)
        {
            // src / exe /stderr/stdout/stdin/compile_err
            std::string SrcPath = Path_Util::Src_Path(file_name);
            if (File_Util::Is_Exist_File(SrcPath.c_str()))
                unlink(SrcPath.c_str());

            std::string ExePath = Path_Util::Exe_Path(file_name);
            if (File_Util::Is_Exist_File(ExePath.c_str()))
                unlink(ExePath.c_str());

            std::string StderrPath = Path_Util::Stderr_Path(file_name);
            if (File_Util::Is_Exist_File(StderrPath.c_str()))
                unlink(StderrPath.c_str());

            std::string StdoutPath = Path_Util::Stdout_Path(file_name);
            if (File_Util::Is_Exist_File(StdoutPath.c_str()))
                unlink(StdoutPath.c_str());

            std::string StdinPath = Path_Util::Stdin_Path(file_name);
            if (File_Util::Is_Exist_File(StdinPath.c_str()))
                unlink(StdinPath.c_str());

            std::string CompileErrPath = Path_Util::Compile_Error_Path(file_name);
            if (File_Util::Is_Exist_File(CompileErrPath.c_str()))
                unlink(CompileErrPath.c_str());
        }
        /**
         * code < 0 -> 内部错误/代码为空
         * code == 0 -> 代码执行无问题
         * code > 0  -> 被信号终止,运行中出现了问题
         */
        static std::string CodeToDesc(int code, const std::string &file_name)
        {
            std::string desc;
            switch (code)
            {
            case 0:
                desc = "编译运行成功";
                break;
            case -1:
                desc = "代码为空";
                break;
            case -2:
                desc = "内部错误";
                break;
            case -3:
                File_Util::ReadFile(Path_Util::Compile_Error_Path(file_name), &desc, true);
                break;
            case SIGABRT: // 6
                desc = "内存超过范围";
                break;
            case SIGXCPU: // 24
                desc = "CPU使用超时";
                break;
            case SIGFPE: // 8
                desc = "浮点数溢出";
                break;
            default:
                desc = "未知错误" + std::to_string(code);
                break;
            }

            return desc;
        }
        /**
         *
         * 输入:
         *     [code]:
         *     [cpu_limit]
         *     [mem.limit]
         *     [input],留出来但我们不做处理
         * 输出:
         * 必填字段:
         *     [statuscode]:状态码
         *     [reason]:状态码对应的原因
         * 选填:
         *     [stderr]:错误原因
         *     [stdout]:代码的输出结果
         */
        static void Start(const std::string &json_in, std::string *json_out)
        {
            // 反序列化
            Json::Value in_value;
            Json::Reader reader;

            reader.parse(json_in,in_value);
            std::string code = in_value["code"].asCString();
            std::string input = in_value["input"].asCString();
            int cpu_limit = in_value["cpu_limit"].asInt();
            int mem_limit = in_value["mem_limit"].asInt();

            std::string _file_name; // 形成唯一的文件名
            std::string _stderr;
            std::string _stdout;
            std::string reason;
            Json::Value out_value;
            int status_code = 0;
            int run_result = 0;
            if (code.size() == 0)
            {
                status_code = -1; // 代码为空
                goto END;
            }

            _file_name = File_Util::UniqueFileName();

            // 将code写入到code.cpp文件中
            if (!File_Util::WriteFile(Path_Util::Src_Path(_file_name), code))
            {
                status_code = -2; // 未知错误
                goto END;
            }

            // 开始编译
            if (!Compiler::Compile(_file_name))
            {
                status_code = -3; // 编译错误
                goto END;
            }

            // 开始运行
            run_result = Runner::Run(_file_name, cpu_limit, mem_limit);
            if (run_result == 0)
            {
                status_code = 0; // 运行成功了
            }

            else if (run_result < 0)
            {
                status_code = -2; // 未知错误
            }

            else if (run_result > 0)
            {
                // 运行错误
                status_code = run_result;
            }

        END:
            reason = CodeToDesc(status_code, _file_name);
            if (status_code == 0)
            {
                // 运行没有问题
                File_Util::ReadFile(Path_Util::Stdout_Path(_file_name), &_stdout, true);
                File_Util::ReadFile(Path_Util::Stderr_Path(_file_name), &_stderr, true);

                out_value["stdout"] = _stdout;
                out_value["stderr"] = _stderr;
            }

            out_value["code"] = status_code;
            out_value["reason"] = reason;

            Json::StyledWriter Writer;
            (*json_out) = Writer.write(out_value);

            RemoveTempFile(_file_name);
        }
    };
};

程序流程图

Compile_server.cc的实现

Compile_server.cc文件主要对Compile_and_run模块的功能做一个调用,然后以网络通信的形式给外部提供出去。

我们采用httplib库来快速构建网络服务,httplib 是一个轻量级的C++11 HTTP/HTTPS库,由 Yoshito Umaoka 开发。它旨在简化在C++中进行HTTP请求和响应处理的过程。httplib 支持同步通信,适用于需要简单而高效地与HTTP服务器交互的应用场景。

  1. 创建httplib::server对象。
  2. 添加一个POST方法到容器中,客户端访问这个路径就会直接路由到这个方法中执行相关操作。
  3. 调用http::Server对象的listen函数, bind ip地址和port,并开始提供网络服务(死循环监听外部连接)。
c++ 复制代码
#include "Compile_and_run.hpp"
#include "../Comm/httplib.h"

using namespace compile_and_run_ns;
using namespace httplib;

void Usage(char *proc)
{
    std::cout << "Usage:" << proc << "port" << std::endl;
    exit(1);
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }
    Server svr;

    svr.Post("/Compile_Run", [](const Request &req, Response &response)
 {
       std::string json_in = req.body;
       std::string json_out;

       if(!json_in.empty())
       {
           CompileAndRun::Start(json_in,&json_out);
           response.set_content(json_out,"application/json; charset=UTF-8");
        } 
        else
       {
            response.set_content("请求错误","text/html; charset=UTF-8");
       }    
   });

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

测试Compile_server模块

我们采用postman工具来进行Compile_server模块的测试:

postmanwindows上的一个测试工具,它可以向部署在公网上的服务发送http请求报文(支持设置请求方法、请求体、请求头),并拿到响应报文。它是图形化界面,使用起来十分方便快捷。

  1. 选择POST方法,按照我们在Compile_server模块精心设计好的json字符串的格式构建(key值一定要对的上),json字符串中必须有以下几个k-v

    • code:你想提交给Compile_server模块的代码。
    • cpu_limit:代码执行的最大cpu时间。单位1/s
    • mem_limit:代码可以申请的虚拟内存的上限。单位1/KB
    • input:这是选填字段,内容可以为空,但必须要有,否则Compile_server模块在进行反序列化的时候就会出错。
  2. 填写要发送的json串:

  3. 填写服务的url后,一键发送得到编译运行结果:

至此我们的后端服务的Compile_server模块编写、测试完成。

Comm模块编写

代码

Log.hpp:提供日志服务,显示日志的行号、文件名、时间戳、日志等级、基本信息。

c++ 复制代码
#pragma once
#include<iostream>
#include<string>
#include"Util.hpp"

namespace Log_ns
{

using namespace util_ns;

enum{
    INFO = 1,
    DEBUG,
    WARNNING,
    ERROR,
    FATAL
};


inline std::ostream& Log(const std::string& level,const std::string& filename,int line)
{
    std::string message;

    //添加日志水平
    message += "[";
    message += level;
    message += "]";

    //添加文件名
    message += "[";
    message += filename;
    message += "]";

    //添加文件的行号
    message += "[";
    message += std::to_string(line);
    message += "]";

    //添加时间戳信息
    message += "[";
    message += Time_Util::GetTimeStamp();
    message += "]";

    //将message加入到缓冲区中
    std::cout << message;//先不要将内容从缓冲区中刷新出来
    
    return std::cout;
}

#define LOG(level) Log(#level,__FILE__,__LINE__)
};

Util.hpp:提供一些常用的方法,放在特定的工具类中,以静态成员函数的方式存在,让外部无需创建对象可以直接使用:

c++ 复制代码
#pragma once
#include <string>
#include <sys/stat.h>
#include <sys/time.h>
#include <atomic>
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>


namespace util_ns
{
    
    const static std::string temp = "./temp/";

    class Time_Util
    {
    public:
        static std::string GetTimeStamp()
        {
            struct timeval tv;

            gettimeofday(&tv, nullptr);
            return std::to_string(tv.tv_sec);
        }

        static std::string GetMsTimeStamp() // 得到毫秒级的时间戳
        {
            struct timeval tv;

            gettimeofday(&tv, nullptr); // 不关心时区

            return std::to_string(tv.tv_sec * 1000 + tv.tv_usec / 1000);
        }
    };

    class Path_Util
    {
    private:
        static std::string Spilt_Path(const std::string &filename, const std::string &suffic)
        {
            return temp + filename + suffic;
        }

    public:
        static std::string Compile_Error_Path(const std::string &filename)
        {
            return Spilt_Path(filename, ".CompileError");
        }

        static std::string Src_Path(const std::string &filename)
        {
            return Spilt_Path(filename, ".cpp");
        }

        static std::string Exe_Path(const std::string &filename)
        {
            return Spilt_Path(filename, ".exe");
        }

        static std::string Stderr_Path(const std::string &filename)
        {
            return Spilt_Path(filename, ".stderr");
        }

        static std::string Stdin_Path(const std::string &filename)
        {
            return Spilt_Path(filename, ".stdin");
        }

        static std::string Stdout_Path(const std::string &filename)
        {
            return Spilt_Path(filename, ".stdout");
        }
    };

    class File_Util
    {
    public:
        static bool Is_Exist_File(const std::string &file_name)
        {
            struct stat st;
            if (stat(file_name.c_str(), &st) == 0)
            {
                // 成功获取文件属性,说明该文件是存在的
                return true;
            }

            return false;
        }
        /**
         * 毫秒级的时间戳+原子计数器保证文件名的唯一性
         */
        static std::string UniqueFileName()
        {
            static std::atomic<int> id(0);

            std::string res = Time_Util::GetMsTimeStamp();

            res += ("_" + std::to_string(id));
            id++;

            return res;
        }

        /**
         *写入文件->将内容输出进文件
         */
        static bool WriteFile(const std::string &file_name, const std::string &content)
        {
            std::ofstream out(file_name);
            if (!out.is_open())
            {
                return false;
            }

            out.write(content.c_str(), content.size());
            out.close();

            return true;
        }

        /*
         *从文件中读取内容->将文件中的内容输入到用户缓冲区
         *
         */
        static bool ReadFile(const std::string &file_name, std::string *content, bool keep)
        {
            (*content).clear();
            std::ifstream in(file_name);

            if (!in.is_open())
            {

                return false;
            }
            std::string line;
            while (std::getline(in, line))
            {
                (*content) += line;
                (*content) += keep ? "\n" : "";
            }

            return true;
        }
    };

    class Spilt_Util
    {
    public:
        static void Split(const std::string &str, std::vector<std::string> *out, const std::string &seq)
        {
            // 调用boost库,并分割str,存入out
            boost::split(*out, str, boost::is_any_of(seq), boost::algorithm::token_compress_on);
        }
    };
}

stat函数介绍

c 复制代码
int stat(const char *restrict pathname,
                struct stat *restrict statbuf);
  • 函数功能 :得到以pathname为路径的文件的属性的信息。
  • 函数参数
    • const char *restrict pathname:输入型参数,指定你要查看属性的文件的路径。
    • struct stat *restrict statbuf:输出型参数,可以传nullptr,如果你不关心文件的属性信息的话。
  • 返回值:获取成功返回0,获取失败返回-1。

我们使用这个函数,主要用来判读某个文件是否存在,例如在编译后,调用它通过判断可执行文件是否生成来知道是否编译成功/在使用unlink函数删除一个文件前也需要调用这个函数,保证代码的健壮性。

unlink函数介绍

c 复制代码
int unlink(const char *pathname);
  • 函数功能 :如果没有进程打开这个文件 && 这个文件是最后一个硬链接,这个文件的内容才会被删除,空间被系统回收。 :这个函数不能删除目录文件,若需要可以调用rmdir函数。

gettimeofday与time函数介绍

这两个函数似乎都可以获取时间戳(时间戳 (timestamp)指的是自 1970年1月1日 00:00:00 UTC(称为Unix纪元或Epoch)以来经过的秒数)。

示例程序

c++ 复制代码
#include<iostream>
#include<time.h>
#include <sys/time.h>

int main()
{
    time_t t1 = time(nullptr);
    std::cout << t1 << std::endl;

    struct timeval tv;
    gettimeofday(&tv,nullptr);
    std::cout << "tv.sec " <<  tv.tv_sec << " tv.usec: " << tv.tv_usec << std::endl;
    return 0;
}

运行结果:

两者的区别

  • 头文件不同time在头文件time.h中声明,而gettimeofday在头文件sys/time.h中声明。

  • 精度不同 :这是两者最大的区别,time()只提供了秒级别的精度,而gettimeofday提供微妙级的精度。

  • 输出型参数不同 :两者都可以通过传参拿到当前时间戳,但是gettimeofday的是一个描述时间的结构体(包括秒、微秒),但是time只是一个简单的time_t的整数。

    c 复制代码
    struct timeval {
        time_t       tv_sec;   /* Seconds */
        suseconds_t  tv_usec;  /* Microseconds */
    };
  • 返回值不同time的返回值也是一个time_t的整数,表示当前时间戳,给用户提供两种方式获取时间戳,当参数为nullptr时就可以通过返回值来获取,而gettimeofday的返回值则是0/-1,用来标识函数是否执行成功。

boost::split函数介绍

boost库是对标准库的补充,它有很多标准库未提供的功能,它是一个开源的库,很多标准库在经过一段时间的成熟和发展后,被标准库所采纳,如智能指针。

boost::split 函数是Boost库的一部分,具体来说,它位于 Boost.StringAlgorithms 库中。

c 复制代码
template <typename SequenceSequenceT, typename RangeT, typename PredicateT>
SequenceSequenceT& split(SequenceSequenceT& Result, const RangeT& Input, PredicateT Pred, token_compress_mode_type eCompress = token_compress_off);
  • 函数功能 :这个函数用于将一个字符串按照指定的分隔符分割成多个子字符串,并将这些子字符串存储在一个容器中(如 std::vector <std::string>)。
  • 函数参数
    • SequenceSequenceT& Result:用来存放分割后的子字符串的容器。
    • const RangeT& Input:要被分割的源字符串。
    • PredicateT:谓词(predicate),定义了如何识别分隔符。可以是一个字符或一组字符,也可以是满足特定条件的判断函数。
    • 可选参数,控制是否压缩连续的分隔符,默认为 token_compress_off,即不压缩;如果设置为 token_compress_on,则会忽略连续出现的分隔符之间的空隙。
  • 返回值:该函数的返回值也是返回一个容器,该容器中存储切割后的字符串。
  • 头文件<boost/algorithm/string.hpp>

示例函数

c++ 复制代码
#include<iostream>
#include<time.h>
#include<boost/algorithm/string.hpp>
#include<vector>

int main()
{
    std::string src = "每天,都要,吃饭,对吗???????????你";
    std::vector<std::string> res1;
    std::vector<std::string> res2;

    res2 = boost::split(res1,src,boost::is_any_of("?,"));

    std::cout << "res1: ";

    for(auto& s:res1)
        std::cout << s << " ";
    
    std::cout << "\n";

    std::cout << "res2: ";

    for(auto& s:res2)
        std::cout << s << " ";
    
    std::cout << "\n";
    return 0;
}

注意is_any_ofboost库中的一个函数,这个函数的作用是生成一个谓词对象,它用来检查一个字符是否属于给定的一组字符。

运行结果:

这是为压缩的结果,压缩后,想让boost:spilt函数有压缩效果,最后一个参数需要传token_compress_on,表示开启压缩,默认是off

生成唯一文件名的方法介绍(原子计数器+毫秒级的时间戳)

采用两种方式保证文件名的唯一性:

  1. 原子计数器 :生成一个静态的原子计数器变量,每次使用之后++C++11atomic类提供类似功能,它对数据做访问、修改操作都是原子的。
  2. 毫秒级时间戳 :获取当前时间戳,调用gettimeofday函数,拿到毫秒级的时间戳。
c 复制代码
        static std::string UniqueFileName()
        {
            static std::atomic<int> id(0);

            std::string res = Time_Util::GetMsTimeStamp();

            res += ("_" + std::to_string(id));
            id++;

            return res;
        }

Oj_Server模块编写

Oj_Server模块的功能

  1. 向客户端(浏览器)提供网络服务,浏览器给客户端发送请求后,Oj_Server处理。
  2. 如果客户端请求的是获取所有题目和获取单个题目,就无需和编译服务交互,直接返回渲染后的html
  3. 如果用户提交代码,Oj_Server需要负载均衡的选择编译服务的主机,让其提供编译运行的服务,最后Oj_Server模块将Compile_Server服务返回的结果直接返回给客户端。

基于MVC结构的Oj_Server模块的设计

我们的Oj_Server模块主要分为以下三个小的模块:

  1. Oj_control模块:控制模块,里面实现直接为客户端提供服务的方法,我们Oj_Server模块的核心业务逻辑在这个模块中,包括获取题库、获取单个题目、判题功能,未来可能还会增加登录、注册的功能。
  2. Oj_Model模块:这个模块主要与数据打交道,例如题目的数据(题目编号、题目的难度、题目的描述、题目的给用户的代码、测试用例代码)等,我们将题目相关的信息都存在数据库中,后面可能加入与用户相关的数据,也放在这个模块中。所以这个模块就主要负责与数据进行交互
  3. Oj_View模块:这个模块主要和前端相关,它主要负责网页渲染 的功能,当Oj_Model模块取出题目数据后,由这个模块将题目的数据渲染成一张html的网页后返回给客户端,我们的网页是服务器动态生成的(利用已经准备好的模板)。

代码实现

Oj_control.hpp

c++ 复制代码
#pragma once
#include<iostream>
#include"../Comm/Log.hpp"
#include"../Comm/Util.hpp"
#include<string>
#include<vector>
#include"OJ_Model.hpp"
#include"Oj_View.hpp"
#include<jsoncpp/json/json.h>
#include<mutex>
#include<fstream>
#include<assert.h>
#include"../Comm/httplib.h"
#include<algorithm>

//核心控制模块
namespace control_ns
{
    using namespace model_ns;
    using namespace httplib;
    using namespace view_ns;
    using namespace util_ns;
    using namespace httplib;

    class HostMachine
    {
        public:
            std::string _ip;//编译运行服务主机的ip地址
            int _port;//端口号
            uint64_t _load;//负载情况
            std::mutex* _mutex;
        public:
            HostMachine():_load(0),_mutex(nullptr){}
            ~HostMachine(){}
        public:
            void IncLoad()
            {
                _mutex->lock();
                ++_load;
                _mutex->unlock();
            }

            void DecLoad()
            {
                _mutex->lock();
                --_load;
                _mutex->unlock();
            }

            void ReLoad()
            {
                _mutex->lock();
                _load = 0;
                _mutex->unlock();
            }

            int GetLoad()
            {
                uint64_t load = 0;
                _mutex->lock();
                load = _load;
                _mutex->unlock();

                return load;
            }
    };

    static const std::string hostpath = "./Config/compile_service_host.conf";
    class LoadBalance
    {
        private:
            std::vector<HostMachine> _host;//所有的主机

            std::vector<int> _online;//所有在线的主机
            std::vector<int> _offline;//所有离线的主机
            std::mutex _mut;//锁,保护当前类中的数据安全
        public:
            LoadBalance()
            {
                assert(LoadConfig());
            }
            ~LoadBalance()
            {
            
            }
        public:
            bool LoadConfig()
            {
                //0.导入配置文件进入内存
                std::ifstream ifs(hostpath);

                //1.判断是否正确打开文件
                if(!ifs.is_open())
                {
                    LOG(FATAL) << "编译服务配置打开失败,请检查路径是否设置正确..."  << "\n";
                    return false;
                }

                //2.开始按行读取内容,同时配置主机
                std::string line;

                while(std::getline(ifs,line))
                {
                    std::vector<std::string> res;
                    Spilt_Util::Split(line,&res,":");

                    if(res.size() != 2)
                    {
                        LOG(WARNNING) << "此行配置出错..." << "\n";
                        continue;
                    }
                    HostMachine machine;
                    machine._ip = res[0];
                    machine._port = atoi(res[1].c_str());
                    machine._mutex = new std::mutex;

                    //将主机添加到_host中
                    _online.emplace_back(_host.size());
                    // std::cout << "_onlinesize" << _online.size() << std::endl;
                    _host.emplace_back(machine);
                }

                LOG(INFO) << "导入主机配置成功..."  << "\n";
                return true;
            }

            //id,hm都是输出型参数
            bool SmartChoice(int* id,HostMachine** hm)
            {
                _mut.lock();//加锁保护在线主机、离线主机列表等公共数据

                int onlinenum = _online.size();

                if(onlinenum == 0)
                {
                    _mut.unlock();
                    LOG(FATAL) << "所有后端编译主机的已经离线,服务无法继续..." << "\n";
                    return false;
                }

                //负载均衡算法
                //1.随机数+hash
                //2.轮询+hash->我们选择这种
                (*id) = _online[0];
                (*hm) = &_host[*id];
                uint64_t min_load = _host[_online[0]].GetLoad();

                for(int i = 1;i < _online.size();++i)
                {
                    uint64_t cur_load  = _host[_online[i]].GetLoad();
                    if(cur_load < min_load)
                    {
                        min_load = cur_load;
                        (*id) = _online[i];
                        (*hm) = &_host[*id];
                    }
                }

                _mut.unlock();
                return true;
            }

            void OfflineHost(int which)
            {
                _mut.lock();
                for(auto iter = _online.begin();iter != _online.end();++iter)
                {
                    if((*iter) == which)
                    {
                        _host[which].ReLoad();
                        _online.erase(iter);
                        _offline.emplace_back(which);
                        break;
                    }
                }
                _mut.unlock();
            }

            void OnlineHost()
            {
                _mut.lock();
                //将所有的离线主机中的全加入在线主机中
                _online.insert(_online.end(),_offline.begin(),_offline.end());
                _offline.erase(_offline.begin(),_offline.end());
                _mut.unlock();

                LOG(INFO) << "所有主机都上线了" << "\n";
            }

 
            void ShowHostlist()
            {
                _mut.lock();
                //打印在线主机列表
                std::cout << "在线主机列表: ";

                for(int i = 0;i < _online.size();++i)
                {
                    std::cout << _online[i] << " ";
                }

                std::cout << "\n";

                std::cout << "离线主机列表: ";

                for(int i = 0;i < _offline.size();++i)
                {
                    std::cout << _offline[i] << " ";
                }

                std::cout << "\n";
                _mut.unlock();
            }
    };


    class Control
    {
        private:
            Model _model;
            View _view;
            LoadBalance _LoadBalance;
        public:
            Control(){}
            ~Control(){}
        public:

            void RecoveryMachine()
            {
                _LoadBalance.OnlineHost();
            }

            //返回给外层回调一个包含所有题目的html文件,要经过网页渲染,View中的功能
            bool AllQuestions(std::string* html)
            {
                bool ret;
                //将所有的题目的题目列表信息放入我们的question_v中
                std::vector<Question*> question_v;
                if(_model.GetAllQuestions(&question_v))
                {
                    sort(question_v.begin(),question_v.end(),[](Question* a,Question*b)->bool{
                        return atoi(a->number.c_str()) < atoi(b->number.c_str());
                    });//升序排序
                    _view.AllRenderHtml(question_v,html);
                    ret = true;
                }

                else
                {
                    *html = "获取所有题目列表失败";
                    ret = false;
                }

                return true;
            }

            bool OneQuestion(const std::string& number,std::string* html)
            {
                Question* question;
                bool ret;
                if(_model.GetOneQuestion(number,&question))
                {
                    LOG(INFO) << "获取题目成功,number is " << number << "\n";
                    _view.OneRenderHtml(question,html);
                }

                else
                {
                    LOG(INFO) << "获取题目失败,number is " << number << "\n";
                    ret = false;
                    *html = "获取题目失败,number is " + number;
                }

                
                return ret;
            }

            void  Judge(const std::string& number,const std::string& in_json,std::string * out_json)
            {
                //0.根据题目编号,拿到题目细节
                Question* question;
                _model.GetOneQuestion(number,&question);

                //1.1将用户提交的in_json,解析出来 -- 反序列化
                Json::Value in_Value;
                Json::Reader reader;
                reader.parse(in_json,in_Value);

                //1.1拿到结构化数据
                std::string submit_code = in_Value["code"].asCString();//用户提交的代码
                std::string input = in_Value["input"].asCString();

                //2.构建给后端编译服务的请求json串
                //2.1将拿到用户提交的代码和测试用例拼接在一起
                std::string code = submit_code+"\n"+ question->tail;
                Json::Value out_Value;
                
                //2.2构建json数据
                out_Value["code"] =  code;
                out_Value["input"] =  input;
                out_Value["cpu_limit"] = question->cpu_limit;
                out_Value["mem_limit"] = question->mem_limit;
                Json::FastWriter writer;
                
                //2.3反序列化,得到发送给compile_run服务的json串
                std::string compile_json = writer.write(out_Value);

                //3.负载均衡式的选择主机
                while(true)
                {
                    int id = 0;
                    HostMachine* host;
                    //3.1负载均衡式的获取主机
                    if(!_LoadBalance.SmartChoice(&id,&host))
                    {
                        LOG(ERROR) << "主机都未上线,id is " << id << "\n";
                        break;
                    }

                    //走到这里,一定获取到了一个在线主机
                    // 开始发送http post请求
                    //3.2创建httplib client
                    Client client(host->_ip,host->_port);
                    host->IncLoad();

                    //设置client的超时时间
                    client.set_read_timeout(std::chrono::seconds(20));//20s超时
                    client.set_connection_timeout(std::chrono::seconds(20));
                    LOG(INFO) << "当前请求的主机,id: " << id << " 主机详情: " << host->_ip <<  ":" << host->_port << "\n";
                    //发送post请求向目标主机
                    auto res = client.Post("/Compile_Run",compile_json,"application/json;charset=utf-8");
                    
                    if(res)
                    {
                        if(res->status == 200)//响应成功
                        {
                            host->DecLoad();
                            (*out_json) = res->body;
                            LOG(INFO) << "编译运行成功" << "\n";
                            break;
                        }

                        (*out_json) = res->body;
                        host->DecLoad();
                        LOG(INFO) << "请求成功" << "\n";
                        break;
                    }

                    else
                    {
                        LOG(ERROR) << "请求编译运行服务失败,可能已经离线当前主机id: " << id << " 主机详情: " << host->_ip <<  ":" << host->_port << "\n";
                        _LoadBalance.OfflineHost(id);
                        _LoadBalance.ShowHostlist();
                    }
                    
                
                }
            }

    };
}

Oj_view.hpp

c++ 复制代码
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include"OJ_Model.hpp"
#include<ctemplate/template.h>

namespace view_ns
{
    using namespace model_ns;
    using namespace ctemplate;

    const static std::string comm_path = "./Template_html/";
    class View//帮助我们完成网页的渲染工作
    {
        public:
            View(){}
            ~View(){}
        public:
            void AllRenderHtml(std::vector<Question*> question_arr,std::string* html)
            {
                //1.创建字典
                TemplateDictionary root("AllQuestionList");

                //2.往字典中插入数据
                for(int i = 0;i < question_arr.size();++i)
                {
                    TemplateDictionary* sub = root.AddSectionDictionary("OneQuestionList");
                    sub->SetValue("number",question_arr[i]->number);
                    sub->SetValue("title",question_arr[i]->title);
                    sub->SetValue("star",question_arr[i]->level);
                }

                //3.加载模板html
                Template* tpl = Template::GetTemplate(comm_path+"All_Question.html",DO_NOT_STRIP);

                //4.渲染模板
                tpl->Expand(html,&root);

                for(auto& ptr:question_arr)
                    delete ptr;
            }

            void OneRenderHtml(Question* question,std::string* html)
            {
                LOG(INFO) << "开始渲染网页了..." << "\n";
                //1.创建字典
                TemplateDictionary root("OneQuestion");
                
                //2.设置变量的值
                root.SetValue("desc",question->desc);
                root.SetValue("star",question->level);
                root.SetValue("pre_code",question->header);
                root.SetValue("number",question->number);
                root.SetValue("title",question->title);

                //3.加载模板文件
                Template* tpl = Template::GetTemplate(comm_path+"One_Question.html",DO_NOT_STRIP);

                //4.将模板文件渲染
                tpl->Expand(html,&root);

                delete question;
            }
    };
}

Oj_Model1.hpp

c++ 复制代码
#pragma once
#include <string>
#include <vector>
#include <assert.h>
#include <fstream>
#include "../Comm/Log.hpp"
#include "../Comm/Util.hpp"
#include <unordered_map>
#include "include/mysql/jdbc.h"

namespace model_ns
{

    using namespace Log_ns;
    using namespace util_ns;

    struct Question
    {
        std::string number; // 题目编号
        std::string title;  // 题目名
        std::string level;  // 题目难度
        int cpu_limit;
        int mem_limit;
        std::string desc;   // 题目的内容/描述
        std::string header; // 给用户的编写代码区域
        std::string tail;   // 测试用例,最终要与用户返回的代码拼接在一起,交给后台的编译运行服务
    };

    const static std::string path = "./Questions/Questions.list"; // 题目列表文件路径
    const static std::string comm = "./Questions/";               // 文件的公共目录

    const static int port = 3306;
    const static std::string schem = "oj";//哪个数据库
    const static std::string hostname = "127.0.0.1";//ip地址/url
    const static std::string passward = "123456";//ip地址/url
    const static std::string username = "oj_client";//ip地址/url
    const static std::string tablename = "questions";

    // 1.给Control提供对题库增删查改的功能
    class Model
    {
    private:
        std::unordered_map<std::string, Question *> questions;

    public:
        Model()
        {
        }
        ~Model() {}

    public:
        bool GetQuery(const std::string &sql, std::vector<Question *> *out)
        {
            LOG(INFO) << "开始连接数据库了... " << "\n";
            // 1.连接数据库
            sql::mysql::MySQL_Driver *driver;
            sql::Connection *con;
            sql::ConnectOptionsMap connection_detail;
            sql::Statement* st;
            sql::ResultSet* res;
            connection_detail[OPT_PORT] = port;
            connection_detail[OPT_SCHEMA] = schem;
            connection_detail[OPT_HOSTNAME] = hostname;
            connection_detail[OPT_PASSWORD] = passward;
            connection_detail[OPT_USERNAME] = username;
            connection_detail[OPT_CHARSET_NAME] = "utf8mb4";//客户端和服务器之间的所有通信(双向)。
            connection_detail[OPT_RECONNECT] = true;//设置该连接的编码格式

            //初始化driver
            driver = sql::mysql::get_mysql_driver_instance();

            if(!(con = driver->connect(connection_detail)))
            {
                LOG(FATAL) << "连接数据库失败,检查参数..." << "\n";
                return false;
            }

            LOG(INFO) << "连接数据库成功..." << "\n";

            st = con->createStatement();
            assert(st);

            res = st->executeQuery(sql);
            assert(res);

            while(res->next())
            {
                LOG(INFO) << "拿到一行数据了" << "\n";
                Question* question = new Question;
                question->number = res->getString("number");
                question->desc = res->getString("question_desc");
                question->header = res->getString("header");
                question->tail = res->getString("tail");
                question->level = res->getString("star");
                question->mem_limit = res->getInt("mem_limit");
                question->cpu_limit = res->getInt("cpu_limit");
                question->title = res->getString("title");

                (*out).emplace_back(question);
            }

            delete st;
            delete con;
            delete res;
            return true;
        }
        // 读取所有题目的内容
        bool GetAllQuestions(std::vector<Question *> *out)
        {
            std::string sql = "select * from ";
            sql += tablename;

            return GetQuery(sql,out);
        }

        // 获取一个题目的信息
        bool GetOneQuestion(const std::string &number, Question **question)
        {
            std::string sql = "select * from ";
            sql += tablename;
            sql += " where number=";
            sql += number;
            
            std::vector<Question*> res;
            if(GetQuery(sql,&res))
            {
                *question = res[0];
                return true;
            }

            return false;
        }
    };   
}

Oj_Server.cc

c++ 复制代码
#include <iostream>
#include <string>
#include "../Comm/httplib.h"
#include "OJ_Control.hpp"
#include<signal.h>
using namespace httplib;
using namespace control_ns;

Control* controlptr;

void HandlerSignal(int signal)
{
    controlptr->RecoveryMachine();
}

int main()
{
    signal(SIGQUIT,HandlerSignal);//ctrl /
    Server svr;
    Control control;
    controlptr = &control;

    // 1.可以看到首页
    // 2.可以请求所有题目列表 -GET
    // 3.可以看到详细的单个题目 -GET
    // 4.提供判题功能 -POST
    svr.Get("/All_Questions", [&control](const Request &req, Response &resp)
    {
       std::string html;
       control.AllQuestions(&html);
       resp.set_content(html,"text/html; charset=UTF-8"); 
    });

     svr.Get(R"(/Question/(\d+))", [&control](const Request &req, Response &resp)// 正则表达式要加括号,括号表示定义捕获组
     { 
        std::string number = req.matches[1];                                       // 下标[1]存储的是第一个匹配的结果
        std::string html;

        control.OneQuestion(number, &html);
        resp.set_content(html, "text/html; charset=UTF-8");
    });

    svr.Post(R"(/judge/(\d+))", [&control](const Request &req, Response &resp){
        std::string number = req.matches[1];
        std::string out_json;
        std::string in_json = req.body;

        if(!in_json.empty())
        {
            control.Judge(number,in_json,&out_json);
        }
        if(!out_json.empty())
            resp.set_content(out_json,"application/json; charset=UTF-8"); 
        });
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0", 8080);
    return 0;
}

程序流程图

  1. 当客户端请求url/All_Questions的服务:

    补:调用model模块中的GetAllQuestions的程序流程图:

    C++连接/操作mysql不是我们项目的重点,不做过多介绍。

    补:调用 oj_view模块中的AllRenderHtml函数流程图:

  2. 当客户端请求urlQuestion/(\d+)的服务:

  3. 当客户端请求urljudge/(\d+)的服务:

负载均衡功能介绍

下面是我们负载均衡相关的代码:

c 复制代码
    class HostMachine
    {
        public:
            std::string _ip;//编译运行服务主机的ip地址
            int _port;//端口号
            uint64_t _load;//负载情况
            std::mutex* _mutex;
        public:
            HostMachine():_load(0),_mutex(nullptr){}
            ~HostMachine(){}
        public:
            void IncLoad()
            {
                _mutex->lock();
                ++_load;
                _mutex->unlock();
            }

            void DecLoad()
            {
                _mutex->lock();
                --_load;
                _mutex->unlock();
            }

            void ReLoad()
            {
                _mutex->lock();
                _load = 0;
                _mutex->unlock();
            }

            int GetLoad()
            {
                uint64_t load = 0;
                _mutex->lock();
                load = _load;
                _mutex->unlock();

                return load;
            }
    };

    static const std::string hostpath = "./Config/compile_service_host.conf";
    class LoadBalance
    {
        private:
            std::vector<HostMachine> _host;//所有的主机

            std::vector<int> _online;//所有在线的主机
            std::vector<int> _offline;//所有离线的主机
            std::mutex _mut;//锁,保护当前类中的数据安全
        public:
            LoadBalance()
            {
                assert(LoadConfig());
            }
            ~LoadBalance()
            {
            
            }
        public:
            bool LoadConfig()
            {
                //0.导入配置文件进入内存
                std::ifstream ifs(hostpath);

                //1.判断是否正确打开文件
                if(!ifs.is_open())
                {
                    LOG(FATAL) << "编译服务配置打开失败,请检查路径是否设置正确..."  << "\n";
                    return false;
                }

                //2.开始按行读取内容,同时配置主机
                std::string line;

                while(std::getline(ifs,line))
                {
                    std::vector<std::string> res;
                    Spilt_Util::Split(line,&res,":");

                    if(res.size() != 2)
                    {
                        LOG(WARNNING) << "此行配置出错..." << "\n";
                        continue;
                    }
                    HostMachine machine;
                    machine._ip = res[0];
                    machine._port = atoi(res[1].c_str());
                    machine._mutex = new std::mutex;

                    //将主机添加到_host中
                    _online.emplace_back(_host.size());
                    // std::cout << "_onlinesize" << _online.size() << std::endl;
                    _host.emplace_back(machine);
                }

                LOG(INFO) << "导入主机配置成功..."  << "\n";
                return true;
            }

            //id,hm都是输出型参数
            bool SmartChoice(int* id,HostMachine** hm)
            {
                _mut.lock();//加锁保护在线主机、离线主机列表等公共数据

                int onlinenum = _online.size();

                if(onlinenum == 0)
                {
                    _mut.unlock();
                    LOG(FATAL) << "所有后端编译主机的已经离线,服务无法继续..." << "\n";
                    return false;
                }

                //负载均衡算法
                //1.随机数+hash
                //2.轮询+hash->我们选择这种
                (*id) = _online[0];
                (*hm) = &_host[*id];
                uint64_t min_load = _host[_online[0]].GetLoad();

                for(int i = 1;i < _online.size();++i)
                {
                    uint64_t cur_load  = _host[_online[i]].GetLoad();
                    if(cur_load < min_load)
                    {
                        min_load = cur_load;
                        (*id) = _online[i];
                        (*hm) = &_host[*id];
                    }
                }

                _mut.unlock();
                return true;
            }

            void OfflineHost(int which)
            {
                _mut.lock();
                for(auto iter = _online.begin();iter != _online.end();++iter)
                {
                    if((*iter) == which)
                    {
                        _host[which].ReLoad();
                        _online.erase(iter);
                        _offline.emplace_back(which);
                        break;
                    }
                }
                _mut.unlock();
            }

            void OnlineHost()
            {
                _mut.lock();
                //将所有的离线主机中的全加入在线主机中
                _online.insert(_online.end(),_offline.begin(),_offline.end());
                _offline.erase(_offline.begin(),_offline.end());
                _mut.unlock();

                LOG(INFO) << "所有主机都上线了" << "\n";
            }

 
            void ShowHostlist()
            {
                _mut.lock();
                //打印在线主机列表
                std::cout << "在线主机列表: ";

                for(int i = 0;i < _online.size();++i)
                {
                    std::cout << _online[i] << " ";
                }

                std::cout << "\n";

                std::cout << "离线主机列表: ";

                for(int i = 0;i < _offline.size();++i)
                {
                    std::cout << _offline[i] << " ";
                }

                std::cout << "\n";
                _mut.unlock();
            }
    };

负载均衡模块主要有两个类:

  • HostMachine类:描述一台提供编译网络服务的主机,它有如下字段:
    • std::string _ip:主机部署的ip地址。
    • int _port:主机服务进程对应的端口号。
    • uint64_t _load:主机当前的负载情况(正在给多少客户端提供编译运行服务)。
    • std::mutex* _mutex:锁,用于保持线程安全,httplib内部是多线程的,可能有多个线程同时对同一主机的_load进行操作,不加锁就会有数据不一致的情况。
  • LoadBalance类:对主机进行管理,包括管理在线主机列表、离线主机列表,创建LoadBalance对象时,同时会将配置文件加载到内存中,这个时候就会创建HostMachine对象,使用vector存储它,同时将下标作为id添加进在线主机列表:
    • std::vector<HostMachine> _host:维护所有的主机对象。
    • std::vector<int> _online:维护在线主机的id(对应在_host中的下标)。
    • std::vector<int> _offline:所有离线的主机的id。
    • std::mutex _mut:锁,保护当前类中的数据安全,当前类的_online_offline可能会有多个线程对其进行add/del,不加锁保护,就会出现数据不一致的情况。

SmartChoice方法介绍:

c 复制代码
 bool SmartChoice(int *id, Machine **m)
  • 函数功能:负载均衡的选择当前负载最小的主机。
  • 函数参数
    • int* id:输出型参数,获得当前选择的主机的id
    • Machine **m:输出型参数,获得当前选择主机的地址。
  • 函数返回值bool值,true表示选择成功,反之失败。

函数流程图:

因为是互斥锁,只能有一个线程同时获取,上锁之后由于其它会对online数组的值有影响的操作都会申请这个互斥锁,所以在上述方法中加锁就可以保证一定不会出现数组越界的情况。

一键上线功能介绍

有时候我们的编译服务可能因为网络的原因离线了,在Oj_Server服务中难道需要重启启动吗?并不需要,我们利用信号机制就可以做到一键上线所有主机 ,上线所有主机的过程就是将所有离线主机的id都添加到在线主机列表,并清空离线主机列表中的内容:

c++ 复制代码
"Oj_Control.hpp":
 class LoadBalance
 {
public:
    void OnlineHost()
    {
        _mut.lock();
        //将所有的离线主机中的全加入在线主机中
        _online.insert(_online.end(),_offline.begin(),_offline.end());
        _offline.erase(_offline.begin(),_offline.end());
        _mut.unlock();

        LOG(INFO) << "所有主机都上线了" << "\n";
    }

 }

class Control
{
public:
    void RecoveryMachine()
    {
        _LoadBalance.OnlineHost();
    }
}
"Oj_Server.cc":
#include<signal.h>
using namespace control_ns;
Control* controlptr;

void HandlerSignal(int signal)
{
    controlptr->RecoveryMachine();
}

int main()
{
    signal(SIGQUIT,HandlerSignal);//ctrl /
    ...
}

测试Oj_Server模块

现在我们没有前端页面,只能借助Postman工具进行功能的基本测试。

  1. 测试获取所有题目的功能:

  2. 测试获取单个题目的详细信息的功能:

  3. 测试判题功能:

这里我们是随便输入的代码肯定会报编译错误。

前端的设计

  1. 主页:

    html 复制代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>在线OJ平台</title>
        <style>
            /* 全局样式 */
            body {
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                margin: 0;
                padding: 0;
                background-color: #f5f5f5;
                color: #333;
            }
    
            /* 页头 */
            header {
                background-color: #2c3e50;
                color: white;
                padding: 40px 20px;
                text-align: center;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }
    
            header h1 {
                font-size: 36px;
                margin-bottom: 10px;
            }
    
            header p {
                font-size: 18px;
                color: #ecf0f1;
            }
    
            /* 导航栏 */
            nav {
                background-color: #34495e;
                overflow: hidden;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }
    
            nav a {
                float: left;
                display: block;
                color: white;
                text-align: center;
                padding: 14px 20px;
                text-decoration: none;
                transition: background-color 0.3s ease;
            }
    
            nav a:hover {
                background-color: #3b536b;
            }
    
            /* 内容区域 */
            .container {
                padding: 30px;
                max-width: 800px;
                margin: 0 auto;
                background-color: white;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
                border-radius: 8px;
                margin-top: 30px;
                margin-bottom: 80px; /* 避免内容被页脚遮挡 */
            }
    
            .container h2 {
                color: #2c3e50;
                font-size: 28px;
                margin-bottom: 20px;
            }
    
            .container p {
                font-size: 16px;
                line-height: 1.6;
                color: #555;
            }
    
            .container ul {
                list-style-type: none;
                padding: 0;
            }
    
            .container ul li {
                background-color: #f8f9fa;
                margin: 10px 0;
                padding: 10px;
                border-radius: 4px;
                font-size: 16px;
                color: #333;
                box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            }
    
            /* 页脚 */
            footer {
                background-color: #34495e;
                color: white;
                text-align: center;
                padding: 15px;
                position: fixed;
                bottom: 0;
                width: 100%;
                box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
            }
    
            footer p {
                margin: 0;
                font-size: 14px;
            }
        </style>
    </head>
    <body>
        <!-- 页头 -->
        <header>
            <h1>在线OJ平台</h1>
            <p>欢迎来到在线编程评测平台</p>
        </header>
    
        <!-- 导航栏 -->
        <nav>
            <a href="/">首页</a>
            <a href="/All_Questions">题库</a>
            <a href="/">竞赛</a>
            <a href="/">排名</a>
            <a href="/">关于</a>
        </nav>
    
        <!-- 内容区域 -->
        <div class="container">
            <h2>平台简介</h2>
            <p>在线OJ平台是一个提供编程题目和在线评测的系统,支持多种编程语言,帮助用户提升编程能力。</p>
            <h2>最新动态</h2>
            <ul>
                <li>新增了C++题目。</li>
                <li>2025年秋季编程竞赛即将开始。</li>
                <li>优化了评测系统的性能。</li>
            </ul>
        </div>
    
        <!-- 页脚 -->
        <footer>
            <p>&copy; 2025 在线OJ平台. 保留所有权利.</p>
        </footer>
    </body>
    </html>
  2. 获取所有题的模板html

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>在线OJ平台-题目列表</title>
        <style>
            /* 起手式, 100%保证我们的样式设置可以不受默认影响 */
            * {
                /* 消除网页的默认外边距 */
                margin: 0px;
                /* 消除网页的默认内边距 */
                padding: 0px;
                box-sizing: border-box;
            }
    
            html,
            body {
                width: 100%;
                height: 100%;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                background-color: #f5f5f5;
            }
    
            .container .navbar {
                width: 100%;
                height: 60px;
                background-color: #2c3e50;
                /* 给父级标签设置overflow,取消后续float带来的影响 */
                overflow: hidden;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }
    
            .container .navbar a {
                /* 设置a标签是行内块元素,允许你设置宽度 */
                display: inline-block;
                /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
                width: 100px;
                /* 设置字体颜色 */
                color: #ecf0f1;
                /* 设置字体的大小 */
                font-size: 16px;
                /* 设置文字的高度和导航栏一样的高度 */
                line-height: 60px;
                /* 去掉a标签的下划线 */
                text-decoration: none;
                /* 设置a标签中的文字居中 */
                text-align: center;
                transition: background-color 0.3s ease;
            }
    
            /* 设置鼠标事件 */
            .container .navbar a:hover {
                background-color: #34495e;
            }
    
            .container .navbar .login {
                float: right;
            }
    
            .container .question_list {
                padding-top: 50px;
                width: 800px;
                height: 100%;
                margin: 0px auto;
                text-align: center;
            }
    
            .container .question_list table {
                width: 100%;
                font-size: 16px;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                margin-top: 30px;
                background-color: white;
                border-collapse: collapse;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
                border-radius: 8px;
                overflow: hidden;
            }
    
            .container .question_list h1 {
                color: #2c3e50;
                font-size: 32px;
                margin-bottom: 20px;
            }
    
            .container .question_list table th,
            .container .question_list table td {
                padding: 15px;
                text-align: center;
            }
    
            .container .question_list table th {
                background-color: #34495e;
                color: white;
                font-weight: bold;
            }
    
            .container .question_list table tr:nth-child(even) {
                background-color: #f8f9fa;
            }
    
            .container .question_list table tr:hover {
                background-color: #e9ecef;
            }
    
            .container .question_list table .item a {
                text-decoration: none;
                color: #2c3e50;
                transition: color 0.3s ease;
            }
    
            .container .question_list table .item a:hover {
                color: #3498db;
                text-decoration: underline;
            }
    
            .container .footer {
                width: 100%;
                height: 60px;
                text-align: center;
                line-height: 60px;
                color: #7f8c8d;
                margin-top: 30px;
                background-color: #ecf0f1;
                box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
            }
    
            .container .footer h4 {
                font-size: 14px;
                font-weight: normal;
            }
        </style>
    </head>
    
    <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>在线OJ题目列表</h1>
                <table>
                    <tr>
                        <th class="item">编号</th>
                        <th class="item">标题</th>
                        <th class="item">难度</th>
                    </tr>
                    {{#OneQuestionList}}
                    <tr>
                        <td class="item">{{number}}</td>
                        <td class="item"><a href="/Question/{{number}}">{{title}}</a></td>
                        <td class="item">{{star}}</td>
                    </tr>
                    {{/OneQuestionList}}
                </table>
            </div>
            <div class="footer">
                <h4>@小镇敲码人</h4>
            </div>
        </div>
    </body>
    
    </html>
  3. 单个题目的html模板:

    html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{{number}}.{{title}}</title>
        <!-- 引入ACE插件 -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript" charset="utf-8"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"
            charset="utf-8"></script>
        <!-- 引入jquery CDN -->
        <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
    
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
    
            html,
            body {
                width: 100%;
                height: 100%;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                background-color: #f5f5f5;
            }
    
            .container .navbar {
                width: 100%;
                height: 60px;
                background-color: #2c3e50;
                overflow: hidden;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            }
    
            .container .navbar a {
                display: inline-block;
                width: 100px;
                color: white;
                font-size: 16px;
                line-height: 60px;
                text-decoration: none;
                text-align: center;
                transition: background-color 0.3s ease;
            }
    
            .container .navbar a:hover {
                background-color: #34495e;
            }
    
            .container .navbar .login {
                float: right;
            }
    
            .container .part1 {
                width: 100%;
                height: 600px;
                overflow: hidden;
                margin-top: 20px;
            }
    
            .container .part1 .left_desc {
                width: 50%;
                height: 600px;
                float: left;
                overflow: scroll;
                background-color: white;
                padding: 20px;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
                border-radius: 8px;
                margin-right: 10px;
            }
    
            .container .part1 .left_desc h3 {
                color: #2c3e50;
                font-size: 24px;
                margin-bottom: 15px;
            }
    
            .container .part1 .left_desc pre {
                font-size: 16px;
                line-height: 1.6;
                color: #555;
                white-space: pre-wrap;
                word-wrap: break-word;
            }
    
            .container .part1 .right_code {
                width: calc(50% - 10px);
                height: 600px;
                float: right;
                background-color: white;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
                border-radius: 8px;
            }
    
            .container .part1 .right_code .ace_editor {
                height: 600px;
                border-radius: 8px;
            }
    
            .container .part2 {
                width: 100%;
                overflow: hidden;
                margin-top: 20px;
            }
    
            .container .part2 .result {
                width: calc(100% - 140px);
                float: left;
                background-color: white;
                padding: 20px;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
                border-radius: 8px;
                margin-right: 10px;
            }
    
            .container .part2 .result pre {
                font-size: 16px;
                color: #333;
            }
    
            .container .part2 .btn-submit {
                width: 120px;
                height: 50px;
                font-size: 16px;
                float: right;
                background-color: #26bb9c;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                transition: background-color 0.3s ease;
            }
    
            .container .part2 .btn-submit:hover {
                background-color: #1f9c81;
            }
        </style>
    </head>
    
    <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>
            // 初始化ACE编辑器
            var editor = ace.edit("code");
            editor.setTheme("ace/theme/monokai");
            editor.session.setMode("ace/mode/c_cpp");
            editor.setFontSize(16);
            editor.getSession().setTabSize(4);
            editor.setReadOnly(false);
    
            // 启用提示菜单
            ace.require("ace/ext/language_tools");
            editor.setOptions({
                enableBasicAutocompletion: true,
                enableSnippets: true,
                enableLiveAutocompletion: true
            });
    
            function submit() {
                // 收集代码和题号
                var code = editor.getSession().getValue();
                var number = $("#number").text();
                var judge_url = "/judge/" + number;
    
                // 发起AJAX请求
                $.ajax({
                    method: 'POST',
                    url: judge_url,
                    dataType: 'json',
                    contentType: 'application/json;charset=utf-8',
                    data: JSON.stringify({
                        'code': code,
                        'input': ''
                    }),
                    success: function (data) {
                        show_result(data);
                    }
                });
            }
    
            function show_result(data) {
                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);
                }
            }
        </script>
    </body>
    
    </html>

顶层makefile设计

一键编译:

makefile 复制代码
.PHONY:all
all:
	@cd Compile_Run;\
	make;\
	cd -;\
	cd Oj_Server;\
	make;\
	cd -;

.PHONY:output
output:
	@mkdir -p output/Compile_Server;\
	 mkdir -p output/Oj_Server;\
	 cp -rf Compile_Run/Compile_Server output/Compile_Server;\
	 cp -rf Compile_Run/temp output/Compile_Server;\
	 cp -rf Oj_Server/Oj_Server output/Oj_Server;\
	 cp -rf Oj_Server/Template_html output/Oj_Server;\
	 cp -rf Oj_Server/lib output/Oj_Server;\
	 cp -rf Oj_Server/include output/Oj_Server;\
	 cp -rf Oj_Server/Config output/Oj_Server;\
	 cp -rf Oj_Server/wwwroot output/Oj_Server;

.PHONY:clean
clean:
	@cd Compile_Run;\
	make clean;\
	cd -;\
	cd Oj_Server;\
	make clean;\
	cd -;\
	rm -rf output;

本项目的亮点

  1. 技术选型与模块化设计

    • 核心技术栈

      项目采用 C++ 作为后端核心语言,结合 cpp-httplib 实现轻量级网络服务,利用 Ctemplate 进行动态网页渲染,jsoncpp 处理数据序列化与反序列化,技术选型高效且轻量。
      优势:高性能、低延迟,适合处理高并发编译请求。

    • 模块化设计

      系统分为 Comm(公共模块)、compile_server(编译服务)、oj_server(前端与负载均衡)三大模块,职责清晰。
      亮点

      • 通过 compile_server 独立处理编译运行,实现服务解耦。
      • oj_server 采用 MVC 模式(Model-View-Control),提升代码可维护性。
  2. 负载均衡与容错机制

    • 动态负载均衡

      基于主机负载的智能选择算法,优先分配低负载节点,避免单点故障。

      创新点:支持一键上线所有离线主机(通过信号机制),提升服务可用性。

    • 错误隔离与恢复

      自动检测编译服务节点故障,将其移出在线列表,保障服务稳定性。

  3. 安全与资源控制

    • 代码沙箱隔离

      使用 setrlimit 限制进程资源(CPU 时间、内存),防止恶意代码耗尽系统资源。

      亮点 :通过信号机制(如 SIGXCPU)强制终止超限进程,确保系统安全。

    • 临时文件管理

      编译生成的临时文件(源码、可执行文件)在运行后自动清理,避免磁盘空间泄漏。

  4. 前端与用户体验

    • 动态网页渲染

      使用 Ctemplate 模板引擎动态生成题目列表和详情页。

      亮点:集成 ACE 编辑器,提供代码高亮、自动补全功能,用户体验接近主流 OJ 平台。

    • 异步判题反馈

      前端通过 AJAX 异步提交代码并实时显示编译结果,减少页面刷新。

项目扩展方向

1. 功能扩展

  • 多语言支持
    扩展 compile_server,支持 Python、Java 等语言的编译/解释运行,需适配不同语言的沙箱配置。
  • 用户系统与竞赛功能
    • 增加用户注册/登录模块,集成 JWT 鉴权。
    • 支持创建编程竞赛,实时排名榜(基于 Redis 缓存提升性能)。
  • 题目管理与测试用例自动化
    • 开发管理员后台,支持题目上传、测试用例批量导入。
    • 实现自动化测试框架,验证题目正确性。

2. 性能优化

  • 分布式编译集群
    compile_server 部署为多节点集群,结合 ZooKeeper 实现服务发现与动态扩缩容。
  • 结果缓存与异步队列
    • 使用 Redis 缓存高频题目的编译结果,减少重复计算。
    • 引入 RabbitMQ 异步处理判题请求,削峰填谷。

3. 安全性增强

  • 代码沙箱强化
    • 使用 Docker 或 Linux Namespace 实现进程级隔离,彻底防止恶意代码逃逸。
    • 静态代码分析(如 Clang 静态检查器),拦截危险系统调用(如 fork)。
  • 流量控制与防刷
    集成 Nginx 限流模块,防止高频提交攻击。

4. 运维与监控

  • Prometheus + Grafana 监控
    采集编译节点负载、请求耗时等指标,实现可视化监控与告警。
  • 日志聚合
    使用 ELK(Elasticsearch + Logstash + Kibana)集中管理日志,便于故障排查。
相关推荐
奇妙之二进制1 小时前
基于linux平台的C语言入门教程(8)算术运算符
linux·运维·c语言
Estrella-q2 小时前
深入理解Linux网络随笔(五):深度理解本机网络I/O
linux·运维·网络
堕落年代2 小时前
WebRTC建立Description的通信的实际的原理
网络·webrtc
全栈开发圈2 小时前
新书速览|云原生Kubernetes自动化运维实践
运维·云原生·kubernetes
CZIDC4 小时前
网络知识编-数据链路层(以太网 & 局域网通信 & ARP协议 & ARP 欺骗 & DDos 攻击)
网络·网络协议·ddos
仙女很美哦4 小时前
bp抓IOS的包
websocket·网络协议·tcp/ip·http·网络安全·https·udp
开开心心就好4 小时前
免费提供多样风格手机壁纸及自动更换功能的软件
android·python·网络协议·tcp/ip·macos·智能手机·pdf
柃歌5 小时前
【USTC 计算机网络】第二章:应用层 - TCP & UDP 套接字编程
websocket·网络协议·tcp/ip·计算机网络·udp
怀璧其罪6 小时前
安装 OpenSSL 1.1.1 的完整脚本适用于 Ubuntu 22.04 系统
linux·服务器·数据库
fttony20206 小时前
ubuntu20.04关机进程阻塞解决方法
运维·服务器·ubuntu