当前位置: 首页 > article >正文

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

主页:醋溜马桶圈-CSDN博客

专栏:实战项目_醋溜马桶圈的博客-CSDN博客

gitee:mnxcc (mnxcc) - Gitee.com
项目源码
文件版:OnlineJudge_file: 负载均衡在线OJ项目基于文件版本
数据库版:mnxcc/OnlineJudge_MySQL

目录

1.项目目标

2.技术与开发环境

2.1 技术

2.2 开发环境

3.项目宏观结构

​编辑

3.1 设计思路

4.compiler_server设计

4.1 compiler

4.1.1 Compile()

4.2 runner

4.2.1 Run()

4.2.2 Run超时/内存超限

1.超时退出

​编辑

2.内存超限退出

​编辑

3.终止进程

​编辑

4.进程资源限制函数SetProcRlimit()

4.3 compile_run

4.3.1 jsoncpp

4.3.2 Start()

4.3.3 获取唯一文件名

4.3.4 将code写入.cpp文件

4.3.5 读取文件

4.3.6 测试用例

4.3.7 移除临时文件

4.3.4 形成网络服务

​编辑

4.3.4.1 测试get服务

​编辑

4.3.4.2 测试post请求

​编辑

4.3.5 compile_server.cc

5.基于MVC结构的oj服务

5.1 oj_server.cc

5.1.1 测试

5.1.2 注意事项

5.2 文件版题目设计

​编辑

5.2.1 oj_model设计

5.2.1.1 class Model

5.2.1.2 加载题目列表函数

5.2.1.3 SplitString设计-使用boost实现

5.2.2 oj_control设计

5.2.2.1 class Contril 设计

5.2.2.2 AllQuestions()设计

5.2.2.3 Question()设计

5.2.2.4 Judge()设计

5.2.3 oj_view设计

5.2.3.1 ctemplate库安装

​编辑

5.2.3.2 测试ctemplate

5.2.3.3 渲染题目列表页

5.2.3.4 渲染题目描述页

5.2.4 class Machine设计

5.2.5 class LoadBalance设计

6.前端页面设计

6.1 丐版的页面-首页

6.2 所有题目列表的页面

6.3 OJ指定题目页面-代码提交

6.3.1 ACE在线编辑器

6.3.2 前后端交互jquery

6.3.2.1 通过ajax向后端发起请求

6.3.3 onequestion.html代码

7.基于数据库的版本

7.1 数据库设计

7.1.1mysql workbench创建表结构

7.1.1.1 oj_questions

7.1.2 数据库连接和设计问题

7.2 oj_model重新设计

7.2.1 引入三方库

7.2.2 oj_model代码

8.项目扩展

9.顶层makefile


1.项目目标

前端:题目列表 (自动录题) 在线OJ 报错提醒

后端:oj_server和compile_server服务器 负载均衡 数据库+文件

2.技术与开发环境

2.1 技术

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

2.2 开发环境

  • Ubuntu 云服务器
  • vscode
  • MySQL Workbench

3.项目宏观结构

类似LeetCode模式:题目列表+在线编程功能


 

3.1 设计思路

(1) compile_server

(2) oj_server

(3) 基于文件的在线OJ

(4) 前端的页面设计

(5) 基于MySQL的在线OJ

4.compiler_server设计

编译并运行代码,得到格式化的相关结果

4.1 compiler

4.1.1 Compile()

Compiler::Compile()核心代码:编译功能

        // 编译成功则生成可执行文件 返回true 否则返回false (只判断编译是否成功)
        static bool Compile(const std::string &file_name)
        {
            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(ERROR) << "内部错误,创建子进程失败" << "\n";
                return false;
            }
            else if (pid == 0)
            {
                umask(0);
                int _compile_err = open(PathUtil::CompilerErr(file_name).c_str(), O_CREAT | O_WRONLY, 0644); // 以创建|读写方式打开file.stderr文件
                if (_compile_err < 0)
                {
                    LOG(WARNING) << "没有成功形成compile_err文件" << "\n";
                    exit(1);
                }
                // 重定向标准错误到_compile_err
                dup2(_compile_err, 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);
                // 程序替换并不影响进程的文件描述符表
                LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";
                exit(2);
            }
            else
            {
                // 父进程:等待子进程 等待失败直接返回false 等待成功再判断是否生成可执行程序(是否编译成功)
                waitpid(pid, nullptr, 0);
                if (FileUtil::IsFileExist(PathUtil::Exe(file_name)))
                {
                    LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";
                    return true;
                }
            }
            LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";
            return false;
        }

4.2 runner

4.2.1 Run()

Runner::Run()核心代码:运行功能

        /********
         * 返回值>0:程序异常,返回退出信号
         * 返回值==0:程序运行成功,结果保存到stdout
         * 返回值<0:内部错误 文件错误 进程创建错误
         * cpu_rlimit:运行时消耗cpu资源的上限
         * mem_rlimit:运行时消耗内存资源的上限 以KB为单位
         ********/
        static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
        {
            const std::string _execute = PathUtil::Exe(file_name);
            const std::string _stdin = PathUtil::Stdin(file_name);
            const std::string _stdout = PathUtil::Stdout(file_name);
            const std::string _stderr = PathUtil::Stderr(file_name);

            umask(0);
            int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_WRONLY, 0644);
            int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
            int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);

            if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0)
            {
                LOG(ERROR) << "运行时打开标准文件失败" << "\n";
                return -1; // 表示文件打开或创建失败
            }
            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(ERROR) << "创建子进程失败" << "\n";
                // 子进程创建失败
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                return -2;
            }
            else if (pid == 0)
            {
                // 子进程 运行
                dup2(_stdin_fd, 0);
                dup2(_stdout_fd, 1);
                dup2(_stderr_fd, 2);

                SetProcRlimit(cpu_limit, mem_limit);

                execl(_execute.c_str(), _execute.c_str(), nullptr);
                exit(1);
            }
            else
            {
                // 父进程 等待
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                int status;
                waitpid(pid, &status, 0);
                LOG(INFO) << PathUtil::Exe(file_name) << " 运行完毕,退出信号:" << (status & 0x7F) << "\n";
                return status & 0x7F;
                // 通过退出信号判断运行成功与否
                // 成功 status==0
                // 失败 status!=0
            }

