• 负载均衡式的在线OJ系统



    1、项目演示

    服务首页页面
    在这里插入图片描述
    题库中的题目列表
    在这里插入图片描述
    点击题目,进入编辑页面,完成编写,进行提交
    在这里插入图片描述
    左边区域为题目的介绍,右边区域为代码编辑区。代码编辑完成后,点击提交按钮,无论运行结果如何,都会将信息返回到左下角,以便提示用户。

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

    • 技术栈:C++ STL、boost库的字符串分割,cpp-httplib第三方开源网络库,ctemplate第三方开源前端网页渲染库、jsoncpp序列化反序列化,负载均衡的设计算法,MySQL C语言连接数据库。
    • 开发环境:centos 7云服务器,vscode

    3、项目整体框架

    项目的整体框架分为三大模块
    comm公共模块:主要提供字符串操作、文件操作、网络请求和日志功能
    compile_server模块:对用户提交的代码进行编译,运行,得到最终的结果
    oj_server模块:采用MVC的设计模式。提供访问数据库或文件的功能。将题目列表和编辑页面展示给用户,并将用户请求负载均衡式的提交给后端的编译服务器,最终将结果返回给用户。

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

    在这里插入图片描述

    4、日志功能

    系统日志策略可以在故障刚刚发生时就向你发送警告信息,系统日志帮助我们在最短的时间内发现问题。为此,我们实现一个简单的日志系统来记录整个OJ系统的运行情况。

    因为日志功能,整个项目都要使用,所以将其放在comm模块下的log.hpp中。
    日志分为5个等级

    1. NORMAL:表示正常
    2. DEBUG:表示用来dubug
    3. WARNING:表示警告,但是不影响后续使用该服务
    4. ERROR:表示错误,用户无法正常使用
    5. DEADLY:表示服务器出现严重错误,所有服务都不能正常使用

    日志等级用枚举进行保存。
    日志信息主要包括日志等级、产生日志的文件名称、所在行号、发生时间以及产生日志的主要原因
    日志的调用格式为:LOG(level)<<" 日志信息 "<

    采用C语言中内置的宏,包装出一个宏函数,用于日志调用。

    #pragma once
    
    #include 
    #include 
    #include 
    #include "util.hpp"
    
    namespace ns_log
    {
        using namespace ns_util;
    
        //日志等级
        enum
        {
            INFO,    //正常
            DEBUG,   //调试
            WARNING, //警告,不影响后续运行
            ERROR,   //用户完蛋
            FATAL    //整个系统都完蛋
        };
    
        // level出错等级  file_name哪个文件出错  line在哪一行出错
        //因为日志经常被使用,普通函数调用慢,设计为内联函数,提高效率
        inline std::ostream &Log(const std::string &level, const std::string file_name, int line)
        {
            //添加日志等级
            std::string message = "[";
            message += level;
            message += "]";
    
            //添加文件名称
            message += "[";
            message += file_name;
            message += "]";
    
            //添加行号
            message += "[";
            message += std::to_string(line);
            message += "]";
    
            //获取当前时间
            message += "[";
            message += TimeUtil::GetNowTime();
            message += "]";
            // cout是有缓冲区的,不用endl就不会刷新缓冲区,除非缓冲区满了
            std::cout << message;
    
            return std::cout;
        }
    
    // LOG() << "message"
    //开放式日志
    //宏参带#:以字符串的形式展示,就不需要自己再将整数(日志等级为整数--->enum)转为字符串或string
    #define LOG(level) Log(#level, __FILE__, __LINE__)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    而用于获取当前时间的函数—GetNowTime()存放在comm模块下的ulit.hpp中,它主要用来存放工具,例如:获取当前时间、文件名的拼接的拼接等。

    static std::string GetNowTime()
    {
        time_t nowtime;
        struct tm *t;
        time(&nowtime);
        t = localtime(&nowtime);
        std::string _nowtime;
        _nowtime += std::to_string(t->tm_year + 1900) + "-" + std::to_string(t->tm_mon + 1) + "-" + std::to_string(t->tm_mday + 1);
        _nowtime += " ";
        _nowtime += std::to_string(t->tm_hour) + ":" + std::to_string(t->tm_min) + ":" + std::to_string(t->tm_sec);
        return _nowtime;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5、compilerServer模块设计

    compilerServer整体功能为:将用户请求的json串进行反序列化,形成名字唯一的源文件, 对这个源文件进行编译,运行,形成最终结果,将结果序列化,然后返回给用户,并且清理所有产生的临时文件

    而compilerServer又分为四个模块,整体结果如图所示:
    在这里插入图片描述

    5.1 compiler模块

    compiler的核心功能就是编译用户提交的代码,形成可执行程序。
    首先创建子进程,用execlp函数进行程序替换,替换成编译的代码,进行编译。
    父进程需要等待,并判断是否生成了可执行程序。如果没有生成可执行程序,则表示编译失败,此时需要打开临时的stderr文件(如果没有就创建),采用dup2重定向的方式,将编译失败的原因保存到该临时文件中。

    为了保持文件的整洁,统一将所有的临时文件放在一个tmp目录中,方便后期的统一管理。当编译成功后,tmp目录下会多出三个文件,分别是:

    源文件:./tempfile/文件名.cpp
    可执行:./tempfile/文件名.exe
    编译错误文件:./tempfile/文件名.cerr

    在创建临时文件时,需要将umask置为0,使后续创建文件,赋予权限时,不受umask值的影响

    因为创建文件和判断文件是否存在都是文件操作,所以将它们都放入util.hpp下统一管理

    namespace ns_compiler
    {
        //引入路径拼接功能
        using namespace ns_util;
        //引入文件日志
        using namespace ns_log;
        class Compiler
        {
        public:
            Compiler() {}
            ~Compiler() {}
    
            //参数:编译的文件名
            //返回值:编译成功返回true,否则返回false
            static bool Compile(const std::string &file_name)
            {
                pid_t pid = fork();
                //创建子进程失败
                if (pid < 0)
                {
                    LOG(ERROR) << "内部错误,创建子进程失败" << std::endl;
                    return false;
                }
                else if (pid == 0)
                {
                    //打开file_name对应的.stderr文件,没有就创建
                    umask(0);
                    int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                    //打开.stderr失败
                    if (_stderr < 0)
                    {
                        LOG(WARNING) << "没有成功形成stderr文件" << std::endl;
                        exit(1);
                    }
                    //重定向标准错误到_stderr
                    dup2(_stderr, 2);
    
                    //程序替换,并不影响文件描述符表
                    //子进程:调用编译器,完成对应代码的编译工作
                    // g++ -o target src -std=c++11
                    execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),
                           PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr /*不能忘记以nullptr结尾*/);
    
                    //如果代码运行到这里,表明execlp调用失败
                    LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << std::endl;
                    exit(2);
                }
                else
                {
                    //父进程
                    waitpid(pid, nullptr, 0); // 0表示阻塞等待
                    if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
                    {
                        LOG(INFO) << PathUtil::Src(file_name) << " 编译成功" << std::endl;
                        return true;
                    }
    
                    LOG(ERROR) << "编译失败,没有形成可执行程序" << std::endl;
                    return false;
                }
            }
        };
    }
    
    //在util.hpp下的FileUtil类中
    static bool IsFileExists(const std::string &path_name)
    {
        struct stat st;
        if (stat(path_name.c_str(), &st) == 0)
        {
            //获取文件属性成功,表明存在该文件
            return true;
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    5.2 run模块

    run的核心功能就是运行编译所形成的可执行程序。run不需要考虑代码结果是否正确,结果正确与否是由测试结果决定的!run只考虑是否正常运行完毕!
    所以程序的运行分为三种情况:

    1.代码跑完,结果正确
    2.代码跑完,结果不正确
    3.代码没跑完,异常了

    一般的在线OJ系统会对每个题进行时间和空间上的限制,目的是为防止用户有意或者恶意的消耗服务器的资源,导致服务器卡死或宕机。所以,我们也需要这样做。

    因此,该模块一共有两个主要的功能:

    1. 对程序的进行时间和空间上的限制
    2. 判断程序的执行情况并返回给用户

    对于程序的执行,无论是否正常运行,或者结果是否正确,都会有对应的信息。如果程序崩溃,就会将错误信息打印到标准错误中。如果程序运行正常,就会将最终的结果打印到标准输出中。但我们希望的是将这些信息返回给用户。因此我们就需要将这些信息保存到tmp目录下对应的临时文件中,方便上层提取。
    规定对应的文件格式为:

    xxx.stdin ---- 程序的输入信息,暂时不处理
    xxx.stdout ---- 保存程序正常退出的信息
    xxx.stderr ---- 保存程序运行时的报错信息

    每个程序运行前都会默认打开的三个文件:标准输入标准输出标准错误。就需要在运行程序之前使用dup2对它们进行重定向。

    实际在运行程序时,如果让主进程去运行可执行程序,如果运行是出错,整个进程也就崩溃了,最终就会导致compiler_server崩溃,无法再执行后续任务,还想运行compiler_server就需要重启服务。但是我们希望的是如果这次执行程序失败,也不会影响下次执行其他程序,也不需要重启服务。
    对于这个问题,我们就需要创建子进程,让子进程去执行程序。父进程通过等待的方式,获取子进程的退出情况,从而判断子进程执行程序的情况。假设子进程在执行程序时发生了崩溃,那么子进程一定是被信号终止,父进程就可以通过阻塞等待的方式获取这个信号,并且利用函数返回的方式将信号交付给上层,上层通过返回值就可知道错误的原因。
    规定:如果是我们模块内部的错误,比如打开文件失败等等,返回值就小于0,如果执行成功没有异常,返回值就等于0,如果代码崩溃收到信号,就把返回信号。

    程序运行起来,就是一个进程,所以我们需要对这个进程进行时间和空间上的限制,如果超过限制,进程也会被信号终止。
    使用系统接口:

    #include
    #include
    int setrlimit(int resource,const struct rlimit* rlim);

    对于resource,我们将用到以下两种取值:

    RLIMIT_CPU 限制进程占用CPU的时间
    RLIMIT_AS 限制进行使用的虚拟地址的大小

    在Linux系统中,Resouce limit(rlim是一个结构体)指在一个进程的执行过程中,它所能得到的资源的限制,比如进程的core file的最大值,虚拟内存的最大值等。
    Resouce limit的大小可以直接影响进程的执行状况。其有两个最重要的概念:soft limithard limit

    struct rlimit {
    rlim_t rlim_cur; /soft limit /
    rlim_t rlim_max; /
    hard limit (ceiling for rlim_cur)
    /
    };
    hard limit在资源中只是作为soft limit的上限。当你设置hard limit后,你以后设置的soft limit只能小于hard limit。要说明的是,hard limit只针对非特权进程,也就是进程的有效用户ID不是0的进程

    限制时间和内存

    static void SetPrcoLimit(int _cpu_limit, int _mem_limit)
    {
        //设置CPU时长,超出时间限制,系统会发送24号信号给该进程
        struct rlimit cpu_limit;
        cpu_limit.rlim_max = RLIM_INFINITY;
        cpu_limit.rlim_cur = _cpu_limit;
        setrlimit(RLIMIT_CPU, &cpu_limit);
    
        //设置内存大小,超出内存限制,系统会发送6号信号给该进程
        struct rlimit mem_limit;
        mem_limit.rlim_max = RLIM_INFINITY;
        mem_limit.rlim_cur = _mem_limit * 1024; //转换称为KB
        setrlimit(RLIMIT_AS, &mem_limit);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    整体代码:

    namespace ns_runner
    {
        using namespace ns_util;
        using namespace ns_log;
        class Runner
        {
        public:
            Runner() {}
            ~Runner() {}
    
        public:
            //设置进程占用资源大小的接口
            static void SetPrcoLimit(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);
            }
    
            //指明文件名即可,不需要代理路径,不需要后缀
            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);
                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) << "运行时打开标准文件失败" << std::endl;
                    return -1; //代表打开文件失败
                }
    
                pid_t pid = fork();
                if (pid < 0)
                {
                    LOG(ERROR) << "运行时创建子进程失败" << std::endl;
                    close(_stdin_fd);
                    close(_stdout_fd);
                    close(_stderr_fd);
                    return -2; //代表创建子进程失败
                }
                else if (pid == 0)
                {
                    dup2(_stdin_fd, 0);
                    dup2(_stdout_fd, 1);
                    dup2(_stderr_fd, 2);
    
                    SetPrcoLimit(cpu_limit, mem_limit);
                    execl(_execute.c_str() /*我要执行谁*/, _execute.c_str() /*我想在命令行上如何执行该程序*/, nullptr);
                    //如果走到这里,表示execl失败
                    exit(1);
                }
                else
                {
                    //父进程并不需要这三个文件,所以需要关闭
                    close(_stdin_fd);
                    close(_stdout_fd);
                    close(_stderr_fd);
    
                    int status = 0;
                    waitpid(pid, &status, 0);
    
                    LOG(INFO) << "运行完毕, info: " << (status & 0x7f) << std::endl;
                    //程序运行异常,一定是收到了信号!
                    return status & 0x7f;
                    //return WIFEXITED(status);//错误:WIFEXITED查看进程是否是正常退出
                    //如果为真(非零),就是正常退出,此时就可以用WEXITSTATUS(status)提取进程退出码
                }
            }
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85

    如果子进程正常退出,status的低8位将设置为0,如果子进程被信号终止,status的低位将设置为信号,所以根据status & 0x7f就能知道子进程的运行情况。

    5.3 compiler_run模块

    compiler_run的主要功能:
    因为用户提交的代码是以json串的形式传送过来的,所以我们需要进行反序列化,然后形成一个唯一名字的源文件,然后调用上面的编译模块和运行模块编译并执行该源文件,然后再把结果构建成json串返回给上层。最后清理编译和运行时产生的所有临时文件。

    用户发送的json串一定包含以下内容

    code:对应用户的代码
    input:对应用户的自测输入
    maxtime:对应该题目的时间限制
    maxmem:对应该题目的内存限制

    返回给用户的json串包含以下内容

    必有内容:
    status:我们规定的状态码
    reason:状态码对应的含义
    如果代码编译成功,则还会包含:
    _stdout:运行正常的结果(是否通过全部的测试用例)
    _stdrerr:运行时报错的结果

    状态码用来表示编译运行的状态,规定:

    -1:提交的代码是空
    -2:各种服务器的内部错误
    -3:编译失败
    0:编译运行成功
    0:进程运行崩溃时收到的信号

    static void Start(const std::string &in_json /*传入的json串*/, std::string *out_json /*传出的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();
        int mem_limit = in_value["mem_limit"].asInt();
    
        int status_code = 0;
        Json::Value out_value;
        int run_result = 0;
        std::string file_name; //需要内部形成的唯一文件名
    
        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; //未知错误
        }
        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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    5.3.1 相关的文件操作

    无论读取文件还是写入文件,都要使用文件操作,所以文件操作必不可少。读写文件也属于工具类里的文件操作,所以他们的位置就应该是Comm->util->FileUtil

    向文件中写入内容

    static bool WriteFile(const std::string &target, const std::string &content)
    {
        std::ofstream out(target);
        if (!out.is_open())
        {
            return false;
        }
        out.write(content.c_str(), content.size());
        out.close();
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    读取文件的内容

    // keep为false 表示不保留换行符   为true表示保留换行符
    static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
    {
        (*content).clear();
        std::ifstream in(target);
        if (!in.is_open())
        {
            return false;
        }
        std::string line;
        // getline不保存换行符,有些情况需要保留换行符
        while (std::getline(in, line))
        {
            (*content) += line;
            (*content) += (keep ? "\n" : "");
        }
        in.close();
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    清理临时文件
    用户提交一次代码,都会在编译和运行时产生临时文件,因为这些临时文件只保存了本次提交代码的编译和运行结果,所以也只对本次提交有用,对后续没用。如果我们放任不管的话,就会产生越来越多的临时文件,会占用后台大量的存盘空间,所以在将结果返回给用户后,需要清理这些临时文件。

    因为有些临时文件可能会不产生,所以我们也不知道需要清理的个数,好在这些临时文件的后缀名不同,我们可以依次判断该文件是否存在的方式,清理文件。

    清理文件就需要用到系统提供的接口

    #include
    int unlink(const char* pathname);

    参数(pathname)就是要删除文件的路径+文件名+后缀组成的完整文件名。而路径工具类里面刚好就有得到对应完整文件名的方法,直接调用得到文件名然后删除即可。

    static void RemoveTempFile(const std::string &file_name)
    {
        // unlink 系统调用,删除对应的文件
        //清理文件的个数是不确定的
        std::string _src = PathUtil::Src(file_name);
        if (FileUtil::IsFileExists(_src))
            unlink(_src.c_str());
    
        std::string _compiler_error = PathUtil::CompilerError(file_name);
        if (FileUtil::IsFileExists(_compiler_error))
            unlink(_compiler_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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    5.3.2 获得状态码含义

    根据最开始定义的规定,编译运行模块最终给上层返回的json串里面一定要包含状态码字段和状态码描述字段,状态码可以让上层代码得知编译运行的情况,状态码描述则是告诉用户是怎么回事,所以不同的状态码会有不同的含义,需要封装一个接口完成这个功能

    // code > 0 : 进程收到了信号导致异常奔溃
    // code < 0 : 整个过程非运行报错(代码为空,编译报错等)
    // code = 0 : 整个过程全部完成
    static std::string CodeToDesc(int code, const std::string &file_name)
    {
        std::string desc;
        switch (code)
        {
        case 0:
            desc = "编译运行成功";
            break;
        case -1:
            desc = "提交的代码是空";
            break;
        case -2:
            desc = "未知错误";
            break;
        case -3:
            // desc = "代码编译的时候发生了错误";
            FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);
            break;
        case SIGABRT: // 6
            desc = "内存超过范围";
            break;
        case SIGFPE: // 8
            desc = "浮点数溢出";
            break;
        case SIGSEGV: // 11
            desc = "野指针";
            break;
        case SIGXCPU: // 24
            desc = "CPU使用超时";
            break;
        default:
            desc = "未知: " + std::to_string(code);
            break;
        }
        return desc;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    虽然我们只写了六个信号,但是后续还可以添加其他的信号。

    5.4 compiler_server模块

    compiler_server把整个模块打包成一个网络服务,用户使用POST方法请求服务器上的compiler_server服务,请求的正文就是compiler_run所需要的json串,将json串反序列化后再进行编译和运行,然后返回给用户。

    compiler_server作为一个可以在多个主机上部署的网络服务,我们需要把他的端口号暴露给用户,用户才能使用该服务。

    void Usage(std::string proc)
    {
    
        std::cerr << "Usage: " << proc << " port" << std::endl;
    }
    
    //编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性
    //不然多个用户可能会相互影响
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            return 1;
        }
    
        Server svr;
    
        svr.Get("/",[](const Request& req, Response &resp){
            resp.set_content("hello http, 你好!","text/plain;charset=utf-8");
        });
    
        svr.Post("/compile_and_run", [](const Request &req, Response &resp)
                 {
            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;");
            } });
            
        svr.listen("0.0.0.0", atoi(argv[1]));
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    5.5 如何形成名字唯一的文件

    在线OJ系统可能存在多人同时使用,同时提交的情况。此时我们就需要一个方法,生成唯一名字的源文件,这样才不会导致用户提交的代码和编译运行的结果不一致。

    因为时间戳是一直在变化且不会重复的,所以采用毫秒级时间戳的方式来命名一个文件。但是这还不能保证文件名的唯一性。因为存在同一时刻提交文件的情况。所以我们还需要加上原子性递增唯一值来保存唯一性。

    tatic std::string GetTimeStamp()
    {
        struct timeval _time;
        //获取时间戳
        gettimeofday(&_time, nullptr);
        return std::to_string(_time.tv_sec);
    }
    
    //获得毫秒时间戳
    static std::string GetTimeMs()
    {
        struct timeval _time;
        gettimeofday(&_time, nullptr);
        return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
    }
    
    static std::string UniqFileName()
    {
        static std::atomic_int id(0);
        id++;
        //毫秒级时间戳+原子性递增唯一值:来保证唯一性
        std::string ms = TimeUtil::GetTimeMs();
        std::string uniq_id = std::to_string(id);
        return ms + "_" + uniq_id;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    此时我们只是形成了一个没有后缀的文件名,想要形成源文件还得需要加上后缀才行,不仅如此,对于其它需要形成的临时文件,也是只要加上后缀即可。

    //以下3个是编译时需要有的临时文件
    //构建源文件路径+后缀的完整文件名
    // 1234-> ./temp/1234.cpp
    static std::string Src(const std::string &file_name)
    {
        return AddSuffix(file_name, ".cpp");
    }
    
    //构建可执行程序的完整路径+后缀名
    // 1234-> ./temp/1234.exe
    static std::string Exe(const std::string &file_name)
    {
        return AddSuffix(file_name, ".exe");
    }
    
    //构建编译时报错文件的完整路径+后缀名
    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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    此时,文件名的不同可以区分不同时刻用户提交的请求,而后缀的不同可以区分用户提交的请求从而生成不同的临时文件。
    所以文件名+后缀在整个tmp目录下都具有唯一性

    6、OJServer模块设计

    6.1 整体结构设计

    OJServer模块是直接和用户交互的,因为我们的OJ系统属于Web应用,用户访问OJ系统,我们需要提供一个首页,其次我需要有一个题目列表网页供用户选择题目,再者我还需要一个可以给用户写代码做题的网页,并且可以判断用户提交的代码是否正确。
    所以用户的请求分为三种:

    1.请求题目列表
    2.请求一个具体的题目,需要有题目描述和编辑区
    3.提交,判题请求,返回结果

    它们的共同点是:以网页的形式呈现给用户

    最开始我们也提到过,OJServer模块将采用MVC的设计模式

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

    这样设计的好处就是把数据,业务逻辑和界面进行了分离。

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

    6.2 oj_server模块——用户请求的服务路由功能

    主要功能就是搭建一个http服务,通过用户请求不同的资源,完成对应功能路由的任务,调用oj_control模块的功能:

    int main()
    {
    
        //用户请求的服务路由功能
        Server svr;
    
        Control ctrl;
    
        // 获取所有的题目列表
        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");
        svr.listen("0.0.0.0"/*表示任意IP地址*/, 8080);//设置端口为8080
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    6.3 oj_model模块——提供对数据的操作

    oj_model模块的主要功能就是和后端的题库交互,主要是为了完成请求题目列表和请求单个题目的功能,得到题库中对应的题目信息。

    为了完整的表示题目的信息,我们需要用一个struct将题目的相关属性封装起来

    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拼接,形成完整代码
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们给用户提供的是一个根据题目描述而设计的一个函数接口。当用户编辑完成后,并不能直接编译成可执行程序,需要添加提前编写好的测试用例和头文件,将其组合成一个源文件,再交给编译服务器,如果用户实现部分没有语法错误,才能形成可执行文件。

    总的来说,oj_model模块将实现两个功能:

    1. 获取所有的题目列表
    2. 通过题目编号获取题目的相关信息

    oj_model模块我们将实现两个版本,一个是文件操作版,一个是数据库版(mysql),虽然在实现细节上大有不同,但是在整体的功能上是完全一致的。

    6.3.1 文件版oj_model

    将题目编号,题目标题,题目难度,时间限制和内存限制这些字段信息存放在一个文件里面。将头文件放在另一个文件里。

    再根据题目编号建立一个文件夹,将题目描述,预置代码,测试用例放在这个文件夹下。到时候就可以通过题目编号找到题目对应的路径,然后读取对应的文件就可以得到对应的信息,这样不仅读取方便,还便于我们录题。

    在获取题目时,我们需要用到unordered_map,用于存放题目编号和题目的映射关系。后面获取单个题目时,只需要根据题目编号,就能找到对应的题目。

    namespace ns_model
    {
        using namespace std;
        using namespace ns_log;
        using namespace ns_util;
    
        struct Question
        {
            std::string number; //题目编号,唯一
            std::string title;  //题目的标题
            std::string star;   //难度: 简单 中等 困难
            int cpu_limit;      //题目的时间要求(S)
            int mem_limit;      //题目的空间要去(KB)
            std::string desc;   //题目的描述
            std::string header; //题目预设给用户在线编辑器的代码
            std::string tail;   //题目的测试用例,需要和header拼接,形成完整代码
        };
    
        const std::string questins_list = "./questions/question.list";//question.list存放所有题目的大致信息
        const std::string questins_path = "./questions/";//./questions/+上题目编号为存放题目详细信息的路径
    
        class Model
        {
        private:
            //题号 : 题目细节
            unordered_map<string, Question> questions;
        public:
            Model()
            {
                assert(LoadQuestionList(questins_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 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 = questins_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.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 std::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()
            {}
        };
    } // namespace ns_model
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    因为用到了字符串分割,该函数属于工具类,所以存放到了util.hpp下StringUtil类中

    // boost split
    //第一个参数:存放结果的地方
    //第二个参数:需要进行分割的数据源
    //第三个参数:分隔符
    //第四个参数:是否对进行压缩 例如:fff\3\3\3lll不压缩->fff  lll   fff\3\3\3lll压缩->ffflll
    static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string sep)
    {
        boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on /*是否要压缩,是*/);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    6.3.2 数据库版oj_model

    和文件不同,数据库的读取比较简单,所有不需要把信息分开存储,可以使用一张表存储一个题目的所有信息,使用的是MySQL实现。

    首先创建一个oj库当作题库,表结构的设计创建如下:

    create table if not exists `oj_questions`(
    	`number` int primary key auto_increment COMMENT'题目的编号',
        `title` varchar(128) NOT NULL COMMENT'题目的标题',
        `star` varchar(8) NOT NULL COMMENT'题目的难度',
        `desc` text NOT NULL COMMENT'题目的描述',
        `header` text NOT NULL COMMENT'对应题目预设给用户看的代码',
        `tail` text NOT NULL COMMENT'对应题目的测试用例代码',
        `cup_limit` int default 1 COMMENT'对应题目的超时时间',
        `mem_limit` int default 50000 COMMENT'对应题目的最大开辟内存空间'
    )engine=InnoDB default charset=utf8;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后创建一个专门的oj_client用户,把oj数据库的所有权限都给这个用户
    创建用户

    mysql> create user oj_client@'%' identified by '123456';
    
    • 1

    赋权

    mysql> grant all on oj.* to oj_client@'%';
    
    • 1

    注意:如果想要远程访问数据库,需要开放mysql的默认端口号3306
    最后将所以题目一一录入即可。

    namespace ns_model
    {
        using namespace std;
        using namespace ns_log;
        using namespace ns_util;
    
        struct Question
        {
            std::string number; //题目编号,唯一
            std::string title;  //题目的标题
            std::string star;   //难度: 简单 中等 困难
            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 password = "123456";
        const std::string db = "oj";
        const int port = 3306;
        class Model
        {
        public:
            Model()
            {
            }
            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(), password.c_str(), db.c_str(), port, nullptr, 0))
                {
                    LOG(FATAL) << "连接数据库失败!!" << std::endl;
                    return false;
                }
                LOG(INFO) << "连接数据库成功" << std::endl;
                //执行sql语句
                if (0 != mysql_query(my, sql.c_str()))
                {
                    LOG(FATAL) << sql << "execute error!" << std::endl;
                    return false;
                }
                //设置编码
                mysql_set_character_set(my, "utf8");
    
    
                //提取数据
                MYSQL_RES *result = mysql_store_result(my);
                //获取行数
                int rows = mysql_num_rows(result);
                //获取列数
                int cols = mysql_num_fields(result);
    
                //获取表中数据
                Question q;
                for (int i = 0; i < rows; ++i)
                {
                    MYSQL_ROW line = mysql_fetch_row(result); //获取完整的一行记录【可能包含多列】
                    q.number = line[0];
                    q.title = line[1];
                    q.star = line[2];
                    q.desc = line[3];
                    q.header = line[4];
                    q.tail = line[5];
                    q.cpu_limit = atoi(line[6]);
                    q.mem_limit = atoi(line[7]);
    
                    //将这个题目插入到vector中
                    out->push_back(q);
                }
                //关闭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 std::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))
                {
                    if (result.size() == 1)
                    {
                        *q = result[0];
                        res = true;
                    }
                }
                return res;
            }
            ~Model()
            {
            }
        };
    } // namespace ns_model
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    6.4 oj_view模块——负责渲染网页

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

    渲染网页首先要有对应的网页,所有就要在OJServer模块的目录下新建两个html文件,负责题目列表和单个题目的两张网页,整个模块的功能就是通过上层传来的题目属性attribute,提取出不同网页需要的字段,渲染到网页中。规定题目列表只显示题目的编号,标题,难度即可,而单个题目则需要题目的编号,标题,难度,描述和预设代码字段。

    view模块通过把这些数据交给前端的网页,然后再根据前端部分构建出对应的网页,两者配合起来,形成给用户返回的页面。渲染工作通过ctemplate库实现,安装和使用方法在后面有写。

    namespace ns_view
    {
        const std::string template_path = "./template_html/";
    
        using namespace ns_model;
        class View
        {
        public:
            View() {}
            ~View() {}
    
        public:
            void AllExpandHtml(const vector<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);
            }
    
            void OneExpandHtml(const 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);
            }
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    6.5 oj_control模块——逻辑控制

    oj_control的主要功能是将用户提交的代码进行反序列化,得到题目的编号,通过题目编号找到对应的题目,将用户代码和对应的测试用例拼接在一起,重新组合成新的代码,再进行序列化,形成新的json串,最后再根据负载均衡算法,选择对应的编译服务器进行编译运行。

    6.5.1 编译主机设计

    因后台可能存在多台提供编译服务的主机,为了区分不同的主机,我们就需要一个结构来保存主机的相关信息,这些信息包括

    std::string ip; //编译服务的ip
    int port; //编译服务的port
    uint64_t load; //编译服务的负载

    当用户提交代码后,编译服务的负载将增加。当代码编译完成并运行成功后,编译服务的负载将减少。如果中途服务主机突然挂了,还需要清空对应主机的负载。

    因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证我对负载操作的安全性,就需要一个mutex互斥锁保护对负载的操作。我们将主机信息和相关操作封装成一个类

    // 提供服务的主机
    class Machine
    {
    public:
        std::string ip;  //编译服务的ip
        int port;        //编译服务的port
        uint64_t load;   //编译服务的负载
        std::mutex *mtx; // 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();
        }
    
        //重置负载为0
        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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    6.5.2 负载均衡模块设计

    因为后台存在多个主机提供编译服务,因此需要我们将这些主机有序的组织起来。并且我们需要为每台主机进行编号。
    为此,采用vector作为存放主机的容器,因为vector的下标很好的与主机编号相匹配。在提供编译服务之前,我们需要知道有哪些主机能为我们提供服务,所以规定在当前路径下的conf文件夹下的一个.conf文件里面会存放所有的可以提供服务的主机信息,包括IP地址和端口号,中间采用":"号分割。当调用负载均衡模块时,它会自动读取该文件,并初始化vector中的主机信息。

    除此之外,当多个服务同时请求主机时,可能会导致负载不均衡的情况,所以也需要加锁控制

    主机存在多个,当一个服务请求主机时,选择了一个负载最低的主机,如果这个主机挂掉,那么它会选择其他负载最低的主机。同时也需要记录已经挂掉的主机。因为我们需要两个vector,一个用来存储当前可用主机,另一个用来存储已经挂掉的主机。

    如果服务请求主机,然而所有主机全部挂掉,此时该请求服务就得不到任何响应,唯一能做的就是为后台开发人员提供相关的日志信息。

    负载均衡算法:
    到这里我们已经有了一个vector,里面存放了所有可用的主机。当请求服务到来时,只需要通过遍历的方式,找到负载最下的主机即可

    class LoadBlance
    {
    private:
        // 可以给我们提供编译服务的所有的主机
        // 每一台主机都有自己的下标,充当当前主机的id
        std::vector<Machine> machines;
        // 所有在线的主机id
        std::vector<int> online;
        // 所有离线的主机id
        std::vector<int> offline;
        // 保证LoadBlance它的数据安全
        std::mutex mtx;
    
    public:
        LoadBlance()
        {
            assert(LoadConf(service_machine));
            LOG(INFO) << "加载 " << service_machine << " 成功" << std::endl;
        }
    
        ~LoadBlance()
        {
        }
    
    public:
        bool LoadConf(const std::string &machine_conf)
        {
            std::ifstream in(machine_conf);
            if (!in.is_open())
            {
                LOG(FATAL) << " 加载: " << machine_conf << " 失败" << std::endl;
                return false;
            }
            std::string line;
            while (std::getline(in, line))
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, ":");
                if (tokens.size() != 2)
                {
                    LOG(WARNING) << " 切分 " << line << " 失败" << std::endl;
                    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();
            // 负载均衡的算法
            // 轮询
            int online_num = online.size();
            if (online_num == 0)
            {
                mtx.unlock();
                LOG(FATAL) << " 所有的后端编译主机已经离线, 请运维的同事尽快查看" << std::endl;
                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 curr_load = machines[online[i]].Load();
                if (min_load > curr_load)
                {
                    min_load = curr_load;
                    *id = online[i];
                    *m = &machines[online[i]];
                }
            }
            mtx.unlock();
            return true;
        }
    
        void OfflineMachine(int which)
        {
            mtx.lock();
    
            auto it = std::find(online.begin(), online.end(), which);
            if (it != online.end())
            {
                //要离线的主机已经找到啦
                machines[which].ResetLoad();
                online.erase(it);
                offline.push_back(which);
            }
            mtx.unlock();
        }
    
        void OnlineMachine()
        {
            mtx.lock();
            online.insert(online.end(), offline.begin(), offline.end());
            offline.erase(offline.begin(), offline.end());
            mtx.unlock();
    
            LOG(INFO) << "所有的主机有上线啦!" << std::endl;
        }
    
        // for test
        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();
        }
    
        int GetOnlineMachine()
        {
            return online.size();
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

    6.5.3 Control核心业务逻辑的控制器

    用户的请求有多种,包括请求所有题目列表,请求单个题目和详细内容,用户提交代码,请求判题。

    如果是请求题目列表或者单个题目加详细信息,则需要调用oj_view模块,构建网页。
    如果是请求判题功能,需要对用户提交的代码进行反序列化,重新拼接成新的代码,选择负载最小的主机进行编译和运行,最后将运行结果返回给oj_server

    class Control
    {
    private:
        Model _model;            //提供后台数据
        View _view;              //提供html渲染功能
        LoadBlance _load_blance; //核心负载均衡器
    public:
        Control(){}
        ~Control(){}
    public:
        void RecoveryMachine()
        {
            _load_blance.OnlineMachine();
        }
        //根据题目数据构建网页
        // 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.OneExpandHtml(q, html);
            }
            else
            {
                *html = "指定题目: " + number + " 不存在!";
                ret = false;
            }
            return ret;
        }
    
        // code: #include...
        // input: ""
        void Judge(const std::string &number, const std::string in_json, std::string *out_json)
        {
            // LOG(DEBUG) << in_json << " \nnumber:" << number << std::endl;
    
            // 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);
            std::string code = in_value["code"].asString();
    
            // 2. 重新拼接用户代码+测试用例代码,形成新的代码
            std::string head;
            FileUtil::ReadFile("./questions/head.hpp", &head, true);
            Json::Value compile_value;
            compile_value["input"] = in_value["input"].asString();
            compile_value["code"] = head + "\n" + code + "\n" + q.tail;
            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)
            {
            END:
                int id = 0;
                Machine *m = nullptr;
                if (!_load_blance.SmartChoice(&id, &m))
                {
                    break;
                }
    
                // 4. 然后发起http请求,得到结果
                Client cli(m->ip, m->port);
                m->IncLoad();
                LOG(INFO) << " 选择主机成功, 主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 当前主机的负载是: " << m->Load() << std::endl;
                if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
                {
                    // 5. 将结果赋值给out_json
                    if (res->status == 200)
                    {
                        *out_json = res->body;
                        m->DecLoad();
                        LOG(INFO) << "请求编译和运行服务成功..." << std::endl;
                        break;
                    }
                    m->DecLoad();
                }
                else
                {
                    //请求失败
                    LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线" << std::endl;
                    _load_blance.OfflineMachine(id);
                    if (_load_blance.GetOnlineMachine() != 0)
                    {
                        goto END;
                    }
                    _load_blance.ShowMachines(); //仅仅是为了用来调试
                    break;
                }
            }
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    7、前端页面设计

    前端页面分为三大部分:

    1. 首页页面
    2. 题目列表页面
    3. 指定题目的编写提交页面

    7.1 首页页面

    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>
            * {
                /* 消除网页的默认外边距和内边距 */
                margin: 0px;
                padding: 0px;
            }
            
            html,
            body {
                width: 100%;
                height: 100%;
            }
            
            .container .navbar {
                width: 100%;
                height: 50px;
                background-color: #000;
                /* 给父级标签设置overflow,取消后续float带来的影响 */
                overflow: hidden;
            }
            
            .container .navbar a {
                display: inline-block;
                /*设置a标签的宽度*/
                width: 80px;
                /* 设置字体颜色 */
                color: white;
                /* 设置字体大小 */
                font-size: large;
                /* 设置文字的高度和导航栏一样 */
                line-height: 50px;
                /*取消链接的下划线*/
                text-decoration: none;
                /*设置文字居中*/
                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;
                /*取消链接的下划线*/
                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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    7.2 题目列表页面

    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>
            * {
                /* 消除网页的默认外边距和内边距 */
                margin: 0px;
                padding: 0px;
            }
            
            html,
            body {
                width: 100%;
                height: 100%;
            }
            
            .container .navbar {
                width: 100%;
                height: 50px;
                background-color: #000;
                /* 给父级标签设置overflow,取消后续float带来的影响 */
                overflow: hidden;
            }
            
            .container .navbar a {
                display: inline-block;
                /*设置a标签的宽度*/
                width: 80px;
                /* 设置字体颜色 */
                color: white;
                /* 设置字体大小 */
                font-size: large;
                /* 设置文字的高度和导航栏一样 */
                line-height: 50px;
                /*取消链接的下划线*/
                text-decoration: none;
                /*设置文字居中*/
                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;
                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(247, 248, 243);
            }
            
            .container .question_list h1 {
                color: rgb(201, 235, 64);
            }
            
            .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>OnlineJudge题目列表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">
                <h4>@冯蕾h4>
            div>
        div>
    
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137

    7.3 指定题目的编写提交页面

    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>
        
        
        
        
        
        <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>
        
        <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: #000;
                /* 给父级标签设置overflow,取消后续float带来的影响 */
                overflow: hidden;
            }
            
            .container .navbar a {
                display: inline-block;
                /*设置a标签的宽度*/
                width: 80px;
                /* 设置字体颜色 */
                color: white;
                /* 设置字体大小 */
                font-size: large;
                /* 设置文字的高度和导航栏一样 */
                line-height: 50px;
                /*取消链接的下划线*/
                text-decoration: none;
                /*设置文字居中*/
                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: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, 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: white;
                /* 给按钮带上圆角 */
                /* border-radius: 0.5pc; */
                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>
        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() {
                //alert("hehe");
                //1.收集当前页面的有关数据  1.题号   2.代码
                //获取用户代码
                var code = editor.getSession().getValue();
                console.log(code);
                //获取题号
                var number = $(".container .part1 .left_desc h3 #number").text();
                //console.log(number);
                //构建访问的url
                var judge_url = "/judge/" + number;
                //console.log(judge_url);
    
                //2.构建json,并通过ajax向后台发起基于http的json请求
                $.ajax({
                    method: 'Post', //想后端发起后台请求
                    url: judge_url, //向后端发起指定的url请求
                    dataType: 'json', //告知server端,我需要什么格式
                    contentType: 'application/json;charset=utf-8', //告知server端,我给你的是什么格式
                    data: JSON.stringify({
                        'code': code,
                        'ipunt': ''
                    }),
                    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 = $("

    ", { text: _reason }); reason_lable.appendTo(result_div); if (status == 0) { //请求是成功的,编译运行过程没问题,但是结果是否通过看测试用例的结果 var _stdout = data.stdout; var _stderr = data.stderr; var stdout_labe = $("

    ", {
                            text: _stdout
                        });
    
                        var stderr_labe = $("
    ", {
                            text: _stderr
                        });
    
                        stdout_labe.appendTo(result_div);
                        stderr_labe.appendTo(result_div);
                    } else {
                        //编译运行失败
    
                    }
                }
            }
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260

    8、顶层项目部署Makefile

    在顶层新建一个Makefile文件,该文件的功能就是可以make时可以同时编译CompilerServer服务和OJServer服务,当输入make submit时会自动形成一个output文件,里面包含了compiler_server和oj_server的应用程序和一些运行程序必须的文件,时间打包的功能。最后输入make clean不光会清理掉创建的可执行程序,还会清理掉output的内容。

    # 编译功能
    .PHONY:all
    all:
    	@cd CompilerServer;\
    	make;\
    	cd -;\
    	cd OJServer;\
    	make;\
    	cd -;
    
    # 部署功能
    .PHONY:submit
    submit:
    	@mkdir -p output/CompilerServer;\
    	mkdir -p output/OJServer;\
    	cp -rf CompilerServer/compiler_server output/CompilerServer;\
    	cp -rf CompilerServer/tempfile output/CompilerServer;\
    	cp -rf OJServer/oj_server output/OJServer;\
    	cp -rf OJServer/conf output/OJServer;\
    	cp -rf OJServer/lib output/OJServer;\
    	cp -rf OJServer/TopicBank output/OJServer;\
    	cp -rf OJServer/Rendered_html output/OJServer;\
    	cp -rf OJServer/wwwroot output/OJServer;\
    
    # 清除功能
    .PHONY:clean
    clean:
    	@cd CompilerServer;\
    	make clean;\
    	cd -;\
    	cd OJServer;\
    	make clean;\
    	cd -;\
    	rm -rf output;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
  • 相关阅读:
    某车联网App 通讯协议加密分析
    Docker 使用 IDEA 内置插件构建上传镜像 与 SSH、FTP 功能使用
    简陋的vue-cli项目改写成nuxt2项目
    【ACWing 算法基础】字符串哈希
    React 组件实例的三大核心—refs
    ch0_OSI 七层网络协议介绍
    MYSQL之DQL(数据库查询语言)
    【基础篇】第1章 Elasticsearch 引言
    kafka3.X基本概念和使用
    【MySQL】左连接右连接内连接与Hash连接、子查询原理与实战(MySQL专栏启动)
  • 原文地址:https://blog.csdn.net/qq_56044032/article/details/126453703