Load-Balanced-Online-OJ(负载均衡式在线OJ)

负载均衡式在线OJ

前言

项目源代码负载均衡式在线OJ源代码

手机端如果打不开,可以复制下面链接到浏览器中访问

https://gitee.com/hou-shanlin/linux/tree/master/load-balanced-online-OJ

1. 项目介绍

  • 本项目主要实现的是类似于 leetcode 的题目列表 + 在线编程功能。
  • 该项目采用负载均衡算法 (轮询检测) 使得多个服务器协同处理大量的提交请求和编译请求。
  • 可支持多用户在网站中同时选择题目、答题、提交代码、代码的编译与运行,以及查看题目的通过情况

2. 所用技术与环境

所用技术栈

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

开发环境

  • Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-113-generic x86_64)云服务器
  • VS Code

3. 项目宏观结构

3.1 项目核心模块

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

  1. comm : 公共模块(主要包含:httplib<网络服务>、log<日志信息>、util<项目中都需要使用到的工具类的集合>)

  2. compile_server : 编译与运行模块(主要包含:编译服务、运行服务、编译和运行服务)

  3. oj_server : 获取题目列表,查看题目编写题目界面,负载均衡

3.2 项目的宏观结构

  • B/S模式(Browser/Server):将应用程序分为两部分:客户端和服务器端。客户端通常是Web浏览器,用户通过浏览器与服务器进行交互。
  • compile_server 和 oj_server 会采用网络套接字的方式进行通信,这样就能将编译模块部署在服务器后端的多台主机上。
  • 而 oj_server 只有一台,这样子就会负载均衡的选择后台的编译服务。

4. comm公共模块

该模块主要为所有模块提供文件操作、字符串处理、网络请求、打印日志等公共功能

其中的 httplib.h 文件是第三方开源网络库 cpp-httplib 所提供的,因此之后不展示其代码。

文件名 功能
httplib.h 提供网络服务
log.hpp 提供日志打印功能
util.hpp 提供各种工具类

4.1 日志(log.hpp )

该模块主要是提供打印日志的功能,方便后续代码调试。

4.1.1 日志主要内容

  • 日志等级
  • 打印该日志的文件名
  • 对应日志所在的行号
  • 添加对应日志的时间
  • 日志信息

4.1.2 日志使用方式

cpp 复制代码
LOG(日志等级) << "message" << "\n";	// 如: LOG(INFO) << "这是一条日志" << "\n";

4.1.2 日志代码

cpp 复制代码
#pragma once
#include <iostream>
#include "util.hpp"
namespace ns_log
{
    using namespace ns_util;
    // 日志等级
    enum
    {
        INFO,
        DEBUG,
        WARNING,
        ERROR,
        FATAL
    };
    // level:等级
    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::TimeStampExLocalTime();
        message += "]";

        // cout内部包含缓冲区
        std::cout << message; // 刷新

        return std::cout;
    }
    // LOG(INFO)<<"message(信息)"

#define LOG(level) Log(#level, __FILE__, __LINE__)
//编译时,__FILE__ 会被替换为包含当前代码的文件名,__LINE__会被替换为当前所在行数

}

4.2 工具(util.hpp)

该模块主要提供的工具类及其说明如下:

类名 说明 提供的功能
TimeUtil 时间工具 获取当前时间
PathUtil 路径工具 根据文件名和路径构建 .cpp 后缀的文件完整名 根据文件名和路径构建 .exe 后缀的完整文件名 根据文件名和路径构建 .compile_error 后缀的完整文件名 根据文件名和路径构建 .stdin 后缀的完整文件名 根据文件名和路径构建 .stdout 后缀的完整文件名 根据文件名和路径构建 .stderr 后缀的完整文件名
FileUtil 文件工具 判断指定文件是否存在 用 毫秒级时间戳 + 原子性递增的唯一值 形成一个具有唯一性的文件名 将用户代码写到唯一的目标文件中, 形成临时的 .cpp 源文件 读取目标文件中的所有内容
StringUtil 字符串工具 根据指定的分隔符切割字符串,并将切分出的子串用数组存储返回

工具类代码

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

namespace ns_util
{

