【项目】负载均衡OnlineJudge

在这里插入图片描述


所用技术与开发环境

  • 所用技术

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

    • Centos 7 云服务器
    • vscode
    • Mysql Workbench

1. 项目宏观结构

  • 项目核心
  1. comm :公共模块
  2. compile_server:编译与运行模块
  3. oj_server :获取题目列表,查看题目,编写界面,负载均衡

实现类似于常见oj网站中的:题目列表+在线编程+判题 功能。

在这里插入图片描述

  • 编写思路
  1. 编写 compile_server
  2. oj_server
  3. 基于文件版的在线OJ
  4. 前端设计
  5. 基于MySQL的在线OJ

2. compile_server 模块设计

服务需求:将用户提交的代码暂存在本地进行编译运行,得到格式化的结果返回。

在这里插入图片描述

编译模块 —— compile.hpp

在这里插入图片描述

compile 函数代码

#pragma once 
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include "../comm/tool.hpp"
#include "../comm/log.hpp"
//只负责进行代码的编译

namespace ns_compiler
{
    //引入路径拼接功能
    using namespace ns_tool;
    //引入日志
    using namespace ns_log;
    class Compiler
    {
    public:
        Compiler()
        {}
        ~Compiler()
        {}

        //返回值:编译成功:true,否则:false
        //输入参数file_name:用户编译的代码形成的临时文件
        //file_name:1234
        //不同的文件需要不同的后缀名,使用路径拼接工具集
        //1234 -> ./temp/1234.cpp
        //1234 -> ./temp/1234.exe
        //1234 -> ./temp/1234.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);
                //错误收集文件(收录编译出错的信息)
                int _stderr=open(PathTool::CompileError(file_name).c_str(),O_CREAT|O_WRONLY,0644);
                if(_stderr<0)
                {
                    LOG(WARNING)<<"没有成功形成stderr文件"<<"\n";
                    exit(1);
                }
                //重定向:编译的标准错误信息 输出重定向到_stderr
                dup2(_stderr,2);


                //调用编译器,完成对用户提交代码的编译工作
                // g++ -o target src -std=c++11
                //对file_name 进行路径名和后缀名拼接,方便程序替换
                execlp("g++","g++","-o",PathTool::Exe(file_name).c_str(),\
                PathTool::Src(file_name).c_str(),"-std=c++11","-D","COMPILER_ONLINE"/*编译时定义该宏*/,nullptr/*nullptr结束不能遗漏*/);
                LOG(ERROR)<<"启动编译器g++失败,可能参数出错"<<"\n";
                //进程替换失败,直接终止
                exit(2);
            }
            else
            {
                waitpid(pid,nullptr,0);
                //用户代码是否能编译成功,就看有没有生成可执行程序
                if(FileTool::IsFileExists(PathTool::Exe(file_name)))
                {
                    LOG(INFO)<<PathTool::Exe(file_name)<<" 编译成功"<<"\n";
                    return true;
                }
            }
            LOG(ERROR)<<"编译失败,没有形成可执行程序"<<"\n";
            return false;
        }
    };
}

其中的构建路径函数Src,Exe,Stderr,查看文件是否存在函数IsFileExists日志函数LOG等,都定义在 comm目录下的 tool.hpplog.hpp中:

在这里插入图片描述

  • tool.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
namespace ns_tool
{
    const std::string temp_path="./temp/";

    /*-------------------------路径拼接工具集-----------------------*/
    class PathTool
    {
    //传入的参数file_name 只有文件名(无路径以及后缀)
    //为其添加路径名+后缀名
    public:

        //拼接函数
        static std::string AddSuffix(const std::string& file_name,const std::string& suffix)
        {
            std::string path_name=temp_path;
            path_name+=file_name;
            path_name+=suffix;
            return std::move(path_name);
        }  

        /*-----编译时需要的临时文件-----*/
        //构建 源文件路径+后缀 的完整文件名
        //123 -> ./temp/123.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 CompileError(const std::string& file_name)
        {
            return  AddSuffix(file_name,".compile_error");
        }
    };

    /*-------------------------文件管理工具集-----------------------*/
    class FileTool
    {
    public:

        //查看文件是否存在
        static bool IsFileExists(const std::string& path_name)
        {
            struct stat buf;
            //文件属性,文件存在返回0,如果文件不存在返回-1
            if(stat(path_name.c_str(),&buf)==0)
            {
                //文件存在
                return true;
            }
            else 
            {
                return false;
            }
        }
    };

    /*-------------------------时间戳工具集-----------------------*/
    class TimeTool
    {
    public:
        //获取时间戳
        static std::string GetTimeStamp()
        {
            struct timeval _time;
            gettimeofday(&_time,nullptr);
            return std::to_string(_time.tv_sec);
        }
    };
} // namespace ns_tool
  • log.hpp
#pragma once 
#include <iostream>
#include <string>
#include "tool.hpp"
namespace ns_log
{
    using namespace ns_tool;
    //日志等级
    enum{
        INFO,
        DEBUG,
        WARNING,
        ERROR,
        FATAL
    };

    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+=TimeTool::GetTimeStamp();//时间戳函数,在tool.hpp中
        message+="]";
        std::cout<<message;//不要endl刷新缓冲区
        return std::cout;
    }

    //#号其后面的宏参数进行字符串化操作
    //LOG(INFO)<<"message"  
    //开放式日志
    #define LOG(level) Log(#level, __FILE__, __LINE__)
} // namespace ns_log

测试compile函数

到目前为止我们的思路如下

在这里插入图片描述

为了测试上述代码是否能正常运转,我预先在temp目录中存放了一段测试代码:

在这里插入图片描述

  • code.cpp
#include <iostream>
using namespace std;

int main()
{
    cout<<"hello world"<<endl;
    return 0;
}
  • compile_server.cc
#include "compiler.hpp"

using namespace ns_compiler;

int main()
{
    std::string code="code";
    Compiler::Compile(code);//编译用户代码
  
    return 0;
}

编译compile_server.cc,运行后查看temp目录,生成了可执行文件

运行code.exe

在这里插入图片描述

如果我们让code.cc中故意出点编译错误,随后重新编译compile_server.cc(注意提前先将temp目录下除code.cpp以外的文件删除)

  • code.cc
#include <iostream>

using namespace std;

int main()
{
    hello
    cout<<"hello world"<<endl;
    return 0;
}

此时没有生成可执行文件,错误信息也重定向到错误收录文件中:

在这里插入图片描述

运行模块 —— runner

思路框架:

在这里插入图片描述

针对运行模块我们需要判定的是,程序是否能正常运行,

  • 代码跑完:
    • 结果正确 -> 结果保存至temp下的 .stdout文件
    • 结果错误 -> 结果保存至temp下的 .stderr文件
  • 代码异常
    • 捕捉异常返回的信号编号。

设定用户代码使用资源的权限

限定用户代码的使用时间和空间,防止过多占用CPU的资源。

使用如下函数限制:

#include<sys/time.h>
#include<sys/resource.h>
int setrlimit(int resource, const struct rlimit *rlim);

我们的resource参数可以选择:

  • RLIMIT_AS :设置占用虚拟内存大小
  • RLIMIT_CPU:设置占用CPU时间

结构体rlimit设置资源上限

//test.cc
#include <iostream>
#include<sys/time.h>
#include<sys/resource.h>

int main()
{
    //限制累计时长
    struct rlimit r;
    r.rlim_cur=1;//软上限(不能超过硬上限)
    r.rlim_max=RLIM_INFINITY;//硬上限设置为无穷
    setrlimit(RLIMIT_CPU,&r);//设置资源约束为1s
    while(1);
    return 0;
}

在这里插入图片描述

//test.cc
#include <iostream>
#include<sys/time.h>
#include<sys/resource.h>
#include <unistd.h>
int main()
{
    //限制内存
    struct rlimit r;
    r.rlim_cur=1024*1024*40;
    r.rlim_max=RLIM_INFINITY;
    setrlimit(RLIMIT_AS,&r);
    int count=0;
    while(true)
    {
        int* p=new int[1024*1024];
        count++;
        std::cout<<"size: "<<count<<std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

该函数是通过信号来终止程序的,为了验证是通过哪个信号,我们写以下代码来查看:

  • 限制内存时,终止程序的信号捕捉
#include <iostream>
#include<sys/time.h>
#include<sys/resource.h>
#include <unistd.h>
#include <signal.h>

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

int main()
{

    for(int i=1;i<=31;++i)
    {
        signal(i,handler);
    }

    //限制内存
    struct rlimit r;
    r.rlim_cur=1024*1024*40;
    r.rlim_max=RLIM_INFINITY;
    setrlimit(RLIMIT_AS,&r);
    int count=0;
    while(true)
    {
        int* p=new int[1024*1024];
        count++;
        std::cout<<"size: "<<count<<std::endl;
        sleep(1);
    }
    return 0;
}

是6号信号:SIGABRT

在这里插入图片描述

  • 限制时间时,终止程序的信号捕捉
#include <iostream>
#include<sys/time.h>
#include<sys/resource.h>
#include <unistd.h>
#include <signal.h>

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

int main()
{
    for(int i=1;i<=31;++i)
    {
        signal(i,handler);
    }
    //限制累计时长
    struct rlimit r;
    r.rlim_cur=1;//软上限(不能超过硬上限)
    r.rlim_max=RLIM_INFINITY;//硬上限设置为无穷
    setrlimit(RLIMIT_CPU,&r);//设置资源约束为1s
    while(1);
    return 0;
}

CPU使用超时终止程序的信号是:24号信号(SIG)

在这里插入图片描述

Run函数代码

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

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

namespace ns_runner
{
    using namespace ns_tool;
    using namespace ns_log;
    class Runner
    {
    public:
        Runner()
        {}

        ~Runner()
        {}
    public:
        //控制进程占用资源的接口
        static void SetProcLimit(int _cpu_limit,int _mem_limit)
        {   
            //占用CPU时间上限设置
            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);
        }
        //指明文件名即可,不需要带路径与后缀,使用PathTool工具即可完成名称拼接
        /******************************
         * 返回值
         * >0 :程序异常了,退出时收到的信号,返回值就是信号编号
         * =0 :程序正常运行完毕,结果保存到了临时文件中
         * <0 :内部错误
         * 
         * cpu_limit:该程序运行时,CPU使用时间上限(S)
         * mem_limit:该程序运行时,内存使用上线(KB)
         * ****************************/
        static int Run(const std::string& file_name,int cpu_limit,int mem_limit)
        {
            /*****************************************
            * 程序运行的三种结果
            * 1.代码跑完,结果正确
            * 2.代码跑完,结果不正确
            * 3.代码异常,没有跑完
            * Run() 需要考虑代码跑完后,结果的正确与否吗?不考虑
            * 结果正确与否是由测试用例决定的
            * Run() 只考虑是否运行完毕
            * 
            * 必须知道可执行程序是谁?
            * 
            * 一个程序启动时的
            * 标准输入:不让用户自输入测试用例,全部使用平台的用例
            * 标准输出:程序运行完成,输出结果
            * 标准错误:运行时错误信息
            *****************************************/
            std::string _execute=PathTool::Exe(file_name); 
            //程序的输入文件
            std::string _stdin=PathTool::Stdin(file_name);
            //程序的输出文件
            std::string _stdout=PathTool::Stdout(file_name);
            //程序的错误文件
            std::string _stderr=PathTool::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);

                //设置进程的资源限制(运行市场+空间占用)
                SetProcLimit(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=0;//获取退出异常的信息
                waitpid(pid,&status,0);
                //程序运行异常可以通过退出信号进行判别
                LOG(INFO)<<"运行完毕,info:"<<(status & 0x7F)<<"\n";
                return status & 0x7F;
            }
        }
    };
}

