项目介绍
我们要实现一个高效并发的在线判题(Online Judge)系统,类似leetcode,处理大量用户提交的代码,返回测评结果。
采取负载均衡技术,将要处理的服务负载均衡式派发给不同的服务器,实现高效并发OJ。
完整样式:

宏观结构
我们的项目主要分成三个模块:
- comm:公共模块
- complie_server:编译于运行模块
- 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
接下来我们要实现运行模块,首先我们也是传入文件名。
这时候我们要考虑运行完毕和运行异常两种清空。因此我们要返回可执行程序的退出信号。
这里我们定义我们的返回值:
- >0,为退出信号
- ==0,为正常运行完毕
- <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中维护题目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>

总结
最后对代码做一些细微调整,再优化一下前端我们的项目就完成了。
事实上这里还有很多值得拓展的地方,例如用数据库存放用户数据和题库。
这里我们后续再重新实现。
完整代码。