    // 时间功能
    class TimeUtil
    {
    public:
        static std::string GetTimeStamp()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec);
        }
        // 毫秒
        static std::string GetTimeMs()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
        }
        static std::string TimeStampExLocalTime()
        {
            time_t currtime = time(nullptr);
            struct tm *curr = localtime(&currtime);
            char time_buffer[128];
            snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
                     curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
                     curr->tm_hour, curr->tm_min, curr->tm_sec);
            return time_buffer;
        }
    };

    const std::string temp_path = "./temp/";
    // 路径操作
    class PathUtil
    {
    public:
        // 添加后缀功能
        static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
        {
            std::string path_anme = temp_path;
            path_anme += file_name;
            path_anme += suffix;
            return path_anme;
        }
        // 编译时需要的临时文件
        //  构建源文件路径+后缀的完整文件名
        //  hsl -> ./temp/hsl.cpp
        static std::string Src(const std::string &file_name)
        {
            return AddSuffix(file_name, ".cpp");
        }
        // 构建可执行程序完整路径+后缀名
        static std::string Exe(const std::string &file_name)
        {
            return AddSuffix(file_name, ".exe");
        }

        // 构建该程序对应的标准错误完整的路径+后缀名
        static std::string Stderr(const std::string &file_name)
        {
            return AddSuffix(file_name, ".stderr");
        }

        // 运行时需要的临时文件
        //  构建编译时报错的文件路径
        static std::string CompilerError(const std::string &file_name)
        {
            return AddSuffix(file_name, ".compilererror");
        }
        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");
        }
    };
    // 文件操作功能
    class FileUtil
    {
    public:
        static bool IsFileExists(const std::string &path_name)
        {
            struct stat st;
            if (stat(path_name.c_str(), &st) == 0)
            {
                // 获取文件属性成功,表示已经存在
                return true;
            }
            return false;
        }
        static std::string UniqFileName()
        {
            std::atomic_uint id(0);
            id++;
            // 根据毫秒级时间戳+原子性递增唯一性:来保证唯一性
            std::string ms = TimeUtil::GetTimeMs();
            std::string uniq_id = std::to_string(id);
            return ms + "_" + uniq_id;
        }
        // content:内容
        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;
        }
        // keep:是否保留"\n"
        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不保留分割符("\n"),但是有些时候需要保留
            // getline内部重载了强制类型转换
            while (std::getline(in, line))
            {
                (*content) += line;
                (*content) += keep ? "\n" : "";
            }
            in.close();
            return true;
        }
    };

    class StringUtil
    {
    public:
        /***********************************************************************
         * str: 传入的待切分的字符串
         * target: 输出型参数,传回分割完毕的结果
         * 分割标识符(空格,斜杠等等...)
         ************************************************************************/
        static void SplitString(const std::string &str, std::vector<std::string> *target, std::string sep)
        {
            boost::split(*target, str, boost::is_any_of(sep), boost::algorithm::token_compress_on);
        }
    };

}

5. compiler_server设计

提供的服务:编译并运行代码,得到格式化的相关的结果

第一个功能:编译功能(compiler.hpp)

  • 当用户提交代码的时候,需要为提交的代码提供编译服务,可以将提交的代码打包,使用进程替换的方式进行 g++ 编译。
  • 为了防止远端代码是程序错误的代码或者恶意代码,需要 fork 出子进程去执行进程替换对用户提交的代码执行编译功能。
  • 编译服务只关心编译有没有出错,如果出错,则需要知道是什么原因导致的错误。
    • 需要形成一个临时文件,保存编译出错的结果。
cpp 复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>

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

// 只负责代码编译
namespace ns_compiler
{
    using namespace ns_util;
    using namespace ns_log;

    // 引入路径拼接功能
    class Compiler
    {
    public:
        Compiler()
        {
        }

        // 返回值:编译成功:true,编译失败:false
        // file_name: hsl
        // hsl ->./temp/hsl.cpp
        // hsl ->./temp/hsl.exe
        // hsl ->./temp/hsl.stderr
        static bool Compile(const std::string &file_name)
        {
            pid_t res = fork();
            if (res < 0)
            {
                LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
                return false;
            }
            else if (res == 0) // 子进程
            {
                umask(0);
                // 创建一个stderr文件,将编译错误信息重定向到该文件中
                int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if (_stderr < 0)
                {
                    LOG(WARNING) << "没有成功形成compilererror文件" << "\n";
                    exit(1);
                }
                // 重定向错误到_stderr
                dup2(_stderr, 2);

                // 子进程:调用编译器,完成对代码的编译工作
                // execlp :用于替换当前进程为一个新程序(不会创建子进程,所以要配合fork使用)
                // g++ -o target(目标) src(源文件) -std=c++11
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), "-std=c++11","-D","COMPILER_ONLONE", nullptr);
                LOG(ERROR) << "启动g++编译器失败,可能是参数错误" << "\n";