4.2.2 Run超时/内存超限

使用setrlimit函数

1.超时退出
    struct rlimit rt;
    rt.rlim_cur=1;//软限制为1秒
    rt.rlim_max=RLIM_INFINITY;//硬限制为无穷
    setrlimit(RLIMIT_CPU,&rt);//CPU的运行时间限制
    while(1);
2.内存超限退出
    struct rlimit rt;
    rt.rlim_cur = 1024 * 1024 * 40; // 软限制为40MB空间
    rt.rlim_max = RLIM_INFINITY;    // 硬限制为无穷
    setrlimit(RLIMIT_AS, &rt);      // 虚拟内存大小限制
    int count = 0;
    while (1)
    {
        int *p = new int[1024 * 1024]; // 一次申请1MB的空间
        count++;
        std::cout << "size: " << count << std::endl;
        sleep(1);
    }
3.终止进程

资源不足,os通过信号终止进程

void handler(int signo)
{
    std::cout << "signo : " << signo << std::endl;
    exit(1);
}

    for (int i = 0; i < 31; i++)
        signal(i,handler);
// 为所有信号都注册了 handler 函数,当收到 SIGXCPU 信号时,handler 函数会被调用,输出信号编号,然后进程终止
4.进程资源限制函数SetProcRlimit()
        static void SetProcRlimit(int _cpu_limit, int _mem_limit)
        {
            struct rlimit cpu_rlimit;
            cpu_rlimit.rlim_cur = _cpu_limit;
            cpu_rlimit.rlim_max = RLIM_INFINITY;
            setrlimit(RLIMIT_CPU, &cpu_rlimit);

            struct rlimit mem_rlimit;
            mem_rlimit.rlim_cur = _mem_limit * 1024;
            mem_rlimit.rlim_max = RLIM_INFINITY;
            setrlimit(RLIMIT_AS, &mem_rlimit);
        }

4.3 compile_run

核心功能:编译运行

//适配用户请求 定制通信协议字段
//正确调用compile and run
//形成唯一文件名
// 整合服务
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>

namespace ns_compile_and_run
{
    using namespace ns_compiler;
    using namespace ns_runner;

    class CompileAndRun
    {
    public:
        // 移除临时文件 使用unlink系统调用
        static void RemoveTempFile(const std::string &file_name)
        {
            // 移除.cpp文件
            std::string _src = PathUtil::Src(file_name);
            if (FileUtil::IsFileExist(_src))
                unlink(_src.c_str());

            // 移除.compile_err文件
            std::string _compile_err = PathUtil::CompilerErr(file_name);
            if (FileUtil::IsFileExist(_compile_err))
                unlink(_compile_err.c_str());

            // 移除.exe文件
            std::string _exe = PathUtil::Exe(file_name);
            if (FileUtil::IsFileExist(_exe))
                unlink(_exe.c_str());

            // 移除.stdin文件
            std::string _stdin = PathUtil::Stdin(file_name);
            if (FileUtil::IsFileExist(_stdin))
                unlink(_stdin.c_str());

            // 移除.stdout文件
            std::string _stdout = PathUtil::Stdout(file_name);
            if (FileUtil::IsFileExist(_stdout))
                unlink(_stdout.c_str());

            // 移除.stderr文件
            std::string _stderr = PathUtil::Stderr(file_name);
            if (FileUtil::IsFileExist(_stderr))
                unlink(_stderr.c_str());
        }
        // 将错误码转化成错误码描述
        /****
         * >0
         * <0
         * =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::CompilerErr(file_name), &desc, true);
                break;
            case SIGABRT: // 6
                desc = "超出内存限制";
                break;
            case SIGXCPU: // 24
                desc = "运行超时";
                break;
            case SIGFPE: // 8
                desc = "浮点数溢出错误";
                break;
            default:
                desc = "debug:" + std::to_string(code);
                break;
            }
            return desc;
        }
        /*******
         * 输入:一个json串
         * code:用户提交的代码
         * input:用户给自己提交的代码的对应的输入(扩展自测用例)
         * cpu_limit:时间要求
         * mem_limit:空间要求
         *
         * 输出:一个json串
         * 必填:
         * status:状态码
         * reason:请求结果 成功/失败
         * 选填:
         * stdout:运行完的结果
         * stderr:运行完的错误结果
         *******/
        // in_json :{"code":"#include...","input":" ","cpu_limit":"1","mem_limit":"1024"};
        // out_json :{"status":"0","reason":"",["stdout":"","stderr":""]};
        static void Start(const std::string &in_json, std::string *out_json)
        {
            // 将传入的in_put 反序列化得到kv值 使用jsoncpp库
            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();

            Json::Value out_value;
            std::string file_name = "";
            int status_code = 0;
            int run_result = 0;

            if (code.size() == 0)
            {
                status_code = -1; // 代码为空
                goto END;
            }

            // 形成的文件名具有唯一性 没有后缀和路径
            file_name = FileUtil::UniqFileName();

            // 形成临时文件src文件 把code写道file.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; // 内部错误:文件错误/创建进程
            }
            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_json
            Json::StyledWriter writer;
            *out_json = writer.write(out_value);

            // 移除临时文件
            RemoveTempFile(file_name);
        }
    };
}

4.3.1 jsoncpp

sudo apt-get install libjsoncpp-dev

问题:序列化后的string汉字变为乱码

#include <jsoncpp/json/json.h>
int main()
{
    // 序列化:将结构化的数据转化成一个字符串 方便在网络中传输
    // Value是一个Json的中间类,可以填充KV值
    Json::Value root;
    root["code"] = "mycode";
    root["name"] = "dc";
    root["age"] = "20";

    // Json::StyledWriter writer;
    Json::FastWriter writer;
    std::string str = writer.write(root);
    std::cout << str << std::endl;
    return 0;
}
// 编译的时候需要链接jsoncpp库 —ljsoncpp

