负载均衡在线oj(文件版)

项目介绍

我们要实现一个高效并发的在线判题(Online Judge)系统,类似leetcode,处理大量用户提交的代码,返回测评结果。

采取负载均衡技术,将要处理的服务负载均衡式派发给不同的服务器,实现高效并发OJ。

完整样式:

宏观结构

我们的项目主要分成三个模块:

  1. comm:公共模块
  2. complie_server:编译于运行模块
  3. oj_server:获取题目列表,查看题目编写题目界面,负载均衡,其他功能

Browser/Server(浏览器/服务器)模式。

接下来先实现编译模块。

Compiler

我们编译模块可以分成两个部分,compiler.hpp实现编译,runner.hpp实现运行,最后在compile_run.hpp整合,在服务器compile_server.cc上运行:

compiler.hpp

首先定义一个Compiler类:

cpp 复制代码
class Compiler
{
public:
    Compiler() {}    
    ~Compiler() {}
};

接下来就是要实现一个Compile方法。我们假设传进来的是一个文件名,对应的文件已经提交了。

那么我们要根据这个文件形成一些列临时文件。

这些临时文件就存储在"./temp"中。

具体需要源文件,可执行程序,还有编译信息文件。

因此我们需要实现根据文件名,拼接路径和后缀的函数。

这个函数可能别的模块也要用的,就放到comm部分的util.hpp中。

util是utility(通用工具)简写。

那么具体实现就很简单:

cpp 复制代码
const std::string temp_path = "./temp/";
class PathUtil
{
private:
    static std::string AddSuffix(const std::string &filename, const std::string &suffix)
    {
        std::string pathname = temp_path;
        pathname += filename;
        pathname += suffix;
        return pathname;
    }

public:
    static std::string Src(const std::string &filename)
    {
        return AddSuffix(filename, ".cpp");
    }
    static std::string Exe(const std::string &filename)
    {
        return AddSuffix(filename, ".exe");
    }
    static std::string Stderr(const std::string &filename)
    {
        return AddSuffix(filename, ".stderr");
    }
};

很好,接下来我们就要编译文件。

编译文件就要用到g++,要在进程用调用g++就要用到进程替换,其中我们选择最简单的execlp:

只需传入文件名,无需传入路径。

我们当然直接替换,需要创建一个子进程替换。

此外我们需要将编译信息存储到临时文件中,因此需要重定向stderr到我们的临时文件中,需要用到dup2:

主进程部分首先要等待子进程编译完成,然后判断是否编译成功。

我们如何判断编译是否成功,可以通过检测有没有实现可执行程序。

这里用到stat:

第二参数无需理会,主要看返回值:

当文件不存在就会失败返回。

因此我们需要实现一个判断文件是否存在的方法,同样放到comm部分:

最后我们就能实现Compile方法了:

日志

像刚刚Compile都有很多可能出错返回的地方,因此我们需要日志来输出错误信息。

虽然我们之前已经实现过日志,但之前实现的方法太C了,这次C++一点。

我希望我们的日志可以这样调用:LOG()<<Message

日志就实现在comm部分

日志格式:[日志等级][文件名][行号][日志时间戳]Message,因此我们需要日志等级:

具体实现:

cpp 复制代码
inline std::ostream &Log(const std::string &level, const std::string &filename, const int line)
{
    std::string message = "[";
    message += level;
    message += "]";

    message += "[";
    message += filename;
    message += "]";

    message += "[";
    message += std::to_string(line);
    message += "]";

    message += "[";
    message += TimeUtil::GetTimeStamp();
    message += "]";

    std::cout<<message;
    return std::cout;
}

因为Log调用可能比较多,这里可以加上inline关键字。

还记得我们的cout是有输出缓冲区的,并且是行刷新策略,我们这里不写入换行符的话不会立刻刷新。

GetTimeStamp还是交给util部分实现。

我们这里用到gettimeofday这个函数:

这两个都是输出型参数,这里只关心第一个:

这个结构体的第一个参数就是秒级的事件戳。

