【项目】:4万字带你深入了解负载均衡在线OJ项目

朋友们、伙计们,我们又见面了,本期来给大家带来负载均衡式在线OJ项目,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

​ 

目录

引言

项目介绍

开发环境 

整体框架

编写思路

1. compile服务 

1.1 compiler.hpp

1.2 日志功能

1.2.1 完善compiler.hpp 

1.3 runner.hpp

1.4 compile_run.hpp

1.4.1 差错处理

1.4.2 状态码解析

1.4.3 生成唯一文件名

1.4.4 写文件与读文件

1.4.5 删除临时文件 

1.5 compile_server.cc

1.5.1 手动模拟实现网络服务

1.5.2 网络服务编写

1.5.3 使用Postman测试

1.5.4 开放式端口设计

2. oj_server服务

2.1 oj_server.cc

2.1.1 基础框架

2.2 基于文件版的题库

2.2 oj_model.hpp

2.2.1 获取所有题目列表和指定题目接口

2.2.2 加载题目列表及信息

2.2.3 引入日志

2.2.4 分割字符串接口

2.3 oj_control.hpp 

2.3.1 基本框架 

2.3.2 安装与测试ctemplate

2.3.3 基本的渲染框架

2.3.4 测试html文件 

2.3.5 全部题目的网页渲染

2.3.6 指定题目的网页渲染

2.4 负载均衡

2.4.1 基本框架

2.4.2 LoadConf接口

2.4.3 SmartChoice接口

2.5 判题(Judge)功能

2.5.1 反序列化与拼接 

2.5.2 选择负载最低的主机

2.5.3 发起http请求与返回结果

2.5.4 使用Postman测试

3. 前端服务

3.1  首页编写

3.2 题目列表编写 

3.3 单个题目的编写

3.4 提交代码 

3.5 题目排序

4. 整体测试 

4.1 上线服务

5. 基于MySQL版的题库

5.1 创建用户并赋权

5.2 设计表结构并录题

5.3 使用MySQL题库 

5.3.1 设计链接MySQL的接口 

5.3.2 链接MySQL获取题库 

5.3 测试MySQL文件版 

6. 发布项目

7. 项目总结


引言

本项目只是用于项目的学习思路以及编写思路,提升对大型代码的组织能力,只是将其核心部分拿出来了解,并不是为了完整的实现出在线OJ的全部功能,但是最基本的功能是要有的,我们只来实现一个基础的基于负载均衡式的在线OJ。

项目介绍

该项目是基于负载均衡的在线OJ,模拟我们平时刷题网站(leetcode和牛客)写的一个在线判题系统。

项目主要分为三个模块:

  • ① comm模块:主要是实现一些公共方法(日志,工具类);
  • ② compile_server模块:对于提交上来代码进行编译与运行;
  • ③ oj_server模块:获取题目列表,查看题目编写界面,实现负载均衡,其他功能。
  •     

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

开发环境 

Ubuntu 22.04 64bit云服务器、C/C++、vscode、MySQL Workbench

整体框架

负载均衡式在线OJ宏观结构:

  

编写思路

  • 先实现compile_server模块;
  • 再实现oj_server模块;
  • 实现version1版本的基于文件的在线OJ
  • 进行一些前端页面的设计(了解)
  • 实现version2版本的基于MySQL的在线OJ
  • 对于comm模块在整个代码的编写环节中哪里用到就直接写进去了,不做单独环节的编写

1. compile服务 

该模块是用来进行编译运行的,所以我们先根据当前所需创建一些需要的文件,后面再根据需要进行调整:

  • compiler.hpp:用于对提交的代码进行编译;
  • runner.hpp:对编译好的代码进行运行;
  • compile_run.hpp:为了方便,将编译和运行结合在一起;
  • compile_server:使用网络相关的接口进行服务请求。
  • Makefile:自动化构建代码  
  •     

1.1 compiler.hpp

在这里我们用于实现代码的编译效果:

编译操作我们用类进行封装,将这些类封装在命名空间中;

首先我们需要知道的是,我们的主进程在运行的时候是不能进行编译操作的,主进程有自己的代码逻辑,所以首先得创建子进程,让子进程去完成编译的操作,创建子进程和父进程等待子进程的操作我们先来实现:

  

拼接文件后缀:

基本的框架实现完成之后,我们来仔细想一下子进程要对源文件进行编译时,需要传递文件名,这也没问题,但是g++在编译时需要根据文件后缀来进行识别,我们传递的文件名需要再添加上后缀,同时,我们编译好的文件也需要添加后缀,再者,当g++编译出错时的这个错误信息我们也需要保存在一个临时文件中,所以这就需要完成一个给文件拼接后缀的接口,这个接口我们可以直接实现在comm模块的工具中,首先在comm模块中创建util.hpp(本文件中用于实现一些公共使用的工具)

  

同样的,对于拼接文件后缀的接口我们也是写进命名空间封装在类中,同时我们需要有一个文件来保存这些临时文件的路径,所以我们在compile模块中添加一个temp文件

    

接下来我们就来实现文件后缀拼接的工作:

在拼接文件后缀时,首先得所要拼接的文件名和拼接的后缀,所以需要传入这两个参数,然后通过string的+=操作来进行拼接,我们需要完成三种文件后缀的拼接工作,第一种是源文件的拼接后缀.cpp,第二种拼接可执行文件的后缀.exe,第三种编译出错时出错信息文件的后缀.stderr;  ​​​​​  

注意:该方法是实现在util.hpp中的

标准错误重定向:

文件后缀拼接好之后,接下来就可以让子进程实现编译功能了,子进程要进行程序编译,就需要使用到程序替换的系统调用接口,那么在程序替换之前呢,我们还需要做一件事情,我们都知道g++在编译出错时会默认将错误信息打印到标准错误中,也就是我们的显示器,但是我们既然创建了保存错误信息的临时文件,那么就需要将错误信息从标准错误中重定向到临时文件中,接下来我们先来实现这个功能:

我们可以先通过open接口将临时文件打开,记录临时文件的文件描述符,然后通过dup2接口对错误信息进行重定向;

  

    

标准错误的文件描述符是2,可以将其重定向。

程序替换执行g++:

接下来就让子进程进行程序替换操作,我们使用execlp接口:

  

此时就可以根据所需进行对应文件后缀的拼接,从而完成程序替换操作;

    

注意:程序替换时,并不会影响进程文件描述符表,不要担心重定向的问题。

检测编译是否成功:

子进程完成了程序替换、编译的工作,那么接下来编译是否成功呢?就需要让父进程来进行了,对于检测编译是否成功直接检测是否生成了对应的可执行文件就可以了,所以我们需要用到一个检测文件是否存在的接口,同样的我们还是写在comm模块中的util.hpp中:

创建一个FileUtil类,实现一个IsFileExists的方法,只需要传递文件名,通过拼接后缀来判断文件是否存在,那么判断一个文件是否存在需要用到的接口是stat接口,该接口是用来获取文件的属性的,换而言之如果文件存在那么可以获取成功,所以我们只需要关系stat是否获取成功即可判断文件是否存在:

  

comm/util.hpp

    

接下来就需要在父进程中调用该接口即可,因为前面我们已经将util.hpp引入了,所以直接使用即可:

compiler.hpp

    

写到这里,compiler.hpp的基本功能就实现的差不多了,后面有需要我们再进行补充。 

1.2 日志功能

可以看到我们上面的程序中的代码只是实现的逻辑,但是各种细节都没有处理,比如子进程创建失败、打开文件失败、编译的成功与失败等等这些逻辑直接返回了,没有一些提示的信息,所以我们实现一个日志功能来将这些错误信息、成功信息显示出来。

我们期望实现的日志是这样子使用的:

LOG(日志等级) << "message" << "\n";

 所以在在实现的日志里面需要包含对应的日志等级,并且我们需要有文件名,该文件中的哪一行,时间戳等信息,并且我们实现的日志使用起来要类似与cout;