                exit(2);
            }
            else
            {
                // 父进程
                waitpid(res, nullptr, 0);
                // 编译是否成功: 判断有没有生成可执行程序
                if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
                {
                    LOG(INFO) << PathUtil::Src(file_name) << " 编译成功" << "\n";
                    return true;
                }
            }
            LOG(ERROR) << "程序编译失败,未形成可执行程序" << "\n";

            return false;
        }
        ~Compiler()
        {
        }
    };

}

第二个功能:运行功能(runner.hpp)

  • 编译完成也要能将代码运行起来才能知道代码的结果是否正确,因此还需要体提供运行服务。运行服务也是需要 fork 出子进程执行运行服务。
  • 运行服务需要有的临时文件分别有 4 个:
    • .exe 可执行程序,没有这个代码可没法运行,在编译时已经创建好了该文件,直接用就行。
    • .stdin 标准输入文件,用来重定向保存用户的输入。
    • .stdout 标准输出文件,只用来保存程序运行完成后的结果。
    • .stderr 标准错误文件,如果用户代码在运行时发生错误了,需要用该文件保存运行时的错误信息。
  • 运行服务只关心程序是否正常运行完成,有没有收到信号 (使用进程等待的方式查看) 即可。运行结果是否正确由测试用例决定。
  • 同时还需要限制用户代码所占用的资源,不能让用户无限制的占用 CPU 资源以及内存资源。这就是平时刷题时最常见的资源限制。
    • 可以借助 setrlimit() 函数去限制用户代码所占用的时空资源。
cpp 复制代码
#pragma once

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

#include "../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_cur=_cpu_limit;
            cpu_limit.rlim_max=RLIM_INFINITY;
            setrlimit(RLIMIT_CPU,&cpu_limit);

            //设置内存大小
            struct rlimit mem_limit;
            mem_limit.rlim_cur=_mem_limit*1024;
            mem_limit.rlim_max=RLIM_INFINITY;
            setrlimit(RLIMIT_AS,&mem_limit);

        }
        // 指明文件名即可
        /**************************************************************
         * 返回值 > 0:程序异常了,退出时收到了信号,返回值就是对应的信号编号
         * 返回值 == 0:正常运行完毕,结果保存到了对应的临时文件
         * 返回值 < 0:内部错误
         *
         * cpu_limit: 该程序运行时,可以使用的最大CPU上限(运行时间【秒】)
         * mem_limit: 该程序运行时,可以使用的最大内存上限【KB】
         ***************************************************************/
        static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
        {
            /**************************************************************
             * 运行有三种情况:
             * 1.运行成功,通过
             * 2.运行成功,不通过
             * 3.编译出错,运行不了
             * 我们只管编译运行成功,具体代码有没有通过题目要求,是另外模块的事情
             * 需要创建stdin,stdout,stderr三个文件来保存运行信息
             ***************************************************************/

            // execute:执行
            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 (_stderr_fd < 0 || _stdin_fd < 0 || _stdout_fd < 0)
            {
                LOG(ERROR) << "运行时打开标准文件失败" << std::endl;
                return -1; // 文件打开失败
            }

            pid_t pid = fork();
            if (pid < 0)
            {
                close(_stderr_fd);
                close(_stdout_fd);
                close(_stdin_fd);
                return -2; // 创建子进程失败
            }
            else if (pid == 0) // child
            {
                dup2(_stdin_fd, 0);
                dup2(_stdout_fd, 1);
                dup2(_stderr_fd, 2);
                SetProcLimit(cpu_limit, mem_limit);
                execl(execute.c_str() /*我要执行谁*/, execute.c_str() /*怎么执行*/, nullptr);
                exit(1);
            }
            else
            {
                close(_stderr_fd);
                close(_stdout_fd);
                close(_stdin_fd);
                int status = 0;
                waitpid(pid, &status, 0);

                LOG(INFO) << "运行完毕,退出码: info: " << (status & 0x7F) << std::endl;
                return status & 0x7F;
            }
        }
    };
}

第三个功能:编译并运行功能(compile_run.hpp)

该模块需要整合编译和运行功能、适配用户请求,定制通信协议字段并正确的调用 compile 和 run 模块。

cpp 复制代码
#pragma once

#include <jsoncpp/json/json.h>
#include <signal.h>
#include <unistd.h>

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