但是现在我们调用Log还是有点丑陋,可以对其封装一个宏函数,是的我们只需传入level参数即可。

cpp 复制代码
#define LOG(level) Log(#level,__FILE__,__LINE__)

注意这里#level是指将level将宏转成字符型,例如传入INFO,原本是0,但会转化成"INFO"。

现在我们可以给compile加一些日志信息:

现在我们可以简单测试一下编译模块:

compile_server.cc

code.cpp

非常完美,我们尝试错误编译:

stderr文件:

runner.hpp

接下来我们要实现运行模块,首先我们也是传入文件名。

这时候我们要考虑运行完毕和运行异常两种清空。因此我们要返回可执行程序的退出信号。

这里我们定义我们的返回值:

  1. >0,为退出信号
  2. ==0,为正常运行完毕
  3. <0,内部错误

此外我们还要对运行结果进行保存,可以创建同名的stdin、stdout、stderr文件进行重定向。这里stderr就和前面编译错位文件重名了,编译错误文件后缀改为compile_error

整体实现思路和编译差不多,创建子进程来执行程序。

然而这还不足够,我们要防止用户提交的代码是恶意代码,同时也要给题目做出时间复杂度和空间复杂度 的限制,因此我们还需要对资源做出限制。这里要用到setrlimit:

这个函数能对后续代码带动的资源做出一定限制,主要看传入的第一个参数。

我们这里关注两个:

RLIMIT_AS

这是进程虚拟内存(地址空间)的最大大小限制。该限制以字节为单位指定,并会向下取整到系统的页面大小。

此限制会影响brk(2)、mmap(2)和mremap(2)等系统调用:当超过该限制时,这些调用会失败并返回ENOMEM错误。此外,自动栈扩展也会失败(若未通过sigaltstack(2)配置备用栈,则会产生SIGSEGV 信号并终止进程)。

由于该限制的值是long类型,在使用 32 位long的机器上,此限制的最大值最多为 2 GiB,否则该资源会被设为无限制。

简单测试一下

可见会收到6号信号。

RLIMIT_CPU

这是对进程可消耗的 CPU 时间的限制,单位为秒。

当进程达到软限制时,会被发送SIGXCPU 信号。该信号的默认行为是终止进程,但信号可以被捕获,处理函数也可以将控制权交还给主程序。若进程继续消耗 CPU 时间,系统会每秒发送一次SIGXCPU,直到达到硬限制------ 此时进程会被发送SIGKILL信号(该行为是 Linux 的实现逻辑,不同系统对 "软限制后继续消耗 CPU 的进程" 的处理方式存在差异)。

对于需要捕获该信号的可移植应用,应在首次接收到SIGXCPU时执行有序终止操作。

再看第二个参数类型:

其中软限制就是我们要限制的数值,硬限制通常设置正无穷。

由此我们能封装一个资源限制函数:

cpp 复制代码
static void SetProcLimit(int _rlimit_cpu,int _rlimit_as)
{
    struct rlimit rlimit_cpu;
    rlimit_cpu.rlim_cur=_rlimit_cpu;
    rlimit_cpu.rlim_max=RLIM_INFINITY;

    struct rlimit rlimit_as;
    rlimit_as.rlim_cur=_rlimit_as*1024;
    rlimit_as.rlim_max=RLIM_INFINITY;         
    
    setrlimit(RLIMIT_CPU,&rlimit_cpu);
    setrlimit(RLIMIT_AS,&rlimit_as);
}

其中RLIM_INFINITY:

这时我们就能完整实现run方法:

cpp 复制代码
static int Run(const std::string &filename,int _rlimit_cpu,int _rlimit_as)
{
    std::string _execute = PathUtil::Exe(filename);

    umask(0);
    int _stdin = open(PathUtil::Stdin(filename).c_str(), O_CREAT | O_RDONLY, 0644);
    int _stdout = open(PathUtil::Stdout(filename).c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
    int _stderr = open(PathUtil::Stderr(filename).c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);

    if (_stdin < 0 || _stdout < 0 || _stderr < 0)
    {
        LOG(ERROR) << "运行时打开文件失败\n";
        return -1;
    }

    pid_t pid = fork();
    if (pid < 0)
    {
        close(_stdin);
        close(_stdout);
        close(_stderr);
        LOG(ERROR) << "运行时,创建子进程失败\n";
        return -2;
    }
    else if (pid == 0)
    {
        dup2(_stdin, 0);
        dup2(_stdout, 1);
        dup2(_stderr, 2);
        //资源限制
        SetProcLimit(_rlimit_cpu,_rlimit_as);
        execl(_execute.c_str(),_execute.c_str(),nullptr);
        LOG(ERROR) << "运行可执行程序失败\n";
        return -3;
    }
    else
    {
        close(_stdin);
        close(_stdout);
        close(_stderr);
        int status;
        waitpid(pid,&status,0);
        LOG(INFO)<<"运行完毕\n";
        return status&0x7F;
    }
}

compile_run.hpp

接下来我们就要整合上面两个模块。

首先我们要考虑第一个问题,我们传给编译和运行函数的都是一个文件名,那么文件从何而来。

我们此时就可以考虑在这个模块进行创建。

因此这个模块传入的参数就不是文件名,那自然就是序列化后的字符串,这里用json串,即外部库jsoncpp帮我们实现的序列化功能。

那么接下来我们要自定义一下协议:

  • 输入部分
    code:用户提交的代码
    input:用户提供的输入,不做处理
    cpu_limit:时间要求
    mem_limit:空间要求
  • 输出部分
    必填
    status:状态码
    reason:请求结果
    选填
    stdout:程序运行完结果
    stderr:程序运行完错误的结果

Start

接下来实现传入json串然后就可以编译和运行程序。

自然我们要在公共部分实现,生成唯一文件名,生成源文件。

大概思路如下:

接下来我们要处理输出的json串。首先需要定义不同的status含义:

status<0代表内部错误,status>=0就是代码运行时收到的信号。

然后我们可以实现一个CodeToDesc将code转化成reason。

使用goto语句将序列化工作统一处理:

cpp 复制代码
static void Start(const std::string &_json_in, std::string *_json_out)
{
    Json::Value root;
    Json::Reader reader;
    reader.parse(_json_in, root);

    std::string code = root["code"].asString();
    std::string input = root["input"].asString();
    int cpu_limit = root["cpu_limit"].asInt();
    int mem_limit = root["mem_limit"].asInt();

    // 唯一文件名
    std::string filename;

    // 定义输出参数
    int status;
    Json::Value value_out;
    int runresult;

    if (code.empty())
    {
        status = -1; // 代码为空
        goto END;
    }

    // 生成唯一文件名,写入源文件
    // 毫秒级时间戳和原子性+实现生成唯一文件名
    filename = FileUtil::UniqFileName();
    if (!FileUtil::WriteFile(PathUtil::Src(filename), code))
    {
        status = -2; // 未知错误
        goto END;
    }

    // 编译运行
    if (!Compiler::Compile(filename))
    {
        status = -3; // 编译错误
        value_out["reason"] = FileUtil::ReadFile((PathUtil::Compile_error(filename)));
        goto END;
    }

    runresult = Runner::Run(filename, cpu_limit, mem_limit);
    if (runresult < 0)
    {
        status = -2;
    }
    else
    {
        status = runresult;
    }
//统一序列化
END:
    //必填内容 
    value_out["status"]=status;
    value_out["reason"]=CodeToDesc(status);
    //选填字段
    if(status==0)
    {
        value_out["stdout"]=FileUtil::ReadFile(PathUtil::Stdout(filename));
        value_out["stderr"]=FileUtil::ReadFile(PathUtil::Stderr(filename));
    }
    //序列化
    Json::StyledWriter writer;
    *_json_out=writer.write(value_out);
}

我们接下来实现CodeToDesc部分:

如果还有需要处理的信号以后处理。

接下来实现生成唯一文件名

依照之前提出的方案// 毫秒级时间戳和原子性+实现生成唯一文件名

那么我们要先获取毫秒级时间戳:

这里tv_sec是秒,转成毫秒要×1000;tv_usec是微秒,转成毫秒要÷1000.

这里id是库提供的原子性递增的整型。

那么最后还要实现读写文件的操作,我们统一一下读写的参数格式,先完成写操作:

后续对start的细微调整就不赘述了。

测试

OK,那么接下来我们要对已经实现做一些测试

注意到我这里漏加了一个引号,因此会编译失败:

如我们所愿形成了唯一文件名.

OK,加上引号重新尝试,顺便加上时间空间复杂度限制:

非常完美。

接下来测试一下超时和超空间限制:



最后测试一下浮点数溢出:

都十分完美。

清理临时文件

要清理临时文件需要用到unlink:

直接传入路径就可以删除对应的文件。

cpp-httplib

接下来我们需要将编译运行打包成网络服务,这里用到一个开源的第三方库:cpp-httplib

这个项目是单头文件项目,因此我们其实只要将其头文件拷贝到comm模块就可以使用了:



然后我们就载入了这个开源库了

注意httplib需要使用高版本的gcc:

我这个版本足够了,如果不够就需要升级一下gcc。

httplib是一个阻塞式的多线程库,我们来简单用一下他的接口:

现在我们要将刚刚实现的编译运行功能打包成网络服务,参考之前写的应用层协议:HTTP,我们事实上只需要提供POST方法:

这里Request定义:

显然是我们学过的http报文格式。

当 HTTP 请求 / 响应的内容是 JSON 格式时,对应的Content-Type类型主要是 application/json。

Postman测试代码

我们在Postman官网下载postman工具对刚刚构建的网络服务进行测试:

可以看到我们输出stderr了,这限制的内存太小了,给他翻十倍:

此时就正常运行了,我们再测试下时间和空间限制:

时间

空间

最后测试下浮点数溢出:

现在我们就已经实现完了编译部分了。

OjServer

我们的oj服务是基于MVC结构的:

MVC 是一种软件架构模式,核心是把系统分成 3 个部分:

Model(模型):负责数据和业务逻辑(比如代码的编译 / 运行、用户数据存储);

View(视图):负责展示界面(比如用户提交代码的网页、结果展示页面);

Controller(控制器):负责接收用户请求,调用 Model 处理,再把结果传给 View 展示。

其中Model部分存放的就是我们的题库。

我们要实现的功能有:

  1. 获取首页,我们的首页就用题目列表充当
  2. 编辑区域页面
  3. 提交判题功能

我们先实现服务器的路由功能:

题库文件版构建

构建题库我们要先参考下别的优秀设计:

可以看到实际上我们的题库有两部分,首先是题目的列表,然后才是每个题目的描述。

我们在题目列表只需给出题目编号,题目名称,题目难度,时间限制,空间限制:

然后我们在文件夹1中维护题目1的内容。

我们继续借鉴一下leetcode:

可以看到正常oj界面可以给我们一些预设代码。

此外就是题目描述:

最后我们设计相应的题目用例即可。

因此完整的题目1文件夹应当有三个文件:

header.cpp

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

cpp 复制代码
#ifndef COMPILER_ONLINE
#include "head.cpp"
#endif

void Test1()
{
    //匿名对象调用方法
    bool ret=Solution().isPalindrome(121);
    if(ret)
    {
        std::cout<<"通过用例1"<<std::endl;
    }
    else
    {
        std::cout<<"未通过用例1:"<<121<<std::endl;
    }
}

void Test2()
{
    bool ret=Solution().isPalindrome(-19);
    if(!ret)
    {
        std::cout<<"通过用例1"<<std::endl;
    }
    else
    {
        std::cout<<"未通过用例1:"<<-19<<std::endl;
    }
}

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

desc.txt

复制代码
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

例如,121 是回文,而 123 不是。
 

示例 1:

输入:x = 121
输出:true
示例 2:

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

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

进阶:你能不将整数转为字符串来解决这个问题吗?

最终我们要将header和tail拼接起来交给编译模块。

model

先设计一下mode的结构:

很好,那么我们先实现简单的获取题目:

cpp 复制代码
bool GetAllQuestion(std::vector<Question> *out)
{
    if (_questions.empty())
        return false;
    for (const auto &q : _questions)
    {
        out->push_back(q.second);
    }
    return true;
}
bool GetOneQuestion(const std::string &number, Question *out)
{
    const auto &it = _questions.find(number);
    if (it == _questions.end())
        return false;
    *out = it->second;
    return true;
}

接下里就要实现加载题目列表的功能,首先我们需要将题目列表的数据做分割:

将其分成五份。然后再填入其他数据。

cpp 复制代码
bool LoadAllQuestion(const std::string &question_list)
{
    std::ifstream in(question_list);
    if (!in.is_open())
    {
        LOG(FATAL) << "打开题库失败,检查是否存在题库\n";
        return false;
    }
    std::string line;
    while (std::getline(in, line))
    {
        std::vector<std::string> tokens;
        StringUtil::SpiltString(line, &tokens, " ");
        if (tokens.size() != 5)
        {
            LOG(WARNING) << "读取题目失败,检查列表格式\n";
            continue;
        }
        Question q;
        q.number = tokens[0];
        q.name = tokens[1];
        q.star = tokens[2];
        q.cpu_limit = atoi(tokens[3].c_str());
        q.mem_limit = atoi(tokens[4].c_str());

        std::string path = question_path + q.number + "/";

        FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);
        FileUtil::ReadFile(path + "header.cpp", &(q.header), true);
        FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);

        _questions.insert({q.number, q});
    }
    LOG(INFO) << "载入题库完毕\n";
    in.close();

    return true;
}