序列化:kv->string

反序列化:string->kv

4.3.2 Start()

CompileAndRun::Start()核心功能:整合编译运行服务

判断六种情况 序列化给 *out_json
1.code为空 -1 reason
2.code写入.cpp失败 -2 reason
3.编译失败 -3 reason
4.内部错误非运行时错误(包含于运行函数) -2 reason
5.收到信号运行失败 signal reason
5.运行成功 0 reason stdout stderr(判断运行结果)
    class CompileAndRun
    {
    public:
        // 将错误码转化成错误码描述
        /****
         * >0
         * <0
         * =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::CompilerErr(file_name), &desc, true);
                break;
            case SIGABRT: // 6
                desc = "超出内存限制";
                break;
            case SIGXCPU: // 24
                desc = "运行超时";
                break;
            case SIGFPE: // 8
                desc = "浮点数溢出错误";
                break;
            default:
                desc = "debug:" + std::to_string(code);
                break;
            }
            return desc;
        }
        /*******
         * 输入:一个json串
         * code:用户提交的代码
         * input:用户给自己提交的代码的对应的输入(扩展自测用例)
         * cpu_limit:时间要求
         * mem_limit:空间要求
         *
         * 输出:一个json串
         * 必填:
         * status:状态码
         * reason:请求结果 成功/失败
         * 选填:
         * stdout:运行完的结果
         * stderr:运行完的错误结果
         *******/
        // in_json :{"code":"#include...","input":" ","cpu_limit":"1","mem_limit":"1024"};
        // out_json :{"status":"0","reason":"",["stdout":"","stderr":""]};
        static void Start(const std::string &in_json, std::string *out_json)
        {
            // 将传入的in_put 反序列化得到kv值 使用jsoncpp库
            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();

            Json::Value out_value;
            std::string file_name = "";
            int status_code = 0;
            int run_result = 0;

            if (code.size() == 0)
            {
                status_code = -1; // 代码为空
                goto END;
            }

            // 形成的文件名具有唯一性 没有后缀和路径
            file_name = FileUtil::UniqFileName();

            // 形成临时文件src文件 把code写道file.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; // 内部错误:文件错误/创建进程
            }
            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_json
            Json::StyledWriter writer;
            *out_json = writer.write(out_value);
        }
    };

4.3.3 获取唯一文件名

通过毫秒级时间戳+原子性递增得到唯一文件名

        // 毫秒级时间戳
        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_uint id(0);
            id++;
            std::string ms = TimeUtil::GetTimeMs();
            std::string uniq_id = std::to_string(id);
            return ms + "_" + uniq_id;
        }

4.3.4 将code写入.cpp文件

        // 形成临时文件src文件 把code写道file.cpp中
        static bool WriteFile(const std::string &target, const std::string &code)
        {
            std::ofstream out(target);
            if (!out.is_open())
            {
                return false;
            }
            out.write(code.c_str(), code.size());
            out.close();
        }

4.3.5 读取文件

        // 读取目标文件内容
        // 输出到输出型参数content里面
        static bool ReadFile(const std::string &target, std::string *content, bool keep)
        {
            (*content).clear();
            std::ifstream in(target);
            if (!in.is_open())
            {
                return false;
            }
            std::string line;
            // getline不保存行分割符,有些时候需要保留\n
            // getline内部重载了强制类型转换
            while (getline(in, line)) // 按行读取 把in里面的内容读到line
            {
                (*content) += line;
                (*content) += (keep ? "\n" : "");
            }
            in.close();
            return true;
        }

4.3.6 测试用例

// 基于网络请求式的服务
#include "compile_run.hpp"
#include <jsoncpp/json/json.h>
using namespace ns_compile_and_run;
int main()
{
    // 将编译服务打包成一个网络服务 
    // cpp-httplib

    // 通过http 让client 上传一个json string
    // in_json :{"code":"#include...","input":" ","cpu_limit":"1","mem_limit":"1024"};
    // out_json :{"status":"0","reason":"",["stdout":"","stderr":""]};
    std::string in_json;

    Json::Value in_value;
    //R"()" raw string
    in_value["code"] = R"(#include<iostream>
    int main(){
        std::cout << "你好" << std::endl;
        int a=10;
        a/=0;
        return 0;
    })";
    in_value["input"] = "";
    in_value["cpu_limit"] = 1;
    in_value["mem_limit"] = 20*1024;
    // 序列化

    Json::StyledWriter writer;
    in_json = writer.write(in_value);
    // 反序列化
    std::cout << in_json << std::endl;
    std::string out_json;

    CompileAndRun::Start(in_json, &out_json);
    std::cout << out_json << std::endl;

    return 0;
}

4.3.7 移除临时文件

        // 移除临时文件 使用unlink系统调用
        static void RemoveTempFile(const std::string &file_name)
        {
            // 移除.cpp文件
            std::string _src = PathUtil::Src(file_name);
            if (FileUtil::IsFileExist(_src))
                unlink(_src.c_str());

            // 移除.compile_err文件
            std::string _compile_err = PathUtil::CompilerErr(file_name);
            if (FileUtil::IsFileExist(_compile_err))
                unlink(_compile_err.c_str());

            // 移除.exe文件
            std::string _exe = PathUtil::Exe(file_name);
            if (FileUtil::IsFileExist(_exe))
                unlink(_exe.c_str());

            // 移除.stdin文件
            std::string _stdin = PathUtil::Stdin(file_name);
            if (FileUtil::IsFileExist(_stdin))
                unlink(_stdin.c_str());

            // 移除.stdout文件
            std::string _stdout = PathUtil::Stdout(file_name);
            if (FileUtil::IsFileExist(_stdout))
                unlink(_stdout.c_str());

            // 移除.stderr文件
            std::string _stderr = PathUtil::Stderr(file_name);
            if (FileUtil::IsFileExist(_stderr))
                unlink(_stderr.c_str());
        }

