目录
所用技术与开发环境
-
所用技术
- C++ STL 标准库
- Boost 准标准库(字符串切割)
- cpp-httplib 第三方开源网络库
- ctemplate 第三方开源前端网页渲染库
- jsoncpp 第三方开源序列化、反序列化库
- 负载均衡设计
- 多进程、多线程
- MySQL C connect
- Ace前端在线编辑器(了解)
- html/css/js/jquery/ajax (了解)
-
开发环境
- Centos 7 云服务器
- vscode
- Mysql Workbench
1. 项目宏观结构
- 项目核心
comm
:公共模块compile_server
:编译与运行模块oj_server
:获取题目列表,查看题目,编写界面,负载均衡
实现类似于常见oj网站中的:题目列表+在线编程+判题 功能。
- 编写思路
- 编写
compile_server
oj_server
- 基于文件版的在线OJ
- 前端设计
- 基于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.hpp
和 log.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 模块会在下一章介绍。
一道题目需要的元素
- 题目的编号 ——
number
- 题目的标题 ——
title
- 题目的难度 ——
star
- 题目的内容 ——
desc
- 题目的预设代码 ——
header
- 题目的测试用例 ——
tail
- 题目的时间要求(内部处理) ——
cpu_limit
- 题目的空间要求(内部处理) ——
mem_limit
而这些元素将由两类文件构成
- 第一类
questions.list
:题目列表(编号+标题+难度+时空要求)。 - 第二类
题目详情
:题目内容(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结构)
本质:建立网站
- 获取首页,采用题目列表
- 编辑区域
- 提交判题功能(编译+运行)
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模块承接两个服务:
- 提供用户所点击的网页:题目列表网页,指定题号的题目网页
- 接收用户提交的代码,经过简单处理后提交给后台空闲的主机 compile_server 判题,再将用户代码测试的结果返回给用户(负载均衡)。
于是总体的 Control模块的思路如下图所示:
oj_control.hpp 代码
该模块的代码如下:
测试
compile_server 可以部署在多台主机上,哪台主机空闲需要由Control模块按照负载均衡策略智能选择,随后将代码测试的工作交予空闲的编译主机。
接下来我们就来测试一次:
启动 oj_server 和 多个 compile_server
使用 postman 仅将预设代码提交,看到结果(编译失败的原因,状态码)
4. 前端页面设计
前端三剑客:html+css+js
首页(丐版)
对标签的样式调整
- 选中标签
- 设置样式
- 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>
该页面制作的重点在于如何让前后端进行沟通
-
使用jquery可以帮助我们获得前端用户代码框editor中用户输入的代码;
-
构建url:http请求所需的url为 oj_server 提供的judge接口+题号;
-
构建json,通过使用ajax向后台发起基于json正文格式的http请求;
-
得到编译主机(compile_server)编译运行后的结果json串,反序列化后,可以输出到前端页面上。
JQuery帮助获取前端的用户代码:
效果如下图所示:
- 超时测试:
- 内存超限测试
- 编译错误测试:
关于信号的解释,可以自己进一步细化。
- 解答错误测试:
- 解答正确测试:
项目代码已上传
代码已上传至Gitee:负载均衡oj