C++项目:【负载均衡式在线OJ】

这篇具有很好参考价值的文章主要介绍了C++项目:【负载均衡式在线OJ】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

文章目录

一、项目介绍

二、技术栈与开发环境

1.所用技术:

2.开发环境:

三、项目演示

1.运行代码

2.进入项目首页

3.题目列表

4.点击具体一道题

5.编辑代码并提交

四、项目思维导图

五、项目宏观结构

六、Comm公共模块

1.日志工具log.hpp

2.其他工具util.hpp

七、CompilerServer模块

1.整体层次如图

2.编译模块compiler.hpp

3.运行模块runner.hpp

4.编译+运行compile_run.hpp

5.Compiler模块compiler_server.cc

八、基于MVC结构的OJServer模块

1. 什么是MVC结构

2.整体层次如图

3.Model模块oj_model.hpp​编辑

3.1文件版本

3.2数据库版本

4.题库的结构

5.view模块oj_view.hpp

6.oj_control模块oj_control.hpp

6.1.构建题目列表和单个题目网页

6.2.负载均衡模块

6.2.1机器类的设计

6.2.2负载均衡模块设计

6.2.3负载均衡的实现:

6.2.4判题模块

7.oj_server模块oj_server.cc

九、前端页面的设计(了解)

1. indx.html

2. all_questions.html

3. one_question.html

十、项目扩展

十一、项目所需工具

1.升级 gcc

2.安装 jsoncpp

3.安装 cpp-httplib

4.安装boost库

5.安装与测试 ctemplate

6.使用Ace在线编辑器

7.MySQL 建表

十二、项目源码


一、项目介绍

        这个项目是基于负载均衡的在线OJ平台,用户可以在浏览器访问各个题目,在编辑区编写代码提交,后端能够自动分配服务器资源,保持平衡的情况下为用户提供良好的编程运行环境,让代码快速运行和提交。


二、技术栈与开发环境

1.所用技术:

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

2.开发环境:

  • Centos 7 云服务器
  • vscode
  • Mysql Workbench

三、项目演示

1.运行代码

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


2.进入项目首页

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


3.题目列表

可以根据需求增加更多的题目 

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


4.点击具体一道题

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


 5.编辑代码并提交

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


四、项目思维导图

超清图片我放在了Gitee上面,需要可以自行下载(文章末尾有链接)

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


五、项目宏观结构

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

  1. comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。
  2. compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。
  3. oj_server : 请求题目列表;请求一个具体题目,且有编辑区 ;提交判题请求。采用MVC的设计模式,使用负载均衡,访问文件或数据库,调用编译模块,以及把题目列表和编辑界面展示给用户。

注意:我们只实现类似 leetcode 的题目列表+在线编程功能。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

        用户直接访问的是OJServer模块,OJServer收到请求后会进行功能路由,根据不同的请求给用户返回不同的结果,如果用户是编写题目提交代码,那么OJServer模块会根据后端的CompilerServer服务器的负载情况,负载均衡地选择主机提供的编译运行服务,然后拿到编译运行结果返回给用户。Compiler服务器和OJ服务器,两个模块之间采用http网络通信,把编译运行模块部署在多台服务器上,OJ服务器只需要一台,能够把用户的请求发送给后端CompilerServer。


六、Comm公共模块

comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


1.日志工具log.hpp

namespace ns_log
{
    // 引入公共功能
    using namespace ns_util;

    // 日志等级
    enum
    {
        INFO, // 常规,正常的
        DEBUG, // 调试
        WARNING, // 告警
        ERROR, // 错误
        FATAL // 致命错误
    };

    // 打印日志
    // 参数:日志等级,在哪一个文件,当前在哪一行
    // 使用方法:Log() << "message"
    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; // 不要endl进行刷新

        return std::cout;
    }

    // 再用宏封装一下
    // 使用方法:例如:LOG(INFO) << "message" << "\n"
    #define LOG(level) Log(#level, __FILE__, __LINE__)
}

2.其他工具util.hpp

// 传入一个文件名,自动形成路径与后缀
namespace ns_util
{
    // 引入存储临时文件的路径
    const std::string temp_path = "./temp/";
    
    // 时间功能
    class TimeUtil
    {
    public:
        // 获取秒级别时间戳
        static std::string GetTimeStamp()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr); // 时区不关心,所以设置成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); // 秒->毫秒=秒*1000,微秒->毫秒=微秒/1000
        }
    };

    // 对路径操作的方法
    class PathUtil
    {
    public:
        // 构建文件路径+后缀的完整文件名
        static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
        {
            std::string path_name = temp_path; // "./temp/"
            path_name += file_name; // "./temp/12345"
            path_name += suffix; // "./temp/12345.后缀"
            return path_name;
        }

        /**************************编译时需要有的临时文件**************************/

        // 构建源文件路径+后缀的完整文件名
        // 例如:12345 -> ./temp/12345.cpp
        static std::string Src(const std::string &file_name)
        {
            return AddSuffix(file_name, ".cpp");
        }

        // 构建可执行程序的完整路径+后缀名
        // 例如:12345 -> ./temp/12345.exe
        static std::string Exe(const std::string &file_name)
        {
            return AddSuffix(file_name, ".exe");
        }

        // 构建该程序对应的编译时错误文件的完整路径+后缀名
        // 例如:12345 -> ./temp/12345.compile_error
        static std::string CompilerError(const std::string &file_name)
        {
            return AddSuffix(file_name, ".compile_error");
        }


        /**************************运行时需要有的临时文件**************************/

        // 构建标准输入文件
        static std::string Stdin(const std::string &file_name)
        {
            return AddSuffix(file_name, ".stdin");
        }

        // 构建标准输出文件
        static std::string Stdout(const std::string &file_name)
        {
            return AddSuffix(file_name, ".stdout");
        }

        // 构建标准错误文件
        static std::string Stderr(const std::string &file_name)
        {
            return AddSuffix(file_name, ".stderr");
        }
    };

    // 对文件操作的方法
    class FileUtil
    {
    public:
        // 判断文件是否存在
        static bool IsFileExists(const std::string &path_name)
        {
            struct stat st;
            if (stat(path_name.c_str(), &st) == 0)
            {
                // 获取属性成功,表示文件已经存在
                return true;
            }

            return false;
        }

        // 形成一个唯一的文件名,没有目录没有后缀
        // 毫秒级时间戳+原子性递增唯一值: 来保证唯一性
        static std::string UniqFileName()
        {
            static std::atomic_uint id(0); // 原子性的计数器
            id++; // 计数器++

            std::string ms = TimeUtil::GetTimeMs(); // 毫秒级时间戳
            std::string uniq_id = std::to_string(id); // 获得唯一id
            
            return ms + "_" + uniq_id;
        }

        // 将代码写入该文件
        // 参数:target为要写入的文件名,content为要写入的内容
        static bool WriteFile(const std::string &target, std::string &content)
        {
            std::ofstream out(target);
            if (!out.is_open())
            {
                // 如果没有被打开成功
                return false;
            }

            out.write(content.c_str(), content.size()); // 写入内容
            out.close(); // 关闭文件

            return true;
        }

        // 把所有的文件内容读出来
        // 参数:target为要读取的文件名,content用于存储读到的数据, keep用于判断是否保留\n
        static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
        {
            (*content).clear(); // 先清空content,为了不影响第一次读取

            std::ifstream in(target, std::ios::binary);
            if (!in.is_open())
            {
                return false;
            }

            std::string line;
            // 注意:
            // getline:不能保存行分隔符
            // getline:内部重载了强制类型转化
            while (std::getline(in, line))
            {
                (*content) += line;
                (*content) += (keep ? "\n" : ""); // 判断是否保留\n
            }

            in.close(); // 关闭文件

            return true;
        }
    };

    // 字符串工具
    class StringUtil
    {
    public:
        // 切分字符串
        /******************************************************
         * 参数:
         * str:输入型,目标要切分的字符串
         * target:输出型,保存切分完毕的结果
         * sep:指定的分隔符
        ******************************************************/
        static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string sep)
        {
            // 用boost库里面的字符串切分功能
            boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on);
        }
    };

    
}