4.3.4 形成网络服务

将编译并运行内容打包成网络服务

接入cpp—httplib库 header-only
1.到gitee搜索cpp-httplib库,选择合适的版本下载压缩包到本地
2.解压到项目文件夹下的third_part文件夹
3.项目中#include <httplib.h>就行
4.cpp-httplib库是阻塞式多线程的 使用原生线程库pthread
    
cpp-httplib需要使用高版本的gcc 需要升级gcc gcc-v 11.4.0 cpp-httplib-v0.16.0
git clone https://gitee.com/magicor/cpp-httplib.git

通过firewalld命令置防火墙规则

4.3.4.1 测试get服务
#include "compile_run.hpp"
#include <jsoncpp/json/json.h>
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;

int main()
{
    // 将编译服务打包成一个网络服务
    // cpp-httplib
    
    Server svr;
    svr.Get("/hello",[](const Request&req,Response&resp){
        resp.set_content("hello httplib,你好 httplib","text/plain;charset=utf-8");
        //基本测试
    });
    // svr.set_base_dir("./wwwroot");//测试网页
    svr.listen("0.0.0.0",8080);//启动 http 服务

    return 0;
}
4.3.4.2 测试post请求
//测试的时候可以采用postman测试
//百度:postman官网下载安装

4.3.5 compile_server.cc

// 基于网络请求式的服务
#include "compile_run.hpp"
#include <jsoncpp/json/json.h>
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;