namespace ns_compile_and_run
{
    using namespace ns_compiler;
    using namespace ns_runner;
    using namespace ns_log;
    using namespace ns_util;
    // 考虑的都是整体
    class CompileAndRun
    {
    public:
        // 清理临时文件(unlink函数)
        static void RemoveTempFile(const std::string &file_name)
        {
            // 清理文件个数不确定,但是有哪些文件我们清楚
            std::string _src = PathUtil::Src(file_name);
            if (FileUtil::IsFileExists(_src))
                unlink(_src.c_str());

            std::string _execute = PathUtil::Exe(file_name);
            if (FileUtil::IsFileExists(_execute))
                unlink(_execute.c_str());

            std::string _compiler_error = PathUtil::CompilerError(file_name);
            if (FileUtil::IsFileExists(_compiler_error))
                unlink(_compiler_error.c_str());

            std::string _stdin = PathUtil::Stdin(file_name);
            if (FileUtil::IsFileExists(_stdin))
                unlink(_stdin.c_str());

            std::string _stdout = PathUtil::Stdout(file_name);
            if (FileUtil::IsFileExists(_stdout))
                unlink(_stdout.c_str());
            
            std::string _stderr = PathUtil::Stderr(file_name);
            if (FileUtil::IsFileExists(_stderr))
                unlink(_stderr.c_str());
        }

        /*********************************************************************
         * code > 0 : 进程收到了信号导异常崩溃
         * code < 0 : 整个过程运行报错(代码为空,编译报错等)
         * 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 SIGXCPU: // 24
                desc = "CPU超时";
                break;
            case SIGFPE: // 8
                desc = "浮点数溢出";
                break;
            default:
                desc = "未知:" + std::to_string(code);
                break;
            }
            return desc;
        }

        /****************************************************************
         * 输入:
         * code:用户提交的代码
         * input:用户提交的代码对应的输入,不做处理
         * cpu_limit:时间要求
         * mem_limit:空间要求(内存)
         *
         * 输出:
         * 必填
         * status:状态码
         * reason:请求结果(出错原因)
         *
         * 选填
         * stdout:我的程序运行完的结果
         * stderr:我的程序运行完的错误结果
         * 参数:
         * in_json: {"code": "#include <iostream>...","input": "","cpu_limit": "1","mem_limit":"10240"}
         * out_json:{"status":"0","reason":"","stdout":"","stderr":""}
         ****************************************************************/
        static void Start(const std::string &in_json, std::string *out_json)
        {
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value); // 反序列化(差错处理)

            std::string code = in_value["code"].asString();
            std::string input = in_value["input"].asString();
            int cpu_limit = in_value["cpu_limit"].asInt();
            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;
            }

            //编译服务可能随时被多人请求,必须保证传递上去的code,形成源文件名称的时候,要具有唯一性,不然多个用户之间会相互影响
            // 根据毫秒级时间戳+原子性递增唯一性:来保证唯一性
            file_name = FileUtil::UniqFileName(); // 唯一的文件名(只有文件ming)

            // 形成临时源文件(.cpp)
            if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
            {
                // 发生未知错误
                status_code = -2;
                goto END;
            }

            if (!Compiler::Compile(file_name))
            {
                // 编译失败
                status_code = -3;
                goto END;
            }

            run_result = Runner::Run(file_name, cpu_limit, mem_limit);
            if (run_result < 0)
            {
                status_code = -2; // 发生未知错误
                //goto END;
            }
            else if (run_result > 0)
            {
                // 程序崩溃
                status_code = run_result;
            }
            else
            {
                // 运行成功
                status_code = 0;
            }
        END:
            out_value["status"] = status_code;
            out_value["reason"] = CodeToDesc(status_code, file_name);
            if (status_code == 0)
            {
                // 整个过程全部完成
                std::string _stdout;
                FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);
                out_value["stdout"] = _stdout;

                std::string _stderr;
                FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
                out_value["stderr"] = _stderr;
            }
            // out_value["emitUTF8"] = true;
            Json::StyledWriter writer;
            *out_json = writer.write(out_value); // 序列化

            // 清理临时文件
            RemoveTempFile(file_name);
        }
    };
}

第四个功能: 把编译并运行功能,形成网络服务(compile_server.cc)

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

using namespace ns_compile_and_run;
using namespace httplib;

// 编译服务可能随时被多人请求,必须保证传递上来的code,形成源文件名称的时候,
// 要具有唯一性,不然多个用户之间会相互影响

void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\n\t" << proc << std::endl;
}