我们将日志写进comm模块,新创建一个log.hpp文件,里面使用命名空间限定,实现一个Log日志接口;

首先要枚举日志的等级(正常、测试、告警、错误、系统崩溃),当我们打日志时为了更清楚,我们要有哪种等级,哪个文件,文件中的哪一行,并且还要有时间戳,前三个当我们调用Log时直接传入并且拼接即可,时间戳我们用系统调用接口获取一下然后拼接在后面。

要注意,我们实现的Log是一种类似于cout标准输出用法的,返回值设置为ostream;

还需要注意的一点就是,cout本质内部是有缓冲区的,在返回前要将拼接好的字符串写入cout的缓冲区;

  这样子写已经可以了,但是可以发现每次调用时都要传入三个参数,是比较麻烦的,所以我们可以使用宏替换,用__FILE__来获取当前源文件名称,用__LINE__来获取当前程序所在行数,这样子就不用在调用的时候传递这两个参数了,但是我们设置的日志等级是枚举的,其实就是一个整数,但是我们原函数的level是一个string类型的,所以在宏替换的时候给level前面加上#当做string来用即可。

#pragma once

#include <iostream>
#include <string>

#include "util.hpp"

namespace ns_log
{
    // 引入获取时间戳
    using namespace ns_util;

    // 日志等级
    enum
    {
        INFO,    // 正常
        DEBUG,   // 测试
        WARNING, // 警告但不影响运行
        ERROR,   // 当前机器发生错误
        FATAL    // 整个系统崩溃
    };

    // LOG(level) << "message" << "\n";
    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 += "]";

        // cout 内部也是有缓冲区的,现将拼接好的message写入cout
        std::cout << message; // 切记!!!不要使用endl进行刷新
        return std::cout;
    }
// 未来在调用日志时,一直传递三个参数太麻烦
// 所以进行宏替换,实现开放式的日志
// 日志等级本质上是整数,在前面添加#可以转为字符串形式
// __FILE__ 和 __LINE__ 可以直接获取当前源文件名以及当前程序的行号
#define LOG(level) Log(#level, __FILE__, __LINE__) // 不要加; 切记!!!
}

1.2.1 完善compiler.hpp 

现在有了日志,我们就可以使用日志来对我们的程序做一些完善了:

  

1.3 runner.hpp

在compiler.hpp中我们实现了代码的编译功能,那么编译好的代码就完了吗?不是,我们还需要将编译好的程序运行起来,查看结果,所以在runner.hpp中我们实现程序的运行操作;

在编写之前我们需要知道,程序编译和程序运行都是不能在主进程上执行,都是需要创建子进程,让子进程通过程序替换来完成编译和运行的工作的,基本的逻辑和程序编译时一样,另外,我们要知道,程序执行的结果有三种:

  • ① 代码跑完,结果正确
  • ② 代码跑完,结果不正确
  • ③ 代码没跑完,异常了

那么,在runner.hpp中我们需要对这三种情况全部处理吗?答案是不需要的,因为runner.hpp只进行程序的运行情况,我们只需要关注程序能否正常运行即可,不需要考虑运行的结果。

我们顺便也把日志引入进去,我们先来实现一个主框架:

实现出主的框架之后我们再来增添一些细节的东西:

一个程序在运行时会默认打开三个流:

① 标准输入 ② 标准输出 ③ 标准错误

那么我们想和编译模块里面对于编译文件存放的方式一样,将输入,输出,错误这三个文件信息也放进temp临时文件中,所以我们就需要三个单独的临时文件,然后通过重定向功能将这些信息写进这些文件中,所以我们先增添三个添加文件后缀的接口(在comm模块中的util.hpp中实现)。

util.hpp:

我们的文件后缀拼接的接口实现好了,那么要实现重定向,先需要将这些文件名先构造出来,然后用open打开,分配文件描述符,通过dup2进行重定向功能(我们对于输入功能进行重定向时,向文件读取的意义就是不想让用户进行自测功能): 

写到这里对于程序运行就快要结束了,但是我们需要想一下,提交代码的用户很多,避免不了一些恶意用户提交一些恶意代码,一直占用我们的计算机资源,所以,最后一步还需要进行资源的限制,我们需要用到的接口是setrlimit:

那么,这个限制是由做测试用例的人进行限制的,我们的Run并不知道,所以在调用Run时我们需要将内存限制和CPU限制传递进来,关于这个对资源进行限制的操作我们肯定是要在程序替换之前就要完成的:

runner.hpp:

#pragma once

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

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

namespace ns_runner
{
    using namespace ns_log;
    using namespace ns_util;

    class Runner
    {
    public:
        Runner() {}
        ~Runner() {}

    public:
        //提供设置进程占用资源大小的接口
        static void SetProcLimit(int _cpu_limit, int _mem_limit)
        {
            // 设置CPU时长
            struct rlimit cpu_limit;
            cpu_limit.rlim_max = RLIM_INFINITY;
            cpu_limit.rlim_cur = _cpu_limit;
            setrlimit(RLIMIT_CPU, &cpu_limit);
            
            // 设置内存大小
            struct rlimit mem_limit;
            mem_limit.rlim_max = RLIM_INFINITY;
            mem_limit.rlim_cur = _mem_limit * 1024; // 转化为KB
            setrlimit(RLIMIT_AS, &mem_limit);
        }
        // 指明文件名即可,不需要代理路径,不需要带后缀
        /*******************************************
         * 返回值 > 0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号
         * 返回值 == 0: 正常运行完毕的,结果保存到了对应的临时文件中
         * 返回值 < 0: 内部错误
         * 
         * cpu_limit: 程序运行的时候,可以使用的最大cpu资源上限
         * mem_limit: 程序运行的时候,可以使用的最大的内存大小(KB)
         * 
         * *****************************************/
        static int Run(std::string &file_name, int cpu_limit, int mem_limit)
        {
            /*********************************************
             * 程序运行:
             * 1. 代码跑完,结果正确
             * 2. 代码跑完,结果不正确
             * 3. 代码没跑完,异常了
             * Run接口是需要考虑代码跑完,结果正确与否
             * 结果的正确与否:是由我们给出的测试用例决定的!
             * 只考虑:程序是否正确运行完毕
             *
             * 要允许可执行我们必须知道可执行程序是谁
             * 一个程序在默认启动的时候会有三个流
             * 标准输入: 不处理
             * 标准输出: 程序运行完成,输出结果是什么
             * 标准错误: 运行时错误信息
             * 这三个流我们想让输出的内容存放在临时文件中,读取时从文件读取
             * *******************************************/
            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);
            /***************************************************
             * 后续需要对标准输入、标准输出、标准错误
             * 这三个流进行重定向至对应的文件
             * 所以先将这些文件描述符记录下来
             ****************************************************/
            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() /*要执行谁*/, _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;
            }
        }
    };
}

1.4 compile_run.hpp

通过compiler.hpp实现了编译功能,并通过runner.hpp实现了运行功能,那么在compile_run.hpp中我们来将这两个功能进行合并,实现一个编译 + 运行的功能;

首先我们需要知道,我们将来上传的代码是通过网络服务传递的,那么我们就需要统一进行约定,我们想通过json串的方式来进行输入,通过json串的方式将编译运行结果返回;

// 安装json Ubuntu 22.04
sudo apt-get install libjsoncpp-dev
// 查看是否安装成功
ls /usr/include/jsoncpp/json/

关于json的使用大家可以搜一些博客,也可以移步至C++ 之 C++ 操作 json 文件(C++读写json文件)

了解了json之后,我们来对编译运行接口进行设计,我们需要一个接口,包含有输入型参数和一个输出型参数,首先我们需要对输入的参数进行一个反序列化的操作对输入的数据进行解析;

