负载均衡的在线OJ

news2024/11/15 11:45:33

文章目录

  • 1.项目宏观结构
    • (1)三个模块
    • (2)项目宏观结构
    • (3)编写顺序
  • 2.compile_server
    • (1)compiler.hpp
    • (2)runner.hpp
    • (3)compile_run.hpp
    • (4)compile_server.cc
    • (5)Makefile
    • (6)temp
    • (7)编译运行模块总结
  • 3.comm
    • (1)util.hpp
    • (2)log.hpp
    • (3)httplib.h
  • 4.基于MVC结构的OJ服务设计(oj_server)
    • (1)设计题库
    • (2)oj_server
    • (3)model.hpp(文件版)
    • (4)model2.hpp(数据库版)
    • (5)view.hpp
    • (6)control.hpp
  • 5.前端模块
    • (1)首页
    • (2)所有题目列表
    • (3)OJ指定题目编写代码页面
  • 6.综合调试
  • 7.结项
    • (1)整体描述
    • (2)编译运行模块
    • (3)通信模块
    • (4)负载均衡的实现
  • 8.扩展

1.项目宏观结构

(1)三个模块

1.comm:公共模块

2.compile_server:编译与运行模块

3.oj_server:获取题目列表,查看题目编写界面,负载均衡,数据库,其他功能

(2)项目宏观结构

在这里插入图片描述

(3)编写顺序

1.complie_server

2.oj_server

3.文件版

4.前端编写

5.MySQL版

2.compile_server

(1)compiler.hpp

编译代码
在这里插入图片描述

#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<fcntl.h>
#include"../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_util;
using namespace ns_log;
namespace ns_compiler
{

    class Compiler
    {
        public:
            Compiler()
            {}
            ~Compiler()
            {}
            //返回编译是否成功
            // file_name:只有文件名,需要后续拼接路径和后缀,共构建三个文件
            //./temp/file_name.cpp
            //./temp/file_name.exe
            //./temp/file_name.stderr
            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);//将权限清0
                    int _stderr=open(PathUtil::Compiler_Error(file_name).c_str(),O_CREAT|O_WRONLY,0644);
                    if(_stderr<0)
                    {
                        LOG(WARNING) << "没有成功形成stderr文件"<< "\n";
                        exit(1);
                    }
                    //重定向标准错误到file_name.stderr
                    dup2(_stderr,2);
                    //程序替换调用g++完成对代码的编译
                    execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr);
                    LOG(ERROR) << "启动g++失败,可能是参数错误"<< "\n";
                    exit(2);//替换失败直接退出
                }
                else
                {
                    //父进程
                    waitpid(pid,nullptr,0);
                    //编译是否成功:是否形成可执行程序(判断文件是否存在)?
                    if (FileUtil::IsFileExists(PathUtil::Exe(file_name).c_str()))
                    {
                        LOG(INFO) << PathUtil::Src(file_name).c_str()<<"文件编译成功"<< "\n";
                        return true;
                    }
                }
                LOG(ERROR)<<"编译失败,没有形成可执行程序"<<"\n";
                return false;
            }
    };    
}

(2)runner.hpp

运行代码
在这里插入图片描述

使用子进程替换实现运行功能,子进程运行时的标准输入、标准输出、标准错误重定向为文件,父进程waitpid()获取子进程退出时状态解析出信号并返回该信号。如果是0则正常,否则则运行异常。

在运行的时候还要进行时间复杂度和空间复杂度的限制:setrlimit

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/time.h>
#include<sys/resource.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_log;
using namespace ns_util;
namespace ns_runner
{
    class Runner
    {
        public:
            Runner()
            {}
            ~Runner()
            {}
            //提供设置进程占用资源大小的接口
            static void SetProcLimit(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;//转换成为KB
                mem_rlimit.rlim_max=RLIM_INFINITY;
                setrlimit(RLIMIT_AS,&mem_rlimit);
            }
            //返回值>0:运行的程序异常了,退出时收到的返回值就是对应的信号编号
            //返回值=0:正常运行完毕
            //返回值<0:内部错误
            //cpu_limit:可以使用的最大的CPU资源
            //mem_limit:可以占用的最大内存大小(KB)
            static int Run(const std::string& file_name,int cpu_limit,int mem_limit)
            {
                /*程序运行
                *代码跑完,结果正确
                *代码跑完,结果不正确
                *代码跑完,结果异常
                //运行模块需要考虑代码跑完结果正确吗?不考虑
                //结果正确与否是由测试用例决定的
                //只考虑是否运行成功
                */
                //一个程序启动的时候
                //标准输入:不处理
                //标准输出:运行输出结果
                //标准错误:运行错误信息
                std::string _execute=PathUtil::Exe(file_name);
                std::string _stdin=PathUtil::Stdin(file_name);
                std::string _stdout = PathUtil::Stdout(file_name);
                std::string _stderr = PathUtil::Stderr(file_name);
                umask(0);
                int _in_fd=open(_stdin.c_str(),O_CREAT|O_RDONLY,0644);
                int _out_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
                int _err_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
                if(_in_fd<0||_out_fd<0||_err_fd<0)
                {
                    LOG(ERROR)<<"运行时,打开文件失败"<<"\n";
                    return -1;
                }
                pid_t pid=fork();
                if(pid<0)
                {
                    LOG(ERROR)<<"运行时,创建子进程失败"<<"\n";
                    close(_in_fd);
                    close(_out_fd);
                    close(_err_fd);
                    return -2;
                }
                else if(pid==0)
                {
                    dup2(_in_fd,0);
                    dup2(_out_fd,1);
                    dup2(_err_fd, 2);
                    SetProcLimit(cpu_limit,mem_limit);
                    execl(_execute.c_str(), _execute.c_str(),nullptr);
                    exit(1);
                }
                else
                {
                    LOG(INFO)<<"运行成功"<<"\n";
                    close(_in_fd);
                    close(_out_fd);
                    close(_err_fd);
                    int status=0;
                    waitpid(pid,&status,0);
                    //程序运行异常,一定是因为收到了信号
                    return status&0x7F;
                }
            }
    };
}

测试资源限制代码:资源约束与进程异常是类似的,都是通过信号

