C++项目 – 负载均衡OJ(三)online_judge
文章目录
- C++项目 -- 负载均衡OJ(三)online_judge
- 一、基于MVC结构的oj服务设计
- 1.结构与功能
- 二、oj_model.hpp
- 1.建立文件版题库
- 2.文件版题库的服务模块
- 3. MySQL版题库
- 3.1.创建名为oj_client的用户,创建数据库oj,并给oj_client赋权
- 3.2. 设计表结构
- 3.3.引入MySQL链接库
- 3.4.在oj_model中访问连接数据库
- 三、oj_view.hpp
- 1.安装与测试ctemplate库
- 2.view模块编写
- 四、oj_control.hpp
- 1.负载均衡模块
- 2.Control模块
- 五、oj_server.cc
- 1.Makefile
- 2.设置用户请求的服务路由功能
- 3.构建正式的OJ功能
- 4.形成正式的oj_server
- 5.前端界面
- 6.测试
- 六、编写顶层makefile
一、基于MVC结构的oj服务设计
1.结构与功能
该模块功能:
- 获取首页,用题目列表充当
- 编辑区域页面
- 提交判题功能(编译并运行)
MVC结构:
- M: Model,通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL)
- V: view, 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
- C: control, 控制器,就是我们的核心业务逻辑
二、oj_model.hpp
这是和数据交互的模块,对外提供访问数据的接口;
1.建立文件版题库
题目的信息包括:
- 题目的编号
- 题目的标题
- 题目的难度
- 题目的描述,题面
- 时间要求(内部处理)
- 空间要求(内部处理)
两批文件构成
- questions.list : 题目列表(不需要题目的内容)
所有的题目都存放在questions路径下
- 题目的描述(
desc.txt
),题目的预设置代码(header.cpp
), 测试用例代码(tail.cpp
)
测试用例tail.cpp
为了在实际编译的时候文件中没有#include “header.hpp”,需要在编译服务调用g++的时候,后面加上一个编译选项-D COMPILER_ONLINE
:
这两个内容是通过题目的编号,产生关联的
2.文件版题库的服务模块
文件版model模块
- 当用户提交代码后,OJ是将用户写好的
header.cpp
拼接上题号对应的测试用例tail.cpp形成新的源代码,并发送到compile_and_run模块运行,运行结果返回给用户 - 测试用例中的条件编译不想让编译器编译的时候,保留它,而是裁剪掉(
g++ -D COMPILER_ONLINE
) - 根据题目list文件,加载所有的题目信息道内存中;
- 题目的所有信息由一个结构体类型存储;
Model
类中,使用哈希表保存题号与题目信息的映射;LoadQuestionList
函数用于加载所有的题目信息道内存中,以哈希表的形式;GetAllQuestions
用于获取所有的题目信息;GetOneQuestion
用于获取指定题目信息;
3. MySQL版题库
3.1.创建名为oj_client的用户,创建数据库oj,并给oj_client赋权
mysql> use mysql
mysql> create user oj_client@'%' identified by 'password';
mysql> create database oj;
mysql> grant all on oj.* to oj_client@'%';
登录oj_client用户,可以看到oj数据库
3.2. 设计表结构
- 使用MySQLWorkbench来进行图形化界面建表:
创建与服务器MySQL的连接:
连接上,在oj数据库创建oj_questions表:
create table if not exists `oj_questions` (`number` int primary key auto_increment comment '题目的编号',`title` varchar(128) not null comment '题目的标题',`star` varchar(8) not null comment '题目的难度',`desc` text not null comment '题目的描述',`header` text not null comment '题目预设给用户的代码',`tail` text not null comment '题目的测试用例代码',`cpu_limit` int default 1 comment '题目的cpu运行时间限制',`mem_limit` int default 50000 comment '题目的内存空间限制'
)engine=InnoDB default charset=utf8;
- 在Workbench中录题:
如果想只执行这一条语句,就选中然后执行;
点击form editer开始录入:
录制完成后点apply;
录制成功:
3.3.引入MySQL链接库
MySQL版model模块
- 如果系统中本身就有MySQL连接的库,就不需要再引入了:
如果系统只有动态库文件,没有devel(开发库文件),可以尝试用yum安装:
yum install -y mysql-community-devel
安装好devel(开发库)后,我们只需要用 #include <mysql/mysql.h>
就可引入mysql库。
编译指令为:
g++ -o oj_server oj_server.cc -std=c++11 -L/usr/lib64/mysql/ -lmysqlclient
- 如果系统中没有,就需要自己安装:
MySQL官网下载:
导入服务器并解压:
重命名:
在oj_server目录下引入软链接:
- 如果在运行时发现找不到MySQL的库:
将库所在的路径写入该配置文件中,这样运行时系统就知道去哪里寻找库了:
3.4.在oj_model中访问连接数据库
oj_server是基于MVC实现的,和数据打交道的只有oj_mode模块,只需要更改该部分代码即可;
QueryMySQL
函数用于执行查询sql语句,并将查询结果封装成Question插入到out中;- 关于MySQL Connector C中的接口作用,见博客MySQL Connection C中的API介绍
GetAllQuestions
用于向MySQL发出查询所有题目的指令;GetOneQuestion
用于向MySQL发出查询单个题目的指令;
三、oj_view.hpp
这是构建网页的模块;
1.安装与测试ctemplate库
ctemplate库的github仓库
这是谷歌开源的cpp网页渲染库;
在ctemplate中数据是以字典的格式存放的
待渲染的网页中写入的是数据的key值,渲染之后将key换成对应的value;
测试网页渲染功能:
- html中待替换的key值需要使用
{{key}}
TemplateDictionary root
是建立ctemplate参数目录结构,相当于unordered_map<string, string> test;
root.SetValue
向目录中添加你要替换的数据,kv的,相当于test.insert({ket, value});
GetTemplate
获取待渲染对象,DO_NOT_STRIP
是指保持html网页原貌;tpl->Expand
开始渲染,替换字典中的kv,返回新的网页结果到out_html;
#include <iostream>
#include <string>
#include <ctemplate/template.h>using namespace std;int main()
{//html网页的地址string html = "./test.html";string html_info = "lmx_xdu";//建立ctemplate参数目录结构//相当于 unordered_map<string, string> test;ctemplate::TemplateDictionary root("test"); //向目录中添加你要替换的数据,kv的//第一个参数是key,第二个参数是value,将html中的key全部替换为value//相当于test.insert({ket, value});root.SetValue("info", html_info);//获取待渲染对象//DO_NOT_STRIP是指保持html网页原貌ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html, ctemplate::DO_NOT_STRIP);//开始渲染,替换字典中的kv,返回新的网页结果到out_htmlstring out_html;tpl->Expand(&out_html, &root);cout << "渲染的带参html是:" << endl;cout << out_html << endl;return 0;
}
源html网页:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><!--渲染参数,会被我们C++代码中的数据替换, info就是上面SetValue("info", html_info)代码中的info,会自动被std::string info_html中的内容替换--><p>{{info}}</p><p>{{info}}</p><p>{{info}}</p><p>{{info}}</p>
</body>
</html>
渲染后的html网页:
2.view模块编写
view模块代码
View类用于网页的渲染;
- 待渲染的网页模板在
/template_html
路径下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><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><a href="/question/{{number}}">{{title}}</a></td><td>{{star}}</td></tr>{{/question_list}}</table>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><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" cols="100" rows="50">{{pre_code}}</textarea></body>
</html>
AllExpandHtml
函数将读取到的所有题目信息都渲染到网页上;- 在创建了root根目录后,再向根目录中添加子目录,用于替换question_list中的内容;
可以将html中{{#question_list}}
修饰的所有内容循环渲染;
在题目的title处加上链接,可以跳转到这道题的做题界面;
- 在创建了root根目录后,再向根目录中添加子目录,用于替换question_list中的内容;
OneExpandHtml
用于单个题目信息的渲染;
四、oj_control.hpp
这是业务的核心逻辑模块;
1.负载均衡模块
负载均衡模块用于帮助Control选取负载最低的编译服务器,所有编译服务器的配置信息都在以下文件中:
Machine
类用于保存每台编译服务器的具体信息,一个编译服务对应一个Machine
:
- 包括ip、端口、负载以及每台服务器的锁;
- 由于mutex禁止拷贝,因此使用指针;
LoadBalance
类用于实现负载均衡算法:
- 类中保存所有服务器的类
Machine
,记录所有上线和下线的主机,并且有一把锁保证LoadBalance的数据安全; LoadConf
函数用于将配置文件中所有的服务器信息全部读取并保存;SmartChoice
函数用于根据所有上线服务器的负载信息,选择负载最低的机器;- 负载均衡的算法有:1.随机数+hash;2.轮询+hash;
这里选取轮询+hash,通过遍历的方式,找到所有负载最小的机器; OfflineMachine
函数用于将指定的主机离线;OnlineMachine
函数用于上线所有已离线的主机,是将所有_offline
中的主机全部插入到_online
中,并删除_offline
中的主机;
2.Control模块
Control模块代码
Control类用于根据Model类中获取的题目数据,来调用View类中的方法构建OJ网页;
RecoveryMachine
用于将所有的离线主机恢复为上线模式;AllQuestions
使用Model模块获取所有的题目信息,再通过View模块将题目信息渲染到网页上;Question
根据指定题目构建网页;Judge
实现判题功能,步骤如下:- 根据题目编号,拿到对应的题目细节;
in_json
进行反序列化,得到题目的id,得到用户提交的源代码,input输入参数;
重新拼接用户代码+测试用例代码,形成新的代码;
选择负载最低的主机;规则:一直选择,直到主机可用,否则,就是全部挂掉;
然后发起http请求,得到结果;
将结果赋值给out_json; - Result中定义了bool类型强转,因此Result可以直接放在if语句里,如果返回值存在就会返回true;
- Post请求:第一个参数是请求,第二个参数是请求的参数,第三个参数是请求的类型;
cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")
- Post请求的返回值是Result对象
成员res_是Response的指针,Result重载了->,能够直接访问到Response;
Response中的成员有statue状态码,其值等于200才证明这个http请求是成功的;
- 根据题目编号,拿到对应的题目细节;
五、oj_server.cc
1.Makefile
oj_server:oj_server.ccg++ -o $@ $^ -std=c++11 -L/usr/lib64/mysql/ -lpthread -ljsoncpp -lctemplate -lmysqlclient.PHONY:clean
clean:rm -f oj_server
2.设置用户请求的服务路由功能
- \d+是正则表达式,能够匹配到用户输入的所有数字;
\d代表数字,+代表一个或多个;
正则匹配到的内容会存放在Request类中的matchs里面;
question/100 ->正则匹配 R"()"
,原始字符串,保持字符串内容的原貌,不用做相关的转义;- 设置首页为wwwroot,在其中添加html网页(vdcode中!+Tab可以生成网页骨架)
#include <iostream>
#include "../Comm/httplib.h"
#include "../Comm/util.hpp"using namespace httplib;int main()
{//用户请求的服务路由功能Server svr;//获取所有题目列表svr.Get("/all_questions", [](const Request &req, Response &resp){resp.set_content("这是所有题目的列表", "text/plain; charset=utf-8");}); //用户要根据题目编号,获取题目的内容//question/100 ->正则匹配//R"()",原始字符串,保持字符串内容的原貌,不用做相关的转义svr.Get(R"(/question/(\d+))", [](const Request &req, Response &resp){string number = req.matches[1];resp.set_content("这是指定的一道题: " + number, "text/plain; charset=utf-8");});//用户提交代码,使用我们的判题功能(1.每道题的测试用例 2.compile_and_run)svr.Get(R"(/judge/(\d+))", [](const Request &req, Response &resp){string number = req.matches[1];resp.set_content("指定题目的判题: " + number, "text/plain; charset=utf-8");});svr.set_base_dir("./wwwroot");svr.listen("0.0.0.0", 8080);return 0;
}
首页:
题目列表:
指定题目:
判题:
3.构建正式的OJ功能
- 通过Control模块获取由所有题目信息构建的网页,形成网页服务
set_content
中的格式设置为html;
- 通过Control模块获取单个题目信息构建的网页,形成网页服务
首页:
题目列表:
做题界面:
4.形成正式的oj_server
oj_server代码
添加Control对象,实现加载题目和判题功能的请求:
- 通过捕捉信号上线所有主机:
通过捕捉ctrl + \信号,触发时调用Recovery
重新上线所有主机;
使用PostMan进行测试:
- 创建三个compile_server服务,端口号都是基于配置文件
service_machine.conf
中的:
使用PostMan进行Post请求,请求的文本形式为json,代码内容为无法运行的初始代码,返回的内容中有报错信息:
5.前端界面
wwwroot首页
template_html界面
index.html
all_questions.html
one_question.html
这部分包含前后端交互:
- 提交给Judge功能判题时,需要的in_json内容主要有input和code;
submit
函数用于获取页面上的题目信息,形成请求url,并通过ajax向后台发起基于http的json请求:show_result
函数用于得到结果,解析并显示到 result中
6.测试
负载均衡测试
每次都会选择负载最低的机器运行;
主机离线上线测试:
所有主机离线后,再次上线,触发ctrl + c信号,就可以上线所有主机;
六、编写顶层makefile
顶层makefile用于项目的编译、清理和发布;
- 语句前面加@是在运行时不显示这条语句;
- 项目的发布:将生成的可执行程序和需要的库文件、网页文件等全部复制到output路径下;
.PHONY: all
all :
#编译@cd compiler_server;\make;\cd -;\cd online_judge;\make;\cd -;#项目发布
.PHONY : output
output :@mkdir -p output/compiler_server;\mkdir -p output/online_judge;\cp -rf compiler_server/compile_server output/compiler_server;\cp -rf compiler_server/temp output/compiler_server;\cp -rf online_judge/conf output/online_judge;\cp -rf online_judge/questions output/online_judge;\cp -rf online_judge/template_html output/online_judge;\cp -rf online_judge/wwwroot output/online_judge;\cp -rf online_judge/oj_server output/online_judge;#项目清理
.PHONY : clean
clean :@cd compiler_server;\make clean;\cd -;\cd online_judge;\make clean;\cd -;\rm -rf output;