当前位置:   article > 正文

C++项目 -- 负载均衡OJ(三)online_judge

C++项目 -- 负载均衡OJ(三)online_judge

C++项目 – 负载均衡OJ(三)online_judge


一、基于MVC结构的oj服务设计

1.结构与功能

该模块功能:

  1. 获取首页,用题目列表充当
  2. 编辑区域页面
  3. 提交判题功能(编译并运行)

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@'%';
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
登录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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 在Workbench中录题:
    在这里插入图片描述
    如果想只执行这一条语句,就选中然后执行;
    点击form editer开始录入:
    在这里插入图片描述
    录制完成后点apply;
    录制成功:
    在这里插入图片描述

3.3.引入MySQL链接库

MySQL版model模块

  • 如果系统中本身就有MySQL连接的库,就不需要再引入了:
    在这里插入图片描述
    如果系统只有动态库文件,没有devel(开发库文件),可以尝试用yum安装:
yum install -y mysql-community-devel
  • 1

安装好devel(开发库)后,我们只需要用 #include <mysql/mysql.h> 就可引入mysql库。
编译指令为:

g++ -o oj_server oj_server.cc -std=c++11 -L/usr/lib64/mysql/ -lmysqlclient
  • 1
  • 如果系统中没有,就需要自己安装:
    MySQL官网下载:
    在这里插入图片描述
    导入服务器并解压:
    在这里插入图片描述
    重命名:
    在这里插入图片描述
    在oj_server目录下引入软链接:
    在这里插入图片描述
  • 如果在运行时发现找不到MySQL的库:
    在这里插入图片描述
    在这里插入图片描述
    将库所在的路径写入该配置文件中,这样运行时系统就知道去哪里寻找库了:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.4.在oj_model中访问连接数据库

oj_server是基于MVC实现的,和数据打交道的只有oj_mode模块,只需要更改该部分代码即可

  • QueryMySQL函数用于执行查询sql语句,并将查询结果封装成Question插入到out中;
  • 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_html
    string out_html;
    tpl->Expand(&out_html, &root);

    cout << "渲染的带参html是:" << endl;
    cout << out_html << endl;

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

源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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

渲染后的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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
<!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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • AllExpandHtml函数将读取到的所有题目信息都渲染到网页上;
    • 在创建了root根目录后,再向根目录中添加子目录,用于替换question_list中的内容;
      在这里插入图片描述
      可以将html中{{#question_list}}修饰的所有内容循环渲染;
      在题目的title处加上链接,可以跳转到这道题的做题界面;
  • 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.cc
	g++ -o $@ $^ -std=c++11 -L/usr/lib64/mysql/ -lpthread -ljsoncpp -lctemplate -lmysqlclient

.PHONY:clean
clean:
	rm -f oj_server
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

首页:
在这里插入图片描述
题目列表:
在这里插入图片描述
指定题目:
在这里插入图片描述
判题:
在这里插入图片描述

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/448452
推荐阅读
  

闽ICP备14008679号