前言:

该篇讲述了实现基于负载均衡式的在线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 *target,std::string sep)

{

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 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:"<

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 online;

//所有离线主机的id

std::vector offline;

//保证选择主机上的这个东西要保证数据安全

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 tokens;

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 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:"<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 :编译服务器的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 &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 :"<

//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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

推荐文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。