七、CompilerServer模块

compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

1.整体层次如图

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


2.编译模块compiler.hpp

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

// 只负责进行代码的编译
namespace ns_compiler
{
    using namespace ns_util; // 引入路径拼接功能
    using namespace ns_log; // 引入日志打印功能


    // 编译功能
    class Compiler
    {
    public:
        Compiler()
        {}

        ~Compiler()
        {}

        // 编译代码:
        // 返回值:编译成功:true,编译错误:false
        // 输入参数:编译的文件名:file_name
        // 例如:file_name: 12345(只传进来一个文件名,不带后缀,后缀我们自己添加)
        // 我们会生成三个文件
        // 12345 -> ./temp/12345.cpp 源文件
        // 12345 -> ./temp/12345.exe 可执行文件
        // 12345 -> ./temp/12345.stderr 标准错误文件
        static bool Compile(std::string &file_name)
        {
            pid_t pid = fork(); // 创建一个子进程进行编译代码

            if (pid < 0)
            {
                // 如果创建子进程失败就直接退出
                LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
                return false;
            }
            else if (pid == 0)
            {
                umask(0); // 先将umask清零,让其不受平台的影响

                // 先将编译时错误文件打开
                int _compile_error = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if (_compile_error < 0)
                {
                    // 如果打开文件失败
                    LOG(WARNING) << "没有成功形成compile_error文件" << "\n";
                    exit(1);
                }
                
                // 重定向标准错误到_compile_error
                dup2(_compile_error, 2);


                // 注意:程序替换,并不影响进程的文件描述符表
                // 子进程:调用编译器,完成对代码的编译工作
                // g++ -o target src -std=c++11
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\
                PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr/*不要忘记写nullptr*/); // 用g++替换当前的子进程

                // 如果程序替换失败
                LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";
                exit(2);
            }
            else
            {
                // 父进程
                waitpid(pid, nullptr, 0);
                
                // 判断编译是否成功(判断是否生成可执行文件)
                if (FileUtil::IsFileExists(PathUtil::Exe(file_name))) 
                {
                    LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";
                    return true;
                }
            }

            // 如果父进程走到这里,就说明编译失败,没有形成可执行文件
            LOG(ERROR) << "编译失败,没有形成可执行文件!" << "\n";
            return false;
        }
    };
}



3.运行模块runner.hpp

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

namespace ns_runner
{
    using namespace ns_util; // 引入路径拼接功能
    using namespace ns_log; // 引入日志打印功能

    class Runner
    {
    public:
        Runner() 
        {}

        ~Runner() 
        {}
    
    public:
        // 提供设置进程占用资源大小的接口
        static void SetProcLimit(int _cpu_limit, int _mem_limit)
        {
            // 设置cpu占用时长
            struct rlimit cpu_rlimit;
            cpu_rlimit.rlim_max = RLIM_INFINITY;
            cpu_rlimit.rlim_cur = _cpu_limit;

            setrlimit(RLIMIT_CPU, &cpu_rlimit);

            // 设置内存大小
            struct rlimit mem_rlimit;
            mem_rlimit.rlim_max = RLIM_INFINITY;
            mem_rlimit.rlim_cur = _mem_limit * 1024; // 乘以1024,让单位KB变成字节

            setrlimit(RLIMIT_AS, &mem_rlimit);
        }

        // 运行并判断运行是否成功
        // 注意:我们这里实现的功能,也只需要指明文件名即可,不需要带路径和后缀
        /***************************************************************
         * 将这里的返回值设置成int而不设置成bool,是因为:
         * 返回值 > 0 : 程序异常了,退出时收到了信号,返回值就是对应的信号编号
         * 返回值 == 0 : 程序正常运行完毕的,结果保存到了对应的临时文件中
         * 返回值 < 0 : 内部错误:打开文件失败、创建子进程失败等
         * 
         * 参数介绍:
         * cpu_limit : 该程序运行的时候,可以使用的最大cpu资源上限
         * mem_limit : 该程序运行的时候,可以使用的最大的内存大小(KB)
        ***************************************************************/
        static int Run(const std::string &file_name, int cpu_limit, int mem_limit )
        {
            // 获取运行需要的文件
            std::string _execute = PathUtil::Exe(file_name);    // 获得可执行程序的完整文件名
            std::string _stdin   = PathUtil::Stdin(file_name);  // 获取标准输入文件
            std::string _stdout  = PathUtil::Stdout(file_name); // 获取标准输出文件
            std::string _stderr  = PathUtil::Stderr(file_name); // 获取标准错误文件

            umask(0); // 先将umask清零,让其不受平台的影响

            // 打开运行需要的文件
            int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);
            int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
            int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);

            // 如果有任何一个文件打开失败,就退出程序(因为如果打开失败就无法获取相应的数据和输出结果,再继续运行就没有意义了)
            if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0) 
            {
                LOG(ERROR) << "运行时打开标准文件失败" << "\n";
                return -1; // 代表文件打开失败
            }


            // 创建一个子进程运行程序
            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(ERROR) << "运行时创建子进程失败" << "\n";

                // 如果创建失败,关闭需要的文件
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);

                return -2; // 代表创建子进程失败
            }
            else if (pid == 0)
            {
                // 子进程创建成功
                // 将标准输入,标准错误,标准错误都进行重定向到刚才打开的文件
                dup2(_stdin_fd, 0);
                dup2(_stdout_fd, 1);
                dup2(_stderr_fd, 2);

                // 设置资源限制
                SetProcLimit(cpu_limit, mem_limit);

                // 然后替换程序,将子进程替换成需要的可执行文件
                // 参数:带路径的可执行文件,要怎么执行......
                execl(_execute.c_str()/*我要执行谁*/, _execute.c_str()/*我想在命令行上如何执行该程序*/, nullptr);

                // 如果替换失败
                exit(1);
            }
            else
            {
                // 父进程用不到这些文件,所以先将其关闭
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);

                // 让父进程等待子进程
                int status = 0; // 拿到子进程的退出结果
                waitpid(pid, &status, 0);

                // 程序如果运行异常,一定是因为收到了信号!
                LOG(INFO) << "运行完毕,info:" << (status & 0x7f) << "\n";
                return status & 0x7f;
            }
        }
    };
}

4.编译+运行compile_run.hpp

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

namespace ns_compile_and_run
{
    using namespace ns_log;      // 引入日志打印功能
    using namespace ns_util;     // 引入路径拼接功能
    using namespace ns_compiler; // 引入编译功能
    using namespace ns_runner;   // 引入运行功能

    class CompileAndRun
    {
    public:
        // 清空所有的临时文件
        static void RemoveTempFile(const std::string &file_name)
        {
            // 特点:清理文件的个数是不确定的,但是有哪些我们是知道的
            
            // 删除源文件
            std::string _src = PathUtil::Src(file_name); // 获取源文件的完整路径
            if (FileUtil::IsFileExists(_src)) unlink(_src.c_str()); // 如果文件存在,就将其移除
            
            // 删除编译错误文件
            std::string _compile_error = PathUtil::CompilerError(file_name);
            if (FileUtil::IsFileExists(_compile_error)) unlink(_compile_error.c_str());

            // 删除可执行程序
            std::string _execute = PathUtil::Exe(file_name);
            if (FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());

            // 删除标准输入
            std::string _stdin = PathUtil::Stdin(file_name);
            if (FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());

            // 删除标准输出
            std::string _stdout = PathUtil::Stdout(file_name);
            if (FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());

            // 删除标准错误
            std::string _stderr = PathUtil::Stderr(file_name);
            if (FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());
        }