//./compile_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    Server svr; // 创建一个 Server 对象 svr

    // 注册 GET 请求处理器(测试)
    // svr.Get("/hello",[](const Request &req, Response &resp){
    //     // 当收到 GET 请求到 "/hello" 时,设置响应内容
    //     resp.set_content("hello http!!你好","text/plain;charset=utf-8");
    // });

    // 注册 POST 请求处理器(只是设置好回调函数,当有请求来的时候才会执行)
    svr.Post("/compile_and_run", [](const Request &req, Response &resp)
             {
        // 用户请求的服务正文是我们想要的 JSON 字符串
        std::string in_json = req.body; // 获取请求体中的 JSON 字符串(表示请求的主体内容)
        std::string out_json; // 用于存储响应的 JSON 字符串

        // 如果输入 JSON 字符串不为空
        if (!in_json.empty())
        {
            // 调用 CompileAndRun::Start 函数进行编译和运行
            CompileAndRun::Start(in_json, &out_json);
            // 设置响应内容为输出的 JSON 字符串
            resp.set_content(out_json, "application/json;charset=utf-8");
        } });

    // 启动 HTTP 服务,监听所有地址的argv[1]端口
    svr.listen("0.0.0.0", atoi(argv[1])); // 启动 HTTP 服务
    // listen
    // 用于启动服务器并开始监听指定地址和端口的客户端请求。
    // 它使得服务器能够接收和处理来自客户端的连接。
    return 0; // 返回 0,表示程序正常结束
}

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

本质:建立一个小型网站

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

MVC结构是什么?

MVC(Model-View-Controller)结构是一种广泛使用的软件设计模式,主要用于构建用户界面。它将应用程序分为三个主要组成部分,以实现关注点分离,增强可维护性和可扩展性

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

第一个功能:用户请求的服务路由功能(oj_server.cc)

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <ctemplate/template.h>

#include "../comm/httplib.h"
#include "oj_control.hpp"

using namespace httplib;
using namespace ns_control;

static Control *ctrl_ptr = nullptr;

void Recovery(int signo)
{
    //Ctrl + C全部上线
    ctrl_ptr->RecoveryMachine();
}

int main()
{ 
    // 1.用户请求的服务器路由功能
    signal(SIGQUIT, Recovery);

    //用户请求的服务路由功能
    Server svr;

    Control ctrl;
    ctrl_ptr = &ctrl;

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

    // question:问题,
    // 2.用户根据题目编号,获取题目内容(返回包含题目具体内容的网页)
    // questions/100(100是题目编号)
    // R"()" :原始字符串,保持字符串的原貌,不用做相关的转义
    svr.Get(R"(/questions/(\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"); });


    // 3.用户提交代码,使用我们的判题功能(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"); });

    // 设置默认根目录
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0", 8888);

    return 0;
}

第二个功能:model功能,提供对数据的操作(oj_model.hpp)

cpp 复制代码
#pragma once

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

#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <stdlib.h>

