文章目录
- 1.项目宏观结构
- (1)三个模块
- (2)项目宏观结构
- (3)编写顺序
- 2.compile_server
- (1)compiler.hpp
- (2)runner.hpp
- (3)compile_run.hpp
- (4)compile_server.cc
- (5)Makefile
- (6)temp
- (7)编译运行模块总结
- 3.comm
- (1)util.hpp
- (2)log.hpp
- (3)httplib.h
- 4.基于MVC结构的OJ服务设计(oj_server)
- (1)设计题库
- (2)oj_server
- (3)model.hpp(文件版)
- (4)model2.hpp(数据库版)
- (5)view.hpp
- (6)control.hpp
- 5.前端模块
- (1)首页
- (2)所有题目列表
- (3)OJ指定题目编写代码页面
- 6.综合调试
- 7.结项
- (1)整体描述
- (2)编译运行模块
- (3)通信模块
- (4)负载均衡的实现
- 8.扩展
1.项目宏观结构
(1)三个模块
1.comm:公共模块
2.compile_server:编译与运行模块
3.oj_server:获取题目列表,查看题目编写界面,负载均衡,数据库,其他功能
(2)项目宏观结构
(3)编写顺序
1.complie_server
2.oj_server
3.文件版
4.前端编写
5.MySQL版
2.compile_server
(1)compiler.hpp
编译代码
#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<fcntl.h>
#include"../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_util;
using namespace ns_log;
namespace ns_compiler
{
class Compiler
{
public:
Compiler()
{}
~Compiler()
{}
//返回编译是否成功
// file_name:只有文件名,需要后续拼接路径和后缀,共构建三个文件
//./temp/file_name.cpp
//./temp/file_name.exe
//./temp/file_name.stderr
static bool Compile(const std::string& file_name)
{
pid_t pid=fork();
if(pid<0)
{
//创建子进程失败
LOG(ERROR)<<"创建子进程失败"<<"\n";
return false;
}
else if(pid==0)
{
//创建子进程
//建立标准错误文件
umask(0);//将权限清0
int _stderr=open(PathUtil::Compiler_Error(file_name).c_str(),O_CREAT|O_WRONLY,0644);
if(_stderr<0)
{
LOG(WARNING) << "没有成功形成stderr文件"<< "\n";
exit(1);
}
//重定向标准错误到file_name.stderr
dup2(_stderr,2);
//程序替换调用g++完成对代码的编译
execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr);
LOG(ERROR) << "启动g++失败,可能是参数错误"<< "\n";
exit(2);//替换失败直接退出
}
else
{
//父进程
waitpid(pid,nullptr,0);
//编译是否成功:是否形成可执行程序(判断文件是否存在)?
if (FileUtil::IsFileExists(PathUtil::Exe(file_name).c_str()))
{
LOG(INFO) << PathUtil::Src(file_name).c_str()<<"文件编译成功"<< "\n";
return true;
}
}
LOG(ERROR)<<"编译失败,没有形成可执行程序"<<"\n";
return false;
}
};
}
(2)runner.hpp
运行代码
使用子进程替换实现运行功能,子进程运行时的标准输入、标准输出、标准错误重定向为文件,父进程waitpid()获取子进程退出时状态解析出信号并返回该信号。如果是0则正常,否则则运行异常。
在运行的时候还要进行时间复杂度和空间复杂度的限制:setrlimit
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/time.h>
#include<sys/resource.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_log;
using namespace ns_util;
namespace ns_runner
{
class Runner
{
public:
Runner()
{}
~Runner()
{}
//提供设置进程占用资源大小的接口
static void SetProcLimit(int cpu_limit,int mem_limit)
{
//设置程序时间控制
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_cur=cpu_limit;
cpu_rlimit.rlim_max=RLIM_INFINITY;
setrlimit(RLIMIT_CPU,&cpu_rlimit);
//设置程序内存控制
struct rlimit mem_rlimit;
mem_rlimit.rlim_cur=mem_limit*1024;//转换成为KB
mem_rlimit.rlim_max=RLIM_INFINITY;
setrlimit(RLIMIT_AS,&mem_rlimit);
}
//返回值>0:运行的程序异常了,退出时收到的返回值就是对应的信号编号
//返回值=0:正常运行完毕
//返回值<0:内部错误
//cpu_limit:可以使用的最大的CPU资源
//mem_limit:可以占用的最大内存大小(KB)
static int Run(const std::string& file_name,int cpu_limit,int mem_limit)
{
/*程序运行
*代码跑完,结果正确
*代码跑完,结果不正确
*代码跑完,结果异常
//运行模块需要考虑代码跑完结果正确吗?不考虑
//结果正确与否是由测试用例决定的
//只考虑是否运行成功
*/
//一个程序启动的时候
//标准输入:不处理
//标准输出:运行输出结果
//标准错误:运行错误信息
std::string _execute=PathUtil::Exe(file_name);
std::string _stdin=PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0);
int _in_fd=open(_stdin.c_str(),O_CREAT|O_RDONLY,0644);
int _out_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
int _err_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
if(_in_fd<0||_out_fd<0||_err_fd<0)
{
LOG(ERROR)<<"运行时,打开文件失败"<<"\n";
return -1;
}
pid_t pid=fork();
if(pid<0)
{
LOG(ERROR)<<"运行时,创建子进程失败"<<"\n";
close(_in_fd);
close(_out_fd);
close(_err_fd);
return -2;
}
else if(pid==0)
{
dup2(_in_fd,0);
dup2(_out_fd,1);
dup2(_err_fd, 2);
SetProcLimit(cpu_limit,mem_limit);
execl(_execute.c_str(), _execute.c_str(),nullptr);
exit(1);
}
else
{
LOG(INFO)<<"运行成功"<<"\n";
close(_in_fd);
close(_out_fd);
close(_err_fd);
int status=0;
waitpid(pid,&status,0);
//程序运行异常,一定是因为收到了信号
return status&0x7F;
}
}
};
}
测试资源限制代码:资源约束与进程异常是类似的,都是通过信号
#include<iostream>
#include<unistd.h>
#include<sys/time.h>
#include<sys/resource.h>
#include<signal.h>
#include<cstdlib>
using namespace std;
void handler(int signal)
{
cout<<"signal:"<<signal<<endl;
exit(-1);
}
int main()
{
//是通过信号终止的
for(int i=1;i<=31;i++)
{
signal(i,handler);
}
struct rlimit r;
// r.rlim_cur=1;//1s
// r.rlim_max=RLIM_INFINITY;
// setrlimit(RLIMIT_CPU,&r);
r.rlim_cur=1024*1024*40;
r.rlim_max=RLIM_INFINITY;
setrlimit(RLIMIT_AS,&r);
int count=0;
while(1)
{
int* p=new int[1024*1024];
cout<<count<<endl;
count++;
}
return 0;
}
//内存限制
[lhb@VM-20-2-centos Online_Judge]$ ./a.out
0
1
2
3
4
5
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
signal:6
//CPU占用时间超时
[lhb@VM-20-2-centos Online_Judge]$ ./a.out
signal:24
(3)compile_run.hpp
整合编译和运行
-
适配用户需求,定制通信协议字段
-
正确调用compile和run方法
-
形成唯一文件名
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <jsoncpp/json/json.h>
#include <signal.h>
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_util;
using namespace ns_log;
namespace ns_compile_run
{
class CompileAndRun
{
public:
//信号转化为描述
// code>0:进程收到了信号导致崩溃
// code<0:非运行报错(代码为空)
// code=0:整个过程全部完成
static std::string CodeToDesc(int code,const std::string& file_name)
{
std::string desc;
switch (code)
{
case 0:
desc = "编译运行成功";
break;
case -1:
desc = "用户提交的代码为空";
break;
case -2:
desc = "未知错误";
break;
case -3:
//desc = "编译发生报错";
FileUtil::ReadFile(PathUtil::Compiler_Error(file_name),&desc,true);
break;
case SIGABRT:
desc = "内存超过范围";
break;
case SIGXCPU:
desc = "CPU使用超时";
break;
case SIGFPE:
desc = "浮点数溢出";
break;
default:
desc = "未知:" + std::to_string(code);
break;
}
return desc;
}
static void RemoveTempFile(std::string& file_name)
{
//清理文件的个数是不确定的,但是有哪些我们是知道的
std::string _src=PathUtil::Src(file_name);
if(FileUtil::IsFileExists(_src))
unlink(_src.c_str());
std::string _compile_error = PathUtil::Compiler_Error(file_name);
if (FileUtil::IsFileExists(_compile_error))
unlink(_compile_error.c_str());
std::string _Stderr = PathUtil::Stderr(file_name);
if (FileUtil::IsFileExists(_Stderr))
unlink(_Stderr.c_str());
std::string _Stdout = PathUtil::Stdout(file_name);
if (FileUtil::IsFileExists(_Stdout))
unlink(_Stdout.c_str());
std::string _Stdin = PathUtil::Stdin(file_name);
if (FileUtil::IsFileExists(_Stdin))
unlink(_Stdin.c_str());
std::string _exe = PathUtil::Exe(file_name);
if (FileUtil::IsFileExists(_exe))
unlink(_exe.c_str());
}
/*************************************
* 输入:
* input:用户给自己提交的代码对应的输入,暂时不做处理
* code:用户提交的代码
* cpu_limit:时间要求
* mem_limit:空间要求
* 输出:
* status:状态码
* reason:对应的请求结果,即状态码对应的原因
* 选填:
* stdout:程序运行完的结果
* stderr:程序运行完的错误结果
*
* in_json={"code":"代码",Input:"输入",cpu_limit:1,mem_limit:1024}
* out_json+{"status":"0","reason":"程序崩溃原因","stdout":"运行结果","stderr":"运行出错结果"}
* ***********************************/
static void Start(const std::string &in_json, std::string *out_json)
{
Json::Value in_value;
Json::Reader reader;
reader.parse(in_json, in_value); //最后再处理查错问题
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
Json::Value out_value;
int status_code = 0;
int run_result = 0; //用于接收run后的信号
std::string file_name; //需要内部形成的唯一文件名
if (code.size() == 0)
{
status_code = -1;
goto END;
}
//形成的文件名只具有唯一性,没有目录没有后缀
//毫秒级时间戳+原子性递增唯一值:保证唯一性
file_name = FileUtil::UniqFileName();
if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) //形成临时源文件
{
status_code = -2;
goto END;
}
if (!Compiler::Compile(file_name))
{
status_code = -3;
goto END;
}
run_result = Runner::Run(file_name, cpu_limit, mem_limit);
if (run_result < 0)
{
status_code = -2;
goto END;
}
else if (run_result > 0)
{
status_code = run_result;
goto END;
}
else
{
//运行成功
status_code = 0;
}
END:
out_value["status"] = status_code;
out_value["reason"] = CodeToDesc(status_code,file_name);
if (status_code == 0)
{
//整个过程全部成功
std::string string_stdout;
FileUtil::ReadFile(PathUtil::Stdout(file_name),&string_stdout,true);
out_value["stdout"] = string_stdout;
std::string string_stderr;
FileUtil::ReadFile(PathUtil::Stderr(file_name),&string_stderr,true);
out_value["stderr"] = string_stderr;
}
Json::StyledWriter writer;
*out_json = writer.write(out_value);
//清理临时文件
RemoveTempFile(file_name);
}
};
}
(4)compile_server.cc
提供网络服务,接入httplib
//使用httplib只需要将.h拷贝到项目中即可直接使用
//cpp-httplib需要使用高版本的gcc
//是一个阻塞式多线程的网络http库,使用了原生线程库
//测试的时候可以使用postman来进行测试
#include"compile_run.hpp"
#include"../comm/httplib.h"
using namespace ns_compile_run;
using namespace httplib;
void Usage(std::string proc)
{
std::cerr<<"Usage: "<<"\n\t"<<proc<<"port"<<std::endl;
}
//编译服务随时可能被多个人请求,当传递上来文件名在形成源文件的时候要具有唯一性。
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
return 1;
}
Server svr;
svr.Post("/compile_and_run", [](const Request &req, Response &res){
//用户请求的服务正文是我们想要的json_string
std::string out_json;
std::string in_json=req.body;
if(!in_json.empty())
{
CompileAndRun::Start(in_json,&out_json);
}
res.set_content(out_json,"application/json;charset=utf-8");
});
svr.listen("0.0.0.0", atoi(argv[1]));
}
(5)Makefile
compile_server:compile_server.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -rf compile_server
(6)temp
临时文件路径
(7)编译运行模块总结
用户向编译运行模块服务传入一个json串,它的内容包括代码,标准输入的内容,cpu时间限制,内存限制。
该模块向用户返回一个json串,包括状态码,状态码描述,标准输出,标准错误内容。
-
使用jsoncpp工具获得输入的各个部分。
-
生成一个唯一的临时文件名,根据该文件名构建源文件,将代码写入到源文件中。
-
然后使用g++进行进程替换进行编译操作,编译过程会产生一个.compiler_error文件,保存编译报错信息。
-
进程替换方式运行可执行程序,将标准输出,标准错误的内容写入文件中,父进程获得退出信号。
-
根据以上过程的各种返回值,自己定义构建状态码,状态码描述可以是运行成功,错误原因,或者异常信号产生原因,并将标准输出以及标准错误填入返回的json串中。
3.comm
(1)util.hpp
工具集
Time_util:获取s和ms时间戳
Path_util:构建.cpp .exe .compile_error .stdin .stdout .stderr文件的完整路径
File_util:判断文件是否存在,生成唯一文件名,写入文件,读取文件
String_util:分割字符串
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<sys/time.h>
#include<fstream>
#include<atomic>
#include<boost/algorithm/string.hpp>
namespace ns_util
{
const std::string temp_path="./temp/";
class TimeUtil
{
public:
//获取时间戳(s)
static std::string GetTimeStamp()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec);
}
//获取时间戳(ms)
static std::string GetTimeMS()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec*1000+_time.tv_usec/1000);
}
};
class PathUtil
{
public:
//添加后缀suffix
static std::string AddSuffix(const std::string& file_name,const std::string& suffix)
{
std::string path=temp_path;
path+=file_name;
path+=suffix;
return path;
}
//构建源文件路径+后缀的完整文件名
//编译时需要的
//1234->./temp/1234.cpp
static std::string Src(const std::string& file_name)
{
return AddSuffix(file_name,".cpp");
}
//构建可执行程序路径+后缀的完整文件名
static std::string Exe(const std::string &file_name)
{
return AddSuffix(file_name, ".exe");
}
//构建标准错误文件路径+后缀的完整文件名
static std::string Compiler_Error(const std::string &file_name)
{
return AddSuffix(file_name, ".complie_error");
}
//运行时需要的
//
static std::string Stdin(const std::string &file_name)
{
return AddSuffix(file_name, ".stdin");
}
static std::string Stdout(const std::string &file_name)
{
return AddSuffix(file_name, ".stdout");
}
static std::string Stderr(const std::string &file_name)
{
return AddSuffix(file_name, ".stderr");
}
};
class FileUtil
{
public:
static bool IsFileExists(const std::string& path_name)
{
//通过获取文件属性是否成功判断文件是否存在
struct stat st;
if(stat(path_name.c_str(),&st)==0)
{
return true;
}
return false;
}
//毫秒级时间戳+原子性唯一值
static std::string UniqFileName()
{
std::atomic_uint id(0);//id是原子的
id++;
std::string ms=TimeUtil::GetTimeMS();
std::string uid=std::to_string(id);
return ms+"_"+uid;
}
static bool WriteFile(const std::string& target,const std::string& content)
{
std::ofstream out(target);
if(!out.is_open())
{
return false;
}
else
{
out.write(content.c_str(),content.size());
}
out.close();
return true;
}
static bool ReadFile(const std::string& target,std::string* content,bool keep=false)
{
(*content).clear();
std::ifstream in(target);
if(!in.is_open())
{
return false;
}
else
{
std::string line;
//getline不保存行分隔符(没有'\n'),有些时候需要保存分隔符
//getline内部重载了强制类型转换
while(std::getline(in,line))
{
(*content)+=line;
(*content)+=(keep?"\n":"");
}
}
in.close();
return true;
}
};
class StringUtil
{
public:
//将str以sep为分隔符切分的内容放在target数组中
static void SplitString(const std::string& str,std::vector<std::string>* target,std::string sep)
{
boost::split((*target),str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
}
};
}
(2)log.hpp
日志
使用宏替换函数来实现的,定义了一个LOG()的函数,返回值是ostream&类型,它会将错误等级,文件名,行数和时间显示出来。
#pragma once
#include<iostream>
#include<string>
#include"util.hpp"
using namespace ns_util;
namespace ns_log
{
//日志等级
enum
{
INFO,//常规
DEBUG,//调试
WARNING,//告警
ERROR,//错误
FATAL//系统崩溃
};
//开放式日志:LOG(level)<<"message"
//经常被调用,设置为内联函数
inline std::ostream& Log(const std::string& level,const std::string& file_name,int line)
{
std::string message="[";
message+=level;
message+="]";
message+="[";
message+=file_name;
message+="]";
//添加报错行
message+="[";
message += std::to_string(line);
message+="]";
//添加日志时间戳
message+="[";
message+=TimeUtil::GetTimeStamp();
message+="]";
std::cout<<message;//不要endl进行刷新
return std::cout;
}
//#level:将数字变成字符串
#define LOG(level) Log(#level,__FILE__,__LINE__)
}
(3)httplib.h
自行下载。
4.基于MVC结构的OJ服务设计(oj_server)
本质就是建立一个小型网站
- 获取首页,用题目列表充当
- 编辑区域页面
- 提交,判题功能(编译并运行)
MVC设计模式:
M:通常是和数据交互的模块,比如对题库的增删查改(文件版,数据库版)
V:通常是拿到数据之后,要进行渲染网页内容,展示给用户(浏览器)
C:控制器,就是我们的核心业务逻辑
(1)设计题库
题目的编号,标题,难度,题面,时间要求(内部处理)和空间要求
有两批文件
1.questions_list:题目列表(不需要出现题目的内容)
2.需要题目的描述,题目的预设置的代码(handler.cpp),测试用例代码(tail.cpp)
这两个内容是通过题目的编号产生关联的设计OJ最大的成本是给每一道题设计测试用例
其中question的目录结构如上,question.txt表示题目列表,目录名表示题目编号,desc.txt存放题目描述,handler.cpp存放预设置代码,tail.cpp存放测试用例。
//1.当用户提交自己的代码时
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
class Solution
{
public:
bool isPalindrome(int x)
{
//请将你的代码写在这里
return true;
}
};
//2.OJ不是只把上面的代码提交给编译运行模块
//而是拼接上该题目对应的测试用例
#ifndef COMPILER_ONLINE
#include "handler.cpp"
#endif
void Test1()
{
bool ret=Solution().isPalindrome(121);
if(ret)
{
cout<<"通过用例1,测试121通过...OK"<<endl;
}
else
{
cout<<"没有通过用例1,测试的值是121...FALSE"<<endl;
}
}
void Test2()
{
//通过定义临时对象来完成方法的调用
bool ret = Solution().isPalindrome(-10);
if (!ret)
{
cout << "通过用例1,测试-10通过...OK" << endl;
}
else
{
cout << "没有通过用例1,测试的值是-10...FALSE" << endl;
}
}
int main()
{
Test1();
Test2();
}
//3.最终提交给编译运行的代码是完整的一份代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
class Solution
{
public:
bool isPalindrome(int x)
{
//请将你的代码写在这里
return true;
}
};
//下面的代码,我们不想让编译器编译的时候保留,而是裁剪掉,仅仅是让我们设计测试用例时不要报语法错误
//g++ -D COMPILER_ONLINE可以在编译的时候定义该宏,这样就在编译时候拿掉了
///
#ifndef COMPILER_ONLINE
#include "handler.cpp"
#endif
///
void Test1()
{
bool ret=Solution().isPalindrome(121);
if(ret)
{
cout<<"通过用例1,测试121通过...OK"<<endl;
}
else
{
cout<<"没有通过用例1,测试的值是121...FALSE"<<endl;
}
}
void Test2()
{
//通过定义临时对象来完成方法的调用
bool ret = Solution().isPalindrome(-10);
if (!ret)
{
cout << "通过用例1,测试-10通过...OK" << endl;
}
else
{
cout << "没有通过用例1,测试的值是-10...FALSE" << endl;
}
}
int main()
{
Test1();
Test2();
}
数据库版本的题库与之相似,只需要一张表即可完成:
(2)oj_server
#include<iostream>
#include <regex>
#include<signal.h>
#include "../comm/httplib.h"
#include"oj_control.hpp"
using namespace httplib;
using namespace ns_control;
std::regex r4("[0-9]", std::regex_constants::basic);
static Control* ctrl_ptr=nullptr;
void Recovery(int signal)
{
ctrl_ptr->RecoveryMachine();
}
int main()
{
signal(SIGQUIT,Recovery);
//用户请求的服务路由功能
Server svr;
Control ctrl;
ctrl_ptr=&ctrl;
svr.set_base_dir("./wwwroot");
//1.获取所有的题目列表
svr.Get("/all_questions",[&ctrl](const Request& req,Response& res){
//返回一个包含所有题目的网页
std::string html;
ctrl.AllQuestions(&html);
res.set_content(html,"text/html;charset=utf-8");
});
//2.用户要根据题目,获取题目的内容
//(\d+)是正则表达式表示数字,+表示多个
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &res){
std::string number=req.matches[1];//1号下标存放正则匹配的结果
std::string html;
ctrl.Question(number,&html);
res.set_content(html, "text/html;charset=utf-8");
});
//3.用户提交代码,使用判题功能(1.每道题的测试用例,2.compile_and_run服务)
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &res){
std::string number=req.matches[1];//1号下标存放正则匹配的结果
std::string result_json;
ctrl.Judge(number,req.body,&result_json);
res.set_content(result_json, "application/json;charset=utf-8");
//res.set_content("这是指定题目的判题" + number, "text/plain;charset=utf-8");
});
svr.listen("0.0.0.0", 8080);
return 0;
}
(3)model.hpp(文件版)
完成对数据的获取
#pragma once
//文件版本
//根据list文件加载所有题目信息到内存中
// model:主要用于数据进行交互,提供访问数据的接口
#include <iostream>
#include <string>
#include <unordered_map>
#include <assert.h>
#include <vector>
#include <fstream>
#include <cstdlib>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_log;
using namespace ns_util;
namespace ns_model
{
struct Question
{
std::string number; //题目标号(唯一的)
std::string title; //题目标题
std::string star; //题目难度(简单,中等,困难)
int cpu_limit; //题目时间要求
int mem_limit; //题目空间要求
std::string desc; //题目的描述
std::string handler; //题目预设给用户在线编辑器代码
std::string tail; //题目的测试用例需要与handler拼接,形成完整代码
};
const std::string question_list = "./oj_questions/question.list";
const std::string question_path = "./oj_questions/";
class Model
{
private:
//题号映射题目细节
std::unordered_map<std::string, Question> questions; //题号与题目对应
public:
Model()
{
assert(LoadQuestionList(question_list));
}
~Model() {}
//加载配置文件:question.list与题目编号文件,就是构建questions
bool LoadQuestionList(const std::string &question_list)
{
std::ifstream in(question_list);
if (!in.is_open())
{
LOG(FATAL)<<"加载题库失败,请检查是否存在题库文件"<<std::endl;
return false;
}
std::string line;
while (getline(in, line))
{
std::vector<std::string> tokens;
StringUtil::SplitString(line, &tokens, " ");
if (tokens.size() != 5)
{
LOG(WARNING)<<"加载部分题目失败,请检查文件格式"<<std::endl;
continue;
}
Question q;
q.number = tokens[0];
q.title = tokens[1];
q.star = tokens[2];
q.cpu_limit = stoi(tokens[3]);
q.mem_limit = stoi(tokens[4]);
std::string path = question_path;
path += q.number;
path += "/";
FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);
FileUtil::ReadFile(path + "handler.cpp", &(q.handler), true);
FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);
questions.insert({q.number,q});
}
LOG(INFO)<<"加载题库成功"<<std::endl;
in.close();
return true;
}
//获取全部题目到数组中
bool GetAllQuestion(std::vector<Question> *out)
{
if (questions.size() == 0)
{
LOG(ERROR)<<"用户获取题目失败"<<std::endl;
return false;
}
for (const auto &q : questions)
{
out->push_back(q.second);
}
return true;
}
//获取一个题目
bool GetOneQuestion(const std::string &number, Question *q)
{
const auto &iter = questions.find(number);
if (iter == questions.end())
{
LOG(ERROR)<<"获取题目失败,题目编号为"<<number<<std::endl;
return false;
}
*q = iter->second;
return true;
}
};
}
(4)model2.hpp(数据库版)
#pragma once
// MySQL版本
#include <iostream>
#include <string>
#include <unordered_map>
#include <assert.h>
#include <vector>
#include <fstream>
#include <cstdlib>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include"./MySQL_include/mysql.h"
using namespace ns_log;
using namespace ns_util;
namespace ns_model
{
struct Question
{
std::string number; //题目标号(唯一的)
std::string title; //题目标题
std::string star; //题目难度(简单,中等,困难)
std::string desc; //题目的描述
std::string handler; //题目预设给用户在线编辑器代码
std::string tail; //题目的测试用例需要与handler拼接,形成完整代码
int cpu_limit; //题目时间要求
int mem_limit; //题目空间要求
};
const std::string oj_questions = "oj_questions";
const std::string user = "stu";
const std::string host = "127.0.0.1";
const std::string password = "*********";
const std::string db = "oj";
const unsigned int port = 3306;
class Model
{
public:
Model() {}
~Model() {}
bool QueryMySQL(const std::string &sql, std::vector<Question> *out)
{
//链接数据库
MYSQL *my = mysql_init(nullptr); //创建并初始化一个MySQL句柄
if (mysql_real_connect(my, host.c_str(), user.c_str(), password.c_str(), db.c_str(), port, nullptr, 0) == nullptr)
{
std::cout << mysql_error(my) << std::endl;
LOG(FATAL)<<"链接数据库失败"<<std::endl;
return false;
}
LOG(INFO)<<"链接数据库成功"<<std::endl;
mysql_set_character_set(my, "utf8");
//执行sql语句
if(mysql_query(my,sql.c_str())!=0)
{
LOG(WARNING)<<sql<<"execute error"<<std::endl;
return false;
}
//提取结果
MYSQL_RES* res=mysql_store_result(my);
//分析结果
int rows=mysql_num_rows(res);//行数
int cols=mysql_num_fields(res);//列数
struct Question q;
for(int i=0;i<rows;i++)
{
MYSQL_ROW row=mysql_fetch_row(res);
q.number=row[0];
q.title=row[1];
q.star=row[2];
q.desc=row[3];
q.handler=row[4];
q.tail=row[5];
q.cpu_limit=atoi(row[6]);
q.mem_limit=atoi(row[7]);
out->push_back(q);
}
free(res);//释放结果空间
mysql_close(my);
return true;
}
//获取全部题目到数组中
bool GetAllQuestion(std::vector<Question> *out)
{
std::string sql = "select * from ";
sql += oj_questions;
return QueryMySQL(sql, out);
}
//获取一个题目
bool GetOneQuestion(const std::string &number, Question *q)
{
bool res = false;
std::string sql = "select * from ";
sql += oj_questions;
sql += " where number=";
sql += number;
std::vector<Question> result;
if (QueryMySQL(sql, &result))
{
if (result.size() == 1)
{
*q = result[0];
res=true;
}
}
return res;
}
};
}
(5)view.hpp
视图模块,将数据处理成网页
核心方法是使用ctemplate库对数据进行渲染
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<ctemplate/template.h>
#include "oj_model.hpp"
using namespace ns_model;
namespace ns_view
{
const std::string template_path="./template_html/";
class View
{
public:
View(){}
~View(){}
void AllExpandHtml(const std::vector<struct Question>& questions,std::string* html)
{
//题目的编号,标题,难度
//推荐使用表格形式
//1.被渲染的网页
std::string src_html=template_path+"all_questions.html";
//2.形成数据字典
ctemplate::TemplateDictionary root("all_questions");
for(const auto& q:questions)
{
//形成子字典
ctemplate::TemplateDictionary* sub=root.AddSectionDictionary("question_list");
sub->SetValue("number",q.number);
sub->SetValue("title",q.title);
sub->SetValue("star",q.star);
}
//3.获取被渲染的网页
ctemplate::Template* tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
//4.开始完成渲染功能
tpl->Expand(html,&root);
}
void OneExpandHtml(const struct Question& q,std::string* html)
{
std::string src_html = template_path + "one_question.html";
ctemplate::TemplateDictionary root("one_question");
root.SetValue("number",q.number);
root.SetValue("title", q.title);
root.SetValue("star", q.star);
root.SetValue("desc", q.desc);
root.SetValue("pre_code", q.handler);
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
tpl->Expand(html, &root);
}
};
}
附加功能:需要有数据渲染
如果后序引入了ctemplate库,一旦对网页进行修改,想看到结果需要将server重新编译,ctemplate有自己的优化策略,可能在内存中存在缓存网页数据。
(6)control.hpp
逻辑控制模块,与服务器直接相连
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<mutex>
#include<cassert>
#include<fstream>
#include<algorithm>
#include<jsoncpp/json/json.h>
#include"../comm/httplib.h"
//#include"oj_model.hpp"
#include "oj_model2.hpp"
#include "oj_view.hpp"
#include"../comm/util.hpp"
#include "../comm/log.hpp"
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
using namespace httplib;
namespace ns_control
{
//负载均衡的逻辑
//提供服务的主机
class Machine
{
public:
std::string ip;//编译服务的ip
int port;//编译服务的端口
uint64_t load;//编译服务的负载
std::mutex* mtx;//C++中mutex是禁止拷贝的,使用指针来完成
public:
Machine():ip(""),port(0),load(0),mtx(nullptr)
{
}
~Machine()
{
}
//增加负载,加锁
void IncLoad()
{
if (mtx)
mtx->lock();
load++;
if (mtx)
mtx->unlock();
}
//减少负载,加锁
void DesLoad()
{
if (mtx)
mtx->lock();
load--;
if (mtx)
mtx->unlock();
}
//获取主机负载,没有太大意义,只是为了统一接口
uint64_t Load()
{
uint64_t _load;
if (mtx)
mtx->lock();
_load=load;
if (mtx)
mtx->unlock();
return _load;
}
void ResetLoad()
{
if(mtx) mtx->lock();
load=0;
if(mtx) mtx->unlock();
}
};
//负载均衡模块
const std::string service_machine="./conf/service_machine.conf";
class LoadBalance
{
std::vector<Machine> machines;//可以提供编译服务的所有主机,下标充当主机编号
std::vector<int> online;//所有在线的主机,存放主机编号
std::vector<int> offline;//所有离线的主机
//保证LoadBalance的数据安全
std::mutex mtx;
public:
LoadBalance()
{
assert(LoadConf(service_machine));
LOG(INFO)<<"加载service_machine配置文件成功"<<"\n";
}
~LoadBalance(){}
//导入配置文件,主机
bool LoadConf(const std::string& machine_conf)
{
std::ifstream in(machine_conf);
if(!in.is_open())
{
LOG(FATAL)<<"load加载machine_conf失败"<<std::endl;
return false;
}
std::string line;
while(getline(in,line))
{
std::vector<std::string> tokens;
StringUtil::SplitString(line,&tokens,":");
if(tokens.size()!=2)
{
LOG(WARNING)<<"切分"<<line<<"失败了"<<std::endl;
continue;
}
Machine m;
m.ip=tokens[0];
m.port=stoi(tokens[1]);
m.load=0;
m.mtx=new std::mutex();
online.push_back(machines.size());//在线的主机
machines.push_back(m);
}
in.close();
return true;
}
//智能选择
//id:选择主机的id
//m:选择的主机
bool SmartChoice(int* id,Machine** m)
{
//1.使用选择好的主机(更新该主机的负载)
//2.我们需要可能离线该主机
mtx.lock();
//负载均衡的算法
//1.随机数法
//2.轮训+随机
int online_num=online.size();//有多少主机在线
if(online_num==0)
{
mtx.unlock();
LOG(FATAL) << "所有的后端编译主机已经全部离线了,请运维尽快查看" << std::endl;
return false;
}
//通过遍历的方式找到所有负载最小的机器
*id=online[0];
*m=&machines[online[0]];
uint64_t min_load=machines[online[0]].Load();
for(int i=1;i<online_num;i++)
{
uint64_t curr_load=machines[online[i]].Load();
if(min_load>curr_load)
{
min_load=curr_load;
*id = online[i];
*m=&machines[online[i]];//想获得主机地址,不想获得主机,空间小,使用二级指针
}
}
mtx.unlock();
return true;
}
//在线主机
void OnLineMachine()
{
//当所有主机都离线的时候,统一上线
//后面统一解决
mtx.lock();
online.insert(online.end(),offline.begin(),offline.end());
offline.erase(offline.begin(),offline.end());
mtx.unlock();
LOG(INFO)<<"所有的主机上线了"<<std::endl;
}
//离线主机
void OffLineMachine(int which)
{
mtx.lock();
for(auto iter=online.begin();iter!=online.end();iter++)
{
machines[which].ResetLoad();//离线的时候将负载置为0
if(*iter==which)
{
//要离线的主机已经找到了
online.erase(iter);
offline.push_back(which);
break;//因为break的存在,因此我们不用考虑迭代器失效的问题
}
}
mtx.unlock();
}
//for test
void ShowMachines()
{
mtx.lock();
std::cout<<"当前在线主机列表"<<std::endl;
for(auto& iter:online)
{
std::cout<<iter<<" ";
}
std::cout<<std::endl;
std::cout << "当前离线主机列表" << std::endl;
for (auto &iter : offline)
{
std::cout << iter << " ";
}
std::cout << std::endl;
mtx.unlock();
}
};
//这是核心业务逻辑的控制器
class Control
{
private:
Model model_;
View view_;
LoadBalance loadbalance_;//核心负载均衡器
public:
Control(){}
~Control(){}
void RecoveryMachine()
{
loadbalance_.OnLineMachine();
}
//根据题目数据构建网页
bool AllQuestions(std::string* html)
{
bool ret=true;
std::vector<struct Question> all;
if(model_.GetAllQuestion(&all))
{
//获取题目信息成功,将所有的题目数据构建成网页并返回
sort(all.begin(),all.end(),[](const struct Question& q1,const struct Question& q2){
return stoi(q1.number)<stoi(q2.number);
});
view_.AllExpandHtml(all,html);
}
else
{
ret=false;
*html="获取文本失败,形成题目列表失败";
}
return ret;
}
bool Question(std::string& number,std::string* html)
{
bool ret=true;
struct Question q;
if(model_.GetOneQuestion(number,&q))
{
//获取指定题目信息成功,构建网页并返回
view_.OneExpandHtml(q,html);
}
else
{
ret=false;
*html="指定题目"+number+"不存在";
}
return ret;
}
// id: code: input
//number:题号
void Judge(const std::string& number,const std::string& in_json,std::string* out_json)
{
//0.根据题号直接拿到对应的题目细节
struct Question q;
model_.GetOneQuestion(number,&q);
//1.对in_json进行反序列化,得到用户提交的源代码
Json::Reader reader;
Json::Value in_value;
reader.parse(in_json,in_value);
//2.重新拼接用户代码,形成一份新的代码
std::string code=in_value["code"].asString();
Json::Value compile_value;
compile_value["input"]=in_value["input"].asString();
compile_value["code"]=code+"\n"+q.tail;
compile_value["cpu_limit"]=q.cpu_limit;
compile_value["mem_limit"]=q.mem_limit;
Json::FastWriter writer;
std::string compile_string=writer.write(compile_value);
//3.选择负载最低的主机(差错处理)
//规则:一直选择,直到编译主机可用,否则就是全部挂掉
while(true)
{
int id;
Machine* m=nullptr;
if(!loadbalance_.SmartChoice(&id,&m))
{
break;//所有主机都挂掉了,都不在线
}
LOG(INFO)<<"选择主机成功,主机id"<<id<<" 详情:"<<m->ip<<":"<<m->port<<"当前主机的负载时"<<m->load<<std::endl;
// 4.发起http请求得到结果
//使用http-lib的client请求
Client cli(m->ip,m->port);
m->IncLoad();
if(auto res=cli.Post("/compile_and_run",compile_string,"application/json;charset=utf-8"))
{
if(res->status==200)
{
LOG(INFO)<<"请求编译和运行服务成功"<<std::endl;
*out_json = res->body;
m->DesLoad();
break;
}
m->DesLoad();
}
else
{
//请求失败
LOG(ERROR) << "当前请求的主机id" << id << "详情:" << m->ip << ":" << m->port << "可能已经离线"<<std::endl;
loadbalance_.OffLineMachine(id);
//仅仅为了用来测试
loadbalance_.ShowMachines();
}
}
//5.将结果赋值给out_json返回给用户
}
};
}
当完成所有功能之后需要注意,要给编译模块添加-D来条件编译去掉对应的头文件handler.cpp
5.前端模块
- 编写页面三剑客:HTML,CSS,JS
- 后端开发不需要关心前端页面
- 后端虽然不关心页面,但是需要了解前后端如何交互
(1)首页
所谓样式调整,就是对html的标签做调整
1.选中标签
2.设置样式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>这是我的个人OJ系统</title>
<style>
/* 编写网页的起手式 */
/* *表示选中所有标签 */
*{
/* 消除网页的默认外边距 */
margin:0px;
/* 消除网页的默认内边距 */
padding: 0px;
}
html,
body{
width: 100%;
height: 100%;
}
.container .navbar
{
width: 100%;
height: 50px;
background-color: #ccc;
/* 给父级标签设置overflow后序浮动带来的影响 */
overflow: hidden;
}
.container .navbar a
{
display: inline-block;
/* 设置字体宽度 */
width: 80px;
color: black;
font-size: large;
/* 设置文字的高度与导航栏一样 */
line-height: 50px;
text-decoration: none;
/* 文字居中 */
text-align: center;
}
/* 为a标签加动态效果,设置鼠标事件 */
.container .navbar a:hover
{
background-color: green;
}
.container .navbar .login
{
/* 将登录放在最右侧 */
float:right;
}
/* .开头表示类选中器 */
.container .content
{
/* 设置标签的宽度 */
width: 800px;
/* 背景颜色 */
/* background-color: #ccc; */
/* 整体居中 */
margin: 0px auto;
/* 设置文字在content中居中 */
text-align: center;
/* 设置上外边距 */
margin-top: 200px;
}
.container .content .font_
{
/* 因为a不是块元素,将整体设为块元素,独占一行,可以设置高度,宽度等属性 */
display: block;
/* 设置每个文字的上外边距 */
margin-top: 20px;
/* 去掉a标签的下划线 */
text-decoration: none;
/* 设置字体大小
font-size: larger; */
}
</style>
</head>
<body>
<div class="container">
<!-- 导航栏,功能不实现 -->
<div class="navbar">
<a href="#">首页</a>
<a href="/all_questions">题库</a>
<a href="#">竞赛</a>
<a href="#">讨论</a>
<a href="#">求职</a>
<a class="login" href="#">登录</a>
</div>
<!-- 网页内容 -->
<div class="content">
<h1 class="font_">这是我个人独立开发的在线Online_Judge平台</h1>
<p class="font_">欢迎来到我的OJ平台</p>
<a class="font_" href="/all_questions">点击我开始编程了</a>
</div>
</div>
</body>
</html>
(2)所有题目列表
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线OJ-题目列表</title>
<style>
/* 起手式, 100%保证我们的样式设置可以不受默认影响 */
* {
/* 消除网页的默认外边距 */
margin: 0px;
/* 消除网页的默认内边距 */
padding: 0px;
}
html,
body {
width: 100%;
height: 100%;
}
.container .navbar {
width: 100%;
height: 50px;
background-color: black;
/* 给父级标签设置overflow,取消后续float带来的影响 */
overflow: hidden;
}
.container .navbar a {
/* 设置a标签是行内块元素,允许你设置宽度 */
display: inline-block;
/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
width: 80px;
/* 设置字体颜色 */
color: white;
/* 设置字体的大小 */
font-size: large;
/* 设置文字的高度和导航栏一样的高度 */
line-height: 50px;
/* 去掉a标签的下划线 */
text-decoration: none;
/* 设置a标签中的文字居中 */
text-align: center;
}
/* 设置鼠标事件 */
.container .navbar a:hover {
background-color: green;
}
.container .navbar .login {
float: right;
}
.container .question_list {
padding-top: 50px;
width: 800px;
height: 100%;
margin: 0px auto;
/* background-color: #ccc; */
text-align: center;
}
.container .question_list table {
width: 100%;
font-size: large;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
margin-top: 50px;
background-color: rgb(243, 248, 246);
}
.container .question_list h1 {
color: green;
}
.container .question_list table .item {
width: 100px;
height: 40px;
font-size: large;
font-family: 'Times New Roman', Times, serif;
}
.container .question_list table .item a {
text-decoration: none;
color: black;
}
.container .question_list table .item a:hover {
color: blue;
text-decoration: underline;
}
.container .footer {
width: 100%;
height: 50px;
text-align: center;
line-height: 50px;
color: #ccc;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<!-- 导航栏, 功能不实现-->
<div class="navbar">
<a href="/">首页</a>
<a href="/all_questions">题库</a>
<a href="#">竞赛</a>
<a href="#">讨论</a>
<a href="#">求职</a>
<a class="login" href="#">登录</a>
</div>
<div class="question_list">
<h1>OnlineJuge题目列表</h1>
<table>
<tr>
<th class="item">编号</th>
<th class="item">标题</th>
<th class="item">难度</th>
</tr>
{{#question_list}}
<tr>
<td class="item">{{number}}</td>
<td class="item"><a href="/question/{{number}}">{{title}}</a></td>
<td class="item">{{star}}</td>
</tr>
{{/question_list}}
</table>
</div>
<div class="footer">
<!-- <hr> -->
<h4>@比特就业课</h4>
</div>
</div>
</body>
</html>
(3)OJ指定题目编写代码页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{number}}.{{title}}</title>
<!-- 引入ACE插件 -->
<!-- 官网链接:https://ace.c9.io/ -->
<!-- CDN链接:https://cdnjs.com/libraries/ace -->
<!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 -->
<!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ -->
<!-- 引入ACE CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"
charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"
charset="utf-8"></script>
<!-- 引入jquery CDN -->
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
}
.container .navbar {
width: 100%;
height: 50px;
background-color: black;
/* 给父级标签设置overflow,取消后续float带来的影响 */
overflow: hidden;
}
.container .navbar a {
/* 设置a标签是行内块元素,允许你设置宽度 */
display: inline-block;
/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
width: 80px;
/* 设置字体颜色 */
color: white;
/* 设置字体的大小 */
font-size: large;
/* 设置文字的高度和导航栏一样的高度 */
line-height: 50px;
/* 去掉a标签的下划线 */
text-decoration: none;
/* 设置a标签中的文字居中 */
text-align: center;
}
/* 设置鼠标事件 */
.container .navbar a:hover {
background-color: green;
}
.container .navbar .login {
float: right;
}
.container .part1 {
width: 100%;
height: 600px;
overflow: hidden;
}
.container .part1 .left_desc {
width: 50%;
height: 600px;
float: left;
overflow: scroll;
}
.container .part1 .left_desc h3 {
padding-top: 10px;
padding-left: 10px;
}
.container .part1 .left_desc pre {
padding-top: 10px;
padding-left: 10px;
font-size: medium;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}
.container .part1 .right_code {
width: 50%;
float: right;
}
.container .part1 .right_code .ace_editor {
height: 600px;
}
.container .part2 {
width: 100%;
overflow: hidden;
}
.container .part2 .result {
width: 300px;
float: left;
}
.container .part2 .btn-submit {
width: 120px;
height: 50px;
font-size: large;
float: right;
background-color: #26bb9c;
color: #FFF;
/* 给按钮带上圆角 */
/* border-radius: 1ch; */
border: 0px;
margin-top: 10px;
margin-right: 10px;
}
.container .part2 button:hover {
color: green;
}
.container .part2 .result {
margin-top: 15px;
margin-left: 15px;
}
.container .part2 .result pre {
font-size: large;
}
</style>
</head>
<body>
<div class="container">
<!-- 导航栏, 功能不实现-->
<div class="navbar">
<a href="/">首页</a>
<a href="/all_questions">题库</a>
<a href="#">竞赛</a>
<a href="#">讨论</a>
<a href="#">求职</a>
<a class="login" href="#">登录</a>
</div>
<!-- 左右呈现,题目描述和预设代码 -->
<div class="part1">
<div class="left_desc">
<h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3>
<pre>{{desc}}</pre>
</div>
<div class="right_code">
<pre id="code" class="ace_editor"><textarea class="ace_text-input">{{pre_code}}</textarea></pre>
</div>
</div>
<!-- 提交并且得到结果,并显示 -->
<div class="part2">
<div class="result"></div>
<button class="btn-submit" onclick="submit()">提交代码</button>
</div>
</div>
<script>
//初始化对象
editor = ace.edit("code");
//设置风格和语言(更多风格和语言,请到github上相应目录查看)
// 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/c_cpp");
// 字体大小
editor.setFontSize(16);
// 设置默认制表符的大小:
editor.getSession().setTabSize(4);
// 设置只读(true时只读,用于展示代码)
editor.setReadOnly(false);
// 启用提示菜单
ace.require("ace/ext/language_tools");
editor.setOptions({
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true
});
function submit() {
// alert("嘿嘿!");
// 1. 收集当前页面的有关数据, 1. 题号 2.代码
var code = editor.getSession().getValue();
// console.log(code);
var number = $(".container .part1 .left_desc h3 #number").text();
// console.log(number);
var judge_url = "/judge/" + number;
// console.log(judge_url);
// 2. 构建json,并通过ajax向后台发起基于http的json请求
$.ajax({
method: 'Post', // 向后端发起请求的方式
url: judge_url, // 向后端指定的url发起请求
dataType: 'json', // 告知server,我需要什么格式
contentType: 'application/json;charset=utf-8', // 告知server,我给你的是什么格式
data: JSON.stringify({
'code': code,
'input': ''
}),
success: function (data) {
//成功得到结果
// console.log(data);
show_result(data);
}
});
// 3. 得到结果,解析并显示到 result中
function show_result(data) {
// console.log(data.status);
// console.log(data.reason);
// 拿到result结果标签
var result_div = $(".container .part2 .result");
// 清空上一次的运行结果
result_div.empty();
// 首先拿到结果的状态码和原因结果
var _status = data.status;
var _reason = data.reason;
var reason_lable = $("<p>", {
text: _reason
});
reason_lable.appendTo(result_div);
if (status == 0) {
// 请求是成功的,编译运行过程没出问题,但是结果是否通过看测试用例的结果
var _stdout = data.stdout;
var _stderr = data.stderr;
var stdout_lable = $("<pre>", {
text: _stdout
});
var stderr_lable = $("<pre>", {
text: _stderr
})
stdout_lable.appendTo(result_div);
stderr_lable.appendTo(result_div);
}
else {
// 编译运行出错,do nothing
}
}
}
</script>
</body>
</html>
6.综合调试
主页:
题目列表:
编程界面:
提交返回结果:
不断提交,负载均衡测试
断开所有主机,一键上线测试
7.结项
(1)整体描述
1.这是一个负载均衡的在线OJ项目,是一个简易版的leetcode,实现了它的访问题目和判题功能。
2.它主要有两个模块构成,一个模块叫oj_server,它是暴露给用户的,用户可以通过它获取题目列表,获取单个题目,答题并且获取编译运行的结果。
3.另一个模块叫compile_server,它是编译运行模块,可以将它部署在多台服务器上,oj_server可以选择负载最低的服务器,并与该服务器上的compile_server进行通信。主要通信的内容就是向compile_server传递代码,然后获取代码编译运行的结果。
(2)编译运行模块
1.oj_server向compile_server传递的是一个json串,通过反序列化可以拿到代码,标准输入,运行时间限制,运行内存限制。
2.使用子进程替换的方式进行编译,编译时会生成一个compile_error文件,将编译的结果写入该文件中,父进程通过是否形成了可执行文件来判断编译是否成功。
3.使用子进程替换的方式进行运行,使用dup2方法将标准输入,标准输出,标准错误分别重定向到文件中,使用rlimit结构体中的成员定义运行时间和空间的限制。父进程获取子进程的退出码来判断运行是否成功。
4.根据不同的编译运行结果定义不同的状态码,并加上相关的错误描述,将状态码,错误描述,标准输出和标准错误构建json串,返回给oj_server。
(3)通信模块
1.使用MVC模式进行编写的,model,view,control
2.当用户请求题目列表或者某道题的时候,model模块会访问数据库,并返回用户请求的内容,并传递给view模块。
3.view模块将数据进行渲染成网页,并将该网页的html返回给control模块,并由control模块返回给用户。
(4)负载均衡的实现
1.用户解答完题目会将代码传递给control模块。
2.将代码,标准输入,时间限制和空间限制构建json串。
3.遍历所有在线主机的负载,选择最小的进行通信
4.通信的主机负载++,接收到返回的结果之后负载–
5.通信失败将该主机离线,如果均离线还设计了一键上线的功能,捕捉了3号信号。
8.扩展
1.基于注册和登录的录题功能
2.业务扩展,写一个论坛,接入到在线OJ中
3.即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在Docker上
4.目前后端compiler服务使用的是http方式请求(仅仅是因为简单),但是也可以将编译服务设计成为远程过程调用(RPC rest_rpc)替换httplib的内容,可以不做
5.功能上更完善,判断一道题正确后自动下一题
6.navbar中的功能可以一个一个都实现一下
7.其他