        // 将信号转化成为报错的原因
        // code > 0 : 进程收到了信号导致异常错误
        // code < 0 : 整个过程非运行报错(代码为空,编译报错等)
        // cod == 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: // 代码编译的时候发生了错误
                FileUtil::ReadFile(PathUtil::CompilerError(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: 用户提交的代码
         * 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 Start(const std::string &in_json, std::string *out_json)
        {
            // 将拿到的数据做反序列化
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value);

            std::string code = in_value["code"].asString();   // 拿到用户提交的代码
            std::string input = in_value["input"].asString(); // 拿到用户输入的数据
            int cpu_limit = in_value["cpu_limit"].asInt();    // 拿到上层给的cpu占用时长
            int mem_limit = in_value["mem_limit"].asInt();    // 拿到上层给的内存大小

            // 创建输出返回的数据
            int status_code = 0; // 状态码
            Json::Value out_value;
            int run_result = 0;    // 运行结果
            std::string file_name; // 需要内部形成的唯一文件名(因为goto语句之间不能出现定义,所以就在这里定义)

            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; // 未知错误
                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;
            }
            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);
        }
    };
}

5.Compiler模块compiler_server.cc

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

// 使用手册
void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}

// 调用方式:./compile_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        // 如果使用方法不对
        Usage(argv[0]);
        return 1;
    }

    Server svr; // 创建一个服务器
    
    // 参数:req:用户的需求,resp:服务器的相应
    svr.Post("/compile_and_run", [](const Request &req, Response &resp){
    // 用户请求的服务正文就是我们想要的json string
    // in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
    // out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}

        std::string in_json = req.body;
        std::string out_json;
        if (!in_json.empty())
        {
            // 编译并运行用户传过来的代码
            CompileAndRun::Start(in_json, &out_json);

            // 要响应的内容
            resp.set_content(out_json, "application/json;charset=utf_8");
        }
    });
 
    // 让服务器在所有ip,指定端口服务
    svr.listen("0.0.0.0", atoi(argv[1])); // 启动http服务

    return 0;
}

八、基于MVC结构的OJServer模块

        OJServer模块是直接和用户交互的,用户访问OJ系统,我需要有一个首页,其次需要有一个题目列表网页供用户选择题目,再者还需要一个可以给用户写代码做题的网页,并且可以提交代码,判断用户提交的代码是否正确。


总结用户的请求分为三种:

  1. 请求题目列表
  2. 请求一个具体的题目,并且需要有编译区域
  3. 提交,判题请求 OJServer模块主要要根据这三种请求提供对应的功能。

整个模块采用的是MVC的设计模式进行设计
通过这个设计模式,把数据,业务逻辑和网页界面进行了分离。

1. 什么是MVC结构

        经典MVC模式中,M是指业务模型,V是指用户界面(视图),C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面。 

  • M:model表示的是模型,代表业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据时中立的,模型与数据格式无关,这样一个模型就能够为多个视图提供数据,由于应用于模型的代码只需要写一次就可以被多个视图重用,所以减少了代码的重复性,
  • V:view表示的视图,代表用户看到并与之交互的界面。在视图中没有真正的处理发生,它只是作为一种输出数据并允许用户操作的方式。
  • C:controller表示的是控制器,控制器接收用户的输入并调用模型(M)和视图(V)去完成用户需求。控制器本身不输出任何东西和任何处理。它只接收请求并决定调用那个模型构建去处理请求,然后再确定用那个视图来显示返回的数据。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

整个模块就包含四个部分:

  1. oj_model模块:负责模块前两个功能的数据部分,通过与题库交互,得到所有题目的信息或者某一个题目的信息
  2. oj_view模块:负责渲染用户得到网页,根据用户提交的不同请求,渲染不同的题目信息
  3. oj_control模块:负责整个OJServer模块的业务逻辑控制。对下负责负载均衡式的选择主机请求编译服务,对上根据用户的三种请求,配合上面两个模块,完成对应的功能。
  4. oj_server模块:搭建http服务,根据用户的请求,完成功能路由,调用control模块的对应方法完成功能

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


2.整体层次如图

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


3.Model模块oj_model.hpp

        oj_model模块主要是和数据交互的,这里的数据就是我们后端文件或者数据库当中的题目信息,题目应该包含如下的信息:

  1. 题目的编号(1)
  2. 题目的标题(求最大值)
  3. 题目的难度(简单、中等、困难)
  4. 题目的时间要求(1s)
  5. 题目的空间要求(30000KB)
  6. 题目的描述(给定一个数组,求最大值)
  7. 题目预设给用户在线编辑的代码(#include<iostream>...)
  8. 题目的测试用例

        到这里我们就需要有自己对应的题库,我们这个模块当中新增一个目录questions,用来存放我们的题库,这个questions目录下包含题目列表(文件形式)和每个题目的文件夹(其中又包含题目的描述、题目预设给用户在线编辑的代码header和题目的测试用例tail)


3.1文件版本

// 文件版
namespace ns_model
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;

    struct Question
    {
        string number; // 题目编号(唯一)
        string title;  // 题目的标题
        string star;   // 题目的难度:简单 中等 困难
        int cpu_limit;      // 题目的时间要求(S)
        int mem_limit;      // 题目的空间要求(KB)
        string desc;   // 题目的描述
        string header; // 题目预设给用户在线编辑器的代码
        string tail;   // 题目的测试用例,需要和header拼接,形成完整代码
    };

    const string questions_list = "./questions/questions.list"; // 配置文件的路径
    const string question_path = "./questions/"; // 题库路径

    class Model
    {
    private:
        // 题号:题目细节
        unordered_map<string, Question> questions;
    public:
        Model()
        {
            // 将题目加载进来
            assert(LoadQuestionList(questions_list));
        }

        bool LoadQuestionList(const string &question_list)
        {
            // 加载配置文件:questions/questions.list + 题目编号文件
            ifstream in(question_list);
            if (!in.is_open())
            {
                // 如果配置文件打开失败
                LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";
                return false;
            }

            // 进行行读取题目
            string line;
            while (getline(in, line))
            {
                // 将一行的字符串(题目描述)进行切分
                vector<string> tokens;
                StringUtil::SplitString(line, &tokens, " ");

                // 例如:1 判断回文数 简单 1 30000
                if (tokens.size() != 5)
                {
                    // 如果配置的内容有问题
                    LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n";
                    continue;
                }

                // 进行填充question
                Question q;
                q.number = tokens[0];
                q.title = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = atoi(tokens[3].c_str());
                q.mem_limit = atoi(tokens[4].c_str());

                // 指定题库路径
                string path = question_path;
                path += q.number; // 加上题号
                path += "/";

                // 读取所有文件里面的内容,并填充到q中
                FileUtil::ReadFile(path+"desc.txt", &(q.desc), true);
                FileUtil::ReadFile(path+"header.cpp", &(q.header), true);
                FileUtil::ReadFile(path+"tail.cpp", &(q.tail), true);

                // 最后将单个题目q提交到题库中(哈希表)
                questions.insert({q.number, q});
            }

            LOG(INFO) << "加载题库...成功!" << "\n";
            in.close(); // 关闭配置文件

            return true;
        }

        // 获取所有的题目
        bool GetAllQuestions(vector<Question> *out)
        {
            if (questions.size() == 0)
            {
                // 如果没有题目
                LOG(ERROR) << "用户获取题库失败" << "\n";
                return false;
            }

            for (const auto &q : questions)
            {
                out->push_back(q.second); // first: key, second value
            }

            return true;
        }

        // 获取单个题目
        bool GetOneQuestion(const string &number, Question *q)
        {
            const auto& iter = questions.find(number);

            if (iter == questions.end())
            {
                // 如果没有找到该题目
                LOG(ERROR) << "用户获取题目失败,题目编号:" << number << "\n";
                return false;
            }

            (*q) = iter->second; // 获取题目成功
            return true;
        }

        ~Model()
        {}
    };
}