我们规定输入的json串包含的kv结构有四种:

  • ① code:传入的代码
  • ② input:对提交的代码进行输入的数据(不做处理)
  • ③ cpu_limit:时间要求
  • ④ mem_limit:空间要求

对传入的数据进行反序列化之后,我们就得到了对应的四个参数,一个是传入的代码,一个是传入的输入数据(不处理),一个是时间限制,一个是空间限制,那么当我们获取到了传入的代码,下一步就需要对代码进行编译,所以需要形成一个具有唯一性的文件名,然后我们添加上后缀,将代码写入这个文件中,然后调用compile进行编译,编译完成之后执行该程序,所以在这一步我们需要做的工作:

  • ① 形成具有唯一性的文件名
  • ② 给该文件名添加后缀
  • ③ 将代码写入该源文件中(对唯一文件名添加源文件后缀)
  • ④ 调用编译接口进行编译
  • ⑤ 编译完成之后调用运行接口执行该程序

我们需要新增的函数接口有:

  • ① 形成具有唯一性的文件名
  • ② 将代码写入文件

所以我们将这两个接口继续实现在comm模块里面的util.hpp中,具体的实现思路我们后面再说,先把主框架构建出来:

1.4.1 差错处理

当主体框架写出来之后,需要进行一些差错处理,比如:传递的是空代码,写入文件失败、编译失败、运行失败等等这些错误信息我们该如何处理呢?另外,这些错误信息上层也是需要看到的,那么我们怎么将错误信息序列化返回给上层呢?

我们当然可以在每一个可能会出错的位置都写一套对应的处理逻辑,然后序列化通过输出型参数返回给上层,差不多要写6套逻辑,这样子实现让我们的代码看起来太臃肿了,那么我们要实现的简便一点还不能失去实用性,所以我们可以这样子实现:

  • 我们可以设置一个status_code状态码,在出现错误的地方需要对状态码进行修改;
  • 比如传递空代码状态码设为-1,出现一些系统错误时设置为-2,编译错误设为-3,由信号终止的就设置为该信号值,如果运行没有问题就设置为0;
  • 如果出现错误时,先修改状态码,然后后面的工作也不需要做了,直接goto语句跳转到解析出错问题的地方;
  • 当出现错误时,上层也需要知道错误码和错误原因,所以我们通过序列化操作将错误码和错误原因通过输出型参数返回给上层;
  • 另外,还需要一个可以解析我们状态码出错的出错原因的一个函数(这个函数后面再实现);
  • 如果程序编译、运行都成功,我们需要将程序的运行结果和程序运行完的错误结果也通过序列化返回给上层;
  • 程序运行成功的结果和运行完错误的结果在temp临时文件中,我们只需要实现一个ReadFile文件读取的接口将信息读取写入序列化的kv结构中。

1.4.2 状态码解析

我们通过switch case语句对依次对各种状态码进行分别解析,这里需要注意的是,当状态码是-3时,代表的是编译报错,那么我们想知道编译报错的具体信息,所以我们依旧读取指定错误文件中的错误信息,所以在调用状态码解析的接口时我们还要传递文件名:

1.4.3 生成唯一文件名

生成唯一文件名的这些接口我们都实现在comm模块的util.hpp中;

在这里我们采用毫秒级时间戳 + 原子性递增唯一值来实现一个唯一的文件名;

所以我们在TimeUtil类中实现一个获取毫秒级时间戳的接口,用到的接口和获取时间戳的接口一样:

我们在FileUtil类中实现生成唯一文件名的接口(comm模块的util.hpp中):

还存在一个原子性递增的唯一值的问题,我们不再使用定义变量再加锁的功能了,我们直接使用C++11中的原子变量(atomic):

1.4.4 写文件与读文件

WriteFile接口:

读取文件的这些接口我们用C++的文件读取实现,这里就不做过多介绍了:

ReadFile接口: 

读取文件的接口我们在设计的时候需要注意,从哪个文件中读取,读到的内容是需要返回给上层的,我们使用getline读取的时候他是不会保存\n的,所以我们要将ReadFile的返回值设为bool类型,提供一个输入型参数用来获取要读取的文件名,一个输出型参数将读取到的内容返回给上层,还需要提供一个标志位,是否保存\n;

当这个接口这样子实现之后,我们之前写的代码就需要进行调整了:

1.4.5 删除临时文件 

程序在编译运行之后最多会生成6个临时文件,那么如果这些文件不及时清理,就会堆积的越来越多,对我们的服务器造成负担,所以我们可以在compile_run.hpp的最后面添加一个删除临时文件的接口:

要删除临时文件我们需要用到的系统调用接口是unlink:

但是有一个问题:我们不清楚到底会生成多少个临时文件,但是我们清楚都会生成哪些临时文件,所以我们可以通过文件名添加后缀的方式再调用之前写的判断一个文件是否存在的接口用来删除临时文件:

后面如果我们想要观察这些临时文件,直接将RemoveTempFile接口注释掉即可。

1.5 compile_server.cc

在compile_server.cc文件中我们主要进行网络服务,通过网络来将我们的编译运行的代码结合在一起,实现网络化的功能;

1.5.1 手动模拟实现网络服务

基于上面的内容,我们实现了对代码的编译功能,对编译好的程序运行的功能,那么写了这么多,到底能不能正常运行呢?我们可以在compile_server.cc中进行一些测试代码的编写:

我们可以手动的通过序列化传递一些json串,模拟实现一下网络服务传递的json串,然后我们将我们的程序执行的结果也打印出来:

同时我们来编写一下Makefile;因为我们使用了jsoncpp,所以需要在编译选项中用-l选项引入jsoncpp库:

compile_server:compile_server.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp
.PHONY:clean
clean:
	rm -f compile_server

此时我们来编译运行一下:

1.5.2 网络服务编写

上面我们手动模拟了一下从网络上请求过来的json string,那么现在我们来实现网络请求服务,当然我们的网络服务我们也可以自己实现一个TCP请求,但是我们可以直接使用现成的cpp-httplib来进行网络请求;

这里建议下载cpp-httplib 0.7.15 版本的(比较稳定)

cpp-httplib gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib? _from=gitee_search

  • 我们下载好压缩包之后直接上传到我们的服务器然后解压到一个任意的路径下面(比较好找的路径下面);
  • 要使用httplib只需要把这个头文件拷贝至我们自己的文件下然后包含这个头文件就可以使用了;
  • 所以我们直接把这个httplib.h文件直接拷贝到我们的comm模块下;
  • 包含对应的头文件即可
  • 搭建服务端的教程(httplib使用)可以移步至这个博客:https://blog.csdn.net/weixin_55582891/article/details/141139338
  • 由于这个网络服务使用了原生线程库,所以我们的Makefile文件也需要引入pthread库

需要注意的是:要使用httplib时,gcc版本如果是旧版(4.8.5),很有可能会报错,所以就需要升级gcc版本,建议升级为7以上,我自己的gcc版本是11的,所以我就不做升级演示了;

未来我们想要客户端以Post的方式访问:

1.5.3 使用Postman测试

那么我们如何验证这个服务端呢?

  • 我们可以使用postman软件,我们直接去百度搜索然后下载注册好之后打开:
  • 接下来我们就可以进行网络请求了:
  • 我们可以来演示一下:

1.5.4 开放式端口设计

我们现在这样子实现的服务器绑定的端口号是固定的,我们想要在运行时将端口号暴露出来,这样子可以绑定任意端口号,所以我们可以通过命令行参数的方式在启动服务的之后进行绑定端口号:

2. oj_server服务

在这个模块我们实现一个基于MVC版本的oj服务,其本质就是一个网站,我们想要实现的基本效果是

  • 1. 获取首页(用题目列表充当)
  • 2. 编辑区域页面
  • 3. 提交判题功能(编译并运行)