#include<iostream>
#include<unistd.h>
#include<sys/time.h>
#include<sys/resource.h>
#include<signal.h>
#include<cstdlib>
using namespace std;
void handler(int signal)
{
    cout<<"signal:"<<signal<<endl;
    exit(-1);
}
int main()
{
    //是通过信号终止的
    for(int i=1;i<=31;i++)
    {
        signal(i,handler);
    }
    struct rlimit r;
    // r.rlim_cur=1;//1s
    // r.rlim_max=RLIM_INFINITY;
    // setrlimit(RLIMIT_CPU,&r);
    r.rlim_cur=1024*1024*40;
    r.rlim_max=RLIM_INFINITY;
    setrlimit(RLIMIT_AS,&r);
    int count=0;
    while(1)
    {
        int* p=new int[1024*1024];
        cout<<count<<endl;
        count++;
    }

    return 0;
}


//内存限制
[lhb@VM-20-2-centos Online_Judge]$ ./a.out
0
1
2
3
4
5
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
signal:6
    
    
    
//CPU占用时间超时
[lhb@VM-20-2-centos Online_Judge]$ ./a.out
signal:24

(3)compile_run.hpp

在这里插入图片描述


整合编译和运行

  • 适配用户需求,定制通信协议字段

  • 正确调用compile和run方法

  • 形成唯一文件名