3.2数据库版本

// MySQL版本
// 根据题目list文件,加载所有的题目信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口

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

    struct Question
    {
        string number; // 题目编号(唯一)
        string title;  // 题目的标题
        string star;   // 题目的难度:简单 中等 困难
        string desc;   // 题目的描述
        string header; // 题目预设给用户在线编辑器的代码
        string tail;   // 题目的测试用例,需要和header拼接,形成完整代码
        int cpu_limit; // 题目的时间要求(S)
        int mem_limit; // 题目的空间要求(KB)
    };

    const std::string oj_questions = "oj_questions"; // 要访问的表名
    const std::string host = "127.0.0.1";            // ip为本地服务器
    const std::string user = "oj_client";            // MySQL用户名
    const std::string passwd = "xujiaming+520";      // MySQL密码
    const std::string db = "oj";                     // 要连接的数据库名
    const int port = 3306;                           // MySQL的端口号

    class Model
    {
    public:
        Model()
        {
        }

        // 查询MySQL
        // 参数:sql:sql查询语句,out:输出查询结果
        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); // 获得列数

            // 获得每行每列的数据
            for (int i = 0; i < rows; ++i)
            {
                MYSQL_ROW row = mysql_fetch_row(res); // 拿到当前这一行的所有数据(这里的row是一个二级指针)
                Question q; // 用于保存结果

                // 拿到当前行,每列的所有数据
                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);
            }

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

            // 关闭MySQL连接
            mysql_close(my);

            return true;
        }

        // 获取所有的题目
        bool GetAllQuestions(vector<Question> *out)
        {
            std::string sql = "select * from ";
            sql += oj_questions;

            return QueryMySql(sql, out);
        }

        // 获取单个题目
        bool GetOneQuestion(const string &number, Question *q)
        {
            bool res = false;

            std::string sql = "select * from ";
            sql += oj_questions;
            sql += " where number=";
            sql += number;

            vector<Question> result;
            if (QueryMySql(sql, &result)) // 判断是否获取题目成功
            {
                // 判断获取的题目个数是否只有1个
                if (result.size() == 1) 
                {
                    *q = result[0];
                    res = true;
                }
            }

            return res;
        }

        ~Model()
        {
        }
    };
}

4.题库的结构

题目的属性大致可以分为2类:

  1. 一种是题目编号,题目标题,题目难度,时间限制和内存限制这些字段,这些字段都比较小,可以把所有题目的这些信息存在一个文件里面。
  2. 另一种是题目描述,预置代码,测试用例等等,这类信息一般都比较大,可以根据题目编号给每道题建立一个与编号对应的文件夹,然后用三个文件保存这三个信息,到时候就可以通过题目编号找到题目对应的路径,然后读取对应的文件,不仅读取方便,还便于我们录题。
     

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


5.view模块oj_view.hpp

        oj_view模块负责渲染给用户显示的网页。比如说用户请求访问题目列表,题目列表里的题目信息是从我们后端的题库中得到的,而把这些信息显示到网页上,这就是渲染网页。所有说view模块也应该提供两个接口,一个渲染题目列表,一个渲染单个题目的网页。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux


 我们需要引入一个第三方库ctemplate;功能如下:

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

namespace ns_view
{
    using namespace ns_model;

    const std::string template_path = "./template_html/"; // 要渲染的路径

    class View
    {
    public:
        View()
        {}
        ~View()
        {}
    public:
        // 将所有的题目数据构建成网页
        void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
        {
            // 题目编号 题目标题 题目难度 
            // 使用表格显示
            // 1. 形成路径
            std::string src_html = template_path + "all_questions.html"; // 要被渲染的网页
            
            // 2. 形成数据字典
            ctemplate::TemplateDictionary root("all_questions");
            
            // 形成一个子字典
            for (const auto &q : questions)
            {
                ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list"); 
                sub->SetValue("number", q.number);
                sub->SetValue("title", q.title);
                sub->SetValue("star", q.star);
            }

            // 3. 获取被渲染的网页
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);

            // 4. 开始完成渲染功能
            tpl->Expand(html, &root); // 这个root字典里面会包含所有的子字典
        }

        // 将单个题目的所有数据构建成网页
        void OneExpand(const struct Question &q, std::string *html)
        {
            // 1. 形成路径
            std::string src_html = template_path + "one_question.html"; // 要被渲染的网页
            
            // 2. 形成数据字典
            ctemplate::TemplateDictionary root("one_questions");
            
            // 2.1 插入题目描述
            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. 获取被渲染的网页
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);

            // 4. 开始完成渲染功能
            tpl->Expand(html, &root); // 这个root字典里面会包含所有的子字典
        }
    };
}

6.oj_control模块oj_control.hpp

        oj_control模块是整个OJSever模块的逻辑功能部分,在上层做好了功能路由之后,通过调用control模块实现各个功能,所有oj_control模块既要可以返回对应的网页,还要可以负载均衡的判题。

提供三个功能:

  1. 构建好题目列表网页的接口
  2. 根据题目编号构建好单个题目网页的接口
  3. 判题接口。

6.1.构建题目列表和单个题目网页

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

// 根据题目数据构建网页
// html:输出型参数
bool AllQuestions(string *html)
{
    bool ret = true;
    vector<struct Question> all;

    if (model_.GetAllQuestions(&all))
    {
        // 给题目按编号进行排序
        sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
            return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
        }); 

        // 获取题目信息成功,将所有的题目数据构建成网页
        view_.AllExpandHtml(all, html);
    }
    else
    {
        // 获取失败
        *html = "获取题目失败,形成题目列表失败";
        ret = false;
    }

    return ret;
}

// 根据题号构建网页
bool Question(const string &number, string *html)
{
    bool ret = true;
    struct Question q;
    if (model_.GetOneQuestion(number, &q))
    {
        // 获取指定题目信息成功,将该题目的所有数据构建成网页
        view_.OneExpand(q, html);
    }
    else
    {
        // 获取失败
        *html = "指定题目:" + number + " 不存在!";
        ret = false;
    }

    return ret;
}

6.2.负载均衡模块

6.2.1机器类的设计

        负载均衡模块,最重要的功能就是可以负载均衡式的选择主机,我如何得知有哪些主机可以供我选择,怎么实现负载均衡。

        所以模块内部要有一个结构包含提供服务的主机信息,用来表述主机的结构命名为Machine,然后用一个vector把所有提供服务的主机组织起来。

        Machine类里有主机的IP,端口,还有负载情况。负载均衡判断的依据就是看主机的负载,所有类里还要提供方法,在有新请求请求该机器时增加负载,服务结束时减少负载,如果中途服务主机突然挂了,还要可以清空负载。
因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证对负载操作的安全性,需要一个mutex互斥锁。