我们先创建一些基础文件:

  • oj_server.cc:实现一个用户请求的网络服务路由的功能
  • oj_view.hpp:构建网页,渲染网页的内容
  • oj_model.hpp:进行数据交互,比如对题库的增删查改
  • oj_control.hpp:核心的业务逻辑(控制器)
  • Makefile:自动化构建代码

2.1 oj_server.cc

在这里我们实现用户请求的服务器路由功能,这里我们想要实现的有:

  • ① 获取所有的题目列表
  • ② 用户想要根据题目编号从而获取题目内容
  • ③ 用户提交代码使用判题的功能

因为这些都是通过网络服务来运行的,所以我们依旧使用我们之前使用的httplib来构建网络服务:

 

2.1.1 基础框架

在这里我们仅仅实现一下我们想要的接口功能,但是关于网页的设置我们还没有写,所以只能通过汉字的方式来简单描绘一下:

  • 在这里我们需要用到正则表达式进行指定的题目编号匹配;
  • 因为\是一个转义字符,所以还需要使用原生字符串让他保持原貌;
  • 用户请求的题目编号在req.matches[1]中。

Makefile文件:

程序在编译运行之后启动起来我们就可以直接进行访问了:

 

基础框架建立好之后,我们后面实现了题库和网页渲染时再回过头完善oj_server.cc

2.2 基于文件版的题库

这里实现的version1版本的题库(文件版的)

一个题需要具备的属性:

  • ① 题目的编号
  • ② 题目标题
  • ③ 题目难度
  • ④ 题目描述、题面
  • ⑤ 时间和空间要求(进行内部处理)

所以我们在oj_server模块中添加一个名为questions的文件夹,用来充当题库;

在里面添加一个名为questions.list的文件,用来存放题目列表(后面录题的时候直接在里面添加即可);

我们可以根据LeetCode来想一下,我们打开LeetCode的题库时,是一排排的题目列表,我们选中一个点进去之后才会具体的给我展示题目描述、示例、我们自主编写代码的区域、编写代码的区域还有预设的代码部分;

所以我们想要实现这个,就需要两批文件构成:

  • 第一批:题目列表(不需要题目的具体内容);
  • 第二批:具体题目的描述,题目的预设代码,以及测试用例。

这两批文件是通过题目编号产生关联的;

所以我们可以将指定题目的题目描述(desc.txt),题目预设代码(header.cpp),题目测试用例(tail.cpp)存放在一个以题目编号命名的文件夹中;

接下来我们就录入一个题目:

但是现在还需要的就是这个题目的有关测试用例,测试用例的编写我们就实现的简单一些,有两个就可以了,如果你自己相加也可以直接加上去,操作很简单;

我们通过定义临时对象的方法,来完成函数的调用:

当测试用例完成之后,未来我们进行代码提交时,oj_server提交给编译运行模块的代码是:

代码预设部分(header.cpp) + 该题号对应的测试用例(tail.cpp)。

但是我们上面实现的测试用例有很多报错,所以我们可以通过条件编译的方式消除这些报错:

这段条件编译的代码我们在编译时通过编译选项g++ -D COMPILER_ONLINE 将其裁剪掉。 

2.2 oj_model.hpp

我们有了题库之后,我们就可以通过oj_model.hpp根据list文件将题目列表加载到内存中,进行数据的交互,这里其实就是对这些题进行一个管理的工作,先描述再组织,所以我们先要有一个对题目细节描述的一个结构体,然后我们想让题目编号和题目细节形成一组映射,通过unordered_map进行管理。

对问题进行管理之后,还没有达到我们的目的,我们想要的是将题目列表加载到内存中,然后我们就可以通过单独设置一些接口来获取题目列表以及具体的信息了;

我们需要设置获取题目的接口分别对应我们在oj_server.cc中设置的需求:

  • 获取全部题目列表(通过输出型参数将获取到的题目信息存放在一个容器中)
  • 获取指定题目的题目内容 (通过指定题目编号将获取到的内容通过输出型参数返回)
  • 对指定题目进行判题(先不实现,后面再实现)

2.2.1 获取所有题目列表和指定题目接口

获取所有题目列表接口

因为我们将来保存题目的容器是unordered_map,所以我们要获取到它里面的value值,所以直接遍历然后直接push_back即可:

获取指定题号的题目内容

你给我一个指定的题目编号,我将这个题目编号所对应的内容返回给你,那么直接可以使用unordered_map的find接口直接查找这个题目编号,找到了直接将value值返回:

2.2.2 加载题目列表及信息

  • 因为我们前面都创建好了一个unordered_map用来保存加载至内存的题目及具体信息,所以我们可以直接通过文件操作的方式,用getline将questions.list的信息读取上来;
  • 因为我们之前录入的题目列表信息是按照空格分割的,但是getline是读取一整行,所以还要对读取到的数据按照空格进行分割
  • 我们分割字符串的接口依旧实现在comm模块的util.hpp中;
  • 分割字符串的接口我们想要实现你传递给我一个完整的字符串,你再给我一个按照哪种字符来分割的,我把分割好的字符串存放在vector中返回给你;
  • 这样子就能获取到一个题目的基本信息,但是还有对应题目的具体描述、预设代码、测试用例;
  • 因为这些都存放在文件中,所以我们直接通过FileUtil类中的ReadFile接口直接读取即可;
  • 当读取到所有的信息之后,我们按照题号为key值,对应题号的具体信息为value值插入的我们预设的unordered_map中。

2.2.3 引入日志

上面实现的好多功能里面的差错处理还没有做,所以我们包含日志所对应的头文件进行一下差错处理:

#pragma once
// 文件版本
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <cassert>

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

namespace ns_model
{
    using namespace ns_util;
    using namespace ns_log;

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

    // 题目列表所在路径
    const std::string questions_list = "./questions/questions.list";
    //
    const std::string questions_path = "./questions/";

    class Model
    {
    private:
        // 题目编号 + 题目的细节
        std::unordered_map<std::string, Question> questions; // 用来保存加载的题
    public:
        Model()
        {
            assert(LoadQuestionList(questions_list));
        }
        // 加载题目列表(先加载到内存)
        bool LoadQuestionList(const std::string &question_list)
        {
            // 加载配置⽂件: questions/questions.list + 题⽬编号⽂件
            std::ifstream in(question_list);
            if (!in.is_open())
            {
                LOG(FATAL) << "加载题库失败,请检查是否存在题库⽂件" << "\n";
                return false;
            }

            std::string line;
            while (std::getline(in, line))
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, " ");
                if (tokens.size() != 5) // 题目 标题 难度 时间 空间
                {
                    LOG(WARNING) << "加载部分题目失败, 请检查文件格式!" << "\n";
                    continue;
                }

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

                // 读取题目描述、预设代码、测试用例
                std::string path = questions_path;
                path += q.number;
                path += "/";

                FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);
                FileUtil::ReadFile(path + "header.cpp", &(q.header), true);
                FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);

                // 将加载的数据保存至questions
                questions.insert({q.number, q});
            }
            LOG(INFO) << "加载题库......成功!" << "\n";
            in.close();
        }

        // 获取所有题目列表
        bool GetAllQuestions(std::vector<Question> *out)
        {
            // 先判断有无数据
            if (questions.size() == 0)
            {
                LOG(ERROR) << "⽤⼾获取题库失败" << "\n";
                return false;
            }
            // 直接遍历然后push_back
            for (const auto &q : questions)
            {
                out->push_back(q.second); // q.first:key q.second: value
            }
            return true;
        }
        // 根据题目编号获取题目内容
        bool GetOneQuestion(const std::string &number, Question *content)
        {
            // 直接进行查找
            const auto &iter = questions.find(number);
            if (iter == questions.end())
            {
                LOG(ERROR) << "⽤⼾获取题⽬失败, 题⽬编号: " << number << "\n";
                return false;
            }
            (*content) = iter->second;
            return true;
        }
        ~Model() {}
    };
}