void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
    // 使用说明
}
int main(int argc, char *argv[])
{
    // 将编译服务打包成一个网络服务
    // cpp-httplib
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    Server svr;
    // svr.Get("/hello",[](const Request&req,Response&resp){
    //     resp.set_content("hello httplib,你好 httplib","text/plain;charset=utf-8");
    //     //get基本测试
    // });

    svr.Post("/compile_and_run", [](const Request &req, Response &resp)
             {
        //测试通过post请求 客户端发送的req.body in_json串 Start后 把out_json写到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.set_base_dir("./wwwroot");//测试网页
    // svr.listen("0.0.0.0",8080);//启动 http 服务
    svr.listen("0.0.0.0", atoi(argv[1])); // 启动 http 服务

    return 0;
}

5.基于MVC结构的oj服务

本质:建立一个小型网站

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

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

5.1 oj_server.cc

5.1.1 测试

// 展示给用户 负载均衡式地调用compile_server
#include <iostream>
#include "../comm/httplib.h"

using namespace httplib;

int main()
{
    Server svr;
    // 1.获取题目列表
    svr.Get("/all_questions", [](const Request &req, Response &resp)
            { resp.set_content("这是所有题目的列表", "text/plain; charset=utf-8"); });
    // 2.用户根据题目编号,获取题目内容
    // R"()" 原始字符串raw string 保持字符串的原貌 不用自己做转义
    // /question/100 -> 正则匹配
    svr.Get(R"(/question/(\d+))", [](const Request &req, Response &resp)
            { 
                std::string number=req.matches[1];
                resp.set_content("这是一个指定题目:"+number, "text/plain; charset=utf-8"); });
    // 3.用户提交代码,使用判题功能(1.测试用例2.compile_and_run)
    svr.Get(R"(/judge/(\d+))", [](const Request &req, Response &resp)
            { 
                std::string number=req.matches[1];
                resp.set_content("指定题目的判题:"+number, "text/plain; charset=utf-8"); });
    svr.set_base_dir("./wwwroot");// 默认的首页
    svr.listen("0.0.0.0", 8080);
    return 0;
}

5.1.2 注意事项

5.2 文件版题目设计

1.题目编号
2.题目标题
3.题目难度
4.题目描述
5.时间要求(不暴露)
6.空间要求(不暴露)

1.questions.list题目列表(题目编号 标题 难度)
2.题目的描述 编辑区域(预设值代码 header.cpp) 测试用例代码(tail.cpp)
通过题目编号产生管理关联

5.2.1 oj_model设计

5.2.1.1 class Model

Model中有一个unordered_map<>存储题目编号和题目信息的映射关系

// 操作数据
// 根据题目list文件 加载所有的题目信息到内存中
// 主要用来和数据进行交互,对外提供访问数据的接口
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <cassert>
#include <fstream>
#include <cstdlib>
#include "../comm/log.hpp"
#include "../comm/util.hpp"

namespace ns_model
{
    // using namespace std;
    using namespace ns_util;
    using namespace ns_log;
    struct Question
    {
        std::string number; // 题号 唯一
        std::string title;  // 题目的标题
        std::string star;   // 题目的难度 简单 中等 困难
        int cpu_limit;      // 时间要求(s)
        int mem_limit;      // 空间要求(KB)
        std::string desc;   // 题目的描述
        std::string header; // 题目预设给用户在线编辑器的代码
        std::string tail;   // 题目的测试用例,需要和header拼接形成完整代码code 发送给compile_server
    };
    class Model
    {
    private:
        // 题号(string) : 题目细节(Question)
        std::unordered_map<std::string, Question> questions;

    public:
        std::string _questions_list = "./questions/questions.list";
        std::string _questions_path = "./questions/";
        Model()
        {
            assert(LoadQuestionsList(_questions_list));
        }
        bool LoadQuestionsList(const std::string &questions_list)
        {
            // 加载配置文件 ./questions/question.list+题目编号文件
            std::ifstream in(questions_list);
            if (!in.is_open())
            {
                LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";
                return false;
            }
            std::string line;
            while (getline(in, line)) // 按行获取
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, " ");
                if (tokens.size() != 5)
                {
                    LOG(WARNING) << "部分题目加载失败,请检查文件格式" << "\n";
                    continue;
                }
                Question q;
                q.number = tokens[0];
                q.title = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = atoi(tokens[3].c_str());
                q.mem_limit = atoi(tokens[4].c_str());
                std::string _path = _questions_path;
                _path += q.number;
                _path += "/"; // 形成./questions/1/路径
                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(std::vector<Question> *out)
        {
            if (questions.empty())
            {
                LOG(ERROR) << "用户获取题库失败" << "\n";
                return false;
            }
            for (const auto &e : questions)
                out->push_back(e.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()
        {
        }
    };
}
5.2.1.2 加载题目列表函数
        bool LoadQuestionsList(const std::string &questions_list)
        {
            // 加载配置文件 ./questions/question.list+题目编号文件
            std::ifstream in(questions_list);
            if (!in.is_open())
            {
                LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";
                return false;
            }
            std::string line;
            while (getline(in, line)) // 按行获取
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, " ");
                if (tokens.size() != 5)
                {
                    LOG(WARNING) << "部分题目加载失败,请检查文件格式" << "\n";
                    continue;
                }
                Question q;
                q.number = tokens[0];
                q.title = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = atoi(tokens[3].c_str());
                q.mem_limit = atoi(tokens[4].c_str());
                std::string _path = _questions_path;
                _path += q.number;
                _path += "/"; // 形成./questions/1/路径
                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;
        }
5.2.1.3 SplitString设计-使用boost实现

boost 测试

#include <iostream>
#include <vector>
#include <boost/algorithm/string.hpp>
int main()
{
    std::vector<std::string> token;
    const std::string str = "1 回文数 简单 1 30000";
    boost::split(token, str, boost::is_any_of(" "), boost::algorithm::token_compress_on);
    for (auto &iter : token)
        std::cout << iter << std::endl;
    return 0;
}

 

static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string &seq)
{
    //boost::split
    boost::split((*target),str,boost::is_any_of(seq),boost::algorithm::token_compress_on);
}

5.2.2 oj_control设计

5.2.2.1 class Contril 设计
oj_control连通model和view

oj_control中有model对象和view对象

题目列表部分 定义一个vector<Question>存放题目列表 通过model.GetAllQuestions()获取题目信息写到vector中 通过view.AllExpandHtml渲染

指定题目部分 定义一个Question对象存放题目信息 通过model.GetOneQuestion()获取题目信息写道q中 通过view.OneExpandHtml渲染

 

    // 核心业务逻辑控制器
    class Control
    {
    private:
        Model _model;              // 操作数据类
        View _view;                // 渲染网页类
        LoadBalance _load_balance; // 负载均衡类
    public:
        Control()
        {
        }
        ~Control()
        {
        }
        bool AllQuestions(std::string *html)
        {
            bool flag = true;
            std::vector<struct Question> all;
            if (_model.GetAllQuestions(&all))
            {
                // 获取所有题目信息成功,将题目数据构建成网页
                _view.AllExpandHtml(all, html);
            }
            else
            {
                *html = "获取题目失败,形成题目列表失败";
                flag = false;
            }
            return flag;
        }
        bool Question(const std::string &number, std::string *html)
        {
            bool flag = true;
            struct Question q;
            if (_model.GetOneQuestion(number, &q))
            {
                // 获取指定题目信息成功,将题目数据构建成网页
                _view.OneExpandHtml(q, html);
            }
            else
            {
                *html = "指定题目: " + number + "不存在!";
                flag = false;
            }
            return flag;
        }
        void Judge(const std::string &number, const std::string in_json, std::string *out_json)
        {
            // 0.根据题号,直接得到题目细节
            struct Question q;
            _model.GetOneQuestion(number, &q);
            // 1.in_json进行反序列化,得到题目的id 看到用户提交的源代码 input
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value);
            // 2.重新拼接用户代码+测试用例,形成新的代码
            Json::Value compile_value;
            Json::FastWriter writer;

            compile_value["input"] = in_value["input"];
            compile_value["cpu_limit"] = q.cpu_limit;
            compile_value["mem_limit"] = q.mem_limit;
            std::string code = in_value["code"].asString();
            compile_value["code"] = code + "\n" + q.tail;

            std::string compile_string = writer.write(compile_value);

            // 3.选择负载最低的主机(差错处理)
            // 一直选择 直到主机可用 否则就是全部离线
            while (true)
            {
                Machine *m = nullptr;
                int id = 0;
                if (!_load_balance.SmartChoice(&id, &m))
                {
                    break;
                }
                LOG(INFO) << "选择主机主机id:" << id << "详情:" << m->_ip << ":" << m->_port << "\n";
                // 4.发起http请求 得到结果
                Client cli(m->_ip, m->_port); // 作为客户端访问指定的编译服务主机
                m->IncLoad();
                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_balance.OfflineMachine(id);
                    _load_balance.ShowMachines();
                }
            }
        }
    };
5.2.2.2 AllQuestions()设计
题目列表部分 定义一个vector<Question>存放题目列表 通过model.GetAllQuestions()获取题目信息写到vector中 通过view.AllExpandHtml渲染
        bool AllQuestions(std::string *html)
        {
            bool flag = true;
            std::vector<struct Question> all;
            if (_model.GetAllQuestions(&all))
            {
                // 获取所有题目信息成功,将题目数据构建成网页
                _view.AllExpandHtml(all, html);
            }
            else
            {
                *html = "获取题目失败,形成题目列表失败";
                flag = false;
            }
            return flag;
        }
5.2.2.3 Question()设计
指定题目部分 定义一个Question对象存放题目信息 通过model.GetOneQuestion()获取题目信息写道q中 通过view.OneExpandHtml渲染
        bool Question(const std::string &number, std::string *html)
        {
            bool flag = true;
            struct Question q;
            if (_model.GetOneQuestion(number, &q))
            {
                // 获取指定题目信息成功,将题目数据构建成网页
                _view.OneExpandHtml(q, html);
            }
            else
            {
                *html = "指定题目: " + number + "不存在!";
                flag = false;
            }
            return flag;
        }
