目录
相关技术
- 多进程、多线程
- 系统级别文件读写、重定向
- C++ STL 标准库
- Boost库(split字符串切割)
- jsoncpp第三方开源序列化、反序列化库
- cpp-httplib第三方开源网络库
- ctemplate第三方开源网页渲染库
- MySQL C connect链接库
开发环境
- CentOS7云服务器
- vscode
- MySQL Workbench
- Postman
项目宏观结构及介绍
用户进行在线oj时,主要有三种行为:
- 查看题目列表
- 进入具体题目
- 编写并提交代码
在服务器上部署了多台主机,执行compile_server服务,即编译运行服务,只负责对代码进行编译运行,
而在其中一台主机上部署oj_server服务,它是前端和后端的桥梁,为用户提供服务路由,负责接收用户请求、
统筹每台主机的任务数,负载均衡式地将用户代码转交给compile_server模块,再将结果返回给用户。
项目共三个核心模块:
- 公共模块:提供各个模块需要的工具类,如日志、文件操作等
- 编译运行模块:只负责对客户端上传的代码进行编译运行,并将运行信息返回给上层
- oj_server: MVC为用户获取题目、提供编写能力,负载均衡、其他功能
项目详细规划和API
编译模块(compiler)
只负责编译接收到的代码
运行模块(run)
运行结果有三种:
- 运行成功,结果正确
- 运行成功,结果错误
- 运行失败,程序异常
运行模块只关心运行成功与否,结果正确与否无需关心。
结果是否正确,是由上层测试用例运行程序后判断结果决定的,与运行模块无关。
编译运行模块(compile_run):
直接与上层交互的模块,整合compile模块和run模块.
上面的compile和run模块虽然都能各自实现了功能,但是还需要我们自己在对应路径下先创建好源文件/可执行文件。
该模块要能够自动将用户上传数据进行序列化并生成文件,最后反序列化通过网络返回给用户。
compile_server:
void Usage()
{
std::cout << "Please Enter 'port'" << std::endl;
exit(-1);
}
int main(int args, char *argv[])
{
//test();
if (args != 2)
{
Usage();
}
// 服务端
Server srv;
// 注册处理Post请求的函数
srv.Post("/compile_run", [](const Request &rqt, Response &rsp)
{
LOG(INFO) << "接收到编译运行请求" << std::endl;
// 提取请求正文
std::string in_json = rqt.body;
std::string out_json;
if (!in_json.empty())
{
// 调用cr模块
CompileAndRun::Start(in_json, out_json);
std::cout << "运行完毕, json: " << std::endl << out_json << std::endl;
// LOG(INFO) << "cr完成,正在构建响应报文并回复..." << std::endl;
// 构建响应正文内容
rsp.set_content(out_json, "application/json;charset=utf-8");
}
else
{
LOG(ERROR) << "请求正文为空" << std::endl;
rsp.set_content("request is empty...", "text/plain;charset=utf8");
}
});
// 绑定ip and 端口号
srv.listen("0.0.0.0", atoi(argv[1]));
}
oj_model模块
获取所有题目列表:
- 首先要有一个题目列表,在某个路径下,一定需要有一个文件夹(question)存放了每个题目文件
./question/questionlist
记录每个文件的属性,包括:
题目编号 题目标题 难度等级 时间限制 空间限制
例如:
1 两数之和 简单 10 300000
2 判断回文数 简单 10 200000
在question目录中,每一道题又都有自己的目录,以题目编号命名,例如:/question/1存放了题目的具体信息,包括:
- desc:题目描述
- header.cpp:题目预设代码,如:
#include <iostream>
class Solution
{
public:
static bool isPalindrome(int x)
{
// write...
}
};
显示给用户该代码,让用户实现该接口
- tail.cpp:添加测试用例,调用head.cpp中用户填写的接口,判断是否通过用例。
最后提交给compile_run模块的代码 = header.cpp + tail.cpp 拼接起来的代码:
#include <iostream>
class Solution
{
public:
static bool isPalindrome(int x)
{
// write...
}
};
//这三行是为了在编写的时候,包含了header.cpp中的接口,不让编译器报一堆红色波浪号,便于编写。
//在实际拼接时,这几行代码我们要去掉。
#ifndef COMPILE_ONLINE
#include "header.cpp"
#endif
void test1()
{
//121
if(Solution::isPalindrome(121))
{
std::cout << "通过测试用例, 测试用例: 121" << std::endl;
}
else
{
std::cout << "未通过测试用例, 测试用例: 121" << std::endl;
}
}
void test2()
{
//-121
if( !Solution::isPalindrome(-121))
{
std::cout << "通过测试用例, 测试用例: -121" << std::endl;
}
else
{
std::cout << "未通过测试用例, 测试用例: -121" << std::endl;
}
}
int main()
{
test1();
test2();
}
oj_view模块
了解ctemplate库的使用
渲染网页
#include <iostream>
using namespace std;
#include <ctemplate/template.h>
#include <string>
int main()
{
string in_html = "./test.html";
//创建数据字典
//dic是对象,test是字典的名字
ctemplate::TemplateDictionary dic("test"); //类比 unordered<> test;
string value = "你好,世界";
//设置kv值
dic.SetValue("key", value); //类比 test.insert({pair{}})
/***
* GetTemplate:
* 获取被渲染网页对象
* 参数1:要渲染的网页
* 参数2:选项,要不要去除空行、空格啥的?不要
*/
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP);
//添加字典数据到网页中
std::string out_html; //渲染后的网页
/**
* 参数1:输出性参数,返回渲染后的网页
* 参数2:把数据字典交给被渲染的网页,共同构成渲染后的网页
*/
tpl->Expand(&out_html, &dic);
cout << out_html;
}
- 在网页中, {{ }}两个花括号中的内容表示将来要被渲染的内容,填的是key值,最后被渲染时替换成value值
看下列代码:
{{#question_list}}
<tr>
<td>{{number}}</td>
<td>{{title}}</td>
<td>{{star}}</td>
</tr>
{{/question_list}}
每一个题目都需要被渲染,每一个题目就需要三列这样的属性,如果有1万道题,那么就要重复写一万个这样的渲染,这显然是很抵消的,所以有了循环渲染
{{#}} :#表示这是循环渲染的起点,后面紧跟着起点名(锚点名)
{{/}} :/表示这是循环渲染的终点,后面紧跟着锚点名(起点名 = 终点名)
//循环渲染
bool AllExpandHtml(std::vector<Question> &all, std::string &html)
{
if (all.size() == 0)
{
LOG(FATAL) << "准备渲染所有题目列表的网页,但题目列表为空" << std::endl;
exit(-1);
}
// 待渲染网页
std::string in_html = template_path + "all_question.html";
// 创建数据字典
ctemplate::TemplateDictionary dic("all_quetion");
for (auto &elm : all)
{
ctemplate::TemplateDictionary* sub = dic.AddSectionDictionary("question_list");
/**
* 创建子字典
* 子字典名称不能随意取,它决定最后循环渲染的是哪部分,
* 当然,如果你没有子字典,说明你不需要循环渲染,只有一个主字典
* 那就像上面说的,主字典怎么取都无所谓,因为只要进行一次kv映射就结束了,整个网页一共就一个要渲染的,那就不用区分
* */
sub->SetValue("number", elm.number);
sub->SetValue("title", elm.title);
sub->SetValue("star", elm.star);
//LOG(INFO) << "创建子字典成功" << std::endl;
}
循环渲染时,需要创建子字典:
-
主字典名字可以随便取,因为有待渲染的网页的路径,所以无所谓
-
子字典名称不能随意取。
循环渲染网页时需要指明子字典名称。所以html文件中渲染字典的名称必须和子字典名称相同。
当然,如果你没有子字典,不需要循环渲染,只有一个主字典
那就像上面说的,主字典怎么取都无所谓,因为只要进行一次kv映射就结束了,整个网页一共就一个要渲染的,那就不用区分。
oj_control模块
oj_server:
#include <iostream>
#include "../comm/httplib.h"
#include "../comm/util.hpp"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_util;
using namespace ns_control;
int main(int args, char* argv[])
{
Server svr;
Controller ctl; //控制器
/**
* 注册资源路由
* 获取所有题目列表
*
* */
svr.Get("/all_question", [&ctl](const Request &req, Response &resp)
{
std::string _html;
//LOG(INFO) << "申请所有题目列表:" << std::endl;
ctl.AllQuestion(_html);
resp.set_content(_html, "text/html;charset=utf-8");
});
/**
* 注册资源路由
* 获取指定题目
* \d 正则表达式,匹配多个数字
* */
svr.Get(R"(/question/(\d+))", [&ctl](const Request &req, Response &resp)
{
std::cout << "收到请求报文, url: " << req.path << std::endl;
std::string number = req.matches[1];
LOG(INFO) << "申请获取题目编号:" << number << std::endl;
std::string _html;
ctl.Question(number, _html);
resp.set_content(_html, "text/html;charset=utf-8"); });
/***
* 注册资源路由
* 提交代码, 测试用例
*/
svr.Post(R"(/judge/(\d+))", [&ctl](const Request &req, Response &resp)
{
// LOG(INFO) << "编译运行路由" << std::endl;
std::string number = req.matches[1];
std::string out_json;
ctl.Judge(number, req.body, out_json);
resp.set_content(out_json, "application/json;charset=utf-8");
});
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
}
svr.Post(R"(/judge/(\d+))", [&ctl](const Request &req, Response &resp)
{
// LOG(INFO) << "编译运行路由" << std::endl;
std::string number = req.matches[1];
std::string out_json;
ctl.Judge(number, req.body, out_json);
resp.set_content(out_json, "application/json;charset=utf-8");
});
(\d+):正则表达式
\d表示匹配数字,+表示一个或多个
合起来就是匹配一个或多个数字。
因为我们的题目编号,比如像 100,101,102…
如果写具体的数字,就把资源路由写死了,比如 /questions/100
,那注册的这个Get函数,只能用于处理路径/questions/100
中的资源。这样的话,如果有很多题,那么每一道题都需要注册一个Get函数处理,不现实。
std::string number = req.matches[1];
matches中保存了正则表达式的内容