2.2.4 分割字符串接口

前面我们使用到的字符串切割的接口,我们当初是这样子设计的:传递给我一个字符串以及要按什么字符分割的字符,然后我将分割的好的字符串存储在一个vector容器中,但是我们还是需要自主的实现一个字符串切割的过程,遍历字符串,根据空格切割,再根据相对位置使用substr来进行分割,但是这样子做太麻烦,我们可以直接使用boost库来进行字符串的分割,那么首先就需要安装boost库:

# ubuntu上安装
sudo apt-get update
sudo apt-get install libboost-all-dev

要使用boost库需要包含的头文件是:

#include <boost/algorithm/string.hpp>

boost库切分字符串的用法:https://blog.csdn.net/jiemashizhen/article/details/130352512?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7EPayColumn-1-130352512-blog-131638856.235%5Ev43%5Econtrol&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7EPayColumn-1-130352512-blog-131638856.235%5Ev43%5Econtrol&utm_relevant_index=1https://blog.csdn.net/jiemashizhen/article/details/130352512?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7EPayColumn-1-130352512-blog-131638856.235%5Ev43%5Econtrol&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7EPayColumn-1-130352512-blog-131638856.235%5Ev43%5Econtrol&utm_relevant_index=1

接下来我们就在comm模块中的util.hpp中实现:

2.3 oj_control.hpp 

在这里我们实现核心业务逻辑的控制器,这里就是将我们的服务模块和数据交互模块以及网页渲染模块结合在一起;

因为这里需要用到oj_server.cc 、oj_model.hpp 、 oj_view.hpp多个文件,所以我们在编写核心控制器时直接对这些文件进行修改了。

2.3.1 基本框架 

我们的核心业务模块要能串联起来这些,首先得要有数据吧,所以我们可以先通过oj_model.hpp直接定义对象的方式将数据获取到,同时后面我们还可能使用到一些工具、日志,所以一次性就全部包含进来:

然后我们的视角转移到oj_server.cc中,可以发现我们之前实现的这些服务接口都是用文本的形式进行测试的, 所以我们根据我们的需求对这些测试接口进行修改,完成我们真正的服务;

对于获取所有题库,指定题目的题目内容这两个接口我们想实现的将获取到的这些题目信息转化成为网页然后进行返回,所以我们在核心控制代码这里就需要提供两个接口,一个是获取全部题目信息然后转化为网页,另一个是获取指定题目信息转化为网页,这里我们想要通过输出型参数的方式返回给上层一个网页;

我们先将基础框架搭建出来,可以看到在获取题目信息时我们可以直接调用model里面的方法即可,但是关于将它转化为网页的形式这就需要用到我们oj_view.hpp里面的内容了,所以等后面再慢慢补充。

有了这些接口,我们的oj_server.cc中的调用方法就需要进行改变了;

更改后的oj_server.cc:

2.3.2 安装与测试ctemplate

因为我们需要实现网页渲染的操作,所以我们需要引入ctemplate库来帮助我们进行网页渲染;

安装:

# 创建一个任意的文件夹然后cd进去
# 依次输入下面的指令
git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git

./autogen.sh

./configure

make

sudo make install
# 同样的我们也需要注意我们的gcc版本不能太低

如果出现这种报错时,大家可以私信我要安装包。

测试:

# 先创建两个文件 一个test.cc 一个test.html
$ 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

要能实现网页的渲染需要具备的两个数据,一个是保存数据的数据字典(kv的映射结构),另外一个是待被渲染的网页结构,渲染网页的本质其实就是用value值替换被渲染的内容;

测试代码:

test.cc

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

int main()
{
    // 原始网页
    std::string in_html = "./test.html";
    // 要替换的值
    std::string value = "测试ctemplate";

    // 形成数据字典
    ctemplate::TemplateDictionary root("test"); // 这一步就类似于创建 unordered_map<> test
    root.SetValue("key", value);                // test.insert({});

    // 获取被渲染的网页对象
    ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP);
    
    // 添加字典数据到网页中
    std::string out_html;
    tpl->Expand(&out_html, &root);
    // 完成渲染
    std::cout << out_html << std::endl;
}

test.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>用来测试</title>
</head>
<body>
    <p>{{key}}</p>
    <p>{{key}}</p>
    <p>{{key}}</p>
    <p>{{key}}</p>
    <p>{{key}}</p>
</body>
</html>

编译:

g++ test.cc -std=c++11 -lctemplate -lpthread

2.3.3 基本的渲染框架

因为我们在核心控制模块里面需要用到一些网页渲染的接口,就比如将所有题目列表渲染,或者是将指定题目内容进行渲染,所以这个渲染的接口我们要是现在oj_view.hpp中;

在oj_view.hpp中我们首先想要实现的接口就是将所有题目列表渲染,你给我传递题目列表(输入型参数),我给你渲染成网页结构(输出型参数),还有我们需要有一个对指定题目内容的渲染接口,你给我传递指定的题目(输入型参数),我将渲染好的网页返回给你(输出型参数)。

oj_view.hpp: 

这里有了具体的渲染接口,那么接下来我们需要对oj_control.hpp文件进行修改一下;

2.3.4 测试html文件 

关于html文件的这些如果你们不想看的话可以直接复制代码,因为博主也不会,关于这些具体的教程在:https://www.w3school.com.cn/html/index.asp

我们在oj_server模块下创建一个wwwroot的文件夹,然后在里面创建一个index.html用于进行网页测试;

在index.html里面我们这样子写,至于为什么这样子写,想了解的可以去上面的链接教程去了解一下,反正博主只知道一些皮毛;

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测试OJ系统</title>
</head>
<body>
    <h1>用于测试在线OJ的网页</h1>
    <p>自主开发的在线OJ平台</p>
    <table>
        <tr>
            <th>题目编号</th>
            <th>题目标题</th>
            <th>题目难度</th>
        </tr>
        <tr>
            <td>111</td>
            <td>222</td>
            <td>333</td>
        </tr>
        <tr>
            <td>111</td>
            <td>222</td>
            <td>333</td>
        </tr>
        <tr>
            <td>111</td>
            <td>222</td>
            <td>333</td>
        </tr> 
    </table>
</body>
</html>

然后我们的网页实现好了,但是我们的oj服务还没有链接上去,所以我们需要在oj_server.cc中添加一段代码;

oj_server.cc:

我们将我们的oj_server.cc编译启动之后,运行起来然后在浏览器上就可以访问我们刚刚实现的基本网页了;

2.3.5 全部题目的网页渲染

根据上面的测试代码,所以我们需要在oj_server模块下创建一个template_html用于存放我们的网页渲染html文件,因为我们需要将全部文件渲染,也需要将指定题目的内容进行渲染,所以我们创建两个html文件:

接下来,我们先来进行所有题目的网页渲染;

