实战项目: 负载均衡
0. 前言
这个项目使用了前后端,实现一个丐版的LeetCode刷题网站,并根据每台主机的实际情况,选择对应的主机,负载均衡的调度
0.1 所用技术与开发环境
所用技术:
C++ STL
标准库Boost 准标准库
(
字符串切割
)cpp-
httplib
第三方开源网络库ctemplate 第三方开源前端网页渲染库
jsoncpp 第三方开源序列化、反序列化库
负载均衡设计
多进程、多线程
MySQL C connect
Ace前端在线编辑器
(
部分
)html/css/js/jquery/ajax (部分
)
开发环境:
Centos 7
云服务器,
vscode,
Mysql Workbench
0.2 建立目录及文件
0.3 项目宏观结构
- 具体的功能类似 leetcode 的题目列表+在线编程功能
1. compile 服务设计
- 由于compiler这个模块管理的是编译与运行,则可以先直接就创建所需要的文件
1.0 书写makefile文件
- 随着后续代码的跟进,并不断引入第三方库,这里还会新增编译选项
1.1 compiler_server
1.1.0 编译功能(compiler.hpp)
- 在编译的时候,无非存在2种情况,a)要么通过,b)要么不通过
- 要确定编译通过:
只需要确定是否生成对应的.exe文件 - 要当编译出错的时候(stderr):
需要将出错信息,重定向到一个临时文件中,保存编译出错的结果
还需要调用fork();子进程完成编译工作
父进程继续执行
- 由于需要频繁的文件名转换,所以在comm模块中,新建util.hpp文件并将文件名转换的函数放在一起
- 还有后面判断编译成功生成的可执行程序,虽然可以直接暴力的打开文件判断是否存在,但这里使用stat函数会好一些
- stat结构体会记录文件的各种信息
- 注意: 程序替换是不会影响进程的文件符描述符表的
1.1.1 日志模块(log.hpp)
由于一般日志都会带上时间, 这里还需要实现一个得到当前时间的函数,则我又在util.hpp把得到时间函数的类封装成了一个类
由于会频繁的调用日志进行打印信息,也为了更简便的调用,我进行了以下处理
- 如果在宏定义中使用#,那么这个宏就被称为带有字符串化操作的宏。这种宏可以将其参数转换成字符串常量,并在预处理阶段进行替换。
由于引入了日志,则就可以把之前所有的输出信息,换成日志输出
1.1.2 测试编译模块
- Compile的参数是文件名,它内部会自动拼接
- 我们还需要再./temp中创建一个code.cpp文件
- 上面我的代码有一个错误,在编译成功的时候,并没有return,导致LOG日志打印有问题
- 要是我们的源文件有问题,错误信息就会重定向到 文件.compile_error中
- 在测试的时候,还需要把 文件.exe 文件.compile_error文件删除,就是上次生成的文件
1.1.3 运行功能(runner.hpp)
程序运行: 1)代码跑完,结果正确 2)代码跑完,结果不正确, 3)代码没跑完,异常了
程序结果是否正确,是由oj_server中的测试用例决定的,则run模块只考虑是否正确运行完毕
#include
#include
#include
#include
#include
#include // fork接口需要
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner
{
using namespace ns_util;
using namespace ns_log;
class Runner
{
public:
Runner(){}
~Runner(){}
static int Run(const std::string &file_name){
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 _stdin_fd = open(_stdin.c_str(),O_CREAT | O_RDONLY,0644);
int _stdout_fd = open(_stdout.c_str(),O_CREAT | O_WRONLY,0644);
int _stderr_fd = open(_stderr.c_str(),O_CREAT | O_WRONLY,0644);
if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){
LOG(ERROR) << "运行时打开标准文件失败" << "
";
return -1;// 代表打开文件失败
}
pid_t pid = fork();
if(pid < 0){
LOG(ERROR) << "运行创建子进程失败" << "
";
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
return -2;// 代表创建自己失败
}
else if(pid == 0){
// 子进程
dup2(_stdin_fd,0);
dup2(_stdout_fd,1);
dup2(_stderr_fd,2);
LOG(INFO) << "123";// 是不是有问题啊
// 这个程序替换等价于 ./tmp/code.exe ./tmp/code.exe
execl(_execute.c_str(),_execute.c_str(),nullptr);
exit(1);
}
else{
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
int status = 0;// 表示输出型参数
waitpid(pid,&status,0);// 阻塞式等待
// 程序运行异常,一定是因为收到信号
LOG(INFO) << "运行完毕,infor: " << (status & 0x7f) << "
";
return status & 0x7f;
}
}
};
}
返回值 > 0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号
返回值 == 0: 正常运行完毕的,结果保存到了对应的临时文件中
返回值 < 0: 内部错误- run.hpp也是一样的,把自己的各种输出信息,输出到一个临时文件中
- 要判断一个程序是否异常,只需要看它是否收到了异常信号
解释waitpid第2个输出型参数
- status并不是按照整数来整体使用的,而是按照比特位的方式,将32个比特位进行划分,只需要学习低16位
- 这也是上面为什么会写成status & 0x7F的原因
那6个程序替换的系统接口,具体使用那个看实际情况
- 没有p就需要带路径
- 有l,就是列表式传命令
- 有v就是数组式传命令
- 有e就需要传自己设置的环境变量
1.1.4 测试运行模块
- 虽然运行模块已经能正常运行了,但是万一code.cpp是恶意程序了,比如死循环,不停消耗CPU资源 , 所以还需要进一步的资源约束
1.1.5 添加资源限制(setrlimit)
- 资源不足,导致OS终止进程,是通过信号终止的
- 为了方便上层调用,我直接在Run函数中增加了cpu_limit和mem_limit形参
这个项目走到这里就需要编写compile_run.hpp,将编译和运行的逻辑连接在一起,且code.cpp需要被处理的源文件,不应该是我们自己添加的,而是需要再客户端中导入的
1.1.6 编译 && 运行功能 (compile_run.hpp)
这个模块要做的是:
a)适配用户请求,引入json定制通信协议字段
b)形成唯一文件名
c)正确调用compile and run
在centos中安装: sudo yum install json-c-devel
头文件 #include
- 注意: 在编译引入了json的文件,需要加上-ljsoncpp
- 虽然这个code就是文件名了,但client可能会提交大量的代码,所以内部就会需要形成唯一的文件名(待完善)
- 还有很多个出错问题怎么解决(待完善)
complie_run.hpp
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include
namespace ns_compile_and_run
{
using namespace ns_log;
using namespace ns_util;
using namespace ns_compiler;
using namespace ns_runner;
// in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
// out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
static void Start(const std::string &in_json,std::string *out_json){
// step1: 反序列化过程
Json::Value in_value;
Json::Reader reader;
// 把in_json中的数据写到in_value中
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;
std::string file_name;// 唯一文件名
if(code.size() == 0){
status_code = -1;// 代码为空
goto END;
}
// 形成的文件名只居有唯一性,没有目录没有后缀
// 使用: 毫秒级时间戳 + 原子性递增唯一值 : 来保证唯一性
file_name = FileUtil::UniqFileName();
// 形成临时的src文件
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 = run_result;// 这里的run_result是信号
}
else{
// 运行成功
status_code = 0;
}
END:
out_value["status"] = status_code;
out_value["reason"] = CodeToDest(status_code,file_name);// 得到错误信息字符串
if(status_code == 0){
// 整个过程全部成功
std::string _stdout;
FileUtil::ReadFile(PathUtil::Stdout(file_name),&_stdout,true);
out_value["stdout"] = _stdout;
std::string _stderr;
FileUtil::ReadFile(PathUtil::Stdout(file_name),&_stderr,true);
out_value["stdout"] = _stdout;
}
// step2: 序列化
Json::StyledWriter writer;
*out_json = writer.write(out_value);
}
}
1.1.7 基于compile_run.hpp对util.hpp的补充
- 注意引入流时需要引入头文件: #include
getline:不保存行分割符,有些时候需要保留
,getline内部重载了强制类型转化
1.1.8 测试编译运行模块
#include "compile_run.hpp"
using namespace ns_compile_and_run;
int main()
{
std::string in_json;
Json::Value in_value;
// R"()", raw string
in_value["code"] = R"(#include
int main(){
std::cout << "你可以看见我了" << std::endl;
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;
}
- 实际上这里的代码应该是client自动提交给我们的,我们直接使用第三方库就行了
- 待优化: 可以把临时生成的这些文件都清理掉,
1.1.9 清理临时文件
- 这个函数直接放在compile_server.cc中的start函数的最后,清理临时文件
1.1.10 引入cpp-httplib 网络库
下载地址: cpp-httplib: C++ http 网络库 – Gitee.com
- 这个就是别人写好的网络库,我们直接使用就行了
1.1.11 更新gcc
安装scl : sudo yum install centos-release-scl scl-utils-build
安装新版本gcc:
sudo yum install
–
y devtoolset
–
9
–
gcc devtoolset
–
9
–
gcc
–
c
++
- 把 scl enable devtoolset–9 bash 放在 ~/.bash_profile中
- 想每次登陆的时候,都是较新的gcc
如果不更新在使用cpp-httplib时可能会报错, 用老的编译器,要么编译不通过,要么直接运行报错
1.1.12 测试cpp-httplib网络库
- 可能会出现服务器的公网ip无法访问的问题,可以试试把防火墙关闭,并打开端口
1.1.13 将compiler_server打包成网络服务
compiler_server.cc
#include "compile_run.hpp"
#include "../comm/httplib.h"// 引入
using namespace ns_compile_and_run;
using namespace httplib;// 引入
void Usage(std::string proc){
std::cerr << "Usage: " << "
" << proc << " port" << std::endl;
}
//./copile_server port
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 & resp){
// 用户请求的服务正文是我们想要的json string
std::string in_json = req.body;
std::string out_json;
if(!in_json.empty()){
CompileAndRun::Start(in_json,&out_json);
resp.set_content(out_json,"application/json;charset=uft-8");
}
});
svr.listen("0.0.0.0",atoi(argv[1]));
return 0;
}
- 由于我这里没有写客户端代码,则这里暂时不好测试,不过可以借助第三方工具进行测试
2. oj_server服务设计
本质:
建立一个小型网站
1. 获取首页,用题目列表充当
2. 编辑区域页面
3. 提交判题功能(编译并运行)
2.1 书写makefile文件
- 随着后续代码的跟进,并不断引入第三方库,这里还会新增编译选项
2.2 服务路由功能(oj_server.cc)
- 为用户实现的路由功能就3个 a. 获取所有的题目列表 b.根据题目编号,获取题目内容 c.判断用户提交的代码
2.3 MVC 结构的oj 服务设计(M)
Model
,
通常是和数据交互的模块
,比如,对题库进行增删改查(文件版,
MySQL
)
2.3.1 安装boost库 && 字符切分功能
sudo yum install –y boost–devel //是boost 开发库
- 第一个参数为缓冲区,第二个参数为被分割的字符串
- 第三个参数为分割符,第四个参数为是否压缩
- 要压缩: 当sep = “空格”时,sepsepsep -> 空格
- 不压缩: 当sep = “空格”时,sepsepsep -> 空格空格空格
2.3.2 数据结构
header.cpp
#include
#include
#include
#include
tail.cpp
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
// 这里先把测试用例 暴露出来
void Test1()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(121);
if(ret){
std::cout << "通过用例1, 测试121通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例1, 测试的值是: 121" << std::endl;
}
}
void Test2()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(-10);
if(!ret){
std::cout << "通过用例2, 测试-10通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例2, 测试的值是: -10" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
- des.txt表示题目信息
- header.cpp表示预设代码
- tail.cpp表示测试用例
- 真正代码 = 用户在head.cpp中的代码 + header.cpp + tail.cpp 并去到COMPILER_ONLINE
- 这个条件编译只是为了编写tail.cpp时不报错
2.3.3 model功能(oj_model.cpp)
数据交互 && 提供接口
#pragma once
// 文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include
#include
#include
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question{
string number;// 题目编号,唯一
string tile;// 题目标题
string star;// 难度: 简单 中等 困难
int cpu_limit;// 题目的时间复杂度(S)
int mem_limit;// 题目的空间复杂度(KB)
string desc;// 题目描述
string header; // 题目预设给用户在线编辑器的代码
string tail;// 题目测试用例,需要和header拼接
};
const string questions_list = "./question/quetions.list";
const string questions_path = "./question";
class Model
{
public:
Model(){
// 加载所有题目:底层是用hash表映射的
assert(LoadQuestionList(questions_list));
}
~Model(){
;
}
// 获取所有题目,这里的out是输出型参数
bool GetAllQuestions(vector*out){
if(questions.size() == 0){
LOG(ERROR) << "用户获取题库失败" <push_back(q.second);
}
return true;
}
// 获取指定题目,这里的q是输出型参数
bool GetOneQuestion(const string& number,Question* q){
const auto& iter = questions.find(number);
if(iter == questions.end()){
LOG(ERROR) << "用户获取题目失败,题目编号: " << number <second;
return true;
}
// 加载配置文件: questions/questions.list + 题目编号文件
bool LoadQuestionList(const string&question_list){
// 加载配置文件: questions/questions.list +题目编号文件
ifstream in(question_list);
if(!in.is_open()){
LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "
";
return false;
}
string line;
while(getline(in,line)){
vectortokens;
StringUtil::SplitString(line,&tokens," ");// 被分割的字符串 缓冲区 分割符
// eg: 1 判断回文数 简单 1 30000
if(tokens.size()!=5){
LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "
";
continue;
}
Question q;
q.number = tokens[0];
q.tile = tokens[1];
q.star = tokens[2];
q.cpu_limit = atoi(tokens[3].c_str());
q.mem_limit = atoi(tokens[4].c_str());
string path = questions_list;
path += q.number;
path += "/";
// 第三个参数代表 是否加上
FileUtil::ReadFile(path+"desc.txt",&(q.desc),true);
FileUtil::ReadFile(path+"header.cpp",&(q.header),true);
FileUtil::ReadFile(path+"tail.txt",&(q.tail),true);
questions.insert({q.number,q});// 录题成功
}
LOG(INFO) << "加载题库...成功" << "
";
in.close();
}
private:
// 题号 : 题目细节
unordered_map questions;
};
}
2.4 MVC 结构的oj 服务设计(C)
2.4.1 负载均衡模块
namespace ns_control
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
using namespace httplib;
// 提供服务的主机
class Machine
{
public:
std::string ip; //编译服务的ip
int port; //编译服务的port
uint64_t load; //编译服务的负载
std::mutex *mtx; // mutex禁止拷贝的,使用指针
public:
Machine() : ip(""), port(0), load(0), mtx(nullptr)
{
}
~Machine()
{
}
public:
// 提升主机负载
void IncLoad()
{
if (mtx) mtx->lock();
++load;
if (mtx) mtx->unlock();
}
// 减少主机负载
void DecLoad()
{
if (mtx) mtx->lock();
--load;
if (mtx) mtx->unlock();
}
void ResetLoad()
{
if(mtx) mtx->lock();
load = 0;
if(mtx) mtx->unlock();
}
// 获取主机负载,没有太大的意义,只是为了统一接口
uint64_t Load()
{
uint64_t _load = 0;
if (mtx) mtx->lock();
_load = load;
if (mtx) mtx->unlock();
return _load;
}
};
const std::string service_machine = "./conf/service_machine.conf";
class LoadBlance
{
private:
// 可以给我们提供编译服务的所有的主机
// 每一台主机都有自己的下标,充当当前主机的id
std::vector machines;
// 所有在线的主机id
std::vector online;
// 所有离线的主机id
std::vector offline;
// 保证LoadBlance它的数据安全
std::mutex mtx;
public:
LoadBlance()
{
assert(LoadConf(service_machine));
LOG(INFO) << "加载 " << service_machine << " 成功"
<< "
";
}
~LoadBlance()
{
}
public:
bool LoadConf(const std::string &machine_conf)
{
std::ifstream in(machine_conf);
if (!in.is_open())
{
LOG(FATAL) << " 加载: " << machine_conf << " 失败"
<< "
";
return false;
}
std::string line;
while (std::getline(in, line))
{
std::vector tokens;
StringUtil::SplitString(line, &tokens, ":");
if (tokens.size() != 2)
{
LOG(WARNING) << " 切分 " << line << " 失败"
<< "
";
continue;
}
Machine m;
m.ip = tokens[0];
m.port = atoi(tokens[1].c_str());
m.load = 0;
m.mtx = new std::mutex();
online.push_back(machines.size());
machines.push_back(m);
}
in.close();
return true;
}
// id: 输出型参数
// m : 输出型参数
bool SmartChoice(int *id, Machine **m)
{
// 1. 使用选择好的主机(更新该主机的负载)
// 2. 我们需要可能离线该主机
mtx.lock();
// 负载均衡的算法
// 1. 随机数+hash
// 2. 轮询+hash
int online_num = online.size();
if (online_num == 0)
{
mtx.unlock();
LOG(FATAL) << " 所有的后端编译主机已经离线, 请运维的同事尽快查看"
<< "
";
return false;
}
// 通过遍历的方式,找到所有负载最小的机器
*id = online[0];
*m = &machines[online[0]];
uint64_t min_load = machines[online[0]].Load();
for (int i = 1; i curr_load)
{
min_load = curr_load;
*id = online[i];
*m = &machines[online[i]];
}
}
mtx.unlock();
return true;
}
void OfflineMachine(int which)
{
mtx.lock();
for(auto iter = online.begin(); iter != online.end(); iter++)
{
if(*iter == which)
{
machines[which].ResetLoad();
//要离线的主机已经找到啦
online.erase(iter);
offline.push_back(which);
break; //因为break的存在,所有我们暂时不考虑迭代器失效的问题
}
}
mtx.unlock();
}
void OnlineMachine()
{
//我们统一上线,后面统一解决
mtx.lock();
online.insert(online.end(), offline.begin(), offline.end());
offline.erase(offline.begin(), offline.end());
mtx.unlock();
LOG(INFO) << "所有的主机有上线啦!" << "
";
}
//for test
void ShowMachines()
{
mtx.lock();
std::cout << "当前在线主机列表: ";
for(auto &id : online)
{
std::cout << id << " ";
}
std::cout << std::endl;
std::cout << "当前离线主机列表: ";
for(auto &id : offline)
{
std::cout << id << " ";
}
std::cout << std::endl;
mtx.unlock();
}
};
}
2.4.1 control功能(oj_control.hpp)
逻辑控制模块
// 这是我们的核心业务逻辑的控制器
class Control
{
private:
Model model_; //提供后台数据
View view_; //提供html渲染功能
LoadBlance load_blance_; //核心负载均衡器
public:
Control()
{
}
~Control()
{
}
public:
void RecoveryMachine()
{
load_blance_.OnlineMachine();
}
//根据题目数据构建网页
// html: 输出型参数
bool AllQuestions(string *html)
{
bool ret = true;
vector all;
if (model_.GetAllQuestions(&all))
{
sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
});
// 获取题目信息成功,将所有的题目数据构建成网页
// ...
}
else
{
*html = "获取题目失败, 形成题目列表失败";
ret = false;
}
return ret;
}
bool Question(const string &number, string *html)
{
bool ret = true;
struct Question q;
if (model_.GetOneQuestion(number, &q))
{
// 获取指定题目信息成功,将所有的题目数据构建成网页
// ....
}
else
{
*html = "指定题目: " + number + " 不存在!";
ret = false;
}
return ret;
}
// code: #include...
// input: ""
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
}
};
- control模块中的判题功能,我打算最后设计
2.5 MVC 结构的oj 服务设计(V)
2.4 安装与测试 ctemplate(网页渲染)
渲染本质就是key-value之间的替换
- 安装镜像源: git clone https://gitee.com/mirrors_OlafvdSpek/ctemplate.git
.
/
autogen
.
sh.
/
configuremake
//
编译 如果报错请
更新gccmake install
test.cpp
#include
#include
#include
int main()
{
std::string html = "./test.html";
std::string html_info = "测试ctemplate渲染";
// 建立ctemplate参数目录结构
ctemplate::TemplateDictionary root("test"); // unordered_map test;
// 向结构中添加你要替换的数据,kv的
root.SetValue("info", html_info); // test.insert({key, value});
// 获取被渲染对象
// DO_NOT_STRIP:保持html网页原貌
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html,ctemplate::DO_NOT_STRIP);
// 开始渲染,返回新的网页结果到out_html
std::string out_html;
tpl->Expand(&out_html, &root);
std::cout << "渲染的带参html是:" << std::endl;
std::cout << out_html << std::endl;
return 0;
}
test.html
Document
{{info}}
{{info}}
{{info}}
{{info}}
错误原因: error while loading shared libraries: libmpc.so.3: cannot open shared object file
export LD\_LIBRARY\_PATH=$LD\_LIBRARY\_PATH:/usr/local/lib
- 在命令行上输入 上面这段命令,注:只在当前会话中有效
# cat /etc/ld.so.conf
include ld.so.conf.d/*.conf
# echo “/usr/local/lib” >> /etc/ld.so.conf
# ldconfig
2.5.2 渲染功能(oj_view.hpp)
#pragma once
#include
#include
#include "./oj_model.hpp"
namespace ns_view
{
using namespace ns_model;
const std::string template_path = "./template_html/";
class View
{
public:
View(){}
~View(){}
// 渲染所有题目
void ALLExpandHtml(const vector&question,std::string *html){
// 题目编号 题目标题 题难度
// 推荐表格实现
// 1.形成路径
string src_html = template_path + "all_quetions.html";
// 2.形成数字典
ctemplate::TemplateDictionary root("all_question");
for(const auto& q: question){
ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
sub->SetValue("number",q.number);
sub->SetValue("title",q.title);
sub->SetValue("star",q.star);
}
// 3. 获取被渲染的html
ctemplate::Template*tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
// 4.开始完成渲染功能
tpl->Expand(html,&root);
}
// 渲染一道题目
void OneExpandHtml(const struct Question &q,string *html){
// 1.形成路径
std::string src_html = template_path + "one_question.html";
// 2. 形成数字典
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("header",q.header);
// 3.获取被渲染的html
ctemplate::Template*tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
// 4.开始完成渲染功能
tpl->Expand(html,&root);
}
};
}
2.6 联动MVC模块并测试
oj_server.cc
#include
#include "../comm/httplib.h"// 引入
#include "oj_control.hpp"
using namespace httplib;// 引入
using namespace ns_control;
int main()
{
// 用户请求的服务器路由功能
Server svr;
Control ctrl;
// 获取所有的题目列表
svr.Get("/all_questions",[&ctrl](const Request&req,Response &resp){
// 返回一张包含所有题目的html网页
std::string html;// 待处理
ctrl.AllQuestions(&html);
resp.set_content(html,"text/html;charset=utf-8");
});
// 根据题目编号,获取题目内容
// \d+ 是正则表达式的特殊符合
svr.Get(R"(/question/(\d+))",[&ctrl](const Request&req,Response &resp){
std::string number = req.matches[1];
std::string html;
ctrl.Question(number,&html);
resp.set_content(html,"text/html;charset=utf-8");
});
// 判断用户提交的代码(1.每道题c测试用例,2.compile_and_run)
svr.Post(R"(/judge/(\d+))",[&ctrl](const Request&req,Response &resp){
std::string number = req.matches[1];
std::string result_json;
ctrl.Judge(number,req.body,&result_json);
resp.set_content(result_json,"application/json;charset=utf-8");
});
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0",8080);
return 0;
}
- 这里的前端都是提前做好了的,我们可以不关心前端;
- control功能还有个判题功能没有实现
2.7 完善oj_control.hpp中的判题功能
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
// LOG(DEBUG) << in_json << "
number:" << number <ip, m->port);
m->IncLoad();
LOG(INFO) << " 选择主机成功, 主机id: " << id << " 详情: " <ip << ":" <port << " 当前主机的负载是: " <Load() <status == 200)
{
*out_json = res->body;
m->DecLoad();
LOG(INFO) << "请求编译和运行服务成功..." <DecLoad();
}
else
{
//请求失败
LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " <ip << ":" <port << " 可能已经离线"<< "
";
load_blance_.OfflineMachine(id);
load_blance_.ShowMachines(); //仅仅是为了用来调试
}
}
2.8 测试oj_server服务
- 在编译时需要加上-D COMPILER_ONLINE条件编译,
- 设计到前端网页,下面会有提及
2.9 一个BUG
- 把tail.txt改成tail.cpp,不然后面无法进行代码拼接
3. 前端页面设计(了解)
3.1 index.html
这是我的个人OJ系统
/* 起手式, 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 .content {
/* 设置标签的宽度 */
width: 800px;
/* 用来调试 */
/* background-color: #ccc; */
/* 整体居中 */
margin: 0px auto;
/* 设置文字居中 */
text-align: center;
/* 设置上外边距 */
margin-top: 200px;
}
.container .content .font_ {
/* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */
display: block;
/* 设置每个文字的上外边距 */
margin-top: 20px;
/* 去掉a标签的下划线 */
text-decoration: none;
/* 设置字体大小
font-size: larger; */
}
首页
题库
竞赛
讨论
求职
登录
欢迎来到我的OnlineJudge平台
这个我个人独立开发的一个在线OJ平台
点击我开始编程啦!
3.2 all_questions.html
在线OJ-题目列表
/* 起手式, 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;
}
首页
题库
竞赛
讨论
求职
登录
OnlineJuge题目列表
编号
标题
难度
{{#question_list}}
{{number}}
{number}}">{{title}}
{{star}}
{{/question_list}}
<!--
-->
@lyc
3.3 one_questions.html(ACE插件&&JQuery&&ajax)
ACE插件是一个
编写代码的编译框收集当前页面的有关数据
, a.
题号
a.
代码
,
我们采用
JQuery
来进行获取
html
中的内容构建json,并通过
ajax向后台
发起基于http的json请求
全部代码
{{number}}.{{title}} * { 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; } 首页 题库 竞赛 讨论 求职 登录{{number}}.{{title}}_{{star}}
{{desc}}
3.4 相关测试
4. MySQL版题目设计
4.1 注册用户 && 赋予权限
- create user ‘oj_client’@’localhost’ identified by ‘123456’;
- create database oj;
- grant select on oj.* to ‘oj_client’@’localhost’;
- select user,Host from user;
4.2 下载第三方工具-workbench
- 下载下来之后,就不断的下一步,下一步就行了
4.3 录题到mysql中
use oj;
drop table if exists oj_table;
create table if not exists oj_table(
_number varchar(200) comment '题目编号',
_titie varchar(200) comment '题目标题',
_start varchar(200) comment '题目简单中等困难',
_desc varchar(2000) comment '题目描述',
_header varchar(2000) comment '题目预设',
_tail varchar(2000) comment '题目测试用例',
_cpu_limit int comment '时间要求',
_mem_limt int comment '空间要求'
);
insert into oj_table values(
1,
'判断回文数',
'简单',
'判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?',
'#include
#include
#include
#include
- 这里我只录入了一道题为了测试
4.4 下载并引入mysql库文件
MySQL :: Download MySQL Community Server
要使用C/C++连接MySQL,需要使用MySQL官网提供的库
下载完毕后需要将其上传到云服务器,这里将下载的库文件存放在下面的目录:
然后使用tar命令将压缩包解压到当前目录下:
xz -d mysql-8.0.37-linux-glibc2.28-i686.tar.xz
tar xvf mysql-8.0.37-linux-glibc2.28-i686.tar
进入解压后的目录当中,可以看到有一个include子目录和一个lib子目录,其中,include目录下存放的一堆头文件。而lib64目录下存放的就是动静态库。
然后在我们的项目中建立软连接
4.5 一个BUG
如果你当时下载myql把mysql-devel也下载了,不需要进行上面步骤
这种引入第三方库的操作,可能会因为版本不兼容,而导致出错
skipping incompatible ./lib/libmysqlclient.so when searching for -lmysqlclient- 建议直接安装: yum -y install mysql-devel
4.5 重新设计oj_model
因为oj_model模块是管理数据,提供接口的模块,所以要把这个项目变成mysql就需要重新设计
#pragma once
// 文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include
#include
#include
#include "./include/mysql.h"
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question{
string number;// 题目编号,唯一
string title;// 题目标题
string star;// 难度: 简单 中等 困难
int cpu_limit;// 题目的时间复杂度(S)
int mem_limit;// 题目的空间复杂度(KB)
string desc;// 题目描述
string header; // 题目预设给用户在线编辑器的代码
string tail;// 题目测试用例,需要和header拼接
};
const std::string oj_questions = "oj_table";
const std::string host = "127.0.0.1";
const std::string user = "oj_client";
const std::string passwd = "123456";
const std::string db = "oj";
const int port = 3306;
class Model
{
public:
Model(){
}
~Model(){
;
}
bool QueryMysql(const std::string &sql,vector*out){
// 这里的out是输出型参数
// 创建mysql句柄
MYSQL *my = mysql_init(nullptr);
// 连接数据库
if(nullptr == mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0)){
LOG(FATAL) << "连接数据库失败!" << "
";
return false;
}
// 一定要设置该链接的编码格式,要不然会出现乱码的问题
mysql_set_character_set(my,"utf8");
LOG(INFO) << "连接数据库成功!" << "
";
// 执行sql语句
if(0 != mysql_query(my,sql.c_str())){
LOG(WARNING) << sql << " execute error! " << "
";
return false;
}
// 提取结果
MYSQL_RES *res = mysql_store_result(my);// 本质就是一个2级指针
// 分析结果
int rows = mysql_num_rows(res);// 获取行的数量
int cols = mysql_num_fields(res);// 获取列的数量
Question q;
for(int i = 0;i push_back(q);
}
// 释放控件
free(res);
// 关闭mysql连接
mysql_close(my);
return true;
}
// 获取所有题目,这里的out是输出型参数
bool GetAllQuestions(vector*out){
std::string sql = "select * from ";
sql += oj_questions;
return QueryMysql(sql,out);
}
// 获取指定题目,这里的q是输出型参数
bool GetOneQuestion(const string& number,Question* q){
bool res = false;
std::string sql = "select * from ";
sql += oj_questions;
sql += " where number=";
sql += number;
vector result;
if(QueryMysql(sql,&result)){
if(result.size() == 1){
*q = result[0];
res = true;
}
}
return res;
}
private:
// 题号 : 题目细节
unordered_map questions;
};
}
- mysql_init: 创建mysql句柄
- mysql_real_connect: 创建mysql连接
mysql_query: 发起mysql请求
mysql_close: 关闭mysql连接
4.6 相关测试
- 编译期间告诉编译器头文件和库文件在哪里 -I指明搜索的头文件,-L指明搜索的lib
- 并加上-lmysqlclient
5. 扩展
功能上更完善一下,判断一道题目正确之后,自动下一道题目
基于注册和登陆的录题功能
…..
6. 完整项目链接
projects/负载均衡/OnlineJudge at main · 1LYC/projects · GitHub