那么接下来实现字符串切割。

我们用boost库的函数切割字符串,这里可以用指令检查当前有没有安装boost库:

shell 复制代码
ls /usr/include/boost

没安装的话,Ubuntu可以通过指令安装:

shell 复制代码
sudo apt update
sudo apt install -y libboost-all-dev

实现:

cpp 复制代码
static void SpiltString(const std::string &str, std::vector<std::string>* tokens, const std::string &sep)
{
    boost::split((*tokens),str,boost::is_any_of(" "),boost::algorithm::token_compress_on);
}

view

现在来编写视图,我们要根据从model部分获取的数据,做成页面展示给客户端。

我们这里要用到一个网页渲染的C/C++第三方库ctemplate

Ubuntu下通过指令

shell 复制代码
sudo apt update
sudo apt install -y libctemplate-dev

即可下载。

然后输入

shell 复制代码
ls /usr/include/ctemplate

检测是否下载成功。

那么这个库我们是用来渲染网页的,目前我们前端还没进一步学习,因此只能粗略讲讲。

首先我们需要一个数据字典和一个待渲染网页:

那么所谓渲染就是将key对应的value将网页中的key替换。

很好,依据这个思路我们先简单使用一下:

输出:

现在编写view的框架:

我们需要根据题目输出渲染的网页。