测试 Run 函数

  • compile_server.cc
#include "compiler.hpp"
#include "runner.hpp"
using namespace ns_compiler;
using namespace ns_runner;
int main()
{
    std::string code="code";
    Compiler::Compile(code);
    Runner::Run(code,1,60);
  
    return 0;
}
  • code.cpp
#include <iostream>
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    return 0;
}

编译运行 compile_server.cc

在这里插入图片描述

在code.cpp出点错误,

    cerr<<"hello error"<<endl;

在运行后看下 code.stderr 文件

在这里插入图片描述

标准错误重定向至文件中。

集成 编译+运行模块 —— compile_run

  • 思路

    • 适配用户请求,定制通信协议字段。
    • 正确的调用 compile和runner模块。
    • 形成唯一文件名,否则多用户间会互相影响。

在这里插入图片描述

安装 json库

$ sudo yum install -y jsoncpp-devel

compile_run 代码

#pragma once 

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

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

namespace ns_compile_and_run
{
    using namespace ns_compiler;
    using namespace ns_runner;
    using namespace ns_log;
    using namespace ns_tool;

    class CompileAndRun
    {
    public:

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

            //获取用户输入
            std::string code=in_value["code"].asString();
            std::string input=in_value["input"].asString();
            int cpu_limit=in_value["cpu_limit"].asInt();
            int mem_limit=in_value["mem_limit"].asInt();

            int status_code=0;//程序编译运行中的状态码
            int run_result=0;//运行的返回值
            std::string file_name;//需要内部形成的唯一文件名
            if(code.size()==0)
            {
                //用户提交代码量为0
                //差错处理
                status_code=-1;

                goto END;
            }

            //形成唯一文件名(没有路径没有后缀)
            file_name=FileTool::UniqueFileName();
            //将用户的code写进file_name
            //在temp目录下生成临时源文件
            if(!FileTool::WriteFile(PathTool::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)//内部错误:fork失败,打开文件失败
            {
                status_code=-2;
            }
            else if(run_result>0)
            {
                status_code=run_result;//程序异常被信号终止,run_result记录了是何信号
            }
            else
            {
                //运行成功
                status_code=0;
            }
  
            //注意:goto语句和跳转目的地之间不能定义变量
            END:
            //status_code状态识别并作出序列化
            out_value["status"]=status_code;
            out_value["reason"]=CodeToDesc(status_code,file_name);
            if(status_code==0)
            {
                //整个编译+运行过程全部顺利完成,此时需要读取运行结果
                std::string _stdout;
                FileTool::ReadFile(PathTool::Stdout(file_name),&_stdout,true);
                out_value["stdout"]=_stdout;

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

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


        //status_code状态解释
        //>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 = "代码编译的时候发生了错误";
                    FileTool::ReadFile(PathTool::CompileError(file_name),&desc,true);
                    break;
                case SIGABRT:   //6
                    desc="内存超限";
                    break;
                case SIGXCPU:   //24
                    desc="CPU超时";
                    break;
                case SIGFPE:    //8
                    desc="浮点数溢出";
                    break;
                default:        //其他异常信号
                    desc="signal:"+std::to_string(code);
                    break;
            }
            return desc;
        }

        //清理临时文件
        static void RemoveTempFile(std::string& file_name)
        {
            //清理文件的个数是不确定的
            //可以确定的是:
            std::string _src=PathTool::Src(file_name);
            if(FileTool::IsFileExists(_src)) unlink(_src.c_str());

            std::string _compile_error=PathTool::CompileError(file_name);
            if(FileTool::IsFileExists(_compile_error)) unlink(_compile_error.c_str());

            std::string _execute=PathTool::Exe(file_name);
            if(FileTool::IsFileExists(_execute)) unlink(_execute.c_str());

            std::string _stdin=PathTool::Stdin(file_name);
            if(FileTool::IsFileExists(_stdin)) unlink(_stdin.c_str());

            std::string _stdout=PathTool::Stdout(file_name);
            if(FileTool::IsFileExists(_stdout)) unlink(_stdout.c_str());

            std::string _stderr=PathTool::Stderr(file_name);
            if(FileTool::IsFileExists(_stderr)) unlink(_stderr.c_str());
        }
    };

} // namespace ns_compile_and_run   

测试

测试代码如下:

  • compile_server.cc
#include "compile_run.hpp"

using namespace ns_compile_and_run;

int main()
{
    //通过HTTP 让client 给我们上传一个json string

    // in_json:{"code":"#include....", "input":"","cpu_limit":1, "mem_limit":10240}
    //out_json={"status":"0","reason":"","stdout":"","stderr":""}

    //下面的工作是充当客户端请求的json串
    std::string in_json;
    Json::Value in_value;
    //R"()" raw string 括号内包含的所有特殊字符都视为原貌(自动帮我们做转义)
    in_value["code"]=R"(#include <iostream>
    using namespace std;
    int main()
    {
        cout<<"hello world"<<endl;
        return 0;
    })";
    in_value["input"]="";
    in_value["cpu_limit"]=1;
    in_value["mem_limit"]=1024*10*3;

    Json::FastWriter writer;
    in_json=writer.write(in_value);
    std::cout<<in_json<<std::endl;
    //将来给客户端返回的json串
    std::string out_json;
  
    CompileAndRun::Start(in_json,&out_json);
    std::cout<<out_json<<std::endl;

    return 0;
}