// 根据list文件,加载所有的题目信息到内存中
// 和数据交互的模块,对外提供数据访问的接口,⽐如,对题库进⾏增删改查

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 string question_list = "./questions/questions.list";
    const string questions_path = "./questions/";

    class Model
    {
    public:
        Model()
        {
            assert(LoadQuestionList(question_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 = 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.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 (auto &q : questions)
            {
                out->push_back(q.second);
            }
            return true;
        }
        // 获取指定一个题目

        bool GetOneQuestions(const string &number, Question *q)
        {
            const auto &iter = questions.find(number);
            if (iter == questions.end())
            {
                LOG(ERROR) << "获取题目失败,题目编号:"<<number << "\n";
                return false;
            }
            (*q) = iter->second;
            return true;
        }
        ~Model()
        {
        }

    private:
        // 题号 : 题目细节
        unordered_map<string, Question> questions;
    };
}

第三个功能:control,逻辑控制模块(oj_control.hpp)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <cassert>
#include <fstream>
#include <jsoncpp/json/json.h>
#include <algorithm>

#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include "../comm/httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "../comm/httplib.h"

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

    // 提供服务的主机【Machine(机器)】
    class Machine
    {
    public:
        Machine() : ip(""), port(0), load(0), mtx(nullptr)
        {
        }
        // 提升主机负载
        void IncLoad()
        {
            if (mtx)
                mtx->lock();
            load++;
            if (mtx)
                mtx->unlock();
        }

        // 减少主机负载
        void DecLoad()
        {
            if (mtx)
                mtx->lock();
            load--;
            if (mtx)
                mtx->unlock();
        }
        //给离线的主机负载清零
        void ResetLoad()
        {
            if (mtx)
                mtx->lock();
            load = 0;
            if (mtx)
                mtx->unlock();
        }
        uint64_t Load()
        {
            uint64_t _load = 0;
            if (mtx) 
                mtx->lock();
            _load = load;
            if (mtx)
                mtx->unlock();
            return _load;
        }

        ~Machine()
        {
        }

    public:
        std::string ip;  // 编译服务的ip
        int port;        // 编译服务的port
        uint64_t load;   // 编译服务的负载(加载)
        std::mutex *mtx; // mutex禁止拷贝
    };
    // 【service(服务)】
    const std::string service_machine = "./conf/service_machine.conf";
    // 【LoadBlancer(负载均衡)】
    //  负载均衡模块
    class LoadBlancer
    {
    public:
        LoadBlancer()
        {
            assert(LoadConf(service_machine));
            LOG(INFO) << "加载" << service_machine << "成功" << "\n";
        }
        // 加载配置
        bool LoadConf(const std::string &machine_list)
        {
            std::ifstream in(machine_list);
            if (!in.is_open())
            {
                LOG(FATAL) << "加载: " << machine_list << "失败" << "\n";
                return false;
            }
            std::string line;
            while (std::getline(in, line))
            {
                vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, ":");
                if (tokens.size() != 2)
                {
                    LOG(WARNING) << "切分" << line << "失败" << "\n";
                    continue;
                }
                Machine m;
                m.ip = tokens[0];
                m.port = atoi(tokens[1].c_str());
                m.load = 0;
                m.mtx = new mutex();

                online.push_back(machines.size());
                machines.push_back(m);
            }

            in.close();
            return true;
        }

        // 智能选择
        bool SmartChoice(int *id, Machine **m)
        {
            // 1. 使用选择好的主机(更新该主机的负载)
            // 2. 我们需要可能离线该主机
            mtx.lock();
            // 负载均衡的算法
            // 1. 随机数 + hash
            // 2. 轮询 + hash(选择这种)
            int online_num = online.size();
            if (online_num == 0)
            {
                mtx.unlock();
                LOG(FATAL) << "所有的后端编译主机已经离线,运维的牛马快来修" << "\n";
                return false;
            }
            // 通过遍历的方式,找到负载最小的机器
            *id = online[0];
            *m = &machines[online[0]];
            uint64_t min_load = machines[online[0]].Load();

            for (int i = 1; i < online_num; i++)
            {
                uint64_t 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();
            for (auto iter = online.begin(); iter != online.end(); iter++)
            {
                if (*iter == which)
                {
                    machines[which].ResetLoad();
                    // 要离线的主机找到了
                    online.erase(iter);
                    offline.push_back(which);
                    break; // 因为break存在,不需要考虑迭代器失效的问题
                }
            }
            mtx.unlock();
        }
        // 在线的机器
        void OnlineMachine()
        {
            /// 统一上线
            mtx.lock();
            online.insert(online.end(), offline.begin(), offline.end());
            offline.erase(offline.begin(), offline.end());
            mtx.unlock();

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

        // for test
        void ShowMachines()
        {
            mtx.lock();

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

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

            mtx.unlock();
        }
        ~LoadBlancer()
        {
        }

    private:
        // 可以给我们提供服务的所有主机
        // 每台主机都有自己的下标。充当当前主机的id
        std::vector<Machine> machines;
        // 所有在线的主机
        std::vector<int> online;
        // 所有离线的主机
        std::vector<int> offline;
        // 给保证LoadBlance它的数据安全
        std::mutex mtx;
    };

    // 业务核心逻辑的控制器
    class Control
    {
    public:
        Control()
        {
        }
        ~Control()
        {
        }

    public:
        // 根据题目数据构建网页
        void RecoveryMachine()
        {
            _load_blance.OnlineMachine();
        }
        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.GetOneQuestions(number, &q))
            {
                // 获取题目信息成功,将指定的题目数据构建成网页
                _view.OneExpandHtml(q, html);
            }
            else
            {
                *html = "指定题目不存在";
                ret = false;
            }
            return ret;
        }

        // 把判题内容构建成网页
        // id : 100
        // code: include....
        // input: "..."
        void Judge(const std::string &number, const std::string in_json, std::string *out_json)
        {
            // 0. 根据题目编号直接拿到对应的题目细节
            struct Question q;
            _model.GetOneQuestions(number, &q);

            // 1. in_json进行反序列化,得到题目id,得到用户提交的题目代码,输出
            Json::Reader reader;
            Json::Value in_value;
            reader.parse(in_json, in_value);
            string code = in_value["code"].asString();

            // 2. 重新拼接用户代码+测试用例代码,形成新的代码
            Json::Value compile_value;
            compile_value["input"] = in_value["input"].asString();
            compile_value["code"] = code + "\n" + q.tail;
            compile_value["cpu_limit"] = q.cpu_limit;
            compile_value["mem_limit"] = q.mem_limit;
            Json::FastWriter writer;
            std::string compile_string = writer.write(compile_value);

            // 3. 选择负载最低的主机(差错处理)
            // 规则:一直选择,直到主机可用,否则就是全部挂掉
            while (true)
            {
                int id = 0;
                Machine *m = nullptr;
                if (!_load_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() << "\n";

                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) << "请求编译和运行服务成功" << "\n";
                        break;
                    }
                    m->DecLoad();
                }
                else
                {
                    // 请求失败
                    LOG(ERROR) << "请求主机失败, 主机id:" << id << "详情: " << m->ip << ":" << m->port << "可能已经离线" << "\n";
                    _load_blance.OfflineMachine(id);
                    _load_blance.ShowMachines(); // 调试
                }
            }
        }

    private:
        Model _model;             // 提供后台数据
        View _view;               // 提供html网页渲染功能
        LoadBlancer _load_blance; // 核心负载均衡器
    };
}

