前言:
该篇讲述了实现基于负载均衡式的在线oj,即类似在线编程做题网站一样,文章尽可能详细讲述细节即实现,便于大家了解学习。
文章将采用单篇不分段形式(ps:切着麻烦),附图文,附代码,代码部署在云服务器上
技术栈
C++ STL标准库 Boost 标准库 cpp-httpib 开源库 ctemplate 第三方开源前端网页渲染库 jsoncpp 第三方开源序列化、反序列化库 负载均衡的设计 多进程、多线程 MYSQL C connect Ace前端在线编辑器 html/cdd/js/jquery/ajax
开发环境
vscodemysql workbenchCentos 7云服务器
宏观结构
comm:公共模块compile_sever:编译运行模块oj_server:获取题目,负载均衡等
项目演示:
https://blog.csdn.net/Obto_/article/details/132558642?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132558642%22%2C%22source%22%3A%22Obto_%22%7Dhttps://blog.csdn.net/Obto_/article/details/132558642?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132558642%22%2C%22source%22%3A%22Obto_%22%7D
项目设计 -- 编译服务
工具类的准备:
供程序中各个部分调用的方法类:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace ns_util
{
class TimeUtil
{
public:
static std::string GetTimeStamp()
{
// 获取时间戳 gettimeofday
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec);
}
// 获得毫秒时间戳
static std::string GetTimeMs()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
}
};
const std::string temp_path = "./temp/";
class PathUtil
{
public:
static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
{
std::string path_name = temp_path;
path_name += file_name;
path_name += suffix;
return path_name;
}
// 构建源文件路径+后缀的完整文件名
// 1234 -> ./temp/1234.cpp
static std::string Src(const std::string &file_name)
{
return AddSuffix(file_name, ".cpp");
}
// 构建可执行程序的完整路径 + 后缀名
static std::string Exe(const std::string &file_name)
{
return AddSuffix(file_name, ".exe");
}
static std::string CompilerError(const std::string &file_name)
{
return AddSuffix(file_name, ".compile_stderr");
}
//-------------------------------------------------------------------
// 构建该程序对应的标准错误完整的路径+后缀名
static std::string Stderr(const std::string &file_name)
{
return AddSuffix(file_name, ".stderr");
}
static std::string Stdin(const std::string &file_name)
{
return AddSuffix(file_name, ".stdin");
}
static std::string Stdout(const std::string &file_name)
{
return AddSuffix(file_name, ".stdout");
}
};
class FileUtil
{
public:
static bool IsFileExists(const std::string &path_name)
{
struct stat st;
if (stat(path_name.c_str(), &st) == 0)
{
// 获取属性成功,文件已经存在
return true;
}
return false;
}
static std::string UniqFileName()
{
static std::atomic_uint id(0);
id++;
// 毫秒时间戳
std::string ms = TimeUtil::GetTimeMs();
std::string uniq_id = std::to_string(id);
return ms + "_" + uniq_id;
}
static bool WriteFile(const std::string &target, const std::string &content)
{
// waiting
std::ofstream out(target);
if (!out.is_open())
{
return false;
}
out.write(content.c_str(), content.size());
out.close();
return true;
}
static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
{
(*content).clear();
std::ifstream in(target);
if (!in.is_open())
{
return false;
}
std::string line;
// getline是按行保存的,不保存行分隔符,自动去掉\n
// 但是有些时候,需要保留行分隔符
// getline内部重载了强制类型转化
while (std::getline(in, line))
{
(*content) += line;
(*content) += (keep ? "\n" : "");
}
in.close();
return true;
}
};
class StringUtil
{
public:
/*
str:输入型,目标要切分的字符串
target:输出型,保存切分完毕的结果
sep:指定的分隔符
*/
static void SplitString(const std::string &str,std::vector
{
boost::split(*target,str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
//boost split
}
};
}
PathUtil:路径工具
形成exe完整路径形成cpp完整路径形成compile_stderr完整路径形成stderr完整路径形成stdin完整路径完整路径指的是当前代码在本地上的保存路径:即输入 1234 要形成 ./temp/1234.cpp等这里的相对路径形成依靠PathUtil工具TimeUtil:时间工具
获取时间戳 get time of day获得好面时间戳,用于形成文件唯一标识(名字)FileUtil:文件工具
IsFileExits:判断某文件是否存在UniqFileName:形成文件唯一名字WriteFile:向指定文件写入指定字符串ReadFile:读取某文件的内容StringUtil:字符串工具
使用boost库中的切分字符串Split()
compiler编译服务设计 :
目的:能够编译并运行代码,得到格式化的相关结果
#pragma once
#include
#include
#include
#include
#include
#include
#include"../comm/util.hpp"
#include"../comm/log.hpp"
//只负责进行代码的编译
namespace ns_compiler{
//引入路径拼接功能
using namespace ns_util;
using namespace ns_log;
class Compiler{
public:
Compiler()
{}
~Compiler()
{}
//返回值是编译成功TRUE;else FALSE
//输入参数是编译的文件名
//file_name : 1234
//1234 -> ./temp/1234.cpp
//1234 -> ./temp/1234.exe
//1234 -> ./temp/1234.stderr
static bool Compile(const std::string &file_name)
{
pid_t pid = fork();
if(pid < 0)
{
LOG(ERROR) << "内部错误,创建子进程失败"<<"\n";
return false;
}
else if(pid == 0)//子进程
{
umask(0);
int _stderr = open(PathUtil::CompilerError(file_name).c_str(),O_CREAT | O_WRONLY,0644);
if(_stderr < 0){
LOG(WARNING)<<"没有成功行成stderr文件"<<"\n";
exit(1);
}
//重定向标准错误到_stderr
dup2(_stderr,2);
//程序替婚,并不影响进程的文件描述符表
//子进程:调用编译器
execlp("g++","g++","-o",PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(),"-std=c++11","-D","COMPILER_ONLINE",nullptr);
LOG(ERROR) <<"启动编译器g++失败,可能是参数错误"<<"\n";
exit(2);
}
else//父进程
{
waitpid(pid,nullptr,0);
//编译是否成功,就看有没有形成对应的可执行程序
if(FileUtil::IsFileExists(PathUtil::Exe(file_name).c_str())){
LOG(INFO) < return true; } } LOG(ERROR) <<"编译失败,没有形成可执行程序,return false"<<"\n"; return false; } }; }; compiler编译服务只管编译传过来的代码,其他一律不管,它只关心程序是否能够编译过 LOG日志的添加: #pragma once #include #include #include"util.hpp" namespace ns_log { using namespace ns_util; //日志等级 enum{ INFO, DEBUG, WARNING, ERROR , FATAL }; //LOG() << "message" inline std::ostream &Log(const std::string &level, const std::string &file_name,int line) { //添加日志等级 std::string message = "["; message+=level; message+="]"; //添加报错文件名称 message+="["; message+=file_name; message+="]"; //添加报错行 message+="["; message+=std::to_string(line); message+="]"; //添加日志时间戳 message += "["; message += TimeUtil::GetTimeStamp(); message += "]"; //cout 本质内部是包含缓冲区的 std::cout< return std::cout; } //LOG(INFo) << "message" //开放式日志 #define LOG(level) Log(#level,__FILE__,__LINE__) } runner运行功能设计: #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "../comm/log.hpp" #include "../comm/util.hpp" namespace ns_runner { using namespace ns_log; using namespace ns_util; class Runner { public: Runner() {} ~Runner() {} public: //提供设置进程占用资源大小的接口 static void SerProcLimit(int _cpu_limit,int _mem_limit) { //设置CPU时长 struct rlimit cpu_rlimit; cpu_rlimit.rlim_max = RLIM_INFINITY; cpu_rlimit.rlim_cur = _cpu_limit; setrlimit(RLIMIT_CPU,&cpu_rlimit); //设置内存大小 struct rlimit mem_rlimit; mem_rlimit.rlim_max = RLIM_INFINITY; mem_rlimit.rlim_cur = _mem_limit * 1024;//转化成kb setrlimit(RLIMIT_AS,&mem_rlimit); } // 指明文件名即可,不需要带路径和后缀 /* 返回值如果是大于 0 :程序异常了,退出时收到了信号,返回值就是对应的信号 返回值 == 0 就是正常运行完毕,结果是什么保存到了临时文件中,我不清楚 返回值 < 0 属于内部错误 cpu_limit:该程序运行的时候,可以使用的最大cpu的资源上限 mem_limit:该程序运行的时候,可以使用的最大内存大小KB */ static int Run(const std::string &file_name,int cpu_limit,int mem_limit) { /*程序运行: 1.代码跑完结果争取 2.代码跑完结果不正确 3.代码没跑完,异常了 run不需要考虑运行完后正确与否,只管跑 首先我们必须知道可执行程序是谁? 标准输入:不处理 标准输入:程序运行完成,输出结果是什么 标准错误:运行时错误信息 */ 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)<<"运行时打开标准文件失败"<<"\n"; return -1; // 代表打开文件失败 } pid_t pid = fork(); if (pid < 0) { LOG(ERROR)<<"运行时创建子进程失败"<<"\n"; 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); SerProcLimit(cpu_limit,mem_limit); execl(_execute.c_str()/*我要执行谁*/,_execute.c_str()/*我想在命令航商如何执行*/,nullptr); exit(1); } else { int status = 0; waitpid(pid,&status,0); //程序运行异常,一定是因为收到了信号 LOG(INFO)<<"运行完毕,info:"<<(status & 0x7F)<<"\n"; close(_stdin_fd); close(_stdout_fd); close(_stderr_fd); return status&0x7F; } } }; } compile_run:编译并运行功能: #pragma once #include "compiler.hpp" #include #include "runner.hpp" #include "../comm/log.hpp" #include "../comm/util.hpp" #include #include namespace ns_compile_and_run { using namespace ns_log; using namespace ns_util; using namespace ns_compiler; using namespace ns_runner; class CompileAndRun { public: static void RemoveTempFile(const std::string& file_name) { //清理文件的个数是不确定的,但是有哪些我们是知道的 std::string _src = PathUtil::Src(file_name); if(FileUtil::IsFileExists(_src))unlink(_src.c_str()); std::string _compiler_error = PathUtil::CompilerError(file_name); if(FileUtil::IsFileExists(_compiler_error))unlink(_compiler_error.c_str()); std::string _execute = PathUtil::Exe(file_name); if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str()); std::string _stdin = PathUtil::Stdin(file_name); if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str()); std::string _stdout = PathUtil::Stdout(file_name); if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str()); std::string _stderr = PathUtil::Stderr(file_name); if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str()); } static std::string CodeToDesc(int code, std::string file_name) // code >0 <0 ==0 { std::string desc; switch (code) { case 0: desc = "编译运行成功"; break; case -1: desc = "用户提交的代码是空"; break; case -2: desc = "未知错误"; break; case -3: // desc = "编译发生报错"; FileUtil::ReadFile(PathUtil::Stderr(file_name), &desc, true); break; case -4: break; case SIGABRT: desc = "内存超过范围"; break; case SIGXCPU: desc = "CPU信号超时"; break; case SIGFPE: desc = "除零错误,浮点数溢出"; break; default: desc = "未知:" + std::to_string(code); break; } return desc; } /* 输入: code:用户提交的代码 input:用户自己提交的代码,对应的输入-》不做处理 cpu_limit:时间要求 mem_limit:空间要求 输出: 必填: status:状态码 reason:请求结果 选填: stdout:我的的程序运行完的结果 stderr:我的程序运行完的错误结构 参数: 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) { LOG(INFO)<<"启动compile_and_run"<<"\n"; Json::Value in_value; Json::Reader reader; 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 men_limit = in_value["mem_limit"].asInt(); int status_code = 0; Json::Value out_value; int run_result = 0; std::string file_name; // 需要内部形成的唯一文件名 if (code.size() == 0) { // 说明用户一行代码都没提交 status_code = -1; goto END; } // 形成的文件名只具有唯一性,没有目录没有后缀 // 毫秒计时间戳+原子性递增的唯一值:来保证唯一性 file_name = FileUtil::UniqFileName(); // 形成唯一文件名字 LOG(DEBUG)<<"调用UniqFileName()形成唯一名字"< run_result = Runner::Run(file_name, cpu_limit, men_limit); if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) // 形成临时src文件.cpp { status_code = -2; // 未知错误 goto END; } if (!Compiler::Compile(file_name)) { // 编译失败 status_code = -3; goto END; } run_result = Runner::Run(file_name, cpu_limit, men_limit); if (run_result < 0) { // 服务器的内部错误,包括不限于文件打开失败,创建子进程失败等待 status_code = -2; // 未知错误 goto END; } else if (run_result > 0) { status_code = run_result; } else { // 运行成功 status_code = 0; } END: std::cout<<"到达end语句"< // status_code out_value["status"] = status_code; out_value["reason"] = CodeToDesc(status_code, file_name); LOG(DEBUG)< if (status_code == 0) { // 整个过程全部成功 , 这时候才需要运行结果 std::string _stdout; FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true); out_value["stdout"] = _stdout; } else { std::string _stderr; FileUtil::ReadFile(PathUtil::CompilerError(file_name), &_stderr, true); out_value["stderr"] = _stderr; } // 序列化 Json::StyledWriter writer; *out_json = writer.write(out_value); //清理所有的临时文件 RemoveTempFile(file_name); } }; } compile_run:它的功能是接收远端传进来的json包,并反序列化得到其中的代码与输入,并调用compile进行编译 编译成功:调用runner将代码运行起来->将执行结果分别保存到.exe、.stdin、.stdout 、.stderr、.compile_stderr文件中编译失败:不调用runner 最后按对应构造json 返回给上级调用,即write进out_json中,收尾清除创建的文件 compile_server .cc文件编写: #include"compile_run.hpp" #include #include"../comm/httplib.h" using namespace ns_compile_and_run; using namespace httplib; //编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,不然影响多个用户 void Usage(std::string proc) { std::cerr <<"Usage:"<<"\n\t"< } // ./compiler_server port int main(int argc,char *argv[]) { if(argc!=2){ Usage(argv[0]); } Server svr; svr.Get("/hello",[](const Request &req,Response &resp) { resp.set_content("hello httplib,你好httplib","content_type: text/plain"); }); //svr.set_base_dir("./wwwroot"); svr.Post("/compile_and_run",[](const Request &req,Response &resp){ //请求服务正文是我们想要的json串 LOG(DEBUG)<<"调用compile_and_run"<<"\n"; std::string out_json; std::string in_json = req.body; if(!in_json.empty()){ LOG(DEBUG)<<"当前的in_json"< CompileAndRun::Start(in_json,&out_json); resp.set_content(out_json,"application/json;charset=utf-8"); } }); svr.listen("0.0.0.0",atoi(argv[1]));//启动http服务了 // std::string code = "code"; // Compiler::Compile(code); // Runner::Run(code); //0-----------------------测试代码------------------- //下面的工作,充当客户端请求的json串 // std::string in_json; // Json::Value in_value; // //R"()" raw string 凡事在这个圆括号里面的东西,就是字符串哪怕有一些特殊的字符串 // in_value["code"] =R"(#include // int main(){ // std::cout<<"测试成功"< // int a = 10; // a /= 0; // return 0; // })"; // in_value["input"] =""; // in_value["cpu_limit"] = 1; // in_value["mem_limit"] = 10240 * 3; // Json::FastWriter writer; // std::cout< // in_json = writer.write(in_value); // //这个是将来给客户端返回的字符串 // std::string out_json; // CompileAndRun::Start(in_json,&out_json); // std::cout< //0----------------------------------------------------- //提供的编译服务,打包新城一个网络服务 //这次直接用第三方库,cpp-httplib return 0; } 直接引入的httplib库, 设置好ip和端口就可以直接监听了 svr.get() :就是当对该服务器发起/hello 请求的时候,我就会接受到该请求,以Get的方式返回resp makefile: 由于当前使用的c++11的新特性,引入了json库,和多线程 compile_server:compile_server.cc g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread .PHONY:clean clean: rm -f compile_server 项目设计 -- 基于MVC结构的oj服务 本质:建立一个小型网站 1.获取首页 2.编辑区域页面 3.提交判题功能(编译并运行) M:Model,通常是和数据交互的模块,比如对题库的增删改查(文件版,mysql版) V:view,通常是拿到数据之后,要进行构建网页,渲染网页内容 C:control,控制器,也就是我们核心业务逻辑 用户的请求服务路由功能: #include "../comm/httplib.h" #include "login.hpp" #include #include #include"oj_control.hpp" using namespace httplib; using namespace ns_control; const std::string login_path = "../oj_login/wwwroot/"; static Control *ctrl_ptr = nullptr; void Recovery(int signo) { ctrl_ptr->RecoveryMachine(); } int main() { signal(SIGQUIT,Recovery); // 用户请求的服务路由功能 Server svr; Control ctrl; Login login; ctrl_ptr = &ctrl; /* 1获取所有的题目列表 */ svr.Get(R"(/all_questions)", [&ctrl](const Request &req, Response &resp) { std::string html; ctrl.AllQuestions(&html); resp.set_content(html, "text/html;charset=utf-8"); }); // 2用户要根据题目编号来选择题目 // 这里的\d是正则表达式 + 是匹配数字 // R"()"保持原始字符串不会被特殊字符影响比如\d \r \n之类的不需要做相关的转义 svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp) { std::string number = req.matches[1]; std::string html; ctrl.OneQuestion(number,&html); resp.set_content(html,"text/html;charset=utf-8"); }); // 3用户提交代码,使用我们的判题功能(1.没道题目的测试用例 2.compile_and_run) svr.Post(R"(/judge/(\d+))",[&ctrl](const Request &req, Response &resp){ std::string number = req.matches[1]; // resp.set_content("这是指定的一道题目的判题:" + number, // "text/plain;charset=utf-8"); std::string result_json; ctrl.Judge(number,req.body,&result_json); resp.set_content(result_json,"application/json;charset=utf-8"); }); svr.Post(R"(/dealregister)",[&ctrl](const Request &req, Response &resp){ int status = 1; std::string in_json = req.body; std::string out_json; if(!ctrl.UserRegister(in_json,&out_json)){ status = 0; } LOG(INFO)<<"用户注册status : "< Json::Value tmp; tmp["status"] = status; Json::FastWriter writer; std::string res = writer.write(tmp); resp.set_content(res,"application/json;charset=utf-8"); }); svr.Get(R"(/my_login)",[&login,&ctrl](const Request &req,Response &resp){ //直接跳转到静态的html std::string html; ctrl.Login(req.body,&html); resp.set_content(html, "text/html;charset=utf-8"); }); svr.Get(R"(/register)",[&login,&ctrl](const Request &req,Response &resp){ //直接跳转到静态的html std::string html; ctrl.Register(req.body,&html); resp.set_content(html, "text/html;charset=utf-8"); }); svr.set_base_dir("./wwwroot"); svr.listen("0.0.0.0", 8080); return 0; } 这样当用户通过http请求我们的oj_server服务器的时候我们可以正确的路由到合适的功能 model功能:提供对数据的操作(文件版) #pragma once //文件版本 /* 编号 标题 难度 描述 时间(内部),空间(内部处理) 两批文件构成 1.question.list:题目列表:不需要出现题目描述 2.需要题目的描述,需要题目的预设置代码(header.cpp),测试用例代码(tail.cpp) 这两个内容是通过题目的编号,产生关联的 */ #pragma once #include "../comm/log.hpp" #include "../comm/util.hpp" #include #include #include #include #include #include #include // 根据题目list文件,加载所有信息到内存中 // model:主要用来和数据进行交互,对外提供访问数据的接口 namespace ns_model { using namespace std; using namespace ns_log; using namespace ns_util; class Question { public: std::string number; // 题目编号 std::string title; // 题目的标题 std::string star; // 难度:简单中等困难 int cpu_limit; // 时间要求 s int mem_limit; // 空间要求 kb std::string desc; // 题目的描述 std::string header; // 题目预设给用户在线编辑器的代码 std::string tail; // 题目的测试用例,需要和header拼接形成完整代码 }; const std::string question_list = "./questions/questions.list"; const std::string question_path = "./questions/"; class Model { private: // 题号:题目细节 unordered_map public: Model() { assert(LoadQuestionList(question_list)); } bool LoadQuestionList(const std::string &question_list) { // 加载配置文件questions/question.list + 题目编号文件 ifstream in(question_list); if (!in.is_open()) { LOG(FATEL) << "加载题库失败,请检查是否存在题库文件" << "\n"; return false; } std::string line; while (getline(in, line)) { vector StringUtil::SplitString(line, &tokens, " "); if (tokens.size() != 5) { LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n"; continue; } Question q; q.number = tokens[0]; q.title = tokens[1]; q.star = tokens[2]; q.cpu_limit = atoi(tokens[3].c_str()); q.mem_limit = atoi(tokens[4].c_str()); std::string path = question_path; path += q.number; path += "/"; FileUtil::ReadFile(path + "desc.txt", &(q.desc), true); FileUtil::ReadFile(path + "header.cpp", &(q.header), true); FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true); questions.insert({q.number, q}); } LOG(INFO) << "加载题库成功!" << "\n"; in.close(); return true; } bool GetAllQuestion(vector if (questions.size() == 0) { LOG(ERROR) << "用户获取题库失败" << "\n"; return false; } for (const auto &q : questions) { out->push_back(q.second); // fir是key' sec是value } return true; } bool GetOneQuestion(const std::string &number, Question *q) { const auto &iter = questions.find(number); if (iter == questions.end()) { LOG(ERROR) << "用户获取题目失败:" << number << "\n"; return false; } (*q) = iter->second; return true; } ~Model() {} }; } // namespace ns_model 该设计中有一个 question的题目清单,像题库的目录一样,填写每道题目的基本信息: 对应的是: 1.题目编号 2.题目名字 3.题目难度 4.时间限制 5.空间限制 model功能:提供对数据的操作(数据库版) #pragma once //这个是mysql版本 /* 编号 标题 难度 描述 时间(内部),空间(内部处理) 两批文件构成 1.question.list:题目列表:不需要出现题目描述 2.需要题目的描述,需要题目的预设置代码(header.cpp),测试用例代码(tail.cpp) 这两个内容是通过题目的编号,产生关联的 */ #pragma once #include "../comm/log.hpp" #include "../comm/util.hpp" #include #include #include #include #include #include #include #include #include"include/mysql.h" // 根据题目list文件,加载所有信息到内存中 // model:主要用来和数据进行交互,对外提供访问数据的接口 namespace ns_model { using namespace std; using namespace ns_log; using namespace ns_util; class Question { public: std::string number; // 题目编号 std::string title; // 题目的标题 std::string star; // 难度:简单中等困难 int cpu_limit; // 时间要求 s int mem_limit; // 空间要求 kb std::string desc; // 题目的描述 std::string header; // 题目预设给用户在线编辑器的代码 std::string tail; // 题目的测试用例,需要和header拼接形成完整代码 }; const std::string oj_questions ="oj_questions"; const std::string oj_user = "oj_user"; 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 { private: // 题号:题目细节 unordered_map public: Model() { } bool QueryMySql(const std::string &sql,vector { //创建mysql句柄 MYSQL *my = mysql_init(nullptr); //连接数据库 if(mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0) == nullptr){ LOG(FATAL)<<"连接数据库失败!"<<"\n"; return false; } //一定要设置该链接的编码格式默认是拉钉的 mysql_set_character_set(my,"utf8mb4"); LOG(INFO)<<"连接数据库成功"<<"\n"; //执行sql语句 if(0 != mysql_query(my,sql.c_str())) { LOG(WARNING) << sql <<"execute error!"<<"\n"; return false; } MYSQL_RES *res = mysql_store_result(my); //分析结果 int rows = mysql_num_rows(res); //获得行数量 int cols = mysql_num_fields(res);//获得列数量 Question q; for(int i = 0;i { MYSQL_ROW row = mysql_fetch_row(res); q.number = row[0]; q.title = row[1]; q.star = row[2]; q.desc = row[3]; q.header = row[4]; q.tail = row[5]; q.cpu_limit = atoi(row[6]); q.mem_limit = atoi(row[7]); out->push_back(q); } //释放结果空间 free(res); //关闭mysql连接 mysql_close(my); return true; } bool GetAllQuestion(vector std::string sql ="select *from "; sql += oj_questions; return QueryMySql(sql,out); } bool GetOneQuestion(const std::string &number, Question *q) { bool res = false; std::string sql = "select *from "; sql+=oj_questions; sql+= " where number="; sql+=number; vector if(QueryMySql(sql,&result)) { if(result.size() == 1) { *q = result[0]; res = true; } } return res; } bool UserRegister(const std::string& in_json,std::string* out_json) { //这里先对in_json反序列化 Json::Reader reader; Json::Value in_value; reader.parse(in_json,in_value); std::string number = in_value["number"].asString(); std::string name = in_value["name"].asString(); std::string password = in_value["password"].asString(); int limit = in_value["limit"].asInt(); int level = in_value["level"].asInt(); //判断账号密码是否可行 std::string sql = " select *from "; sql+=oj_user; sql+=" where number="; sql+=number; //创建数据库 MYSQL *my = mysql_init(nullptr); //连接数据库 if(mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0) == nullptr) { LOG(WARNING)<<"连接到用户数据库失败"<<"\n"; return false; } //一定要记得设置该链接的编码格式 mysql_set_character_set(my,"utf8"); LOG(INFO)<<"连接懂啊用户数据库成功"<<"\n"; if(0 != mysql_query(my,sql.c_str())){ LOG(WARNING)<< sql <<"execute error!"<<"\n"; return false; } MYSQL_RES *res = mysql_store_result(my); if(mysql_num_rows(res) == 0)//获得行数量 { //当前输入的数据可以创建用户 MYSQL_STMT *stmt = mysql_stmt_init(my); const char* query = "insert into oj_user values (?,?,?,?,?)"; if(mysql_stmt_prepare(stmt,query,strlen(query)) != 0){ LOG(WARNING)<<"stmt出现错误"<<"\n"; mysql_stmt_close(stmt); mysql_close(my); return false; } //下面开始绑定 MYSQL_BIND bind_params[5]; memset(bind_params,0,sizeof bind_params); bind_params[0].buffer_type = MYSQL_TYPE_STRING; bind_params[0].buffer = (char*)number.c_str(); bind_params[0].buffer_length = number.size(); bind_params[1].buffer_type = MYSQL_TYPE_STRING; bind_params[1].buffer = (char*)name.c_str(); bind_params[1].buffer_length = name.size(); bind_params[2].buffer_type = MYSQL_TYPE_STRING; bind_params[2].buffer = (char*)password.c_str(); bind_params[2].buffer_length = password.size(); bind_params[3].buffer_type = MYSQL_TYPE_LONG; bind_params[3].buffer = &limit; bind_params[3].is_unsigned = 1; bind_params[4].buffer_type = MYSQL_TYPE_LONG; bind_params[4].buffer = &level; bind_params[4].is_unsigned = 1; if(mysql_stmt_bind_param(stmt,bind_params) !=0){ LOG(WARNING) <<"绑定stmt参数出错"<<"\n"; mysql_stmt_close(stmt); mysql_close(my); return false; } //执行插入语句 if(mysql_stmt_execute(stmt)!=0){ LOG(WARNING)<<"执行stmt语句的时候出现错误..."<<"\n"; mysql_stmt_close(stmt); mysql_close(my); return false; } mysql_stmt_close(stmt); mysql_close(my); return true; } else{ //服务器有重复的用户num ,不允许再创建了 return false; } //保存到服务器 //这里out_json暂时没有用,没有要返回的值 return true; } ~Model() {} }; } // namespace ns_model control:逻辑控制模块 #pragma once #include #include #include #include #include #include #include #include #include"oj_view.hpp" // #include"oj_model.hpp" #include"oj_model2.hpp" #include"../comm/log.hpp" #include"../comm/util.hpp" #include"../comm/httplib.h" namespace ns_control { using namespace std; using namespace httplib; using namespace ns_log; using namespace ns_util; using namespace ns_model; using namespace ns_view; //提供服务的主机的内容 class Machine { public: std::string ip; //编译服务器的ip int port; //编译服务器的端口 uint64_t load; //编译服务器负载 std::mutex *mtx;//mutex是禁止拷贝的,使用指针来完成 public: Machine():ip(""),port(0),load(0),mtx(nullptr) {} ~Machine() {} public: void ResetLoad() { if(mtx)mtx->lock(); load = 0; LOG(DEBUG)<<"当前ip:"< if(mtx)mtx->unlock(); } //提升主机负载 void IncLoad() { if(mtx) mtx->lock(); ++load; if(mtx) mtx->unlock(); } //减少主机负载 void DecLoad() { if(mtx) mtx->lock(); --load; 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 LoadBalance { private: //可以给我们提供编译服务的所有的主机 //每一台主机都有自己的下标,充当当前主机的id std::vector //所有在线的主机 std::vector //所有离线主机的id std::vector //保证选择主机上的这个东西要保证数据安全 std::mutex mtx; public: LoadBalance(){ assert(LoadConf(service_machine)); LOG(INFO)<<"加载"< } ~LoadBalance(){} public: bool LoadConf(const std::string &machine_cof) { std::ifstream in(machine_cof); if(!in.is_open())\ { LOG(FATAL) <<"加载:"< return false; } std::string line; while (getline(in,line)) { std::vector StringUtil::SplitString(line,&tokens,":"); if(tokens.size()!=2) { LOG(WARNING) <<"切分"< std::cout< continue; } //LOG(INFO) <<"切分"< 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) << "所有的后端编译主机已经全部离线,请后端的尽快重启"<<"\n"; return false; } LOG(DEBUG)<<"online:"< //通过编译,找到负载最小的机器 *id = online[0]; *m = &machines[online[0]]; uint64_t min_load = machines[online[0]].Load(); for(int i = 1;i { uint64_t curr_load = machines[online[i]].Load(); if(min_load > 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(); LOG(DEBUG)<<"当前离线主机的负载更改为:"< 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)<<"所有的主机又上线了"<<"\n"; LOG(INFO)<<"online:"< } void ShowMachines() { mtx.lock(); LOG(INFO)<<"online:"< mtx.unlock(); } }; //这是我们核心业务逻辑的控制器 class Control { private: Model model_;//提供后台数据 View view_; //提供网页渲染功能 LoadBalance load_blance_; //核心负载均衡器 public: void RecoveryMachine() { load_blance_.OnlineMachine(); } //根据题目数据构建网页 //html:输出型参数 bool AllQuestions(string *html) { bool ret = true; vector if(model_.GetAllQuestion(&all)) { sort(all.begin(),all.end(),[](const Question &q1,const Question &q2){ return atoi(q1.number.c_str()) < atoi(q2.number.c_str()); }); //获取题目信息 成功,将所有的题目数据构建成网页 view_.AllExpandHtml(all,html); } else { *html="获取题目失败,形成题目列表失败"; ret = false; } return ret; } bool OneQuestion(const string &number,string *html) { Question q; bool ret = true; if(model_.GetOneQuestion(number,&q)) { //获取指定信息的题目成功,构建程网页 view_.OneExpandHtml(q,html); } else { *html="获取指定题目题目失败,形成题目列表失败"; ret = false; } return ret; } void Login(const std::string in_json,std::string *out_json) { //in_json是发送过来的请求数据,用户的账号等待 //返回渲染的登录界面 view_.LoginExpandHtml(out_json); } void Register(const std::string in_json,std::string *out_json) { if(view_.RegisterExpandHtml(out_json)){ LOG(INFO)<<"插入成功"<<"\n"; } else{ LOG(INFO)<<"插入失败,可能是重复的用户"<<"\n"; } } bool UserRegister(const std::string in_json,std::string *out_json) { return model_.UserRegister(in_json,out_json); } //id:: 100 //code:include //input: void Judge(const std::string &number,const std::string in_json,std::string *out_json) { // LOG(INFO)<<"调用Judge功能"<<"\n"; // LOG(DEBUG)< //0.根据题目编号,拿到题目细节 Question q; model_.GetOneQuestion(number,&q); //1.in_json反序列化 ,得到题目的id,得到源代码,input Json::Reader reader; Json::Value in_value; reader.parse(in_json,in_value); std::string code = in_value["code"].asString(); //2.重新拼接用户代码+测试用例代码,形成新的代码 Json::Value compile_value; compile_value["input"] = in_value["input"].asString(); compile_value["code"] = code + q.tail; compile_value["cpu_limit"] = q.cpu_limit; compile_value["mem_limit"] = q.mem_limit; Json::FastWriter writer; std::string compile_string = writer.write(compile_value); //3.选择负载最低的主机,然后发起HTTP请求得到结果 //规则:一直选择,直到主机可用,否则就是全部挂掉 while(true) { int id = 0; Machine *m = nullptr; if(!load_blance_.SmartChoice(&id,&m)) { break; } //4.*out_json = 将结果复制给out_json Client cli(m->ip,m->port); m->IncLoad(); LOG(DEBUG)<<"选择主机成功,主机id:"< if(auto res = cli.Post("/compile_and_run",compile_string,"application/json;charset=utf-8")) { //将我们的结果返回给out_json if(res->status == 200) { *out_json = res->body; m->DecLoad(); LOG(INFO)<<"请求编译和运行服务成功..."<<"\n"; break; } m->DecLoad(); } else { //请求失败 LOG(ERROR)<<"选择主机失败,主机id:"< load_blance_.OfflineMachine(id); load_blance_.ShowMachines();//仅仅为了调试 } //m->DecLoad(); } } Control(){} ~Control(){} }; } control模块实现了 负载均衡 负载均衡 第一种:随机数+hash第二种:轮询+hash , 本文是在用轮询+hash为了实现负载均衡所有要把所有主机管理起来,有了Machine类 std::string ip :编译服务器的ipint port:编译服务器的端口uint64_t load :编译服务器的负载std::mutex *mtx:每个机器可能会同时被多个用户访问,所以要有锁来保证临界资源,并且mutex是不允许拷贝的,所以这里直接用指针,这样在赋值构造和拷贝构造就没事了 view渲染功能:将后端的代码渲染到html返回给前端 这里就要使用到ctemplate库了: #pragma once #include #include #include // #include"oj_model.hpp" #include"oj_model2.hpp" namespace ns_view { using namespace ns_model; const std::string template_path ="./template_html/"; const std::string login_path = "./login_html/"; class View { public: View(){} ~View(){} bool RegisterExpandHtml(std::string *html) { //新城路径 std::string src_html = login_path + "register.html"; //形成数据字典 ctemplate::TemplateDictionary root("register"); //获取渲染的网页 ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP); //开始渲染 tpl->Expand(html,&root); return true; } void LoginExpandHtml(std::string *html) { //形成路径 std::string src_html = login_path + "login.html"; //形成数据字典 ctemplate::TemplateDictionary root("my_login"); //获取渲染网页 ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP); //开始渲染 tpl->Expand(html,&root); } void AllExpandHtml(const vector { // 题目的编号 题目的标题 题目的难度 // 推荐使用表格显示 //1。形成路径 std::string src_html = template_path + "all_questions.html"; LOG(INFO)<<"形成路径成功:"<< src_html <<"\n"; //2.形成数据字典 ctemplate::TemplateDictionary root("all_questions"); for(const auto& q:questions) { 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); LOG(INFO)<<"获取渲染网页的html成功"<<"\n"; //4.开始完成渲染功能 tpl->Expand(html,&root); LOG(INFO)<<"渲染成功"<<"\n"; } void OneExpandHtml(const Question &q,std::string *html) { //形成路径 std::string src_html = template_path + "one_question.html"; LOG(DEBUG)<<"one expand html :"< //q.desc //形成数字典 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("pre_code",q.header); //获取被渲染的html ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP); //开始渲染功能 tpl->Expand(html,&root); } }; } 总结 1.前端的代码在博客最上端绑定的文件当中 ,篇幅太长不展示出来了 2.该项目的技术栈众多,是c++后端和前端进行交互的一个项目 3.项目的难点有:负载均衡的分配到每一台编译服务器、容错处理,能够处理多种不同的错误原因、并发处理要对临界资源的管理、以及高并发访问的话要对效率有所保证,毕竟在线oj服务是具有时效性的 4.debug困难,要在test.cc下测试成功后再进行编写,便于修改bug 推荐文章
发表评论