从结果可以看到,传给用户的json串具备了所有我们想要传输的属性:

在这里插入图片描述

此时,我们装作用户来传输一些有错误的代码,如:

in_value["code"]=R"(#include <iostream>
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    while(1);//设置死循环,触发cpu超时
    return 0;
})";

在这里插入图片描述

我们可以设置一些编译错误,如:

in_value["code"]=R"(#include <iostream>
using namespace std;
int main()
{
    cout<<"hello world"<<endl;
    this is a wrong sentence!
    return 0;
})";

在这里插入图片描述

形成网络服务

下载cpp-httplib

头文件httplib.h 复制到 comm 目录下

使用cpp-httplib

//test.cc
#include "../comm/httplib.h"
using namespace httplib;

int main()
{
    Server svr;

    //当对方请求资源为"/hello",捕捉请求报文req,并返回rsp
    svr.Get("/hello",[](const Request& req,Response& rsp){
        //设置响应正文
        rsp.set_content("hello httplib!","content_type: text/plain");
    });

    svr.listen("0.0.0.0",8081);
}
g++ test.cc -std=c++11 -lpthread

登陆网页(ip+端口号+资源)可得:

在这里插入图片描述

于是我们的compile_server 代码如下:

#include "compile_run.hpp"
#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[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        return 1;
    }

    //形成网络服务,通过HTTP,让client 给我们上传一个json string
    //cpp-httplib
   
    Server svr;
 
    //当对方请求资源为"/hello",捕捉请求报文req,并返回rsp
    // svr.Get("/hello",[](const Request& req,Response& rsp){
    //     //设置响应正文
    //     rsp.set_content("hello httplib!","text/plain;charset=utf8");
    // });

    svr.Post("/compile_and_run",[](const Request& req,Response& rsp){
        //用户请求的正文就是我们想要的json string
        std::string in_json=req.body;
        std::string out_json;
        if(!in_json.empty())
        {
            CompileAndRun::Start(in_json,&out_json);
            //std::cout<<out_json<<std::endl;
            rsp.set_content(out_json,"application/json;charset=utf-8");
        } 
    });

    //svr.set_base_dir("./wwwroot"); 
    svr.listen("0.0.0.0",atoi(argv[1]));
  
    return 0;
}


我们的客户端还没有编码,为了方便测试,使用 postman模仿客户发出json字符串。

我们可以搜索postman官网,在本地下载安装 postman软件,之后启动并输入json字符串:

在这里插入图片描述

在这里插入图片描述

可以看到我们的输出json字符符串返回给了用户。

2.5 .设计文件版题库

题库我们存放在 oj_server 模块的目录下,oj_server 模块会在下一章介绍。

一道题目需要的元素

  1. 题目的编号 —— number
  2. 题目的标题 —— title
  3. 题目的难度 —— star
  4. 题目的内容 —— desc
  5. 题目的预设代码 —— header
  6. 题目的测试用例 —— tail
  7. 题目的时间要求(内部处理) —— cpu_limit
  8. 题目的空间要求(内部处理) —— mem_limit

而这些元素将由两类文件构成

  1. 第一类 questions.list :题目列表(编号+标题+难度+时空要求)。
  2. 第二类 题目详情:题目内容(desc.txt)+题目预设代码(header.cpp)+测试用例(tail.cpp)

上面的两类文件是通过题目的编号关联起来的。

建立题库的文件夹 questions 如下:

在这里插入图片描述

  • question.list 题目列表
//##questions.list: 题目编号 题目标题 题目难度 题目所在路径 题目时间限制(s) 题目空间限制(kb)##
1 回文数 简单 ./oj_questions/1 1 5000
  • desc.txt 题目描述
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

示例 1:
输入: 121
输出: true

示例 2:
输入: -121
输出: false
解释: 从左向右读,-121 。 从右向左读,121- 。因此它不是一个回文数。

示例 3:
输入: 10
输出: false
解释: 从右向左读,01 。因此它不是一个回文数。

进阶:
你能不将整数转为字符串来解决这个问题吗?
  • head.cpp 预设代码
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution {
public:
    bool isPalindrome(int x) 
    {
        //请将你的代码写在这里

        return true;
    }
};
  • tail.cpp 测试用例
#ifndef CompileOnline
// 这是为了编写用例的时候有语法提示. 实际线上编译的过程中这个操作是不生效的.
#include "header.cpp"
#endif
/
// 此处约定:
// 1. 每个用例是一个函数
// 2. 每个用例从标准输出输出一行日志
// 3. 如果用例通过, 统一打印 [TestName] ok!
// 4. 如果用例不通过, 统一打印 [TestName] failed! 并且给出合适的提示.
///
void Test1() 
{
    bool ret = Solution().isPalindrome(121);
    if (ret) 
    {
        std::cout << "Test1 ok!" << std::endl;
    } 
    else 
    {
        std::cout << "Test1 failed! input: 121, output expected true, actual false" <<
        std::endl;
    }
} 
void Test2() 
{
    bool ret = Solution().isPalindrome(-10);
    if (!ret) 
    {
        std::cout << "Test2 ok!" << std::endl;
    }
    else 
    {
        std::cout << "Test2 failed! input: -10, output expected false, actual true" <<std::endl;
}
} 
int main() 
{
    Test1();
    Test2();
    return 0;
}