5.2.2.4 Judge()设计
// 1.in_json进行反序列化,得到题目的id 看到用户提交的源代码 input
// 2.重新拼接用户代码+测试用例,形成新的代码
// 3.选择负载最低的主机,发起http请求 得到结果
// 4.将结果复制给out_json
        void Judge(const std::string &number, const std::string in_json, std::string *out_json)
        {
            // LOG(DEBUG) << in_json << "\nnumber:" << number << "\n";
            // 0.根据题号,直接得到题目细节
            struct Question q;
            _model.GetOneQuestion(number, &q);
            // 1.in_json进行反序列化,得到题目的id 看到用户提交的源代码 input
            Json::Value in_value;
            Json::Reader reader;
            reader.parse(in_json, in_value);
            // 2.重新拼接用户代码+测试用例,形成新的代码
            Json::Value compile_value;
            Json::FastWriter writer;

            compile_value["input"] = in_value["input"];
            compile_value["cpu_limit"] = q.cpu_limit;
            compile_value["mem_limit"] = q.mem_limit;
            std::string code = in_value["code"].asString();
            compile_value["code"] = code + q.tail;

            std::string compile_string = writer.write(compile_value);

            // 3.选择负载最低的主机(差错处理)
            // 一直选择 直到主机可用 否则就是全部离线
            while (true)
            {
                Machine *m = nullptr;
                int id = 0;
                if (!_load_balance.SmartChoice(&id, &m))
                {
                    break;
                }
                // 4.发起http请求 得到结果
                Client cli(m->_ip, m->_port); // 作为客户端访问指定的编译服务主机
                m->IncLoad();
                LOG(INFO) << "选择主机主机id:" << id << " 详情:" << m->_ip << ":" << m->_port << " 当前主机的负载:" << m->Load() << "\n";
                if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
                {
                    // 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_balance.OfflineMachine(id);
                    _load_balance.ShowMachines();
                }
            }
        }

5.2.3 oj_view设计

5.2.3.1 ctemplate库安装
git clone https://gitee.com/src-oepkgs/ctemplate.git
sudo apt-get install libctemplate-dev
使用:

./autogen.sh

./configure

sudo make install
5.2.3.2 测试ctemplate
#include <iostream>
#include <string>
#include <ctemplate/template.h>

int main()
{
    std::string in_html = "./test.html";
    std::string value = "比特";
    // 形成字典数据
    ctemplate::TemplateDictionary root("test"); // unordered_map<> test;
    root.SetValue("key", value);

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

    // 添加字典数据到网页中
    std::string out_html;
    tpl->Expand(&out_html, &root);

    // 完成渲染
    std::cout << out_html << std::endl;
    return 0;
}
5.2.3.3 渲染题目列表页

AllExpandHtml()

        void AllExpandHtml(const std::vector<struct Question> &questions, std::string *html)
        {
            // 题目编号 题目标题 题目难度
            // 表格
            // 1.形成路径
            std::string src_html = template_path + "all_questions.html";
            // 2.形成字典
            ctemplate::TemplateDictionary root("all_questions");
            for (const auto &q : questions)
            {
                ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("questions_list");
                sub->SetValue("number", q.number);
                sub->SetValue("title", q.title);
                sub->SetValue("star", q.star);
            }
            // 3.获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
            // 4.开始完成渲染
            tpl->Expand(html, &root);
        }
5.2.3.4 渲染题目描述页

OneExpandHtml()

        void OneExpandHtml(const struct Question &q, std::string *html)
        {
            // 1.形成路径
            std::string src_html = template_path + "one_question.html";
            // 2.形成字典
            ctemplate::TemplateDictionary root("one_question");
            root.SetValue("number", q.number);
            root.SetValue("title", q.title);
            root.SetValue("star", q.star);
            root.SetValue("desc", q.desc);
            root.SetValue("pre_code", q.header);
            // 3.获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
            // 4.开始完成渲染
            tpl->Expand(html, &root);
        }

5.2.4 class Machine设计

    class Machine
    {
    public:               // 暴露出来不用设置set和get方法
        std::string _ip;  // 编译服务的ip
        int _port;        // 编译服务的端口
        uint64_t _load;   // 编译服务的负载情况
        std::mutex *_mtx; // mutex是禁止拷贝的,使用指针
    public:
        Machine()
            : _ip(""), _port(0), _load(0), _mtx(nullptr)
        {
        }
        ~Machine()
        {
        }
        // 清空负载
        void ResetLoad()
        {
            _mtx->lock();
            _load = 0;
            _mtx->unlock();
        }
        // 增加负载
        void IncLoad()
        {
            if (_mtx)
                _mtx->lock();
            ++_load;
            if (_mtx)
                _mtx->unlock();
        }
        // 减少负载
        void DecLoad()
        {
            if (_mtx)
                _mtx->lock();
            --_load;
            if (_mtx)
                _mtx->unlock();
        }
        // 为了统一接口
        uint64_t Load()
        {
            uint64_t load = 0;
            if (_mtx)
                _mtx->lock();
            load = _load;
            if (_mtx)
                _mtx->unlock();
            return load;
        }
    };

5.2.5 class LoadBalance设计

    const std::string service_machine = "./conf/service_machine.conf";

    // 负载均衡模块
    class LoadBalance
    {
    private:
        std::vector<Machine> _machines; // 每一台主机都有下标 充当主机的编号
        std::vector<int> _online;       // 在线主机id
        std::vector<int> _offline;      // 离线主机id
        std::mutex _mtx;                // 保证Load Balance的安全
    public:
        LoadBalance()
        {
            assert(LoadConf(service_machine)); // 初始化的时候就加载
            LOG(INFO) << "加载" << service_machine << "成功" << "\n";
        }
        ~LoadBalance()
        {
        }

    public:
        bool LoadConf(const std::string &machines_conf)
        {
            std::ifstream in(machines_conf);
            if (!in.is_open())
            {
                LOG(FATAL) << "配置文件" << machines_conf << "加载失败" << "\n";
                return false;
            }

            std::string line;
            while (getline(in, line))
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, ":");
                if (tokens.size() != 2)
                    LOG(WARNING) << "配置文件不完整" << "\n";

                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;
        }
        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 OnlineMachine()
        {
            // 统一上线
        }
        void OfflineMachine(int id)
        {
            _mtx.lock();
            for (auto iter = _online.begin(); iter != _online.end(); iter++)
            {
                if (*iter == id)
                {
                    // 找到需要离线的主机
                    _offline.push_back(*iter);
                    _online.erase(iter);
                    break;
                }
            }
            _mtx.unlock();
        }
        // 查看所有的主机列表
        void ShowMachines()
        {
            _mtx.lock();
            std::cout << "当前在线主机列表:";
            for (auto &id : _online)
                std::cout << id << " ";
            std::cout << std::endl;
            std::cout << "当前离线主机列表:";
            for (auto &id : _offline)
                std::cout << id << " ";
            std::cout << std::endl;
            _mtx.unlock();
        }
    };