6.2.2负载均衡模块设计

        将来选择主机可以在vector中选,在此之前需要知道有哪些主机可以选,在当前路径下加一个.conf文件里面会存放所有的可以提供服务的主机信息,包括了IP和端口,每一行是一个主机的信息,负载均衡模块在构建时就可以读取该文件,初始化自己的vector结构。
        然后就是选择主机功能,首先在同一时刻可能有很多执行流都在选择主机,所以对主机的选择需要加锁,也就是说负载均衡模块也需要一个互斥锁。
        设计在control模块调用负载均衡模块时,如果说后端的编译服务主机出问题挂了,不应该影响我的OJServer服务,OJ服务正常运行,编译服务如果恢复了,那我正常请求,如果有一部分挂了,那我请求别的主机,全挂我就不请求,提示后端。
        这个功能就由负载均衡模块负责,负载均衡模块除了可以选择主机,还要能够知道主机的情况,并能够根据情况更新。使用数组的下标表示每一个主机的编号,用两个数组,一个表示上线主机,元素的值就是主机编号,另一个表示下线的主机。提供方法,在后端编译服务重启时可以更新状态让主机上线,当请求主机失败时要更新状态让主机下线。


6.2.3负载均衡的实现:

        负载均衡就是尽量让每一台机器负责的请求平均,那就需要从所有在线的主机中选择出对应的主机。一是随机挑选主机,但是这种方法不能排除有时候一直选中某几台,有几台又一直选不上。还有一种比较严格,遍历所有在线的主机,找出负载最小的。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux
 
// 提供服务的主机
class Machine
{
public:
    std::string ip;  // 编译服务的ip
    int port;        // 编译服务的端口号
    uint64_t load;   // 编译服务的负载(计数器)
    std::mutex *mtx; // 用于保护计数器的锁(注意:cpp中的mutex是禁止拷贝的,所以我们这里定义指针来进行后面的拷贝)
public:
    Machine()
    :ip(""), port(0), load(0), mtx(nullptr)
    {}

    ~Machine()
    {}
public:
    // 提升主机负载
    void IncLoad()
    {
        if (mtx) mtx->lock(); // 加锁
        ++load;
        if (mtx) mtx->unlock(); // 解锁
    }

    // 减小主机负载
    void DecLoad()
    {
        if (mtx) mtx->lock(); // 加锁
        --load;
        if (mtx) mtx->unlock(); // 解锁
    }

    // 将主机的负载清零
    void ResetLoad()
    {
        if (mtx) mtx->lock();
        load = 0;
        if (mtx) mtx->unlock();
    }

    // 获取主机负载(没有太大的意义,只是为了统一接口)
    uint64_t Load()
    {
        uint64_t _load = 0;
        if (mtx) mtx->lock(); // 加锁
        _load = load;
        if (mtx) mtx->unlock(); // 解锁

        return _load;
    }

};

const std::string service_machine = "./conf/service_machine.conf"; // 提供服务的主机列表的路径
// 负载均衡模块
class LoadBalance
{
private:
    std::vector<Machine> machines; // 可以给我们提供编译服务的主机(每一台主机都有自己的下标,充当当前主机的id)
    std::vector<int> online;       // 所有在线的主机id
    std::vector<int> offline;      // 所有离线的主机id
    std::mutex mtx;                // mtx是LoadBalance的锁,是保证LoadBalance它的数据安全(注意:每个Machine也有自己的小锁,不要与这里的mtx搞混)
public:
    LoadBalance()
    {
        assert(LoadConf(service_machine));
        LOG(INFO) << "加载 " << service_machine << " 成功" << "\n";
    }

    ~LoadBalance()
    {}
public:
    // 将主机加载进来
    // 参数:machine_list:主机列表
    bool LoadConf(const std::string &machine_conf)
    {
        std::ifstream in(machine_conf);
        if (!in.is_open())
        {
            // 如果打开文件失败
            LOG(FATAL) << "加载:" << machine_conf << " 失败" << "\n";
            return false;
        }
        
        std::string line;
        while (std::getline(in, line))
        {
            // 按行读取machine_conf文件数据
            // 进行字符串切割:将字符串分成两部分:ip和port
            std::vector<std::string> tokens;
            StringUtil::SplitString(line, &tokens, ":"); 
            
            if (tokens.size() != 2)
            {
                // 如果切分出来的字符串不是ip和port这两部分
                LOG(WARNING) << " 切分 " << line << " 失败" << "\n";
                continue;
            }

            Machine m;
            m.ip = tokens[0];
            m.port = atoi(tokens[1].c_str());
            m.load = 0;
            m.mtx = new std::mutex();

            online.push_back(machines.size()); // 上线当前主机
            machines.push_back(m);
        }

        in.close(); // 关闭文件

        return true;
    }

    // 智能选择合适的主机提供服务
    // 参数:
    // id:输出型参数
    // m:输出型参数
    bool SmartChoice(int *id, Machine **m)
    {
        // 1. 使用选择好的主机(更新该主机的负载)
        // 2. 我们需要可能离线的主机
        
        mtx.lock(); // 将选择功能加锁
        
        // 使用的负载均衡算法:轮询 + hash
        int online_num = online.size(); // 主机在线数
        if (online_num == 0)
        {
            // 如果所有的主机都离线了
            mtx.unlock(); // 将选择功能解锁
            LOG(FATAL) << " 所有的后端编译主机已经离线,请运维的人尽快查看" << "\n";

            return false;
        }

        // 通过遍历的方式,找到所有负载最小的机器
        *id = online[0]; // 默认最小负载的机器
        *m = &machines[online[0]]; // 默认最小负载主机的地址
        uint64_t min_load = machines[online[0]].Load(); // 默认最小负载数
        for (int i = 1; i < online_num; ++i)
        {    
            uint64_t cur_load = machines[online[i]].Load();
            if (min_load > cur_load)
            {
                min_load = cur_load;
                *id = online[i];
                *m = &machines[online[i]];
            }
        }

        mtx.unlock(); // 将选择功能解锁
        return true;
    }

    // 离线指定主机
    void OfflineMachine(int which)
    {
        mtx.lock(); // 将离线功能加锁(因为在离线的同时,有可能有人正在进行智能选择)

        // 遍历在线主机列表,找到要离线的主机
        for (auto iter = online.begin(); iter != online.end(); ++iter)
        {
            if (*iter == which)
            {
                // 先将要离线的主机的负载清零,不然后面再将其上线的时候,负载还是和现在一样
                machines[which].ResetLoad();

                // 要离线的主机找到了,将其进行离线
                online.erase(iter); 
                offline.push_back(which); // 注意:这里不能写成offline.push_back(*iter);因为这样为导致迭代器失效
                break; // 因为break的存在,所以我们暂时不用考虑迭代器失效的问题
            }
        }

        mtx.unlock(); // 将离线功能解锁
    }

    // 上线对应的主机
    void OnlineMachine()
    {
        // 规定:当所有主机都离线的时候,我们统一上线
        mtx.lock(); // 将上线功能加锁

        // 将离线列表里面的所有主机插入到上线列表里面,并删除离线列表里面的所有主机
        online.insert(online.end(), offline.begin(), offline.end());
        offline.erase(offline.begin(), offline.end()); 

        mtx.unlock(); // 将上线功能解锁

        LOG(INFO) << "所有的主机又上线啦!" << "\n";
    }

    // 显示所有在线和离线的主机(仅仅用于测试)
    void ShowMachines()
    {
        mtx.lock(); // 加锁

        std::cout << "当前在线主机列表: ";
        for (auto &id : online)
        {
            std::cout << id << " ";
        }
        std::cout << std::endl;

        std::cout << "当前离线主机列表: ";
        for (auto &id : offline)
        {
            std::cout << id << " ";
        }
        std::cout << std::endl;

        mtx.unlock(); // 解锁
    }
};