#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <jsoncpp/json/json.h>
#include <signal.h>
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_util;
using namespace ns_log;
namespace ns_compile_run
{
    class CompileAndRun
    {
    public:
        //信号转化为描述
        // 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::Compiler_Error(file_name),&desc,true);
                break;
            case SIGABRT:
                desc = "内存超过范围";
                break;
            case SIGXCPU:
                desc = "CPU使用超时";
                break;
            case SIGFPE:
                desc = "浮点数溢出";
                break;
            default:
                desc = "未知:" + std::to_string(code);
                break;
            }
            return desc;
        }
        static void RemoveTempFile(std::string& file_name)
        {
            //清理文件的个数是不确定的,但是有哪些我们是知道的
            std::string _src=PathUtil::Src(file_name);
            if(FileUtil::IsFileExists(_src)) 
                unlink(_src.c_str());
            std::string _compile_error = PathUtil::Compiler_Error(file_name);
            if (FileUtil::IsFileExists(_compile_error))
                unlink(_compile_error.c_str());
            std::string _Stderr = PathUtil::Stderr(file_name);
            if (FileUtil::IsFileExists(_Stderr))
                unlink(_Stderr.c_str());
            std::string _Stdout = PathUtil::Stdout(file_name);
            if (FileUtil::IsFileExists(_Stdout))
                unlink(_Stdout.c_str());
            std::string _Stdin = PathUtil::Stdin(file_name);
            if (FileUtil::IsFileExists(_Stdin))
                unlink(_Stdin.c_str());
            std::string _exe = PathUtil::Exe(file_name);
            if (FileUtil::IsFileExists(_exe))
                unlink(_exe.c_str());
        }
        /*************************************
         * 输入:
         * input:用户给自己提交的代码对应的输入,暂时不做处理
         * code:用户提交的代码
         * cpu_limit:时间要求
         * mem_limit:空间要求
         * 输出:
         * status:状态码
         * reason:对应的请求结果,即状态码对应的原因
         * 选填:
         * stdout:程序运行完的结果
         * stderr:程序运行完的错误结果
         *
         * in_json={"code":"代码",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)
        {
            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;
            int status_code = 0;
            int run_result = 0;    //用于接收run后的信号
            std::string file_name; //需要内部形成的唯一文件名
            if (code.size() == 0)
            {
                status_code = -1;
                goto END;
            }
            //形成的文件名只具有唯一性,没有目录没有后缀
            //毫秒级时间戳+原子性递增唯一值:保证唯一性
            file_name = FileUtil::UniqFileName();
            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;
                goto END;
            }
            else
            {
                //运行成功
                status_code = 0;
            }
        END:
            out_value["status"] = status_code;
            out_value["reason"] = CodeToDesc(status_code,file_name);
            if (status_code == 0)
            {
                //整个过程全部成功
                std::string string_stdout;
                FileUtil::ReadFile(PathUtil::Stdout(file_name),&string_stdout,true);
                out_value["stdout"] = string_stdout;

                std::string string_stderr;
                FileUtil::ReadFile(PathUtil::Stderr(file_name),&string_stderr,true);
                out_value["stderr"] = string_stderr;
            }
            Json::StyledWriter writer;
            *out_json = writer.write(out_value);
            //清理临时文件
            RemoveTempFile(file_name);
        }
    };
}

(4)compile_server.cc

提供网络服务,接入httplib

//使用httplib只需要将.h拷贝到项目中即可直接使用
//cpp-httplib需要使用高版本的gcc
//是一个阻塞式多线程的网络http库,使用了原生线程库
//测试的时候可以使用postman来进行测试
#include"compile_run.hpp"
#include"../comm/httplib.h"
using namespace ns_compile_run;
using namespace httplib;
void Usage(std::string proc)
{
    std::cerr<<"Usage: "<<"\n\t"<<proc<<"port"<<std::endl;
}
//编译服务随时可能被多个人请求,当传递上来文件名在形成源文件的时候要具有唯一性。
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        return 1;
    }
    Server svr;
    svr.Post("/compile_and_run", [](const Request &req, Response &res){
        //用户请求的服务正文是我们想要的json_string
        std::string out_json;
        std::string in_json=req.body;
        if(!in_json.empty())
        {
            CompileAndRun::Start(in_json,&out_json);
        }
        res.set_content(out_json,"application/json;charset=utf-8");
    });
        svr.listen("0.0.0.0", atoi(argv[1]));
}

(5)Makefile

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

(6)temp

临时文件路径

(7)编译运行模块总结

用户向编译运行模块服务传入一个json串,它的内容包括代码,标准输入的内容,cpu时间限制,内存限制。

该模块向用户返回一个json串,包括状态码,状态码描述,标准输出,标准错误内容。

  • 使用jsoncpp工具获得输入的各个部分。

  • 生成一个唯一的临时文件名,根据该文件名构建源文件,将代码写入到源文件中。

  • 然后使用g++进行进程替换进行编译操作,编译过程会产生一个.compiler_error文件,保存编译报错信息。

  • 进程替换方式运行可执行程序,将标准输出,标准错误的内容写入文件中,父进程获得退出信号。

  • 根据以上过程的各种返回值,自己定义构建状态码,状态码描述可以是运行成功,错误原因,或者异常信号产生原因,并将标准输出以及标准错误填入返回的json串中。

3.comm

(1)util.hpp

工具集

Time_util:获取s和ms时间戳

Path_util:构建.cpp .exe .compile_error .stdin .stdout .stderr文件的完整路径

File_util:判断文件是否存在,生成唯一文件名,写入文件,读取文件

String_util:分割字符串

#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<sys/time.h>
#include<fstream>
#include<atomic>
#include<boost/algorithm/string.hpp>
namespace ns_util
{
    const std::string temp_path="./temp/";
    class TimeUtil
    {
    public:
        //获取时间戳(s)
        static std::string GetTimeStamp()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec);
        }
        //获取时间戳(ms)
        static std::string GetTimeMS()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec*1000+_time.tv_usec/1000);
        }
    };
    class PathUtil
    {
        public:
            //添加后缀suffix
            static std::string AddSuffix(const std::string& file_name,const std::string& suffix)
            {
                std::string path=temp_path;
                path+=file_name;
                path+=suffix;
                return path;
            }
            //构建源文件路径+后缀的完整文件名
            //编译时需要的
            //1234->./temp/1234.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 Compiler_Error(const std::string &file_name)
            {
                return AddSuffix(file_name, ".complie_error");
            }
            //运行时需要的
            //
            static std::string Stdin(const std::string &file_name)
            {
                return AddSuffix(file_name, ".stdin");
            }
            static std::string Stdout(const std::string &file_name)
            {
                return AddSuffix(file_name, ".stdout");
            }
            static std::string Stderr(const std::string &file_name)
            {
                return AddSuffix(file_name, ".stderr");
            }

    };
    class FileUtil
    {
        public:
            static bool IsFileExists(const std::string& path_name)
            {
                //通过获取文件属性是否成功判断文件是否存在
                struct stat st;
                if(stat(path_name.c_str(),&st)==0)
                {
                    return true;
                }
                return false;
            }
            //毫秒级时间戳+原子性唯一值
            static std::string UniqFileName()
            {
                std::atomic_uint id(0);//id是原子的
                id++;
                std::string ms=TimeUtil::GetTimeMS();
                std::string uid=std::to_string(id);
                return ms+"_"+uid;
            }
            static bool WriteFile(const std::string& target,const std::string& content)
            {
                std::ofstream out(target);
                if(!out.is_open())
                {
                    return false;
                }
                else
                {
                    out.write(content.c_str(),content.size());
                }
                out.close();
                return true;
            }
            static bool ReadFile(const std::string& target,std::string* content,bool keep=false)
            {
                (*content).clear();
                std::ifstream in(target);
                if(!in.is_open())
                {
                    return false;
                }
                else
                {
                    std::string line;
                    //getline不保存行分隔符(没有'\n'),有些时候需要保存分隔符
                    //getline内部重载了强制类型转换
                    while(std::getline(in,line))
                    {
                        (*content)+=line;
                        (*content)+=(keep?"\n":"");
                    }
                }
                in.close();
                return true;
            }
    };
    class StringUtil
    {
        public:
            //将str以sep为分隔符切分的内容放在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);
            }
    };
}

(2)log.hpp

日志

使用宏替换函数来实现的,定义了一个LOG()的函数,返回值是ostream&类型,它会将错误等级,文件名,行数和时间显示出来。

#pragma once
#include<iostream>
#include<string>
#include"util.hpp"
using namespace ns_util;
namespace ns_log
{
    //日志等级
    enum
    {
        INFO,//常规
        DEBUG,//调试
        WARNING,//告警
        ERROR,//错误
        FATAL//系统崩溃
    };
    //开放式日志:LOG(level)<<"message"
    //经常被调用,设置为内联函数
    inline std::ostream& Log(const std::string& level,const std::string& file_name,int line)
    {
        std::string message="[";
        message+=level;
        message+="]";
        message+="[";
        message+=file_name;
        message+="]";
        //添加报错行
        message+="[";
        message += std::to_string(line);
        message+="]";
        //添加日志时间戳
        message+="[";
        message+=TimeUtil::GetTimeStamp();
        message+="]";
        std::cout<<message;//不要endl进行刷新
        return std::cout;
    }
    //#level:将数字变成字符串
    #define LOG(level) Log(#level,__FILE__,__LINE__)
}

(3)httplib.h

自行下载。

4.基于MVC结构的OJ服务设计(oj_server)

本质就是建立一个小型网站

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

MVC设计模式:

M:通常是和数据交互的模块,比如对题库的增删查改(文件版,数据库版)

V:通常是拿到数据之后,要进行渲染网页内容,展示给用户(浏览器)

C:控制器,就是我们的核心业务逻辑

(1)设计题库

题目的编号,标题,难度,题面,时间要求(内部处理)和空间要求

有两批文件
1.questions_list:题目列表(不需要出现题目的内容)
2.需要题目的描述,题目的预设置的代码(handler.cpp),测试用例代码(tail.cpp)
这两个内容是通过题目的编号产生关联的

设计OJ最大的成本是给每一道题设计测试用例

在这里插入图片描述

其中question的目录结构如上,question.txt表示题目列表,目录名表示题目编号,desc.txt存放题目描述,handler.cpp存放预设置代码,tail.cpp存放测试用例。

//1.当用户提交自己的代码时
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
class Solution
{
public:
    bool isPalindrome(int x)
    {
        //请将你的代码写在这里
        return true;
    }
};
//2.OJ不是只把上面的代码提交给编译运行模块
//而是拼接上该题目对应的测试用例
#ifndef COMPILER_ONLINE
#include "handler.cpp"
#endif
void Test1()
{
    bool ret=Solution().isPalindrome(121);
    if(ret)
    {
        cout<<"通过用例1,测试121通过...OK"<<endl;
    }
    else
    {
        cout<<"没有通过用例1,测试的值是121...FALSE"<<endl;
    }
}
void Test2()
{
    //通过定义临时对象来完成方法的调用
    bool ret = Solution().isPalindrome(-10);
    if (!ret)
    {
        cout << "通过用例1,测试-10通过...OK" << endl;
    }
    else
    {
        cout << "没有通过用例1,测试的值是-10...FALSE" << endl;
    }
}
int main()
{
    Test1();
    Test2();
}
//3.最终提交给编译运行的代码是完整的一份代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
class Solution
{
public:
    bool isPalindrome(int x)
    {
        //请将你的代码写在这里
        return true;
    }
};
//下面的代码,我们不想让编译器编译的时候保留,而是裁剪掉,仅仅是让我们设计测试用例时不要报语法错误
//g++ -D COMPILER_ONLINE可以在编译的时候定义该宏,这样就在编译时候拿掉了
///
#ifndef COMPILER_ONLINE
#include "handler.cpp"
#endif
///
void Test1()
{
    bool ret=Solution().isPalindrome(121);
    if(ret)
    {
        cout<<"通过用例1,测试121通过...OK"<<endl;
    }
    else
    {
        cout<<"没有通过用例1,测试的值是121...FALSE"<<endl;
    }
}
void Test2()
{
    //通过定义临时对象来完成方法的调用
    bool ret = Solution().isPalindrome(-10);
    if (!ret)
    {
        cout << "通过用例1,测试-10通过...OK" << endl;
    }
    else
    {
        cout << "没有通过用例1,测试的值是-10...FALSE" << endl;
    }
}
int main()
{
    Test1();
    Test2();
}

数据库版本的题库与之相似,只需要一张表即可完成:
在这里插入图片描述

(2)oj_server

#include<iostream>
#include <regex>
#include<signal.h>
#include "../comm/httplib.h"
#include"oj_control.hpp"
using namespace httplib;
using namespace ns_control;
std::regex r4("[0-9]", std::regex_constants::basic);
static Control* ctrl_ptr=nullptr;
void Recovery(int signal)
{
    ctrl_ptr->RecoveryMachine();
}
int main()
{
    signal(SIGQUIT,Recovery);
    //用户请求的服务路由功能
    Server svr;
    Control ctrl;
    ctrl_ptr=&ctrl;
    svr.set_base_dir("./wwwroot");
    //1.获取所有的题目列表
    svr.Get("/all_questions",[&ctrl](const Request& req,Response& res){
        //返回一个包含所有题目的网页
        std::string html;
        ctrl.AllQuestions(&html);
        res.set_content(html,"text/html;charset=utf-8");
    });
    //2.用户要根据题目,获取题目的内容
    //(\d+)是正则表达式表示数字,+表示多个
    svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &res){ 
        std::string number=req.matches[1];//1号下标存放正则匹配的结果
        std::string html;
        ctrl.Question(number,&html);
        res.set_content(html, "text/html;charset=utf-8");
    });
    //3.用户提交代码,使用判题功能(1.每道题的测试用例,2.compile_and_run服务)
    svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &res){
        std::string number=req.matches[1];//1号下标存放正则匹配的结果
        std::string result_json;
        ctrl.Judge(number,req.body,&result_json);
        res.set_content(result_json, "application/json;charset=utf-8");
        //res.set_content("这是指定题目的判题" + number, "text/plain;charset=utf-8"); 
    });
    svr.listen("0.0.0.0", 8080);
    return 0;
}

(3)model.hpp(文件版)

完成对数据的获取
在这里插入图片描述

#pragma once
//文件版本
//根据list文件加载所有题目信息到内存中
// model:主要用于数据进行交互,提供访问数据的接口
#include <iostream>
#include <string>
#include <unordered_map>
#include <assert.h>
#include <vector>
#include <fstream>
#include <cstdlib>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_log;
using namespace ns_util;
namespace ns_model
{
    struct Question
    {
        std::string number; //题目标号(唯一的)
        std::string title;  //题目标题
        std::string star;   //题目难度(简单,中等,困难)
        int cpu_limit;      //题目时间要求
        int mem_limit;      //题目空间要求
        std::string desc;   //题目的描述
        std::string handler; //题目预设给用户在线编辑器代码
        std::string tail;   //题目的测试用例需要与handler拼接,形成完整代码
    };
    const std::string question_list = "./oj_questions/question.list";
    const std::string question_path = "./oj_questions/";
    class Model
    {
    private:
        //题号映射题目细节
        std::unordered_map<std::string, Question> questions; //题号与题目对应
    public:
        Model()
        {
            assert(LoadQuestionList(question_list));
        }
        ~Model() {}
        //加载配置文件:question.list与题目编号文件,就是构建questions
        bool LoadQuestionList(const std::string &question_list)
        {
            std::ifstream in(question_list);
            if (!in.is_open())
            {
                LOG(FATAL)<<"加载题库失败,请检查是否存在题库文件"<<std::endl;
                return false;
            }
            std::string line;
            while (getline(in, line))
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, " ");
                if (tokens.size() != 5)
                {
                    LOG(WARNING)<<"加载部分题目失败,请检查文件格式"<<std::endl;
                    continue;
                }
                Question q;
                q.number = tokens[0];
                q.title = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = stoi(tokens[3]);
                q.mem_limit = stoi(tokens[4]);

                std::string path = question_path;
                path += q.number;
                path += "/";
                FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);
                FileUtil::ReadFile(path + "handler.cpp", &(q.handler), true);
                FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);

                questions.insert({q.number,q});
            }
            LOG(INFO)<<"加载题库成功"<<std::endl;
            in.close();
            return true;
        }
        //获取全部题目到数组中
        bool GetAllQuestion(std::vector<Question> *out)
        {
            if (questions.size() == 0)
            {
                LOG(ERROR)<<"用户获取题目失败"<<std::endl;
                return false;
            }
            for (const auto &q : questions)
            {
                out->push_back(q.second);
            }
            return true;
        }
        //获取一个题目
        bool GetOneQuestion(const std::string &number, Question *q)
        {
            const auto &iter = questions.find(number);
            if (iter == questions.end())
            {
                LOG(ERROR)<<"获取题目失败,题目编号为"<<number<<std::endl;
                return false;
            }
            *q = iter->second;
            return true;
        }
    };
}

(4)model2.hpp(数据库版)

#pragma once
// MySQL版本
#include <iostream>
#include <string>
#include <unordered_map>
#include <assert.h>
#include <vector>
#include <fstream>
#include <cstdlib>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include"./MySQL_include/mysql.h"
using namespace ns_log;
using namespace ns_util;
namespace ns_model
{
    struct Question
    {
        std::string number;  //题目标号(唯一的)
        std::string title;   //题目标题
        std::string star;    //题目难度(简单,中等,困难)
        std::string desc;    //题目的描述
        std::string handler; //题目预设给用户在线编辑器代码
        std::string tail;    //题目的测试用例需要与handler拼接,形成完整代码
        int cpu_limit;       //题目时间要求
        int mem_limit;       //题目空间要求
    };
    const std::string oj_questions = "oj_questions";
    const std::string user = "stu";
    const std::string host = "127.0.0.1";
    const std::string password = "*********";
    const std::string db = "oj";
    const unsigned int port = 3306;
    class Model
    {
    public:
        Model() {}
        ~Model() {}
        bool QueryMySQL(const std::string &sql, std::vector<Question> *out)
        {
            //链接数据库
            MYSQL *my = mysql_init(nullptr); //创建并初始化一个MySQL句柄
            if (mysql_real_connect(my, host.c_str(), user.c_str(), password.c_str(), db.c_str(), port, nullptr, 0) == nullptr)
            {
                std::cout << mysql_error(my) << std::endl;
                LOG(FATAL)<<"链接数据库失败"<<std::endl;
                return false;
            }
            LOG(INFO)<<"链接数据库成功"<<std::endl;
            mysql_set_character_set(my, "utf8");
            //执行sql语句
            if(mysql_query(my,sql.c_str())!=0)
            {
                LOG(WARNING)<<sql<<"execute error"<<std::endl;
                return false;
            }
            //提取结果
            MYSQL_RES* res=mysql_store_result(my);
            //分析结果
            int rows=mysql_num_rows(res);//行数
            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.handler=row[4];
                q.tail=row[5];
                q.cpu_limit=atoi(row[6]);
                q.mem_limit=atoi(row[7]);
                out->push_back(q);
            }
            free(res);//释放结果空间
            mysql_close(my);
            return true;
        }
        //获取全部题目到数组中
        bool GetAllQuestion(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<Question> result;
            if (QueryMySQL(sql, &result))
            {
                if (result.size() == 1)
                {
                    *q = result[0];
                    res=true;
                }
            }
            return res;
        }
    };
}

(5)view.hpp

视图模块,将数据处理成网页

核心方法是使用ctemplate库对数据进行渲染

#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<ctemplate/template.h>
#include "oj_model.hpp"
using namespace ns_model;
namespace ns_view
{
    const std::string template_path="./template_html/";
    class View
    {
    public:
        View(){}
        ~View(){}
        void AllExpandHtml(const std::vector<struct Question>& questions,std::string* html)
        {
            //题目的编号,标题,难度
            //推荐使用表格形式
            //1.被渲染的网页
            std::string src_html=template_path+"all_questions.html";
            //2.形成数据字典
            ctemplate::TemplateDictionary root("all_questions");
            for(const auto& q:questions)
            {
                //形成子字典
                ctemplate::TemplateDictionary* sub=root.AddSectionDictionary("question_list");
                sub->SetValue("number",q.number);
                sub->SetValue("title",q.title);
                sub->SetValue("star",q.star);
            }
            //3.获取被渲染的网页
            ctemplate::Template* tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
            //4.开始完成渲染功能
            tpl->Expand(html,&root);
        }
        void OneExpandHtml(const struct Question& q,std::string* html)
        {
            std::string src_html = template_path + "one_question.html";
            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.handler);
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
            tpl->Expand(html, &root);
        }
    };
}

附加功能:需要有数据渲染

如果后序引入了ctemplate库,一旦对网页进行修改,想看到结果需要将server重新编译,ctemplate有自己的优化策略,可能在内存中存在缓存网页数据。

(6)control.hpp

逻辑控制模块,与服务器直接相连

在这里插入图片描述

#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<mutex>
#include<cassert>
#include<fstream>
#include<algorithm>
#include<jsoncpp/json/json.h>
#include"../comm/httplib.h"
//#include"oj_model.hpp"
#include "oj_model2.hpp"
#include "oj_view.hpp"
#include"../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
using namespace httplib;
namespace ns_control
{
    //负载均衡的逻辑
    //提供服务的主机
    class Machine
    {
    public:
        std::string ip;//编译服务的ip
        int port;//编译服务的端口
        uint64_t load;//编译服务的负载
        std::mutex* mtx;//C++中mutex是禁止拷贝的,使用指针来完成
    public:
        Machine():ip(""),port(0),load(0),mtx(nullptr)
        {

        }
        ~Machine()
        {

        }
        //增加负载,加锁
        void IncLoad()
        {
            if (mtx)
                mtx->lock();
            load++;
            if (mtx)
                mtx->unlock();
        }
        //减少负载,加锁
        void DesLoad()
        {
            if (mtx)
                mtx->lock();
            load--;
            if (mtx)
                mtx->unlock();
        }
        //获取主机负载,没有太大意义,只是为了统一接口
        uint64_t Load()
        {
            uint64_t _load;
            if (mtx)
                mtx->lock();
            _load=load;
            if (mtx)
                mtx->unlock();
            return _load;
        }
        void ResetLoad()
        {
            if(mtx) mtx->lock();
            load=0;
            if(mtx) mtx->unlock();
        }
    };
    //负载均衡模块
    const std::string service_machine="./conf/service_machine.conf";
    class LoadBalance
    {
        std::vector<Machine> machines;//可以提供编译服务的所有主机,下标充当主机编号
        std::vector<int> online;//所有在线的主机,存放主机编号
        std::vector<int> offline;//所有离线的主机
        //保证LoadBalance的数据安全
        std::mutex mtx;
        public:
            LoadBalance()
            {
                assert(LoadConf(service_machine));
                LOG(INFO)<<"加载service_machine配置文件成功"<<"\n";
            }
            ~LoadBalance(){}
            //导入配置文件,主机
            bool LoadConf(const std::string& machine_conf)
            {
                std::ifstream in(machine_conf);
                if(!in.is_open())
                {
                    LOG(FATAL)<<"load加载machine_conf失败"<<std::endl;
                    return false;
                }
                std::string line;
                while(getline(in,line))
                {
                    std::vector<std::string>  tokens;
                    StringUtil::SplitString(line,&tokens,":");
                    if(tokens.size()!=2)
                    {
                        LOG(WARNING)<<"切分"<<line<<"失败了"<<std::endl;
                        continue;
                    }
                    Machine m;
                    m.ip=tokens[0];
                    m.port=stoi(tokens[1]);
                    m.load=0;
                    m.mtx=new std::mutex();

                    online.push_back(machines.size());//在线的主机
                    machines.push_back(m);
                }
                in.close();
                return true;
            }
            //智能选择
            //id:选择主机的id
            //m:选择的主机
            bool SmartChoice(int* id,Machine** m)
            {
                //1.使用选择好的主机(更新该主机的负载)
                //2.我们需要可能离线该主机
                mtx.lock();
                //负载均衡的算法
                //1.随机数法
                //2.轮训+随机

                int online_num=online.size();//有多少主机在线
                if(online_num==0)
                {
                    mtx.unlock();
                    LOG(FATAL) << "所有的后端编译主机已经全部离线了,请运维尽快查看" << std::endl;
                    return false;
                }
                //通过遍历的方式找到所有负载最小的机器
                *id=online[0];
                *m=&machines[online[0]];
                uint64_t min_load=machines[online[0]].Load();
                for(int i=1;i<online_num;i++)
                {
                    uint64_t curr_load=machines[online[i]].Load();
                    if(min_load>curr_load)
                    {
                        min_load=curr_load;
                        *id = online[i];
                        *m=&machines[online[i]];//想获得主机地址,不想获得主机,空间小,使用二级指针
                    }
                }
                mtx.unlock(); 
                return true;
            }
            //在线主机
            void OnLineMachine()
            {
                //当所有主机都离线的时候,统一上线
                //后面统一解决
                mtx.lock();
                online.insert(online.end(),offline.begin(),offline.end());
                offline.erase(offline.begin(),offline.end());
                mtx.unlock();
                LOG(INFO)<<"所有的主机上线了"<<std::endl;
            }
            //离线主机
            void OffLineMachine(int which)
            {
                mtx.lock();
                for(auto iter=online.begin();iter!=online.end();iter++)
                {
                    machines[which].ResetLoad();//离线的时候将负载置为0
                    if(*iter==which)
                    {
                        //要离线的主机已经找到了
                        online.erase(iter);
                        offline.push_back(which);
                        break;//因为break的存在,因此我们不用考虑迭代器失效的问题
                    }
                }
                mtx.unlock();
            }
            //for test
            void ShowMachines()
            {
                mtx.lock();
                std::cout<<"当前在线主机列表"<<std::endl;
                for(auto& iter:online)
                {
                    std::cout<<iter<<" ";
                }
                std::cout<<std::endl;
                std::cout << "当前离线主机列表" << std::endl;
                for (auto &iter : offline)
                {
                    std::cout << iter << " ";
                }
                std::cout << std::endl;
                mtx.unlock();
            }
    };
    //这是核心业务逻辑的控制器
    class Control
    {
    private:
        Model model_;
        View view_;
        LoadBalance loadbalance_;//核心负载均衡器
    public:
        Control(){}
        ~Control(){}
        void RecoveryMachine()
        {
            loadbalance_.OnLineMachine();
        }
        //根据题目数据构建网页
        bool AllQuestions(std::string* html)
        {
            bool ret=true;
            std::vector<struct Question> all;
            if(model_.GetAllQuestion(&all))
            {
                //获取题目信息成功,将所有的题目数据构建成网页并返回
                sort(all.begin(),all.end(),[](const struct Question& q1,const struct Question& q2){
                    return stoi(q1.number)<stoi(q2.number);
                });
                view_.AllExpandHtml(all,html);

            }
            else
            {
                ret=false;
                *html="获取文本失败,形成题目列表失败";
            }
            return ret;
        }
        bool Question(std::string& number,std::string* html)
        {
            bool ret=true;
            struct Question q;
            if(model_.GetOneQuestion(number,&q))
            {
                //获取指定题目信息成功,构建网页并返回
                view_.OneExpandHtml(q,html);
            }
            else
            {
                ret=false;
                *html="指定题目"+number+"不存在";
            }
            return ret;
        }
        // id: code: input
        //number:题号
        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进行反序列化,得到用户提交的源代码
            Json::Reader reader;
            Json::Value in_value;
            reader.parse(in_json,in_value);
            //2.重新拼接用户代码,形成一份新的代码
            std::string code=in_value["code"].asString();
            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;
                Machine* m=nullptr;
                if(!loadbalance_.SmartChoice(&id,&m))
                {
                    break;//所有主机都挂掉了,都不在线
                }
                LOG(INFO)<<"选择主机成功,主机id"<<id<<" 详情:"<<m->ip<<":"<<m->port<<"当前主机的负载时"<<m->load<<std::endl;
                // 4.发起http请求得到结果
                //使用http-lib的client请求
                Client cli(m->ip,m->port);
                m->IncLoad();
                if(auto res=cli.Post("/compile_and_run",compile_string,"application/json;charset=utf-8"))
                {
                    if(res->status==200)
                    {
                        LOG(INFO)<<"请求编译和运行服务成功"<<std::endl;
                        *out_json = res->body;
                        m->DesLoad();
                        break;
                    }
                    m->DesLoad();
                }
                else
                {
                    //请求失败
                    LOG(ERROR) << "当前请求的主机id" << id << "详情:" << m->ip << ":" << m->port << "可能已经离线"<<std::endl;
                    loadbalance_.OffLineMachine(id);
                    //仅仅为了用来测试
                    loadbalance_.ShowMachines();
                }
            }
            //5.将结果赋值给out_json返回给用户

        }
    };
}

当完成所有功能之后需要注意,要给编译模块添加-D来条件编译去掉对应的头文件handler.cpp

5.前端模块

  • 编写页面三剑客:HTML,CSS,JS
  • 后端开发不需要关心前端页面
  • 后端虽然不关心页面,但是需要了解前后端如何交互

(1)首页

所谓样式调整,就是对html的标签做调整

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>这是我的个人OJ系统</title>
    <style>
            /* 编写网页的起手式 */
            /* *表示选中所有标签 */
        *{
            /* 消除网页的默认外边距 */
            margin:0px;
            /* 消除网页的默认内边距 */
            padding: 0px;
        }
        html,
        body{
            width:  100%;
            height: 100%;
        }
        .container .navbar
        {
            width: 100%;
            height: 50px;
            background-color: #ccc;
            /* 给父级标签设置overflow后序浮动带来的影响 */
            overflow: hidden;
        }     
        .container .navbar a
        {
            display: inline-block;
            /* 设置字体宽度 */
            width: 80px;
            color: black;
            font-size: large;
            /* 设置文字的高度与导航栏一样 */
            line-height: 50px;
            text-decoration: none;
            /* 文字居中 */
            text-align: center;
        }
        /* 为a标签加动态效果,设置鼠标事件 */
        .container .navbar a:hover
        {
            background-color: green;
        }
        .container .navbar .login
        {
            /* 将登录放在最右侧 */
            float:right;
        }
        /* .开头表示类选中器 */
        .container .content
        {
            /* 设置标签的宽度 */
            width: 800px;
            /* 背景颜色 */
            /* background-color: #ccc; */
            /* 整体居中 */
            margin: 0px auto;
            /* 设置文字在content中居中 */
            text-align: center;
            /* 设置上外边距 */
            margin-top: 200px;
        } 
        .container .content .font_
        {
            /* 因为a不是块元素,将整体设为块元素,独占一行,可以设置高度,宽度等属性 */
            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_">这是我个人独立开发的在线Online_Judge平台</h1>
            <p class="font_">欢迎来到我的OJ平台</p>
            <a class="font_" href="/all_questions">点击我开始编程了</a>
        </div>
    </div>
</body>
</html>

(2)所有题目列表

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线OJ-题目列表</title>
    <style>
        /* 起手式, 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="/question/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/question_list}}
            </table>
        </div>
        <div class="footer">
            <!-- <hr> -->
            <h4>@比特就业课</h4>
        </div>
    </div>

</body>

</html>

(3)OJ指定题目编写代码页面

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

6.综合调试

主页:

在这里插入图片描述

题目列表:

在这里插入图片描述

编程界面:
在这里插入图片描述

提交返回结果:
在这里插入图片描述

不断提交,负载均衡测试

在这里插入图片描述

断开所有主机,一键上线测试
在这里插入图片描述

在这里插入图片描述

7.结项

(1)整体描述

1.这是一个负载均衡的在线OJ项目,是一个简易版的leetcode,实现了它的访问题目和判题功能。

2.它主要有两个模块构成,一个模块叫oj_server,它是暴露给用户的,用户可以通过它获取题目列表,获取单个题目,答题并且获取编译运行的结果。

3.另一个模块叫compile_server,它是编译运行模块,可以将它部署在多台服务器上,oj_server可以选择负载最低的服务器,并与该服务器上的compile_server进行通信。主要通信的内容就是向compile_server传递代码,然后获取代码编译运行的结果。

(2)编译运行模块

1.oj_server向compile_server传递的是一个json串,通过反序列化可以拿到代码,标准输入,运行时间限制,运行内存限制。

2.使用子进程替换的方式进行编译,编译时会生成一个compile_error文件,将编译的结果写入该文件中,父进程通过是否形成了可执行文件来判断编译是否成功。

3.使用子进程替换的方式进行运行,使用dup2方法将标准输入,标准输出,标准错误分别重定向到文件中,使用rlimit结构体中的成员定义运行时间和空间的限制。父进程获取子进程的退出码来判断运行是否成功。

4.根据不同的编译运行结果定义不同的状态码,并加上相关的错误描述,将状态码,错误描述,标准输出和标准错误构建json串,返回给oj_server。

(3)通信模块

1.使用MVC模式进行编写的,model,view,control

2.当用户请求题目列表或者某道题的时候,model模块会访问数据库,并返回用户请求的内容,并传递给view模块。

3.view模块将数据进行渲染成网页,并将该网页的html返回给control模块,并由control模块返回给用户。

(4)负载均衡的实现

1.用户解答完题目会将代码传递给control模块。

2.将代码,标准输入,时间限制和空间限制构建json串。

3.遍历所有在线主机的负载,选择最小的进行通信

4.通信的主机负载++,接收到返回的结果之后负载–

5.通信失败将该主机离线,如果均离线还设计了一键上线的功能,捕捉了3号信号。

8.扩展

1.基于注册和登录的录题功能

2.业务扩展,写一个论坛,接入到在线OJ中

3.即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在Docker上

4.目前后端compiler服务使用的是http方式请求(仅仅是因为简单),但是也可以将编译服务设计成为远程过程调用(RPC rest_rpc)替换httplib的内容,可以不做

5.功能上更完善,判断一道题正确后自动下一题

6.navbar中的功能可以一个一个都实现一下

7.其他

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/173940.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java访问控制符/导入2023019

访问控制符&#xff08;定义的时候不加访问控制符&#xff0c;默认的就是default&#xff09;&#xff1a; 1.private&#xff08;当前类访问权限&#xff09;&#xff1a;如果类里的一个成员&#xff08;包括成员变量、方法和构造器等&#xff09;使用private访问控制符来修饰…

Java——数组中第k个最大的元素

题目链接 leetcode在线oj题——数组中第k个最大的元素 题目描述 给定整数数组 nums 和整数 k&#xff0c;请返回数组中第 k 个最大的元素。 请注意&#xff0c;你需要找的是数组排序后的第 k 个最大的元素&#xff0c;而不是第 k 个不同的元素。 你必须设计并实现时间复杂…

Move_base_flex(mbf)框架理解

本文章重点在 第二部分类图解析&#xff0c;第四部分代码解析 文章目录1. move_base_flex主体代码结构树2. move_base_flex 类图解析2.1 ROS2 navigation整体架构2.2 mbf类图主体思路详解2.2.1. 抽象层&#xff08;abstract层&#xff09;2.2.2. 外部信号输入&#xff08;Actio…

【NI Multisim 14.0虚拟仪器设计——放置虚拟仪器仪表(4通道示波器)】

目录 序言 &#x1f34d;放置虚拟仪器仪表 &#x1f349;4通道示波器 1.“时基”选项组 2.“通道”选项组 序言 NI Multisim最突出的特点之一就是用户界面友好。它可以使电路设计者方便、快捷地使用虚拟元器件和仪器、仪表进行电路设计和仿真。 首先启动NI Multisim 14.0…

C语言 通讯录最终版(动态内存+实时保存)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; 前言&#xff1a; 上期通讯录我们实现了动态内存的申请&#xff0c;但数据依然是存放在内存中&#xff0c;当程序退出的时候&#xff0c;通讯录中的数据自然就不存在了&#xff0c;等下次运行通讯录程序的时候…

什么是REST和RESTful

REST&#xff08;Representational State Transfer&#xff09;表象化状态转变&#xff08;表述性状态转变&#xff09;&#xff0c;在2000年被提出&#xff0c;基于HTTP、URI、XML、JSON等标准和协议&#xff0c;支持轻量级、跨平台、跨语言的架构设计。是Web服务的一种新的架…

单身福利专场——Python采集某相亲地数据

嗨害大家好鸭&#xff01;我是小熊猫~ 咳咳年前最后一天… 一点单身福利… 我想… 应该会有需要的吧… 环境开发: Python 3.8Pycharm 模块使用: import parselimport requestsimport csvimport re 爬虫基本思路流程: 一. 数据来源分析: 1. 明确需求: 采集数据是什么 —…

Linux基本功系列之ping命令实战

文章目录一. 命令介绍二. 语法格式及常用选项三. 参考案例3.1 测试本机与指定网站服务器之间的网络连通性3.2 指定ping的次数3.3 指定时间间隔和次数3.4 设置TTL为2553.5 极快速的测试使用大包ping四. 使用ping命令常见问题总结前言&#x1f680;&#x1f680;&#x1f680; 想…

Java项目部署到云服务器的思路

Java项目部署到云服务器的思路 1 部署项目的前提条件 1.1 购买云服务器 我购买的是腾讯云的服务器,第一年享优惠88一年 cpu好像两核的,作为入门级的也算够用了 如果第二年该续费的时候,我记得因为收到备案什么因素的影响,要提前三个月就续费了,第二年续费价格应该是510 对于学…

思科与华为设备中的OSFP配置命令以及部分实例(超详细~~!!)

目录 一、OSPF相关配置命令 1.思科设备配置命令 &#xff08;1&#xff09;启动OSPF路由进程 &#xff08;2&#xff09;激活参与OSPF路由协议的接口&#xff0c;并且通告结构属于哪个区域的OSPF &#xff08;3&#xff09;配置路由器ID &#xff08;4&#xff09;配置被动…

苹果再次舍弃3纳米,对ASML是沉重打击,ASML得靠中国救命了

苹果在昨晚发布了新款M2 Pro和M2 Max芯片&#xff0c;这两款芯片都没有采用台积电的3纳米工艺&#xff0c;其实不仅是对台积电的打击&#xff0c;也是对ASML的打击&#xff0c;意味着ASML更先进的第二代EUV光刻机可能面临着没有太大需求的问题。一、ASML的愿望ASML当前的主要利…

day43|● 1049. 最后一块石头的重量 II ● 494. 目标和 ● 474.一和零

1049. 最后一块石头的重量 II 1.代码 class Solution { public:int lastStoneWeightII(vector<int>& stones) {int sum 0;for(int i: stones) {sum i;}int t sum;sum sum /2;vector<int>f(sum 1);for (int i 0; i < stones.size(); i) {for (int j …

怎样防止数据怎么泄露了

近年来&#xff0c;各种数据泄露事件越演越烈&#xff0c;数据泄密日益成为企业管理者的梦魇。数据泄密不仅给企业带来严重的直接经济损失&#xff0c;而且还在品牌价值、投资人关系、社会公众形象等多方面造成损害。因此&#xff0c;要想提升企业数据的安全性&#xff0c;就要…

容器虚拟化技术Docker(二)mysql主从配置案例、redis集群搭建及扩容、缩容案例详解

容器虚拟化技术Docker&#xff08;二&#xff09;mysql主从配置案例、redis集群搭建及扩容缩容案例详解 对docker不熟悉的可以参考&#xff1a; 容器虚拟化技术Docker&#xff08;一&#xff09;简介、安装、常见命令、数据卷、安装常规软件 1、Docker安装mysql主从复制 &am…

2023年微软发布的第一个补丁都有什么?

微软于 10 日发布了 2023 年的第一个更新&#xff0c;修复了其Windows操作系统和其他软件中的近 100 个安全漏洞。 2023 年第一个补丁星期二的亮点包括&#xff1a;Windows 中的零日漏洞、美国国家安全局报告的打印机软件缺陷&#xff0c;以及允许未经身份验证的远程攻击者建立…

建木CI自定义节点说明

数据导出导入可以看下 https://gitee.com/jianmu-runners/jianmu-runner-mysq建木节点基于docker 镜像执行 1. 安装 docker & docker-compose 参考 https://blog.csdn.net/pushiqiang/article/details/78682323 https://docs.docker.com/compose/install/other/2.安装&a…

JDK安装与环境变量配置

经验 先使用LightlyLightlyLightly自带的JDKJDKJDK编辑器&#xff0c;进行运行代码&#xff0c;后续学习深入后&#xff0c;再继续探讨JDK的安装&#xff0c;开发大型项目工程时候会自己使用都行啦的回事与打算。 附带JDK安装链接 安装链接: JDK安装链接 总结 慢慢的将JDKJ…

9、Ubuntu安装Tomcat配置部署web

1、安装 Java 这里使用Ubuntu22.04 其他版本也差不多 tomcat是基于Java开发的&#xff0c;安装前需确保已经按照了jdk apt update apt install openjdk-8-jdk 验证是否按照成功 java -version 环境变量 2、安装 Tomcat 下载链接 Tomcat: Apache Tomcat - Apache Tomcat …

Python备份网络设备配置实验-ensp

一、实验简介 实验拓扑 简介 本实验为物理主机(windows电脑)通过物理主机的环回口虚拟网卡与ENSP模拟器中的Cloud建立通信,再与Cloud下面连接的虚拟网络设备通信。从而实现物理主机上面的python脚本抓取ENSP中网络设备的配置信息,并保存在物理主机磁盘指定位置。 本实验…

零入门容器云实战之测试环境介绍、网盘、联系方式

本篇文章主要介绍: 对测试环境的要求介绍一下我的测试环境都有哪些设备&#xff0c;分享一下我在学习中是如何测试的。 1、网盘资源 我已经将相关资源上传到了网盘 零入门容器云网络实战 链接: https://pan.baidu.com/s/1nPLRkAwjItAHmtEU2T1F4g 提取码: rrpd 2、技术交流…