那么要编写一个粗浅的带渲染页面:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>题目列表</title>
</head>
<body>
    <table>
        <tr>
            <th>编号</th>
            <th>标题</th>
            <th>难度</th>
        </tr>
        {{#question_list}}
        <tr>
            <td>{{number}}</td>
            <td>{{name}}</td>
            <td>{{star}}</td>
        </tr>
        {{/question_list}}
    </table>
</body>
</html>

大致逻辑:

接下来就是获取一道题目的逻辑:

control

现在先编写control整体框架:

获取题目的实现已经非常简单了:

cpp 复制代码
bool AllQuestions(std::string *html)
{
    std::vector<Question> qs;
    if(_model.GetAllQuestion(&qs))
    {
        _view.AllExpandHtml(qs,html);
        return true;
    }
    else
    {
        return false;
    }
}
bool OneQuetion(const std::string &number,std::string *html)
{
    Question q;
    if(_model.GetOneQuestion(number,&q))
    {
        _view.OneExpandHtml(q,html);
        return true;
    }
    return false;
}

现在我们可以稍微修改一下main函数的:

简单测试一下:

非常完美!

负载均衡模块

在编写判题方法前先实现负载均衡模块。

首先我们需要一个文件记录可以调用的主机:

那么我们就需要一个基本类来封装主机的信息:

随后就需要负载均衡模块来派发服务:

接下来首先实现载入所有主机,这个我们自然再熟悉不过了:

cpp 复制代码
bool LoadConf(const std::string &machine_conf)
{
    std::ifstream in(machine_conf);
    if (!in.is_open())
    {
        LOG(FATAL) << "加载编译服务主机失败\n";
        return false;
    }
    std::string line;
    while (std::getline(in, line))
    {
        std::vector<std::string> tokens;
        StringUtil::SpiltString(line, &tokens, ":");
        if (tokens.size() != 2)
        {
            LOG(WARNING) << "加载一台主机:" << line << " 失败\n";
            continue;
        }
        Machine m;
        m.ip = tokens[0];
        m.port = atoi(tokens[1].c_str());
        m.load = 0;
        m.mtx = new std::mutex();
        
        online.push_back(machines.size());
        machines.push_back(m);
    }
    in.close();
    return true;
}

然后就是负载均衡选择,负载均衡选择有多种方法如随机选择。

我们这里采取轮询策略,选择当前在线主机的最低载荷者:

judge

接下来我们先回去编写control模块的judge部分,judge传入json串输出json串:

首先反序列化取出json串数据:

然后获取对应题目:

然后就是拼接代码:

那么接下来就是负载均衡选择一台主机,然后发送请求:

目前judge就编写完成了,来完善负载提高减少和下线主机方法:

那么现在可以调整一下main函数的judge逻辑:

Postman测试代码

最后我们也用postman测试一下刚刚的代码:

发送

对应服务器:

非常完美,再试试超时:

编译错误:

前端编写

页面

由于还没学习前端知识,我这里就照猫画虎了:

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

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

<body>
    <div class="container">
        <!-- 导航栏, 功能不实现-->
        <div class="navbar">
            <a href="#">首页</a>
            <a href="/question_list">题库</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="/question_list">点击我开始编程啦!</a>
        </div>
    </div>
</body>

</html>

题目列表

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>
        /* 起手式, 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', 'LucidaGrande', ' 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="/question_list">题库</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}}">{{name}}
                        </a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/question_list}}
            </table>
        </div>
        <div class="footer">
            <!-- <hr> -->
            <h4>@F_y</h4>
        </div>
    </div>
</body>

</html>

代码编写界面

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}}.{{name}}</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/extlanguage_
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>.{{name}}_{{star}}</h3>
                <pre>{{desc}}</pre>
            </div>
            <div class="right_code">
                <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() {
            // 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>

总结

最后对代码做一些细微调整,再优化一下前端我们的项目就完成了。

事实上这里还有很多值得拓展的地方,例如用数据库存放用户数据和题库。

这里我们后续再重新实现。
完整代码

相关推荐
Chase_______17 小时前
【Linux指南】:vi编辑器
linux·运维·编辑器
Dxy123931021617 小时前
Nginx中的worker_processes如何设置:从“盲目填数”到“精准调优”
运维·nginx
礼拜天没时间.17 小时前
【生产级实战】Linux 集群时间同步详解(NTP + Cron,超详细)
linux·运维·服务器·时间同步·cron·ntp
艾莉丝努力练剑17 小时前
【Linux进程控制(一)】进程创建是呼吸,进程终止是死亡,进程等待是重生:进程控制三部曲
android·java·linux·运维·服务器·人工智能·安全
NEAI_N17 小时前
嵌入式 Linux 中 system() 返回值的正确判定
linux·运维·服务器
瀚高PG实验室17 小时前
无法连接到服务器:连接被拒绝
运维·服务器·瀚高数据库
Jason_zhao_MR17 小时前
米尔T113核心板的农机中控屏显方案解析
linux·嵌入式硬件·嵌入式·交互
CodeAllen嵌入式17 小时前
Rust 正式成为 Linux 永久核心语言
linux·开发语言·rust
水天需01017 小时前
HISTCONTROL 介绍
linux