6.2.4判题模块

        得到的参数是需要判的题目编号和用户传进来的json串形式的代码,通过题目编号,调用model模块得到题目相关的信息,然后通过反序列化用户传来的代码,得到代码内容。有了题目的信息和用户的代码,就可以拼接出可以用来编译的源码内容,构建出CompilerServer需要的json串。请求后端编译服务器主机。
        选到主机之后通过主机的IP+端口,使用网络请求方式发起请求,除了通过请求的返回值判断请求是否成功,还需要判断请求的状态码,只有状态呢是200才表示请求成功。且需要更新请求时机器的负载情况。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

// 判题功能
// 参数:
// number:题号,in_json:客户上传上来的代码,out_json:要返回的结果
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
    // LOG(DEBUG) << in_json << "\nnumber: " << number << "\n";

    // 0. 根据题目编号,直接拿到对应的题目细节
    struct Question q;
    model_.GetOneQuestion(number, &q);

    // 1. 将in_json进行反序列化,得到题目id,得到用户提交的源代码(input)
    Json::Reader reader;
    Json::Value in_value;
    reader.parse(in_json, in_value); // 参数:你想要反序列化谁,你想要反序列化的json_value是谁
    std::string code = in_value["code"].asString();

    // 2. 重新拼接用户代码+测试用例代码,形成新的代码
    Json::Value compile_value;
    compile_value["input"] = in_value["input"].asString();
    compile_value["code"] = code + "\n" + q.tail; // 注意这里需要加一个换行符,否则可能会将#ifndef拼接到 ‘;’ 后面,导致拼接错误
    compile_value["cpu_limit"] = q.cpu_limit;
    compile_value["mem_limit"] = q.mem_limit;
    Json::FastWriter writer;
    std::string compile_string = writer.write(compile_value); // 将拼接好的代码进行序列化
    
    // 3. 选择负载最低的主机(差错处理)
    // 规则:一直选择,直到主机可用,否则,就是全部挂掉
    while (true)
    {
        int id = 0;
        Machine *m = nullptr;
        
        // 进行智能选择
        if (!load_balance_.SmartChoice(&id, &m))
        {
            // 如果选择失败,那么所有的主机都已经挂掉了
            break;
        }
        
        // 4. 然后发起http请求,得到结果
        Client cli(m->ip, m->port);
        m->IncLoad(); // 增加主机负载

        LOG(INFO) << " 选择主机成功, 主机id: " << id << ", 详情: " << m->ip << ":" << m->port << ", 当前主机的负载是:" << m->Load() << "\n";

        if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf_8"))
        {
            if (res->status == 200)
            {
                // 5. 如果http请求成功,将编译运行后的结果赋值给out_json  
                *out_json = res->body; 
                m->DecLoad(); // 减少主机负载
                LOG(INFO) << "请求编译和运行服务成功..." << "\n";

                break;
            }

            m->DecLoad(); // 减少主机负载
        }
        else
        {
            // 请求失败
            LOG(ERROR) << " 当前请求的主机id: " << id << ", 详情: " << m->ip << ":" << m->port << ", 可能已经离线" << "\n";
            load_balance_.OfflineMachine(id); // 将当前主机离线(将主机离线后负载会自动清零)
            
            load_balance_.ShowMachines(); // 仅仅是为了用来调试
        }
    }
}

7.oj_server模块oj_server.cc

        搭建一个http服务,通过用户请求的不同资源,完成功能路由的任务,调用oj_control模块的功能。

C++项目:【负载均衡式在线OJ】,实战项目,c++,负载均衡,linux

static Control *ctrl_ptr = nullptr; // 定义一个控制器指针,让其既可以局部使用,也可以全局使用


// 使用手册
void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}


// 写一个恢复主机的回调方法
void Recovery(int signo)
{
    ctrl_ptr->RecoveryMachine();
}

// 调用方式:./compile_server port
int main(int argc, char *argv[])
{
    // 捕捉3号信号(ctrl + \).用快捷键(ctrl + \)一键上线所有离线的主机
    signal(SIGQUIT, Recovery);

    if (argc != 2)
    {
        // 如果使用方法不对
        Usage(argv[0]);
        return 1;
    }

    // 用户请求的服务路由功能
    Server svr; // 创建有个服务器
    Control ctrl; // 创建一个控制器
    ctrl_ptr = &ctrl;

    // 获取所有的题目列表
    // 参数:req:用户的需求,resp:服务器的相应
    svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){
        // 返回一张包含所有题目的html网页
        std::string html;
        ctrl.AllQuestions(&html); 
        resp.set_content(html, "text/html; charset=utf-8"); 
    });

    // 用户要根据题目编号,获取题目的内容
    // /question/100 -> 正则匹配
    // R"()"作用:原始字符串raw string,保持字符串内容的原貌,不用做相关的转义
    svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){
        std::string number = req.matches[1]; // 拿到题号
        std::string html;
        ctrl.Question(number, &html); 
        resp.set_content(html, "text/html; charset=utf-8");
    }); 
    
    // 用户提交代码,使用我们的判题功能(1. 每道题的测试用例 2. compile_and_run)
    svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){
        std::string number = req.matches[1]; // 拿到题号
        std::string result_json;
        ctrl.Judge(number, req.body, &result_json);
        resp.set_content(result_json, "application/json;charset=utf-8");
        // resp.set_content("指定题目的判题:" + number, "text/plain;charset=utf-8");
    });
    
    svr.set_base_dir("./wwwroot");

    // 让服务器在所有ip,指定端口服务
    svr.listen("0.0.0.0", atoi(argv[1])); // 启动http服务

    return 0;
}

九、前端页面的设计(了解)

前端简单使用:
html/css/js/jquery/ajax
Ace前端在线编辑器


1. indx.html