第四个功能:网页渲染功能(oj_view.hpp)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <ctemplate/template.h>

#include "oj_model.hpp"

namespace ns_view
{
    using namespace std;
    using namespace ns_model;

    const std::string template_path = "./template_html/";
    class View
    {
    public:
        View() {}
        //构建全部题目显示的网页
        void AllExpandHtml(const vector<struct Question>& questions, 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 struct Question &q,string *html)
        {
            //1.形成路径
            std::string src_html  = template_path +"one_questions.html";

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

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

            //4.完成渲染功能(开始渲染)
            tpl->Expand(html,&root);

        }
        ~View() {}

    private:

    };
}

7. 文件版题目设计

(1) 题目要求

  1. 题目的编号
  2. 题目的标题
  3. 题目的难度
  4. 题目的描述,题面
  5. 时间要求(内部处理)
  6. 空间要求(内部处理)

(2) 两批文件构成

  • 第一个:questions.list : 题目列表(不需要题目的内容)
  • 第二个:题目的描述,题目的预设置代码(header.cpp), 测试用例代码(tail.cpp)

这两个内容是通过题目的编号,产生关联的

当用户提交自己代码的时候,看到的代码是这样的(header.cpp):

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

class Solution
{
public:
    int search(vector<int>& nums, int target) {
        //将你的代码写在此处
    }
};

而OJ不只是把header.cpp交给compile_and_run,而是会把测试用例文件(tail.cpp)和header.cpp整合到一起打包给compile_and_run

tail.cpp:

cpp 复制代码
#ifndef COMPILER_ONLONE
#include "header.cpp"
#endif


void Test1()
{
    vector<int> nums={-1,0,3,5,9,12};
    int target =9;
    int ret = Solution().search(nums,target);
    if(ret==4)
    {
        std::cout<<"通过测试用例1, 测试(nums = [-1,0,3,5,9,12], target = 9)通过 OK!"<<std::endl;
    }
    else
    {
        std::cout<<"未通过测试用例1, 测试的值为: (nums = [-1,0,3,5,9,12], target = 9)"<<std::endl;
    }
}

void Test2()
{
    vector<int> nums={-1,0,3,5,9,12};
    int target =2;
    int ret = Solution().search(nums,target);
    if(ret==-1)
    {
        std::cout<<"通过测试用例2, 测试(nums = [-1,0,3,5,9,12], target = 2)通过 OK!"<<std::endl;
    }
    else
    {
        std::cout<<"未通过测试用例2, 测试的值为: (nums = [-1,0,3,5,9,12], target = 2)"<<std::endl;
    }
}

int main()
{
    Test1();
    Test2();
}

最终提交给后台编译运行服务的代码是:

cpp 复制代码
//header.cpp
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

class Solution
{
public:
    int search(vector<int>& nums, int target) {
        //用户(你)提交的代码在这儿
    }
};

//tail.cpp
#ifndef COMPILER_ONLONE
#include "header.cpp"
#endif


void Test1()
{
    vector<int> nums={-1,0,3,5,9,12};
    int target =9;
    int ret = Solution().search(nums,target);
    if(ret==4)
    {
        std::cout<<"通过测试用例1, 测试(nums = [-1,0,3,5,9,12], target = 9)通过 OK!"<<std::endl;
    }
    else
    {
        std::cout<<"未通过测试用例1, 测试的值为: (nums = [-1,0,3,5,9,12], target = 9)"<<std::endl;
    }
}

void Test2()
{
    vector<int> nums={-1,0,3,5,9,12};
    int target =2;
    int ret = Solution().search(nums,target);
    if(ret==-1)
    {
        std::cout<<"通过测试用例2, 测试(nums = [-1,0,3,5,9,12], target = 2)通过 OK!"<<std::endl;
    }
    else
    {
        std::cout<<"未通过测试用例2, 测试的值为: (nums = [-1,0,3,5,9,12], target = 2)"<<std::endl;
    }
}