当用户编写完代码并提交后需要把 head.cpp 和 tail.cpp 拼接起来传向后端。

3. oj_server 模块设计(基于MVC结构)

本质:建立网站

  1. 获取首页,采用题目列表
  2. 编辑区域
  3. 提交判题功能(编译+运行)

MVC设计模式

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

在这里插入图片描述

于是我们的oj_server 模块有如下文件:

在这里插入图片描述

用户请求服务的路由功能

路由分为3个板块:

  • 获取所有的题目列表
  • 根据题目编号,获取题目内容
  • 用户提交代码,使用我们上面完成的判题功能(1.测试用例 2. compile+run)

于是 oj_server.cc的代码框架暂定如下:

#include <iostream>
#include "../comm/httplib.h"
using namespace httplib;

int main()
{
    //用户请求的路由功能
    Server svr;
    //获取所有的题目列表
    svr.Get("/all_questions",[](const Request& req,Response& rsp){
        rsp.set_content("题目列表","text/plain;charset=utf-8");
    });
  
    //根据题目编号获取题目内容
    // /question/100 -> 正则匹配 \d数字为,+表示多个数字
    // R"()" ,原始字符串(不识别\),保持字符串原貌,不做相关的转义
    svr.Get(R"(/question/(\d+))",[](const Request& req,Response& rsp){
        std::string number=req.matches[1];//获取url中的题号
        rsp.set_content("这时指定的题目:"+number,"text/plain;charset=utf-8");
    });


    //用户提交代码,使用判题功能(1.每道题的测试用例 2. compile_and_run)
    svr.Get(R"(/judge/(\d+))",[](const Request& req,Response& rsp){
        std::string number=req.matches[1];//获取url中的题号
        rsp.set_content("这时指定判题的题目:"+number,"text/plain;charset=utf-8");
    });
  
    //设置首页
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0",8080);

    return 0;
}

M —— model 模块 oj_model.hpp

该模块用于与数据交互,对外提供访问题库的接口。

三个功能:

  • 加载题库
  • 提供所有题目
  • 提供一道题目

在这里插入图片描述

  • oj_model.hpp 代码
#pragma once

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

#include <iostream>
#include <string>
#include <fstream>
#include <assert.h>
#include <unordered_map>
#include <vector>
// 根据 question_list 文件,将所有题目信息加载到内存中
//model:主要用于和数据交互,对外提供访问题库的接口

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

    const string questions_list="./questions/questions.list";
    const string questions_path="./questions/";
    class Model
    {
    private:
        //建立 题号:题目细节 的映射关系
        unordered_map<string,Question> questions;

    public:
        Model()
        {
            assert(LoadQuestionList(questions_list));
        }

        //加载配置文件:/questions/questions_list+题目编号文件
        bool LoadQuestionList(const string& questions_list)
        {
            //打开文件列表文件
            ifstream in(questions_list);
            if(!in.is_open())
            {
                LOG(FATAL)<<"加载题库失败,请检查是否存在题库文件"<<"\n";
                return false;
            }

            //按行读取
            string line;
            while(getline(in,line))
            {
                //切分字符串(按空格划分)
                vector<string> tokens;
                StringTool::SplitString(line,&tokens," ");
                //1 判断回文数 简单 1 30000
                if(tokens.size()!=5)
                {
                    LOG(WARNING)<<"加载部分题目失败,请检查文件格式"<<"\n";
                    continue;
                }
        
                Question q;
                q.number=tokens[0];
                q.title=tokens[1];
                q.star=tokens[2];
                q.cpu_limit=stoi(tokens[3]);
                q.mem_limit=stoi(tokens[4]);
                //题目所在路径
                string path=questions_path;
                path+=q.number;
                path+="/";
        
                FileTool::ReadFile(path+"desc.txt",&(q.desc),true);
                FileTool::ReadFile(path+"header.cpp",&(q.header),true);
                FileTool::ReadFile(path+"tail.cpp",&(q.tail),true);

                questions.insert({q.number,std::move(q)});
            }
            LOG(INFO)<<"加载题库成功......."<<"\n";
            in.close(); 
        }

        //获取所有题目的接口
        bool GetAllQuestions(vector<Question>* out) 
        {
            if(questions.size()==0)
            {
                LOG(ERROR)<<"用户获取题库失败"<<"\n";
                return false;
            }

            for(const auto& q:questions)
            {
                out->push_back(q.second);//first:key second:value
            }
            return true;
        }

        //获取一个题目
        bool GetOneQuestion(const 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()
        {}
    };
}

其中切分字符串函数,放在 tool.hpp 中,首先这里借助了boos库的split函数。

终端下载boost库

$ sudo yum install -y boost-devel

如何使用split函数

#include <iostream>
#include <string>
#include <vector>
#include <boost/algorithm/string.hpp>
using namespace std;