6.前端页面设计

编写页面:html+css+js

6.1 丐版的页面-首页

1.选中标签
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>杜超的个人刷题网站</title>
    <style>
        /* 起手式,100%保证我们的样式设置不受默认影响 */
        * {
            /* 消除网页的默认外边距 */
            margin: 0px;
            /* 消除网页的默认内边距 */
            padding: 0px;
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续浮动带来的影响 */
            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: gray;
        }
        .container .navbar .register {
            float: right;
        }
        .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>
            <a class="register" 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>

6.2 所有题目列表的页面

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>题目列表页</title>
    <style>
        /* 起手式,100%保证我们的样式设置不受默认影响 */
        * {
            /* 消除网页的默认外边距 */
            margin: 0px;
            /* 消除网页的默认内边距 */
            padding: 0px;
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续浮动带来的影响 */
            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: gray;
        }

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

        .container .navbar .login {
            float: right;
        }
        .container .questions_list {
            padding-top: 50px;
            width: 800px;
            height: 100%;
            margin: 0px auto;
            /* background-color: #ccc; */
            text-align: center;
        }
        .container .questions_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(236, 246, 237);
        }
        .container .questions_list h1 {
            color: green;
        }
        .container .questions_list table .item {
            width: 100px;
            height: 40px;
            /* padding-top: 10px;
            padding-bottom: 10px; */
            font-size: large;
            /* font-family: 'Times New Roman', Times, serif; */

        }
        .container .questions_list table .item a {
            text-decoration: none;
            color: black;
        }
        /* 实现点击动态效果 */
        .container .questions_list table .item a:hover {
            color: blue;
            /* font-size: larger; */
            
        }
        .container .footer {
            width: 100%;
            height: 50px;
            text-align: center;
            /* background-color: #ccc; */
            line-height: 50px;
            color: black;
            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>
            <a class="register" href="#">注册</a>
        </div>
        <div class="questions_list">
            <h1>OnlineJudge题目列表</h1>
            <table>
                <tr>
                    <td class="item">题号</td>
                    <td class="item">题目</td>
                    <td class="item">难度</td>
                </tr>
                {{#questions_list}}
                <tr>
                    <td class="item">{{number}}</td>
                    <td class="item"><a href="/question/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/questions_list}}
            </table>
        </div>
        <div class="footer">
            <h4>@超子</h4>
        </div>
    </div>

</body>

</html>

6.3 OJ指定题目页面-代码提交

6.3.1 ACE在线编辑器

    <!-- 引入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"

6.3.2 前后端交互jquery

    <!-- 引入jquery CDN -->
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
6.3.2.1 通过ajax向后端发起请求
            $.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);
                }
            });

6.3.3 onequestion.html代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{number}}.{{title}}</title>
    <!-- 引入ACE插件 -->
    <!-- 官网链接:https://ace.c9.io/ -->
    <!-- CDN链接:https://cdnjs.com/libraries/ace -->
    <!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 -->
    <!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ -->
    <!-- 引入ACE CDN -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"
        charset="utf-8"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"
        charset="utf-8"></script>
    <!-- 引入jquery CDN -->
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

    <style>
        * {
            margin: 0;
            padding: 0;
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

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

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

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

        /* 设置鼠标事件 */
        .container .part2 .btn-submit:hover {
            background-color: rgb(195, 224, 218);
        }

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

        .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;
            font-size: large;
        }

        .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>
            <a class="register" href="#">注册</a>
        </div>
        <!-- 左右呈现,题目描述和预设代码 -->
        <div class="part1">
            <div class="left_desc">
                <h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3>
                <!-- 保持文本原貌 -->
                <pre>{{desc}}</pre>
                <!-- <textarea name="code" cols="30" rows="10">{{pre_code}}</textarea> -->
            </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"><p>结果</p></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>

7.基于数据库的版本

1.在数据库中设计可以远程的登录的用户,并给他赋权
	oj_client
2.设计表结构
数据库 oj 表 oj_questions
3.开始编码
	连接访问数据库
	引入第三方库
	oj_sever是基于mvc模式的 只有oj_model和数据交互

计划一张表,结束所有的需求

7.1 数据库设计

create 用户 oj_client

赋权给oj_client

远程连接 修改配置文件 连接地址

7.1.1mysql workbench创建表结构

mysql workbench通过远程连接数据库创建表结构

https://dev.mysql.com/downloads/workbench/
7.1.1.1 oj_questions
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 '对应题目的测试代码',
    `cpu_limit` int default 1 comment '对应题目的超时时间',
    `mem_limit` int default 50000 comment '对应题目的内存限制'
)engine=InnoDB default charset=utf8mb4;

预设一道题目

7.1.2 数据库连接和设计问题

在项目实现过程中出现了数据库连接失败的问题
解决办法:

1.ssl连接存储 禁用
vim /etc/mysql/mysql.conf.d/mysqld.conf ->添加ssl=0

2.MySQL8之前的版本中加密规则是`mysql_native_password`,而在MySQL8之后,加密规则是`caching_sha2_password`
mysql版本为mysql  Ver 8.0.40-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu))
修正为mysql_native_password

7.2 oj_model重新设计

7.2.1 引入三方库

引入第三方库MySQL :: Download MySQL Connector/C (Archived Versions) 不使用mysql自带的开发包