int main()
{
    Test1();
    Test2();
}

8. 前端页面设计

编写页面的时候,需要三剑客: html/css/js

(1) 首页(index.html)

cpp 复制代码
<!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%;
            background-image: url('hsl.jpg'); /* 添加背景图 */
            background-size: cover; /* 使背景图覆盖整个元素 */
            background-position: center; /* 背景图居中显示 */
        }

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

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

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

        .container .content .font_ {
            /* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */
            display: block;
            /* 设置每个文字的上外边距 */
            margin-top: 20px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置字体大小
            font-size: larger; */
        }
    </style>
</head>

<body>
    <div class="container">
        <!-- 导航栏, 功能不实现-->
        <div class="navbar">
            <a href="/">首页</a>
            <a href="/all_questions">题库</a>
            <a href="#">竞赛</a>
            <a href="#">讨论</a>
            <a href="#">求职</a>
            <a class="login" href="#">登录</a>
        </div>
        <!-- 网页的内容 -->
        <div class="content">
            <h1 class="font_">欢迎来到我的OnlineJudge平台</h1>
            <p class="font_">这个我个人独立开发的一个在线OJ平台</p>
            <a class="font_" href="/all_questions">点击我开始编程啦!</a>
        </div>
    </div>
</body>

</html>

(2) 所有题目的列表(all_questions.html)

cpp 复制代码
<!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:'Times New Roman', Times, serif;
        }
        .container .question_list table .item a {
            text-decoration: none;
            color: black;
        }
        .container .question_list table .item a:hover {
            color: blue;
            text-decoration:underline;
        }
        .container .footer {
            width: 100%;
            height: 50px;
            text-align: center;
            line-height: 50px;
            color: #ccc;
            margin-top: 15px;
        }
    </style>
</head>

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

</body>

</html>

(3) 指定题目的编写代码的页面+代码提交(one_questions.html)

cpp 复制代码
<!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(){
            // alert("嘿嘿!");
            // 1. 收集当前页面的有关数据, 1. 题号 2.代码
            var code = editor.getSession().getValue();
            // console.log(code);
            var number = $(".container .part1 .left_desc h3 #number").text();
            // console.log(number);
            var judge_url = "/judge/" + number;
            // console.log(judge_url);
            // 2. 构建json,并通过ajax向后台发起基于http的json请求
            $.ajax({
                method: 'Post',   // 向后端发起请求的方式
                url: judge_url,   // 向后端指定的url发起请求
                dataType: 'json', // 告知server,我需要什么格式
                contentType: 'application/json;charset=utf-8',  // 告知server,我给你的是什么格式
                data: JSON.stringify({
                    'code':code,
                    'input': ''
                }),
                success: function(data){
                    //成功得到结果
                    // console.log(data);
                    show_result(data);
                }
            });
            // 3. 得到结果,解析并显示到 result中
            function show_result(data)
            {
                // console.log(data.status);
                // console.log(data.reason);
                // 拿到result结果标签
                var result_div = $(".container .part2 .result");
                // 清空上一次的运行结果
                result_div.empty();

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

                var reason_lable = $( "<p>",{
                       text: _reason
                });
                reason_lable.appendTo(result_div);

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

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

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

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

</html>

(优化持续更新中...)

相关推荐
岁月漫长_9 分钟前
【Ubuntu 22.04】VMware 17 安装Ubuntu 22.04+配置VSCode+Python开发环境
vscode·python·ubuntu
45W冲冲冲10 分钟前
WIN10+CMAKE+MinGW+Opencv/C++ 和VScode开发环境搭建
c++·vscode·opencv
春蕾夏荷_72829772514 分钟前
MFC 对话框中显示CScrollView实例
c++·mfc·cscrollview
是垚不是土33 分钟前
Ansible--自动化运维工具
运维·git·学习·自动化·云计算·ansible
运维技术小记33 分钟前
Linux命令思维导图
linux
风间琉璃""33 分钟前
shell编写——脚本传参与运算
linux·运维·服务器·bash
暴力的bug制造机42 分钟前
【MySQL】库的操作(增删查改 | 备份 | 恢复)
linux·数据库·mysql·oracle
EEE1even1 小时前
Linux光标快捷键
linux·运维·vim
南风与鱼1 小时前
Linux编辑器 - vim
linux·编辑器·vim
爱丶狸1 小时前
linux命令之openssl用法
linux·运维·服务器·linux命令合集