all_questions.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线OJ--题目列表</title>
</head>
<body>
    <table>
        <tr>
            <th>编号</th>
            <th>标题</th>
            <th>难度</th>
        </tr>
        {{#question_list}}
        <tr>    
            <td>{{number}}</td>
            <td>{{title}}</a></td>
            <td>{{star}}</td>
        </tr>
        {{/question_list}}
    </table>
</body>
</html>

我们想在主页设置一个点击就能跳转到题目列表的按钮,所以还需要在index.html中添加一些设置;

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>测试OJ系统</title>
</head>
<body>
    <h1>用于测试在线OJ的网页</h1>
    <p>自主开发的在线OJ平台</p>
    <a href="/all_questions">点击我进入题目列表</a>
</body>
</html>

对网页创建好了之后,我们需要在我们的oj_view.hpp中使用ctemplate进行网页的渲染;

oj_view.hpp:

这个渲染的操作在前面说到过,具体的细节可以去看看前面的测试代码;

网页创建好了,网页的渲染也做好了;接下来我们编译运行之后,可以直接在浏览器中进行访问:

可以看到显示出来的顺序是乱序的,这个先不用着急,我们后面再调整;

我们现在实现的这些网页渲染只是初步的,比较简陋,后面会有更加美化的版本;

2.3.6 指定题目的网页渲染

对于指定的题目的网页渲染我们想做的是:在全部题目列表中点击指定的题目标题,然后直接跳转到对应题目的编写页面;

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>
</head>
<body>
    <h4>{{number}}.{{title}}.{{star}}</h4>
    <p>{{desc}}</p>
    <textarea name="code" cols="30" rows="10">{{pre_code}}</textarea>
</body>
</html>

all_questions.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线OJ--题目列表</title>
</head>
<body>
    <table>
        <tr>
            <th>编号</th>
            <th>标题</th>
            <th>难度</th>
        </tr>
        {{#question_list}}
        <tr>    
            <td>{{number}}</td>
            <td><a href="/question/{{number}}">{{title}}</a></td>
            <td>{{star}}</td>
        </tr>
        {{/question_list}}
    </table>
</body>
</html>

有了基本的网页结构,接下来我们需要在oj_view.hpp中实现对这些网页的渲染工作;

oj_view.hpp:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <ctemplate/template.h>

#include "oj_model.hpp"

namespace ns_view
{
    using namespace ns_model;

    const std::string template_path = "./template_html/"; // 网页路径

    class View
    {
    public:
        View() {}
        ~View() {}

    public:
        void AllExpandHtml(const std::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. 获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);

            // 4. 开始完成渲染功能
            tpl->Expand(html, &root);
        }
        void OneExpandHtml(const struct Question &q, std::string *html)
        {
            // 1. 形成路径
            std::string src_html = template_path + "one_question.html";

            // 2. 形成数字典
            ctemplate::TemplateDictionary root("one_question");
            root.SetValue("number", q.number);
            root.SetValue("title", q.title);
            root.SetValue("star", q.star);
            root.SetValue("desc", q.desc);
            root.SetValue("pre_code", q.header);

            //3. 获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
           
            //4. 开始完成渲染功能
            tpl->Expand(html, &root);
        }
    };
}

一切工作准备就绪之后,我们再编译运行,在浏览器上面访问我们的网页:

后续我们再对网页界面进行美化操作;

2.4 负载均衡

我们实现了对全部题目的获取,对指定的题目内容的获取,但是我们的判题功能还是没有实现,所以我们的oj_control.hpp中还需要实现判题的功能;

判题接口我们想要传入题号,另外传入一个json串,里面包含对应的代码,以及用户输入,我们想把程序运行的结果也以json串的方式返回;

判题接口的实现分为以下几步:

  • ① 对传入的json串反序列化
  • ② 将得到的用户代码和测试代码拼接起来
  • ③ 选择负载最低的主机
  • ④ 发起http请求
  • ⑤ 将编译运行的结果返回给用户

其中第三步我们需要单独进行实现,所以我们先来实现负载均衡的功能;要能选择负载最低的主机,我们先要在我们的oj_server服务中创建配置文件添加我们需要的主机:

2.4.1 基本框架

我们在oj_control.hpp的文件中创建两个类,一个用于保存提供服务的主机,一个用于负载均衡选择;

提供服务的主机需要有ip、port、负载因子;在进行服务时,负载情况需要进行修改,所以还需要锁将负载因子保护起来;

负载均衡的模块中,需要有保存提供服务的主机的容器,可以用每个主机的下标当做他们各自的编号,有了编号我们就可以保存有哪些主机在线,有哪些主机离线,同样的这些数据也是需要进行保护的;

在负载均衡模块中,我们需要设置一个接口将配置文件中的这些主机给加载进来;

设置一个对加载上来的主机进行智能选择;如果选择的主机可以正常运行,那就表示在线状态,如果不能正常使用那就是离线状态:

2.4.2 LoadConf接口

这个接口主要将我们配置文件中的主机信息加载进来,所以我们需要用到文件接口,因为我们需要主机的ip和端口,所以需要对读取的信息按照 ":" 进行切分,将切分到的ip和port保存起来,并对主机编号也进行保存;

这里注意对应的一些头文件的包含; 

2.4.3 SmartChoice接口

这是我们的负载均衡的接口,首先我们要知道的是,在选择负载最低的主机时,我们需要将选择好的主机的id,以及主机的地址返回给上层,所以这里我们采用输出型参数的方式;

  • 负载均衡的算法有很多,这里我们选择轮询 + hash的方式实现;
  • 在这个整个选择的过程中是需要通过加锁来实现的;
  • 我们通过遍历的方式找到负载最小的主机。

在此之前我们在Machine中添加一些实用的方法:

  • 获取主机的负载;
  • 当有用户访问我们的服务时,需要对负载进行增加;
  • 当用户结束访问时,需要对负载减小;
  • 我们可以对负载进行复位。

接下来就对SmartChoice接口进行实现:

2.5 判题(Judge)功能

前面实现了负载均衡的功能,接下来就来实现判题的功能,前面在负载均衡那里也对判题的功能做了简单的介绍,需要补充的是,在反序列化之前我们需要根据传入的题号获取题目的细节;

注意:我们的判题功能是实现在oj_control.hpp中的核心业务逻辑的模块中的。

oj_control.hpp:

2.5.1 反序列化与拼接 

对传入的json串先进行反序列化,拿到对应的代码,然后将其与测试用例拼接起来,形成一个新的json用于后续的发起请求;(注意要包含使用json对应的头文件)

2.5.2 选择负载最低的主机

我们在进行智能选择时,用到的策略就是一直选择,直到选择出可以用的主机,否则,全部主机都处于离线状态;

2.5.3 发起http请求与返回结果

我们发起http请求时需要用到httplib,这个在之前我们用到过,就不做过多解释了,但是在这里我们实现的是一个客户端的结构;

  • 如果我们选择服务器成功,就表示要访问服务器,所以该服务器的负载就需要增加;
  • 另外,我们请求成功时,要能拿到正确的返回结果,需要保证res的状态码为200;
  • 请求结束后,表示我们访问完了服务器,所以服务器的负载就要减少;
  • 如果请求服务器失败,就表示该主机可能离线,所以我们需要将请求失败的主机进行离线操作;

这里还需要一个将主机设置为离线的接口,这个接口在负载均衡基础框架那里已经设计出来了,我们现在直接补充即可:

  • 因为我们用主机的下标当做主机id,所以我们将在线主机的id删掉,重新插入到离线主机的id;
  • 如果将主机离线,那么此时他的负载是要进行复位的;
  • 需要注意的时,离线的这个过程是需要用锁保护的。

2.5.4 使用Postman测试

在测试之前我们需要对我们的oj_server.cc进行一下修改,因为我们现在完善了判题的功能;

oj_server.cc:

接下来我们就需要对我们测试用例中的条件编译进行细节处理,因为我们是直接拼接,所以之前写的那些要通过编译选项给删除;

接下来我们就来使用Postman来对我们的判题功能进行测试:

当我们手动挂掉一台服务器,再发送请求时,就会选择另外一台机器;

因为Postman只能一次一次发送,所以暂时还是测不了我们的负载均衡,这个到后面网页时再测试;

Postman中的测试代码:

{
    "code": "#include <iostream>\n#include <string>\n#include <vector>\n#include <map>\n#include <algorithm>\nusing namespace std;\nclass Solution\n{\npublic:\nint Max(const vector<int> &v){\nreturn 0;\n}\n};\n",
    "input": ""
}

 由于这个代码比较容易打错,所以感兴趣的老铁可以复制粘贴去测试一下自己的服务;

注意:现在我们的绝大多数接口都实现了,还剩一个OnlineMachine的接口,这个接口后续有需要时再进行实现。

3. 前端服务

关于前端的网页编写代码,感兴趣的可以去具体的学习一下,这里就不做详细的介绍了,因为主要来学习后端开发的思路,不想写的可以直接拷贝到你自己的文件下,就可以实现和我一样的效果;

但是这里我们需要了解的来学习一下前后端的交互,到时候会简单介绍的。

3.1  首页编写

我们之前简单编写的首页是非常不美观的,所以在这里我们使用专业的网页编写方案对我们的首页进行设计;

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

        html,
        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;
            /* 整体居中 */
            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_">@StackY、独立开发的一个在线OJ平台</p>
            <a class="font_" href="/all_questions">点击我开始编程</a>
        </div>
    </div>
</body>

</html>

我们将我们的oj服务启动起来,然后直接访问就可以了:

这种首页就看起来有点美观了,如果想要更加美观的设置可以去搜一些关于背景颜色等等的博客去直接拷贝到你的html文件中即可;

3.2 题目列表编写 

之前我们实现的题库只有1道题,但是后面我又录了一道题,这不影响,为了看起来整体,我们可以再向题库中录入题目;为了方便,我们这里就录入相同的题目也无所谓,如果你们想要细致一点可以自己录入自己想添加的题目;

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

        html,
        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', 'Lucida Sans 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:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-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>@StackY、</h4>
        </div>
    </div>

</body>

</html>

这里的html文件修改完之后,需要看到效果就需要重新启动服务;

3.3 单个题目的编写

我们之前实现的代码框是直接用文本编辑器来实现的,写代码的体验非常不好,既没有缩进,也没有补齐与提示,为了体验效果更好,这里需要引入ACE在线编辑器;

针对这个编辑器的引入我们也不做过多的介绍;

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

        html,
        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.代码
            // 2. 构建json,并通过ajax向后台发起基于http的json请求
            // 3. 得到结果,解析并显示到 result中
        }
    </script>
</body>

</html>

这里仅仅实现的是前端的网页,对于具体的提交代码是没有设置的,这个后面我们详细说明;

3.4 提交代码 

我们现在实现的前端页面的提交按钮按了是没有什么用的,所以我们接下来就对提交(前后端交互)功能进行实现,其中这些内容了解就好了,不想了解的也可以直接复制粘贴代码;

one_question.htm:

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

        html,
        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;

            // 2. 构建json,并通过ajax向后台发起基于http的json请求
            $.ajax({
                method: 'Post',   // 向后端发起请求的方式
                url: judge_url,   // 向后端指定的url发起请求
                dataType: 'json', // 告知server,我需要什么格式
                contentType: 'application/json;charset=utf-8',  // 告知server,我给你的是什么格式
                // 要发送的数据
                data: JSON.stringify({
                    'code':code,
                    'input': ''
                }),
                success: function(data){
                    //成功得到结果
                    show_result(data);
                }
            });
            // 3. 得到结果,解析并显示到 result中
            function show_result(data)
            {
                // 拿到result结果标签
                var result_div = $(".container .part2 .result");
                // 清空上一次的运行结果
                result_div.empty();

                // 首先拿到结果的状态码和原因结果
                var _status = data.status;
                var _reason = data.reason;

                // 设置结果标签
                var reason_lable = $( "<p>",{
                       text: _reason
                });
                reason_lable.appendTo(result_div);

                if(_status == 0){
                    // 请求是成功的,编译运行过程没出问题,但是结果是否通过看测试用例的结果
                    var _stdout = data.stdout;
                    var _stderr = data.stderr;

                    var stdout_lable = $("<pre>", {
                        text: _stdout
                    });

                    var stderr_lable = $("<pre>", {
                        text: _stderr
                    })

                    stdout_lable.appendTo(result_div);
                    stderr_lable.appendTo(result_div);
                }
                else{
                    // 编译运行出错
                }
            }
        }
    </script>
</body>

</html>

这里面的代码都加了注释,其中想要具体了解的可以去搜一些博客结合起来;

我们这次测试就要将compile_server和oj_server一起启动来验证了:

3.5 题目排序

这个问题是我们之前遗留的,解决起来也很简单,当我们获取所有题目列表成功的时候,直接使用sort进行排序即可;

oj_control.hpp:

更改完代码之后我们可以讲我们的oj_server服务重新启动然后去浏览器访问一下题目列表:

4. 整体测试 

我们代码的整体功能都差不多了,现在就需要对他的负载均衡进行测试;

测试的具体流程,我们启动三台编译服务,然后讲我们的oj服务启动,打开浏览器访问我们的OJ服务,然后不断的点击提交代码,测试服务器的基本稳定性;

可以看到可以很均衡的选择我们的编译服务;

如果我们将编译服务全部关闭,然后再提交:

当编译服务都关闭时,也就表示所有的这个主机都下线了,这也正好符合我们的预期;

4.1 上线服务

我们前面还保留了一个上线服务的接口,但是没有实现,现在来对服务进行实现;

我们想实现的功能就是当服务器全部下线之后,然后我们再整体上线,简单的说就是将离线列表的机器全部放到在线列表中;因为我们当时设置时使用的是vector,所以可以很简单的用vector的一些功能进行插入和移除;

oj_control.hpp:

未来当服务器全部挂掉之后,我们使用一键上线功能将所有服务启动,我们的oj服务直接使用 ctrl + \ 就又可以直接进行负载均衡的实现了; 所以这里在oj_server.cc中需要使用信号的捕捉功能,捕捉三号信号,调用的方法就是启动所有主机;

oj_server.cc:

演示:

上面的这个版本是基于文件实现的题库,如果还没有学习到MySQL的话,后面的部分就不用看了,后面我们实现的是基于MySQL版本的题库;

在文章的最后会有整个服务的全部源码。

5. 基于MySQL版的题库

之前我们的题库是用文件进行设计的,接下来我们就来使用MySQL来实现题库;

我们想实现的MySQL题库时首先需要做的预备工作:

  • ① 在MySQL中创建一个可以远程登录的用户oj_client,并给他赋权;
  • ② 设计表结构,并录入题目;
  • ③ 编码实现。 

5.1 创建用户并赋权

关于MySQL的用户管理的部分在博主的另外一个博客中(用户管理),如果不知道怎么用可以去看一看,这里就直接使用了;

创建用户:

创建数据库:

赋权:

以上这些操作都是在MySQL中的root用户进行操作的;

接下来我们登录我们创建好的oj_client用户,查看库:

5.2 设计表结构并录题

  • 我们这里想设计的表名称叫做oj_questions;
  • 在文件版的题库中,我们对题目实现是按照分文件实现的通过题目编号将其联系在一起;
  • 但是在MySQL中设置表结构的话我们直接将这些属性设置在一个表中;
  • 表结构中需要包含的字段有:题号、标题、难度、题目描述、预设代码、测试用例、时间限制、空间限制。

为了方便起见,这里我们使用MySQL Workbench 表结构的设计以及录题;至于这个软件的使用在这个博客里面https://blog.csdn.net/liangzixx/article/details/110137282

如果不想用这个软件,也可以直接在MySQL中使用SQL语句,都可以,两种方式任选其一;

创建好表结构之后, 我们将我们的题目录入到表中;

5.3 使用MySQL题库 

关于如何使用C语言链接MySQL可以去看我的另外一篇博客:使用C语言链接MySQL

因为我们之前已经安装了好了所需要的库,所以我们引入对应的头文件链接使用,在编译选项中指明库,但是今天我们直接把所需要的头文件和库直接引入到我们的oj_server服务中,这样子使用会更加方便一些;

如果之前没有安装过的,我们直接先来演示一遍:

① 下载链接所需要的库和头文件:https://downloads.mysql.com/archives/c-c/

② 上传到Linux并解压:

为了后面方便我们对这个名称重命名:

注意:上传到Linux时需要找一个文件夹,这个文件夹的名称很重要,需要记住!!

③ 直接在我们的oj_server服务中建立软链接:

基本工作做完之后我们就需要对我们的代码部分进行修改了。

5.3.1 设计链接MySQL的接口 

因为我们是基于MVC版本的OJ服务,所以各项功能进行解耦,所以我们只需要把

M(model)模块进行修改,为了实现两个不同的版本,我们之前的文件版本的oj_model.hpp会进行保留,我们新的代码在原来的副本代码中进行修改即可,我们把MySQL版本的文件名命名为oj_model2.hpp;

  • 因为我们现在的题库是MySQL版本的,所以不需要对题目先加载了,所以我们需要将多余的部分删掉,只留下获取所有题目的接口和获取单个题目的接口;
  • 现在来想一下,如果我们在MySQL中想要获取全部内容我们所要使用的SQL语句是 select * from tablename; 如果我们要获取指定的题目,后面需要加上 where子句 进行筛选;所以我们设置一个单独的接口用来调用链接MySQL;
  • 这个接口我们想要传入一个sql语句,然后以输出型参数的方式将题库返回,后面如果获取全部题目的接口要调用就正常传入参数,如果是获取指定题目时我们的vector中就只有一个元素,也可以正常传递参数;

基础逻辑设置 

我们通过字符串拼接的功能将一个完整的sql传递给QuerMySql;

如果是获取指定题目,那么在vector中就只有一个元素。

5.3.2 链接MySQL获取题库 

关于如何链接MySQL在https://blog.csdn.net/Yikefore/article/details/145514293文章中已经详细介绍过了,这里就直接使用了:

#pragma once
// MySQL版本
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <fstream>
#include <cstdlib>

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

namespace ns_model
{
    using namespace ns_util;
    using namespace ns_log;
    struct Question
    {
        std::string number; // 题目的编号
        std::string title;  // 题目的标题
        std::string star;   // 题目难度:简单 中等 困难
        std::string desc;   // 题目的描述
        std::string header; // 题目预设给用户在线编辑器的代码
        std::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";
    const std::string user = "oj_client";
    const std::string passwd = "*********";
    const std::string db = "oj";
    const int port = 3306;

    class Model
    {
    private:
        // 题目编号 + 题目的细节
        std::unordered_map<std::string, Question> questions; // 用来保存加载的题
    public:
        Model()
        {
        }
        // 链接MySQL题库
        bool QueryMySql(const std::string &sql, std::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);
            // 分析结果
            my_ulonglong rows = mysql_num_rows(res);   // 获取行数
            my_ulonglong clos = mysql_num_fields(res); // 获取列数
            struct Question q;
            for(int i = 0; i < rows; i++)
            {
                MYSQL_ROW row = mysql_fetch_row(res);
                // 填充结构体字段
                q.number = row[0];
                q.title = row[1];
                q.star = row[2];
                q.desc = row[3];
                q.header = row[4];
                q.tail = row[5];
                q.cpu_limit = atoi(row[6]);
                q.mem_limit = atoi(row[7]);
                out->push_back(q);
            }
            // 关闭链接
            mysql_close(my);
            return true;
        }
        // 获取所有题目列表
        bool GetAllQuestions(std::vector<Question> *out)
        {
            std::string sql = "select * from ";
            sql += oj_questions;
            return QueryMySql(sql, out);
        }
        // 根据题目编号获取题目内容
        bool GetOneQuestion(const std::string &number, Question *content)
        {
            bool res = false;
            std::string sql = "select * from ";
            sql += oj_questions;
            sql += " where number=";
            sql += number;
            std::vector<Question> result;
            if (QueryMySql(sql, &result))
            {
                if (result.size() == 1)
                {
                    *content = result[0];
                    res = true;
                }
            }
            return res;
        }
        ~Model() {}
    };
}

关于乱码的问题在代码里面已经提到了,如果有乱码就将格式设置为utf8,没有乱码就不需要设置;

5.3 测试MySQL文件版 

因为我们文件版题库和MySQL版题库设计的接口都是一模一样的,所以我们只需要在oj_view.hpp和oj_control.hpp中将头文件换成oj_model2.hpp即可使用;

MakeFile文件:

由于我们引入了第三方库,所以我们你在编译选项中需要指定对应的头文件和库;

oj_server:oj_server.cc
	g++ -o $@ $^ -I./include -L./lib -std=c++11 -lctemplate -lpthread -ljsoncpp -lmysqlclient
.PHONY:clean
clean:
	rm -f oj_server

我们将我们的程序编译并运行之后先查看一下是否对应的库有链接:

如果没有链接成功,我们需要将这个库的路径给添加到配置文件中;

这样操作之后就可以成功链接了;

因为博主的Linux系统是Ubuntu 22.04,所以这种方法不可行,所以我也是搜了好多文章,将MySQL换成MariaDB(MySQL下的一个分支);使用起来和MySQL一模一样,然后需要更改一下Makefile文件:

oj_server:oj_server.cc
	g++ -o $@ $^ `mysql_config --cflags --libs` -I./include -L./lib -std=c++11 -lctemplate -lpthread -ljsoncpp -lmysqlclient
.PHONY:clean
clean:
	rm -f oj_server

至于怎么安装MariaDB大家可以去搜一些博客去安装一下;

然后我们编译我们的程序:

这样子就链接成功了,链接成功之后我们将我们的oj服务启动起来,然后访问就可以得到具体的网页信息了;

因为我只录入了一个题,你如果感兴趣你可以多录几个题;

6. 发布项目

未来如果我们想要发布我们项目,并不是将所有的源码给别人,而是将对应的可执行程序和配置文件打包在一起进行发布,所以我们来实现一下顶层的Makefile文件;

创建一个顶层的Makefile

我们想要实现的是:

  • 进入compile_server模块形成对应的可执行程序,并退出来;
  • 然后进入oj_server模块形成对应的可执行程序,并退出来;
  • 然后将编译服务和oj服务对应的可执行程序和对应的配置文件打包起来;
  • 在清理工作时,将所有的可执行和打包好的文件进行删除。

Makefile:

.PHONY:all
all:
	@cd compile_server;\
	make;\
	cd -;\
	cd oj_server;\
	make;\
	cd -;

.PHONY:output
output:
	@mkdir -p output/compile_server;\
	mkdir -p output/oj_server;\
	cp -rf compile_server/compile_server output/compile_server;\
	cp -rf compile_server/temp output/compile_server;\
	cp -rf oj_server/conf output/oj_server/;\
	cp -rf oj_server/lib output/oj_server/;\
	cp -rf oj_server/questions output/oj_server/;\
	cp -rf oj_server/template_html output/oj_server/;\
	cp -rf oj_server/wwwroot output/oj_server/;\
	cp -rf oj_server/oj_server output/oj_server/;

.PHONY:clean
clean:
	@cd compile_server;\
	make clean;\
	cd -;\
	cd oj_server;\
	make clean;\
	cd -;\
	rm -rf output;

未来我们想要将我们的项目给别人用时,只需要make output便可以生成一个发布版的项目:

7. 项目总结

我们的负载均衡在线OJ项目已经完结了,这里我们用到了很多知识点,比如:

  • ① C++ STL标准库
  • ② Boost库切分字符串
  • ③ cpp-httplib第三方网络库
  • ④ jsoncpp第三方序列化、反序列化库
  • ⑤ ctemplate第三方网页渲染库
  • ⑥ 负载均衡的设计
  • ⑦ MySQL C connect
  • ⑧ Ace前端编辑器、Html、css、js、jquery、ajax(了解) 

如果大家感兴趣还可以对项目进行一个拓展:

  • 比如添加登录注册功能;
  • 完善导航栏的功能;
  • 将我们的编译服务部署在docker;
  • 实现一个下一道题的功能等等 

该项目有两个版本,一个是基于文件版的题库,一个是基于MySQL版的题库,根据自己的情况实现对应的版本; 

项目源码以及板书:https://gitee.com/yue-sir-bit/load-balancing-type---oj

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

stackY、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值