当用户访问根目录时显示的网页

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

        h tml,
        body {
            width: 100%;
            height: 100%;
        }

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续float带来的影响 */
            overflow: hidden;
        }

        .container .navbar a {
            /* 设置a标签是行内块元素,允许你设置宽度 */
            display: inline-block;
            /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: white;
            /* 设置字体的大小 */
            font-size: large;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 50px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置a标签中的文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件 */
        .container .navbar a:hover {
            background-color: green;
        }

        .container .navbar .login {
            float: right;
        }

        .container .content {

            /* 设置标签的宽度 */
            width: 800px;
            /* 用来调试 */
            /* background-color: #ccc; */
            /* 整体居中 */
            margin: 0px auto;
            /* 设置文字居中 */
            text-align: center;
            /* 设置上外边距 */
            margin-top: 200px;
        }

        .container .content .font_ {
            /* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */
            display: block;
            /* 设置每个文字的上外边距 */
            margin-top: 20px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置字体大小
            font-size: larger; */
        }
    </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="content">
            <h1 class="font_">欢迎来到我的OnlineJudge平台</h1>
            <p class="font_">这个我个人独立开发的一个在线OJ平台</p>
            <a class="font_" href="/all_questions">点击我开始编程啦!</a>
        </div>
    </div>
</body>

</html>

2. all_questions.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;
        }

        h tml,
        body {
            width: 100%;
            height: 100%;
        }

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续float带来的影响 */
            overflow: hidden;
        }

        .container .navbar a {
            /* 设置a标签是行内块元素,允许你设置宽度 */
            display: inline-block;
            /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: white;
            /* 设置字体的大小 */
            font-size: large;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 50px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置a标签中的文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件 */
        .container .navbar a:hover {
            background-color: green;
        }

        .container .navbar .login {
            float: right;
        }

        .container .question_list {
            padding-top: 50px;
            width: 800px;
            height: 100%;
            margin: 0px auto;
            /* background-color: #ccc; */
            text-align: center;
        }

        .container .question_list table {
            width: 100%;
            font-size: large;
            font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'LucidaSans Unicode', Geneva, Verdana, sans-serif;
            margin-top: 50px;
            background-color: rgb(243, 248, 246);
        }

        .container .question_list h1 {
            color: green;
        }

        .container .question_list table .item {
            width: 100px;
            height: 40px;
            font-size: large;
            font-family: 'Times New Roman', Times, serif;
        }

        .container .question_list table .item a {
            text-decoration: none;
            color: black;
        }

        .container .question_list table .item a:hover {
            color: blue;
            text-decoration: underline;
        }

        .container .footer {
            width: 100%;
            height: 50px;
            text-align: center;
            line-height: 50px;
            color: #ccc;
            margin-top: 15px;
        }
    </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>OnlineJuge题目列表</h1>
            <table>
                <tr>
                    <th class="item">编号</th>
                    <th class="item">标题</th>
                    <th class="item">难度</th>
                </tr>
                {{#question_list}}
                <tr>
                    <td class="item">{{number}}</td>
                    <td class="item"><a href="/question/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/question_list}}
            </table>
        </div>
        <div class="footer">
            <!-- <hr> -->
            <h4>@不一样的烟火a</h4>
        </div>
    </div>
</body>

</html>

3. one_question.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插件 -->
    <!-- 官网链接:https://ace.c9.io/ -->
    <!-- CDN链接:https://cdnjs.com/libraries/ace -->
    <!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 -->
    <!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/-->
    <!-- 引入ACE CDN -->
    <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;
        }

        h tml,
        body {
            width: 100%;
            height: 100%;
        }

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续float带来的影响 */
            overflow: hidden;
        }

        .container .navbar a {
            /* 设置a标签是行内块元素,允许你设置宽度 */
            display: inline-block;
            /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: white;
            /* 设置字体的大小 */
            font-size: large;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 50px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置a标签中的文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件 */
        .container .navbar a:hover {
            background-color: green;
        }

        .container .navbar .login {
            float: right;
        }

        .container .part1 {
            width: 100%;
            height: 600px;
            overflow: hidden;
        }

        .container .part1 .left_desc {
            width: 50%;
            height: 600px;
            float: left;
            overflow: scroll;
        }

        .container .part1 .left_desc h3 {
            padding-top: 10px;
            padding-left: 10px;
        }

        .container .part1 .left_desc pre {
            padding-top: 10px;
            padding-left: 10px;
            font-size: medium;
            font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
        }

        .container .part1 .right_code {
            width: 50%;
            float: right;
        }

        .container .part1 .right_code .ace_editor {
            height: 600px;
        }

        .container .part2 {
            width: 100%;
            overflow: hidden;
        }

        .container .part2 .result {
            width: 300px;
            float: left;
        }

        /* 按钮属性 */
        .container .part2 .btn-submit {
            width: 120px;
            height: 50px;
            font-size: large;
            float: right;
            background-color: #26bb9c;
            color: #FFF;
            /* 给按钮带上圆角 */
            border-radius: 1ch;
            border: 0px;
            margin-top: 10px;
            margin-right: 10px;
        }

        .container .part2 button:hover {
            color: green;
        }

        .container .part2 .result {
            margin-top: 15px;
            margin-left: 15px;
        }

        .container .part2 .result pre {
            font-size: large;
        }
    </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>
        //初始化对象
        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,我给你的是什么格式
                // 构建一个json串(给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
                });
                
                // 将手动创建的标签插入到result_div里面
                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
                    });
                    
                    // 将手动创建的标签插入到result_div里面
                    stdout_lable.appendTo(result_div);
                    stderr_lable.appendTo(result_div);

                }
                else
                {
                    // 请求失败
                    // 编译运行出错
                    // do nothing
                }
            }
        }
    </script>
</body>

</html>

十、项目扩展

  1. 基于注册和登陆的录题功能
  2. 业务扩展,自己写一个论坛,接入到在线OJ中
  3. 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
  4. 目前后端compiler的服务我们使用的是http方式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调用,推荐:rest_rpc,替换我们的httplib(建议,可以不做)
  5. 功能上更完善一下,判断一道题目正确之后,自动下一道题目
  6. navbar中的功能可以一个一个的都实现一下
  7. 其他
     

十一、项目所需工具

1.升级 gcc

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --
infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enablebootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-systemzlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --
enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,objc++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --
with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --
with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install -
-enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-
redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)

cpp-httplib 用老的编译器,要么编译不通过,要么直接运行报错

百度搜索:scl gcc devsettool升级gcc

//安装scl
$ sudo yum install centos-release-scl scl-utils-build
//安装新版本gcc,这里也可以把7换成8或者9,我用的是9,也可以都安装
$ sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
$ ls /opt/rh/

//启动: 细节,命令行启动只能在本会话有效
$ scl enable devtoolset-7 bash
$ gcc -v

//可选:如果想每次登陆的时候,都是较新的gcc,需要把上面的命令添加到你的~/.bash_profile中
$ cat ~/.bash_profile
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH

#添加下面的命令,每次启动的时候,都会执行这个scl命令
scl enable devtoolset-7 bash

or

scl enable devtoolset-8 bash

or

scl enable devtoolset-9 bash

2.安装 jsoncpp

$ sudo yum install -y jsoncpp-devel
[sudo] password for whb:
Loaded plugins: aliases, auto-update-debuginfo, fastestmirror, protectbase
Repository epel is listed more than once in the configuration
Loading mirror speeds from cached hostfile
* base: mirrors.aliyun.com
* epel-debuginfo: mirrors.tuna.tsinghua.edu.cn
* extras: mirrors.aliyun.com
* updates: mirrors.aliyun.com
0 packages excluded due to repository protections
Package jsoncpp-devel-0.10.5-2.el7.x86_64 already installed and latest version
//我已经安装了

3.安装 cpp-httplib

最新的cpp-httplib在使用的时候,如果gcc不是特别新的话有可能会有运行时错误的问题

建议:cpp-httplib 0.7.15

下载zip安装包,上传到服务器即可

cpp-httplib gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib?_from=gitee_search
v0.7.15版本链接: https://gitee.com/yuanfeng1897/cpp-httplib/tree/v0.7.15

把httplib.h拷贝到我们的项目中即可,就这么简单

使用样例:

$ cat http_server.cc

#include "httplib.h"

int main()
{
    httplib::Server svr;

    svr.Get("/hi", [](const httplib::Request &req, httplib::Response &rsp){
            rsp.set_content("你好,世界!", "text/plain; charset=utf-8");
            });

    svr.listen("0.0.0.0", 8080);
    return 0;
} 

更多的细节可以看gitee上面的使用手册

4.安装boost库

$ sudo yum install -y boost-devel //是boost 开发库

5.安装与测试 ctemplate  

# 国内github镜像网站,如果挂掉可以私信老师要
https://hub.fastgit.xyz/OlafvdSpek/ctemplate

$ git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git
$ ./autogen.sh
$ ./configure
$ make         //编译
$ make install //安装到系统中

# 注意gcc版本
# 如果安装报错,注意使用sudo

测试样例:

1. 建立文件
$ ll
total 8
-rw-rw-r-- 1 whb whb 529 May 12 11:52 test.cc
-rw-rw-r-- 1 whb whb 230 May 12 11:52 test.html

2. 编写ctemplate代码
$ cat test.cc

#include <iostream>
#include <string>
#include <ctemplate/template.h>

