前言:
该篇讲述了实现基于负载均衡式的在线oj,即类似在线编程做题网站一样,文章尽可能详细讲述细节即实现,便于大家了解学习。
文章将采用单篇不分段形式(ps:切着麻烦),附图文,附代码,代码部署在云服务器上
技术栈
C++ STL标准库
Boost 标准库
cpp-httpib 开源库
ctemplate 第三方开源前端网页渲染库
jsoncpp 第三方开源序列化、反序列化库
负载均衡的设计
多进程、多线程
MYSQL C connect
Ace前端在线编辑器
html/cdd/js/jquery/ajax
开发环境
- vscode
- mysql workbench
- Centos 7云服务器
宏观结构
- comm:公共模块
- compile_sever:编译运行模块
- oj_server:获取题目,负载均衡等
项目演示:
项目设计 -- 编译服务
供程序中各个部分调用的方法类:
- #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
*target,std::string sep) - {
- boost::split(*target,str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
- //boost split
- }
- };
-
- }
目的:能够编译并运行代码,得到格式化的相关结果
- #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) <
Src(file_name)<<"编译成功!"<<"\n"; - return true;
- }
- }
- LOG(ERROR) <<"编译失败,没有形成可执行程序,return false"<<"\n";
- return false;
-
- }
- };
- };
compiler编译服务只管编译传过来的代码,其他一律不管,它只关心程序是否能够编译过
- #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<
//不要endl刷新 - return std::cout;
- }
- //LOG(INFo) << "message"
- //开放式日志
- #define LOG(level) Log(#level,__FILE__,__LINE__)
- }
- #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;
- }
- }
- };
- }
- #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()形成唯一名字"<
"\n"; -
- 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)<<CodeToDesc(status_code, file_name);
- 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"<
"port"< - }
- // ./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"<
"\n"; - 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 : "<
"\n"; - 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
questions; -
- 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
tokens; - 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
*out) { - 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
questions; - public:
- Model() { }
- bool QueryMySql(const std::string &sql,vector
*out) - {
- //创建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
*out) { - 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
result; - 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:"<
"端口:"<"的load已经清除load = "<"\n"; - 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
machines; - //所有在线的主机
- std::vector<int> online;
- //所有离线主机的id
- std::vector<int> offline;
- //保证选择主机上的这个东西要保证数据安全
- std::mutex mtx;
- public:
- LoadBalance(){
- assert(LoadConf(service_machine));
- LOG(INFO)<<"加载"<
"成功"<<"\n"; - }
- ~LoadBalance(){}
- public:
- bool LoadConf(const std::string &machine_cof)
- {
- std::ifstream in(machine_cof);
- if(!in.is_open())\
- {
- LOG(FATAL) <<"加载:"<
"失败"<<"\n"; - return false;
- }
- std::string line;
- while (getline(in,line))
- {
- std::vector
tokens; - StringUtil::SplitString(line,&tokens,":");
- if(tokens.size()!=2)
- {
- LOG(WARNING) <<"切分"<
"失败"<<"\n"; - std::cout<
0]<<":"<1]< - 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:"<
size()<<"\n"; - //通过编译,找到负载最小的机器
- *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:"<
size()<<"offline"<size()<<"\n"; -
-
- }
- void ShowMachines()
- {
- mtx.lock();
- LOG(INFO)<<"online:"<
size()<<"offline"<size()<<"\n"; - 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
all; - 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:"<
"\n详情:"<ip<<":"<port<<"当前主机负载:"<Load()<<"\n"; - 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:"<
"详情:"<ip<<":"<port<<"可能已经离线"<<"\n"; - load_blance_.OfflineMachine(id);
- load_blance_.ShowMachines();//仅仅为了调试
-
- }
- //m->DecLoad();
- }
-
-
- }
- Control(){}
- ~Control(){}
- };
- }
control模块实现了 负载均衡
- 负载均衡
- 第一种:随机数+hash
- 第二种:轮询+hash , 本文是在用轮询+hash
- 为了实现负载均衡所有要把所有主机管理起来,有了Machine类
- std::string ip :编译服务器的ip
- int 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
&questions,std::string *html) - {
- // 题目的编号 题目的标题 题目的难度
- // 推荐使用表格显示
- //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 :"<
"\n"; - //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
-
相关阅读:
车企数智化丨帷幄SDP数字化护航经销商业务健康度
2022年最新安徽建筑施工信号工(建筑特种作业)考试真题题库及答案
LeetCode[636]函数的独占时间
Tableau自学四部曲_Part3:基础图表制作
【软考软件评测师】第二十五章 系统安全设计(网络攻击)
【图论 树 深度优先搜索】2246. 相邻字符不同的最长路径
第十二章 配置 Apache 以与 Web 网关配合使用 (Windows)
厉害了!阿里内部都用的Spring+MyBatis源码手册,实战理论两不误
docker安装anaconda3 python环境
GEE16: 区域日均降水量计算
-
原文地址:https://blog.csdn.net/Obto_/article/details/132558354