int main()
{
    vector<string> tokens1;
    vector<string> tokens2;
    string str="1:判断回文数;简单 1:::30000";
    string sep=" :;";
    boost::split(tokens1,str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
    boost::split(tokens2,str,boost::is_any_of(sep),boost::algorithm::token_compress_off);

    cout<<"print tokens1"<<endl;
    for(auto& e:tokens1)
    {
        std::cout<<e<<std::endl;
    }
    cout<<"---------------------"<<endl;

    cout<<"print tokens2"<<endl;
    for(auto& e:tokens2)
    {
        std::cout<<e<<std::endl;
    }
}

查看输出结果,可以看到split的最后一个参数是选择是否压缩相邻的分隔符:

在这里插入图片描述

于是我们分隔字符串的函数 StringSplit如下:

//tool.hpp
//...
namespace ns_tool
{
     /*-------------------------------------------------字符串处理工具集--------------------------------------------------*/
    class StringTool
    {
    public:
        //切分字符串
        static void SplitString(const std::string& str,std::vector<std::string> *words,const std::string& sep)
        {
            //boost split 
            boost::split(*words,str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
        }
    };
}

v —— view 模块

view模块的整体框架为:通过model获取题目列表或者指定题号的题目信息,随后将这些信息渲染至网页,随后提交给 control 模块。

在这里插入图片描述

我们在 oj_server 目录下添加这两个网页

在这里插入图片描述

ctemplate 数据渲染至网页第三方库

需要使用 ctemplate 第三方库

# 国内github镜像网站
https://hub.fastgit.xyz/OlafvdSpek/ctemplate

$ git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git
$ ./autogen.sh
$ ./configure
$ make //编译
$ sudo make install //安装到系统中

这里我直接将文件安装在了目录 thirdpart

在这里插入图片描述

先学会使用 ctemplate

//test.cc
#include <iostream>
#include <string>
#include <ctemplate/template.h>
using namespace std;

int main()
{
    string in_html="./test.html";
    string value="你好世界";

    //形成数据字典
    ctemplate::TemplateDictionary root("test"); // 类比 unordered_map<string,string> test;
    root.SetValue("key1",value);//  类比 test.insert({key,value})

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

    //添加字典数据到网页中,形成新的网页 out_html
    string out_html;
    tpl->Expand(&out_html,&root);

    //完成了渲染
    cout<<out_html<<endl;
    return 0;
}

其中我们的测试网页 test.html为:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用于测试</title>
</head>
<body>
    <!-- 双花括号里的内容就是会被替换的key值 -->
    <p>{{key1}}</p>
    <p>{{key2}}</p>
    <p>{{key3}}</p>
    <p>{{key4}}</p>
    <p>{{key5}}</p>

</body>
</html>

我们对test.cc文件进行编译并运行:

在这里插入图片描述

可以看到原来的key值得地方被替换成了value值。

之后,就可以把该渲染功能制作成view功能,即数据传给view后即可生成网页。

渲染题目列表网页

首先需要制作获取所有题目列表的网页 all_questions.html

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

这里只是初步的演示与测试,后面会优化网页代码,

随后oj_view.hpp中的题目列表数据渲染网页的 allExpandHtml 函数如下:

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

#include "oj_model.hpp"

namespace ns_view
{
    using namespace ns_model;

    //准备渲染的网页目录
    const std::string template_path="./template_html/";

    class View
    {
    public:
        View()
        {}

        ~View()
        {}
    public:
        void AllExpandHtml(const vector<Question>& questions,std::string *html)
        {
            //题目的编号+标题+难度
            //使用表格显示
            //1.形成路径
            std::string src_html=template_path+"all_questions.html";
            //2.形成数据字典
            ctemplate::TemplateDictionary root("all_questions");
            for(const auto& q:questions)
            {
                ctemplate::TemplateDictionary* sub=root.AddSectionDictionary("question_list");
                sub->SetValue("number",q.number);
                sub->SetValue("title",q.title);
                sub->SetValue("star",q.star);
            }

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

            //4.开始进行渲染
            tpl->Expand(html,&root);
        }

        void OneExpandHtml(const Question& q,std::string *html)
        {
            //待定
        }
  
    }; 
}

为了便于演示,我们建立了首页 index.html,并在其中添加了跳转至题目列表的链接:

  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>oj系统</title>
</head>
<body>
    <h1>首页</h1>
    <p>这是我开发的在线oj平台</p>
    <a href="./all_questions">点开题库</a>
</body>
</html>

然后我们需要修改下 oj_server.cc 中的 “获取所有的题目列表” 的部分代码,将网页资源传给用户

#include <iostream>
#include "../comm/httplib.h"
#include "oj_control.hpp"

using namespace httplib;
using namespace ns_control;

int main()
{
    //用户请求的路由功能
    Server svr;
    Control ctrl;
    //获取所有的题目列表
    svr.Get("/all_questions",[&ctrl](const Request& req,Response& rsp){
        //返回一张包含所有题目的html网页
        std::string html;
        //control 模块
        ctrl.AllQuestions(&html);
        rsp.set_content(html,"text/html;charset=utf-8");//返回用户请求的网页
    });

    //其余功能后续完善
    //....

    //设置首页
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0",8080);

    return 0;
}

make oj_server.cc 后并运行,然后打开网页:

在这里插入图片描述

这部分代码成功返回了题目列表的网页。

渲染指定题目网页

我们指定题目的网页模板如下:

  • one_question.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{number}}.{{title}}</title>
</head>
<body>
    <h4>{{number}}.{{title}}.{{star}}</h4>
    <!-- 题目描述 -->
    <p>{{desc}}</p>

    <!-- 文本编辑框:预先填上预设的代码 -->
    <textarea name="code" id="" cols="100" rows="50">{{pre_code}}</textarea>

</body>
</html>

当然上面的网页代码后续还会美化。

随后 oj_view.hpp 中指定的题目数据渲染网页的 OneExpandHtml 函数代码如下

//...
void OneExpandHtml(const Question& q,std::string *html)
{
    //1.形成路径
    std::string src_html=template_path+"one_question.html";

    //2.形成数据字典
    ctemplate::TemplateDictionary root("one_question");
    root.SetValue("number",q.number);
    root.SetValue("title",q.title);
    root.SetValue("star",q.star);
    root.SetValue("desc",q.desc);//题目描述
    root.SetValue("pre_code",q.header);//预设代码

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

    //4.开始进行渲染
    tpl->Expand(html,&root);

}

为了能够在题目列表网页中点击题目进入指定的题目网页,我们对 all_questions.html 中的标题加一个跳转链接:

 <!-- 循环 -->
{{#question_list}} 
<tr> 
    <td>{{number}}</td>
    <!-- 点击题目标题可跳转至指定题目网页 -->
    <td><a href="/question/{{number}}">{{title}}</a></td>
    <td>{{star}}</td>
</tr>
{{/question_list}}

随后,我们的 oj_server.cc代码在加入访问指定题目网页后,更新如下:

#include <iostream>
#include "../comm/httplib.h"
#include "oj_control.hpp"

using namespace httplib;
using namespace ns_control;

int main()
{
    //用户请求的路由功能
    Server svr;
    Control ctrl;
    //获取所有的题目列表
    svr.Get("/all_questions",[&ctrl](const Request& req,Response& rsp){
        //返回一张包含所有题目的html网页
        std::string html;
        //control 模块
        ctrl.AllQuestions(&html);
        rsp.set_content(html,"text/html;charset=utf-8");
    });
  

    //根据题目编号获取题目内容
    // /question/100 -> 正则匹配 \d数字为,+表示多个数字
    // R"()" ,原始字符串(不识别\),保持字符串原貌,不做相关的转义
    svr.Get(R"(/question/(\d+))",[&ctrl](const Request& req,Response& rsp){ 
        std::string number=req.matches[1];//获取url中的题号
        string html;
        ctrl.OneQuestion(number,&html);
        rsp.set_content(html,"text/html;charset=utf-8");
    });


    //判题功能待定.....
  
    //设置首页
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0",8080);


    return 0;
}

于是我们对 oj_server.cc 编译运行,并打开网页,

在这里插入图片描述

我们成功获取了指定题目的网页。

至此 view 模块就全部搞定了

C —— control 模块 oj_control.hpp

该模块主要与用户交互,进行逻辑控制。

oj_server.cc 的主要工作是获取用户的http请求,然后通过Control模块,完成各个路由功能的解耦,即对于所有请求处理都交给Control模块完成。

显然整个MVC架构的逻辑便是,oj_server.cc调用control模块,随后 control 调用 model模块(获取题库)和 view 模块(网页展示),将最后的结果交给oj_server.cc。

总结下来,Control模块承接两个服务:

  1. 提供用户所点击的网页:题目列表网页,指定题号的题目网页
  2. 接收用户提交的代码,经过简单处理后提交给后台空闲的主机 compile_server 判题,再将用户代码测试的结果返回给用户(负载均衡)。

于是总体的 Control模块的思路如下图所示:

在这里插入图片描述

oj_control.hpp 代码

该模块的代码如下:


测试

compile_server 可以部署在多台主机上,哪台主机空闲需要由Control模块按照负载均衡策略智能选择,随后将代码测试的工作交予空闲的编译主机。

接下来我们就来测试一次:

启动 oj_server 和 多个 compile_server

在这里插入图片描述

使用 postman 仅将预设代码提交,看到结果(编译失败的原因,状态码)

在这里插入图片描述

4. 前端页面设计

前端三剑客:html+css+js

首页(丐版)

在这里插入图片描述

对标签的样式调整

  1. 选中标签
  2. 设置样式
  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>oj系统</title>
    <style>
        /* 起手式:保证样式设置不受默认影响 */
        *{
            /* 消除网页的默认外边距 */
            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标签的宽度 */
            width: 80px;
            /* 设置导航栏的字体颜色 */
            color: white;
            /* 设置导航栏的字体大小 */
            font-size: large;
            /* 设置文字的高度与导航栏一样的高度 */
            line-height: 50px;
            /* 去除a标签的下划线 */
            text-decoration: none;
            /* 设置a标签的文字居中 */
            text-align: center;
        }
        /* 设置鼠标事件 */
        .container .navbar a:hover{
            background-color: grey;
        }

        .container .navbar .login{
            float:right;
        }
        .container .content{
            /*设置content 标签的宽度*/
            width: 800px;
            /* 整体居中 */
            margin:0px auto;
            /* 设置上边距 */
            margin-top: 200px;
            /* 设置文字居中 */
            text-align:center;
        }
        .container .content .font_{
            /* 设置标签为块级元素,独占一行可以设置高度宽度属性 */
            display:block;
            /* 设置每个文字的上外边距 */
            margin-top: 20px; 
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置字体大小 */

        }
    </style>
</head>
<body>
    <div class="container">
        <!-- 导航栏,功能不实现 -->
        <div class="navbar">
            <a href="#">首页</a>
            <a href="./all_questions">题库</a>
            <a href="#">论坛</a>
            <a href="#">竞赛</a>
            <a href="#">求职</a>
            <a class="login" href="#">登录</a>
        </div>
        <!-- 网页的内容 -->
        <div class="content">
            <h1 class="font_">欢迎来到我的个人OnlineJudge平台</h1>
            <p class="font_">这是我开发的在线oj平台</p>
            <a class="font_" href="./all_questions">点开题库</a>
        </div>
    </div>
</body>
</html>

题目列表页面 all_questions.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线oj_题目列表</title>
    <style>
         /* 起手式:保证样式设置不受默认影响 */
         *{
            /* 消除网页的默认外边距 */
            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标签的宽度 */
            width: 80px;
            /* 设置导航栏的字体颜色 */
            color: white;
            /* 设置导航栏的字体大小 */
            font-size: large;
            /* 设置文字的高度与导航栏一样的高度 */
            line-height: 50px;
            /* 去除a标签的下划线 */
            text-decoration: none;
            /* 设置a标签的文字居中 */
            text-align: center;
        }
        /* 设置鼠标事件 */
        .container .navbar a:hover{
            background-color: grey;
        }

        .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(218, 244, 229);
        }
        .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: #000;
        }
        .container .question_list table .item a:hover{
            color:blue;
            /* font-size: larger; */
            text-decoration: underline;
        }
        .container .footer{
            width: 100%;
            height: 50px;
            text-align: center;
            /* background-color: #ccc; */
            color:#ccc;
            line-height: 50px;
            margin-top: 15px;
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- 导航栏,功能不实现 -->
        <div class="navbar">
            <a href="/">首页</a>
            <a href="./all_questions">题库</a>
            <a href="#">论坛</a>
            <a href="#">竞赛</a>
            <a href="#">求职</a>
            <a class="login" href="#">登录</a>
        </div>
        <div class="question_list"> 
            <h1>OnlineJudge题目列表</h1>
            <table>
                <tr>
                    <th class="item">编号</th>
                    <th class="item">标题</th>
                    <th class="item">难度</th>
                </tr>
                <!-- 循环 -->
                {{#question_list}} 
                <tr> 
                    <td class="item">{{number}}</td>
                    <td class="item"><a href="/question/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/question_list}}
            </table>
        </div>
        <div class="footer">

            <h4>坚持带来改变</h4>
        </div>
    </div>

</body>
</html>

整体制作完后的效果如图:

在这里插入图片描述

想要更多题目可以自行录入:

在这里插入图片描述

指定题目页面 one_question.html

工具:Ace在线编辑器

用户可在其中键入自己的代码,这里不再介绍如何使用,可自行查找教程。

one_question.html的代码如下:

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{number}}.{{title}}</title>
    <!-- 引入ACE 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标签的宽度 */
            width: 80px;
            /* 设置导航栏的字体颜色 */
            color: white;
            /* 设置导航栏的字体大小 */
            font-size: large;
            /* 设置文字的高度与导航栏一样的高度 */
            line-height: 50px;
            /* 去除a标签的下划线 */
            text-decoration: none;
            /* 设置a标签的文字居中 */
            text-align: center;
        }
        /* 设置鼠标事件 */
        .container .navbar a:hover{
            background-color: grey;
        }

        .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: 20px;
        }

        .container .part1 .left_desc pre{
            padding-top: 30px;
            padding-left: 20px;
            font-size: medium;
            font-family: Verdana, Geneva, Tahoma, sans-serif;            
        } 

        .container .part1 .right_code{
            width:50%;
            height: 600px;
            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">
                <!-- ace需要的标签 -->
                <pre id="code" class="ace_editor"><textarea class="ace_textinput">{{pre_code}}</textarea></pre>
            </div>
        </div>

        <!-- 提交并且得到结果并显示 -->
        <div class="part2">
            <div class="result"></div>
            <button class="btn_submit" onclick="submit()">提交代码</button>
        </div>

    </div>


    <script>
        //初始化对象
        editor = ace.edit("code");
        //设置风格和语言(更多风格和语言,请到github上相应目录查看)
        // 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html
        editor.setTheme("ace/theme/monokai");
        editor.session.setMode("ace/mode/c_cpp");
        // 字体大小
        editor.setFontSize(16);
        // 设置默认制表符的大小:
        editor.getSession().setTabSize(4);
        // 设置只读(true时只读,用于展示代码)
        editor.setReadOnly(false);
        // 启用提示菜单
        ace.require("ace/ext/language_tools");
        editor.setOptions({
            enableBasicAutocompletion: true,
            enableSnippets: true,
            enableLiveAutocompletion: true
        });

        function submit(){
            //1. 收集当前页面的有关数据:1.题号 2.代码,采用JQuery来获取html中的内容
            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;//请求的url
            //console.log(judge_url);

            //2.构建json,并通过ajax向后台发起基于json正文格式的http请求
            $.ajax({
                method:'Post',//向后端发起请求的方式
                url:judge_url,//像后端指定url发起请求
                dataType:'json',//告知服务端,我接收数据的格式
                contentType:'application/json;charset=utf-8',//告知服务端,我发出数据的格式
                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_label=$("<p>",{
                    text:_reason
                });
                reason_label.appendTo(result_div);

                if(status==0)
                {
                    //说明编译运行是成功的,结果是否正确需查看stdout文件
                    var _stdout=data.stdout;
                    var _stderr=data.stderr;

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

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

                    stdout_label.appendTo(result_div);
                    stderr_label.appendTo(result_div);
                }
                else{
                    //do nothing
                }
            }
        }
    </script>
</body>

</html> 

该页面制作的重点在于如何让前后端进行沟通

  1. 使用jquery可以帮助我们获得前端用户代码框editor中用户输入的代码;

  2. 构建url:http请求所需的url为 oj_server 提供的judge接口+题号;

  3. 构建json,通过使用ajax向后台发起基于json正文格式的http请求;

  4. 得到编译主机(compile_server)编译运行后的结果json串,反序列化后,可以输出到前端页面上。

JQuery帮助获取前端的用户代码:

在这里插入图片描述

效果如下图所示:

  • 超时测试:

在这里插入图片描述

  • 内存超限测试

在这里插入图片描述

  • 编译错误测试

在这里插入图片描述

关于信号的解释,可以自己进一步细化。

在这里插入图片描述

  • 解答错误测试:

在这里插入图片描述

  • 解答正确测试:

在这里插入图片描述

项目代码已上传

代码已上传至Gitee:负载均衡oj

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值