int main()
{
    std::string html = "./test.html";
    std::string html_info = "Hello";

    //建立ctemplate参数目录结构
    ctemplate::TemplateDictionary root("test"); //unordered_map<string,string> test;

    //向结构中添加你要替换的数据,kv的
    root.SetValue("info", html_info); //test.insert({key, value});

    //获取被渲染对象
    ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html,
    ctemplate::DO_NOT_STRIP); //DO_NOT_STRIP:保持html网页原貌

    //开始渲染,返回新的网页结果到out_html
    std::string out_html;
    tpl->Expand(&out_html, &root);

    std::cout << "渲染的带参html是:" << std::endl;
    std::cout << out_html << std::endl;

    return 0;
} 

3. 编写简单html
$ cat test.html
<html>
    <header>
    </header>
    <body>
        <!--渲染参数,会被我们C++代码中的数据替换, info就是上面SetValue("info", html_info)代码中的
info,会自动被std::string info_html中的内容替换-->
        <p>{{info}}</p>
        <p>{{info}}</p>
        <p>{{info}}</p>
        <p>{{info}}</p>
    </body>
</html>

4. 编译
$ g++ test.cc -o mytest -lctemplate -lpthread
$ ls
mytest test.cc test.html
$ ./mytest

5. 对比结果
渲染前:
<html>
    <header>
    </header>
    <body>
        <!--渲染参数,会被我们C++代码中的数据替换-->
        <p>{{info}}</p>
        <p>{{info}}</p>
        <p>{{info}}</p>
        <p>{{info}}</p>
    </body>
</html>

渲染后:
渲染的带参html是:
<html>
    <header>
    </header>
    <body>
        <!--渲染参数,会被我们C++代码中的数据替换-->
        <p>Hello</p>
        <p>Hello</p>
        <p>Hello</p>
        <p>Hello</p>
    </body>
</html>

6.使用Ace在线编辑器

直接复制粘贴即可

<!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>Ace测试</title>

    <!-- 引入ACE插件 -->

    <!-- 官网链接:https://ace.c9.io/ -->

    <!-- CDN链接:https://cdnjs.com/libraries/ace -->

    <!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 -->

    <!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-
%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ 
-->

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

    <style>

       * {
            margin: 0;

比特就业课
            padding: 0;
       }
        html,
        body {
            width: 100%;
            height: 100%;
       }
        div .ace_editor {
            height: 600px;
            width: 100%;
       }
    </style>
</head>
<body>

    <div>

        <pre id="code" class="ace_editor"><textarea class="ace_textinput">#include<iostream>

int
 main()
{
   std::cout << "hello ace editor" << std::endl;

   return 0;
}</textarea></pre>

        <button class="bt" onclick="submit()">提交代码</button><br/>

    </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

       });
    </script>
</body>
</html>

7.MySQL 建表

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;

十二、项目源码

        项目到这里就已经圆满结束了,但是由于篇幅已经很长了,项目中有些地方只给了文字说明与框架,具体的完整实现代码和项目的超清思维导图我会放在Gitee上供大家参考。

项目源码https://gitee.com/what-you-want-a/load-balanced-online-oj文章来源地址https://www.toymoban.com/news/detail-714865.html

到了这里,关于C++项目:【负载均衡式在线OJ】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【项目实战】Kafka 的 Leader 选举和负载均衡

    👉 博主介绍 : 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO TOP红人 Java知识图谱点击链接: 体系化学习Java(Java面试专题) 💕💕 感兴趣的同学可以收藏关注下 , 不然下次找不到哟

    2024年02月16日
    浏览(30)
  • 【负载均衡oj】(七)ojserver

    负责负载均衡,获取网站首页,通过网络罗调用编译并运行并提供结果给用户。根据用户的操作提供不同的功能。采用mvc结构。使用 ctemplate文字模板做数据渲染 m在这里指的是和数据的交互,可以用文件或者sql交互。在使用文件的情况下,就必须要有对应的文件目录存放每一

    2024年01月24日
    浏览(22)
  • 【java苍穹外卖项目实战三】nginx反向代理和负载均衡

    我们思考一个问题: 前端发送的请求,是如何请求到后端服务的? 前端请求地址:http://localhost/api/employee/login 后端接口地址:http://localhost:8080/admin/employee/login 很明显,两个地址不一致,那是如何请求到后端服务的呢? 1、nginx反向代理 nginx 反向代理 ,就是将前端发送的动态

    2024年02月21日
    浏览(37)
  • 【Linux】Linux+Nginx部署项目(负载均衡&动静分离)

    接下来看看由辉辉所写的关于Linux的相关操作吧   目录 🥳🥳Welcome Huihui\\\'s Code World ! !🥳🥳 一.Nginx负载均衡 1.什么是负载均衡 2.实现负载均衡的步骤 ①Nginx安装 一键安装4个依赖 解压安装包 进入安装包目录 ②启动 nginx 服务 进入到指定目录 启动 检测是否成功启动 ③开放防

    2024年02月06日
    浏览(36)
  • Nginx负载均衡 以及Linux前后端项目部署

    Nginx是一款高性能的开源Web服务器和反向代理服务器。它由俄罗斯的程序设计师Igor Sysoev创建,旨在解决传统Web服务器的性能限制问题。 Nginx采用事件驱动的架构和异步非阻塞的处理方式,使其能够处理大量并发连接,并具备良好的性能表现。它能够处理静态文件、索引文件以

    2024年02月06日
    浏览(40)
  • nginx配置负载均衡--实战项目(适用于轮询、加权轮询、ip_hash)

    👨‍🎓 博主简介   🏅云计算领域优质创作者   🏅华为云开发者社区专家博主   🏅阿里云开发者社区专家博主 💊 交流社区: 运维交流社区 欢迎大家的加入! 🐋 希望大家多多支持,我们一起进步!😄 🎉如果文章对你有帮助的话,欢迎 点赞 👍🏻 评论 💬 收藏

    2024年02月08日
    浏览(31)
  • 【Linux】Nignx的入门使用&负载均衡&前端项目部署---超详细

              Nginx是一个高性能的开源Web服务器和反向代理服务器。它使用事件驱动的异步框架,可同时处理大量请求,支持负载均衡、反向代理、HTTP缓存等常见Web服务场景。Nginx可以作为一个前端的Web服务器,也可以作为反向代理服务器,将客户端的请求转发给后端的应用服

    2024年02月06日
    浏览(34)
  • 【Linux】Nginx安装使用负载均衡及动静分离(前后端项目部署),前端项目打包

             Nginx 是一款高性能的 Web 服务器和 反向代理服务器 ,也可以充当负载均衡器、HTTP 缓存和安全防护设备。它的特点是内存占用小、稳定性高、并发性强、易于扩展,因此在互联网领域得到了广泛的使用。 总结出以下三点: 负载均衡 :流量分摊 反向代理 :处理外

    2024年02月06日
    浏览(33)
  • Linux-----nginx的简介,nginx搭载负载均衡以及nginx部署前后端分离项目

    目录 nginx的简介 是什么 nginx的特点以及功能 Nginx负载均衡 下载  安装  负载均衡 Nginx 是一个高性能的开源Web服务器和反向代理服务器。它的设计目标是为了解决C10k问题,即在同一时间内支持上万个并发连接。 Nginx采用事件驱动的异步架构,能够高效地处理大量并发请求,同

    2024年02月06日
    浏览(33)
  • Nignx安装&负载均衡&动静分离以及Linux前端项目部署&将域名映射到特定IP地址

    目录 一、nginx简介 1.1 定义 1.2 背景 1.3 作用 二、nginx搭载负载均衡提供前后分离后台接口数据 2.1 nginx安装 2.1.1 下载依赖 2.1.2 下载并解压安装包 2.1.3 安装nginx 2.1.4 启动nginx服务 2.2 tomcat负载均衡 2.2.1 负载均衡所需服务器准备 2.2.2 配置修改 2.2.3 重启nginx 2.2.4 效果展示 2.3 后端

    2024年02月05日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包