核心是三个板块
提供的服务:编译并运行代码,得到格式化的相关的结果。
compiler.hpp
:只负责实现代码的编译功能.
输入需要编译的临时文件名到Compile方法中,只需要文件名,文件路径和后缀在comm/util.hpp中的PathUtil自动完成拼接。AddSuffix()
util.hpp
:装填着公共工具方法。
创建子进程来进行文件的编译:子进程采用程序替换execlp()
,程序替换成程序名为g++
实现了将file_name的源文件编译形成的同名可执行文件.exe,若编译时报错形成.stderr同名文件。
检验编译是否成功就是查看是否形成了同名可执行程序,所以需要引进判断文件是否存在的功能函数:temp/util.hpp/FileUtil::IsFileExits();
使用系统调用接口stat,检测特定路径下文件的相关属性放到struct stat结构体中,获取文件属性成功即文件存在返回0.
newfd作为old的一份拷贝,将原来的显示器文件重定向到标准错误文件中。因为如果g++编译失败会打印到显示屏上,为了不让他打印到显示器所以重定向到文件中,便于后续返回到前端。
Log.hpp
。当中添加了日志时间戳,所以引入comm/util.hpp/TimeUtil()::GetTimeStamp()
;runner.hpp
:
指明文件名即可,不需要带路径和后缀。
创建子进程去执行新形成的.exe,也就是temp/同名.exe文件。
运行结果存在
Run方法不需要考虑结果是否正确,正确与否是有给定的测试用例决定的!只考虑是否正确运行完毕
必须知道可执行程序是谁?comm/util.hpp/PathUtil(filename)
;
一个程序在默认启动的时候
把这些临时文件东西全都写到同名文件当中,运行函数只是帮你跑完,结果不关心,最关心的是有没有运行时异常中止。所以报错文件有两个:.Compiler_err编译时报错文件,.std_err运行时报错文件。运行时需要的临时文件也需要.stdin,.stdout,.stderr三个临时文件。
文件保存运行结果,所以对应的文件需要先都打开。
子进程负责将012的内容重定向到三个文件中,然后调用程序替换函数,因为我们有可执行程序的全称,所以使用execl();
如何知道程序运行完是异常呢?一定是因为收到了信号!好奇是什么原因异常的?退出信号判定
返回值>0.程序异常,返回值就是对应的信号编号
返回值==0,正常运行完毕,结果不关心,保存到对应临时文件中。
返回值<0,内部错误。
测试并且添加日志
设置时间限制和内存限制,以及不同的错误映射不同的原因并且返回。
终止进程是通过信号中止的。
//测试超出资源限制
#include
#include
#include
#include
#include
void handler(int signo)
{
std::cout << "signo : " << signo << std::endl;
exit(1);
}
int main()
{
//资源不足,导致OS终止进程,是通过信号终止的
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);
//while(1);
struct rlimit r;
r.rlim_cur = 1024 * 1024 * 40; //20M
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;
}
//内存申请失败
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
signo : 6
[whb@bite-alicloud OnlineJudge]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
...
//CPU使用超时
[whb@bite-alicloud OnlineJudge]$ ./a.out
signo : 24
[whb@bite-alicloud OnlineJudge]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
compile_run.hpp
:
temp目录下的用于测试编译运行功能的code.cpp应该是从客户端传过来的。所以,
需要引进网络功能,通过http,client上传一个json cpp。认识jsoncpp
所以需要jsoncpp来实现网络的序列化和反序列化。将结构化数据转化为一个字符串,结构化数据就是用户的各种信息,时间,提交的代码,用户名等等,我们需要将他之转化为字符串。
输入应该有字段:
输出的字段应该有:
如果有报错:还需要stderr
解析json串,也就是反序列化的过程,将一大串字符串转化为K-V结构的结构化数据,所以可以根据[]进行获取对应字符串。
形成唯一文件名放到temp目录下->/comm/util.hpp/UniqFileName();
,没有路径和后缀。
毫秒级时间戳+原子性递增的唯一值:来保证唯一性。
原子性唯一定增的值,C++11中的
将code的代码写到文件中,/comm/util.hpp/WriteFile();
形成src源文件
编译并运行文件全路径下的文件 ,
编译可能存在成功或者失败的情况,都需要进行差错处理。
编译出错,将Compile_err文件中的内容交给reason,作为失败的原因。/comm/util.hpp/ReadFile();
读取文件,一行一行读取getline(),不保存行分隔符,但是有的时候需要保存分隔符比如代码中的换行是需要的,还有一些特殊格式是需要保存的。所以添加一个标识符keep=false来确定是否需要保存换行符。
getline()内部重载了强制类型转换,返回之时bool类型
运行结果,<0属于服务器内部错误,==0是正常运行结束,结果在stdout文件中,使用函数读出来就行,>0就是异常中止信号。而每一种报错都对应着一种信息,所以设计一个映射关系的描述函数:CodeToDes();
特殊几个情况采用switch-case语句进行处理。引入
输出out_json就是序列化的过程。
差错处理
为了统一化管理避免出现代码冗余,在结尾设置END标签,在每一种情况中设置goto END;
注意:在goto语句和END标签之间定义变量是不被允许的,所以都要提前定义好。
测试时的问题;
include "compile_run.hpp"
using namespace ns_compile_and_run;
//编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有
//唯一性,要不然多个用户之间会互相影响
int main()
{
//提供的编译服务,打包形成一个网络服务
//cpp-httplib
// in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
// out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
// 通过http 让client 给我们 上传一个json string
// 下面的工作,充当客户端请求的json串
std::string in_json;
Json::Value in_value;
//R"()", raw string
in_value["code"] = R"(#include
int main(){
std::cout << "你可以看见我了" << std::endl;
aaaaaaaa
return 0;
})";
in_value["input"] = "";
in_value["cpu_limit"] = 1;
in_value["mem_limit"] = 10240*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;
}
删除每一次产生的很多临时文件:RemoveTempFile ();
引入cpp-httplib 0.7.15,
用法测试:Makefile中还需要引入-lpthread,因为cpp-httplib是阻塞式原生线程库。
网页访问时,不指定资源时,默认是根目录,在访问hello资源时自动返回后面的内容响应,但是会出现乱码。所以后续添加charset=utf-8编码设置。
所以,当我们收到req的json串,在Lambda表达式中进行编译运行程序调用,并返回一个json串作为Rsponse的串就可以实现交互。
这样,响应构建完成因为没有客户端,所以直接建立请求和响应。为了方便测试可以使用postman 进行发送请求个访问。
本质:建立一个小型网站
获取首页,用题目列表充当
编辑区域页面
提交编译判断题目功能(编译并运行)
Modle:数据交互模块,对题库的增删查的功能(文件版,数据库版)oj_model.hpp
v:view,通常是拿到数据之后,要进行构建网页,渲染网页内容(浏览器)oj_view.hpp
c:control,控制器,核心业务逻辑。oj_control.hpp
获取所有题目列表
用户根据编号获取题目内容
提交代码使用判题功能(测试用例&编译运行功能)
根据题目list文件,加载所有的题目信息到内存中。,主要用来和数据进行交互,对外提供访问数据的接口。
根据题目属性,构建Question结构体。
建立题号-题目细节的映射关系unordered_map
.
加载配置文件:先获取到questions/questionlist+题目编号文件 。读取文件列表的同时,填充结构体字段属性。
打开文件 ifstream in(question_list);
按行读取getline();对一行字符串进行指定分隔符的切分,将切分出来的子字符串放到vector中作为输出型参数,comm/util.hpp/StringUtil::SplitString();
使用boost库中的split方法。安装boost库
#include
将vector中的元素一次对应填充给Question.注意转型。
根据题目编号,补充某一个题目的路径字符串,然后根据路径,找到对应文件夹下的三个文件。然后再对文件描述属性,文件预设代码和测试用例属性进行填充。FileUtil::Readfile();
填充好题目属性之后,将题目添加到unordered_map中。
获取所有题目的函数GetAllQuestion();
将unordered_map中的题目都放到vector数组当中输出。
获取单个题号对应的题目GetOneQuestion();
逻辑控制模块,完成对于请求处理的路由功能。
当用户发起请求之后,我们要返回的是一张填充信息的网页。调用control的方法,获取所有数据然后填充网页。AllQuestions();
ojserver负责获取用户请求,通过control控制器完成服务的路由功能,其中组合了model和view,将数据构建成网页。引入ctemplate
网页渲染的库,goole,C/C++的渲染程序。实际上就是将头文件和库文件分别安装到了usr路径下。渲染原理:就是K-V的方式指定。
获取数据成功之后,返回一个渲染网页。View _view;Expandhtml();需要一个被渲染的网页。所以提供两个网页,template目录下allquestions_html 和onequestion_html供view代码进行渲染。
渲染allquestion:oj_view.cc中指定渲染模板路径,对每一个题目形成数字典。根字典,子字典,开始完成渲染功能。
加载到题目列表之后,根据题号完成对某一个题目的路由。
渲染onequestion:onequestion的网页中,应该具备代码编辑区,题目描述,题目预设代码等。
形成路径,形成数据字典,获取被渲染的html,开始渲染。在title里添加a标签,使得他具备跳转功能。
如果后续引入了ctemplate,一旦对网页结构进行修改,尽量的每次想看到结果,将server重启一下。ctemplate有 自己的优化加速策略,可能在内存中存在缓存网页数据(old) 。
control调用后端服务,提供判题功能。用户提交上来的json 串应该包含code:input: id:
根据题号得到题目信息,GetOneQuestion();
反序列化in_json得到题目信息。
重新拼接用户代码和测试用例代码。客户传进来的连main函数都没有,所以需要和我提供的tailer.hpp进行拼接。
选择负载最低的主机;一直选择,直到有主机可用,都这就是全部挂掉。
发起http请求。->cpp-httplib,定义client对象cli就可以向对端发起请求。
class httplib::Result res->cli.post(); out_json=res->body;
result类型就是一个智能指针指向response,是否存在response决定是否请求成功,请求失败是没有response的。
如果状态码status是200,才是成功。如果不是200,即使访问成功也不是我们想要额,就需要重新选择主机。
离线offline();就是将Online下标放到offline数组中,可萌同时有人选择主机,所以要加锁保证安全性。
将结果赋值给out_json返回给用户。
可以提供服务的主机列表conf文件中,保存机器的部署信息。(一台主机提供不同接口)。
class LoadBalance{};class Machine{};提供服务的主机machine
bool SmartChoice(int *id, Machine **m) //解引用是Machine* ,让外面通过地址得到主机
加载配置文件:打开machine_conf文件,行读取配置机器内容。还需要将ip和端口号进行分割用/comm/util.hpp/stringutil::StringSplit();
将配置信息填入Machine对象中,然后将所有主机放到online数组中。
智能选择:更新主机负载进行选择,后续可能离线该主机。
多执行流可能会同时访问,需要保证数据安全性,上锁。
需要两个文件:
在题目列表同级文件夹下,创建以题目编号命名的文件夹,文件夹中放着同名题目描述文件,预设代码文件,以及测试用例代码文件。
用户点击提交之后,是将header.cpp和同名的tail.cpp一起提交给后端的compile_run进行编译运行。
下面的代码,我们不想让编译器编译的时候,保留它,而是裁剪掉(g++ -D COMPILER_ONLINE)
仅仅是为了让我们设计测试用例的时候,不要报错.在后端调用g++的时候加两个选项就行。
ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
所以最大的成本是设计测试用例。
数据库建立用户并赋权oj_yuanwei,设置数据库和表结构questions。
录题功能使用MySQL Workbench图形化界面设置。
连接数据库
使用第三方库引入的方式,不进行安装。C语言连接
oj_server基于MVC模式,升级为MYSQL版本只需要将oj_model模块进行更改就行。
因为有数据库的存在,所以获取题目列表和单个题目只有SQL语句的区别。
遇到问题内存不够了,需要增加swap区:https://blog.csdn.net/qq_34980668/article/details/119562307
解决这个问题,需要链接-ldl, dl库是用来做动态库加载的(dynamic load),将-ldl链接选项改到最后才可以 。
/:一共有两个用,一个是防止转义,一个就是续行;
前面加@:可以让他不在命令行上显示。
发布模式:文件夹下只有两个可执行程序和各种配置文件,不然个被人看代码。
只需要将output文件夹发给他就行。
负载均衡式在线OJ项目中,