mysql-connector-c-6.1.11-linux-glibc2.12-x86_64

如果mysql不带开发包,按照下面的操作

1.cd /etc/ld.conf.so.d/
2.touch oj_search.conf
3.vim oj_search.conf -> /home/dc/OnlineJudge_mysql/oj_server/lib
4.sudo ldconfig
5.makefile -> -I./include -L./lib -lmysqlclient

页面乱码 需要考虑链接编码 utf8

7.2.2 oj_model代码

// 操作数据
// 根据题目list文件 加载所有的题目信息到内存中
// 主要用来和数据进行交互,对外提供访问数据的接口
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <cassert>
#include <fstream>
#include <cstdlib>

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

namespace ns_model
{
    // using namespace std;
    using namespace ns_util;
    using namespace ns_log;
    struct Question
    {
        std::string number; // 题号 唯一
        std::string title;  // 题目的标题
        std::string star;   // 题目的难度 简单 中等 困难
        std::string desc;   // 题目的描述
        std::string header; // 题目预设给用户在线编辑器的代码
        std::string tail;   // 题目的测试用例,需要和header拼接形成完整代码code 发送给compile_server
        int cpu_limit;      // 时间要求(s)
        int mem_limit;      // 空间要求(KB)
    };
    const std::string oj_questions = "oj_questions";
    const std::string host = "127.0.0.1";
    const std::string user = "oj_client";
    const std::string passwd = "041116Dc!";
    const std::string db = "oj";
    const int port = 3306;
    class Model
    {
    public:
        Model()
        {
        }
        bool QueryMySql(std::string &sql, std::vector<struct Question> *out)
        {
            // 创建一个mysql句柄
            MYSQL *my = mysql_init(nullptr);
            // 连接数据库
            if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0))
            {
                LOG(FATAL) << "连接数据库失败!" << "\n";
                return false;
            }
			// 设置链接的编码格式 utf8
            mysql_set_character_set(my, "utf8");
            LOG(INFO) << "连接数据库成功" << "\n";
            // 执行sql语句
            if (0 != mysql_query(my, sql.c_str()))
            {
                // 访问失败
                LOG(WARNING) << sql << "execute error!" << "\n";
                return false;
            }
            // 提取结果
            MYSQL_RES *res = mysql_store_result(my);
            // 分析结果
            // 1.获得行数
            int rows = mysql_num_rows(res);
            // 2.获得列数
            // int cols = mysql_num_fields(res);
            struct Question q;
            // 获取题目信息
            for (int i = 0; i < rows; i++)
            {
                MYSQL_ROW row = mysql_fetch_row(res);
                q.number = row[0];
                q.title = row[1];
                q.star = row[2];
                q.desc = row[3];
                q.header = row[4];
                q.tail = row[5];
                q.cpu_limit = atoi(row[6]);
                q.mem_limit = atoi(row[7]);
                out->push_back(q);
            }
            // 释放结果空间
            free(res);
            // 关闭mysq连接
            mysql_close(my);
            return true;
        }
        bool GetAllQuestions(std::vector<Question> *out)
        {
            std::string sql = "select * from ";
            sql += oj_questions;
            return QueryMySql(sql, out);
        }
        // 根据题目编号拿到一个题目
        bool GetOneQuestion(const std::string &number, Question *q)
        {
            bool res = false;
            std::string sql = "select * from ";
            sql += oj_questions;
            sql += " where number=";
            sql += number;
            std::vector<struct Question> result;
            if (QueryMySql(sql, &result))
            {
                if (result.size() == 1)
                {
                    *q = result[0];
                    res = true;
                }
            }
            return res;
        }
        ~Model()
        {
        }
    };
}

8.项目扩展

项目扩展的思路

1.基于注册和登录的录题功能
2.业务扩展 论坛接入在线OJ
3.docker 可以将编译服务部署在docker上
4.目前的后端服务是使用http方式请求 是因为简单 可以设计成远程过程调用 使用reset_rpc替换httplib
5.功能完善 判题正确后跳转下一题
6.navbar中的功能

9.顶层makefile

项目的整体编译和清理

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

.PHONY:clean
clean:
	@cd compile_server/;\
	make clean;\
	cd ../;\
	cd oj_server/;\
	make clean;\
	cd ../;

http://www.kler.cn/a/488328.html

相关文章:

  • 汽车基础软件AutoSAR自学攻略(三)-AutoSAR CP分层架构(2)
  • HTML - <script>,<noscript>
  • 之前手写的两个好用开源组件优化升级
  • 04、Redis深入数据结构
  • idea全局替换显示不全(ctrl+shift+R)
  • 腾讯云AI代码助手编程挑战赛——贪吃蛇小游戏
  • C#里使用libxl读取EXCEL文件的例子
  • 在Ubuntu中使用systemd设置后台自启动服务
  • ffmpeg7.0 aac转pcm
  • Docker入门篇[SpringBoot之Docker实战系列] - 第534篇
  • 商品详情API接口数据解析,API接口系列(示例返回数据(JSON格式))
  • THB6128安森美步进电机驱动芯片
  • leetcode 面试经典 150 题:两数之和
  • 算法题(31):两数之和-输入有序数组
  • 2025年新出炉的MySQL面试题
  • 基于http协议的天气爬虫
  • 计算机视觉算法实战——YOLOv7在姿态识别上的应用
  • 【IDEA】快捷键篇
  • 麦田物语学习笔记:创建DragItem实现物品的拖拽跟随显示
  • 【Flutter】使用ScrollController配合EasyRefresh实现列表预加载:在还未滑动到底部时加载下一页数据
  • 图像传感器的视觉演变与创新
  • Unity自定义编辑器:基于枚举类型动态显示属性
  • sql 函数
  • 联邦大语言模型典型系统: FATE - LLM、FedLLM、FederatedScope - LLM、PrimiHub
  • 【Flink】flink或java异常日志输出不完整问题解决
  • 10步打造完美ASP.NET、Web API和控制台应用程序文件夹结构