【项目】均衡代码评测

@TOC

目录

项目介绍

开发环境

主要技术

项目实现

公共模块

日志

工具类

编译运行模块

介绍

编译

运行

编译和运行结合起来

业务逻辑模块

介绍

MVC模式框架

模型(Model)

视图(View)

控制器(Controller)

负载均衡设计

判题

会话模块

项目总结 


项目介绍


该项目是基于负载均衡的在线oj,模拟我们平时刷题网站(leetcode和牛客)写的一个在线判题系统。

项目主要分为五个模块:

  • 编译运行模块:基于httplib库搭建的编译运行服务器,对用户提交的代码进行测试
  • 业务逻辑模块:基于httplib库并结合MVC模式框架搭建oj服务器,负责题目获取,网页渲染以及负载均衡地将用户提交代码发送给编译服务器进行处理
  • 数据管理模块:基于MySQL数据库对用户的数据、题目数据进行管理
  • 会话模块:基于cookie和session针对登录用户创建唯一的会话ID,通过cookie返回给浏览器
  • 公共模块:包含整个项目需要用到的第三方库以及自己编写的工具类的函数

开发环境

Centos7.6、C/C++、vim、g++、MySQL Workbench、Postman

主要技术

  • C++ STL 标准库
  • cpp-httplib 第三方开源网络库
  • ctemplate google第三方开源前端网页渲染库
  • jsoncpp 第三方开源序列化、反序列化库
  • 负载均衡设计
  • MVC模式框架
  • ajax
  • MySQL

项目实现

公共模块

日志

为了方便后期编码调试和项目演示,这里设计了一个日志打印函数,日志打印的格式如下:

日志的五个级别:

  • INFO:正常信息
  • DEBUG:调试信息
  • WARNING:警告信息
  • ERROR:错误信息
  • FATAL:致命信息

实现如下:

#include <iostream>
#include <string>
#include <ctime>#define INFO 1
#define DEBUG 2
#define WARNING 3
#define ERROR 4
#define FATAL 5#define LOG(level, msg) Log(#level, msg, __FILE__, __LINE__)void Log(const std::string level, const std::string msg, std::string filename, int line)
{std::cout << "[" + level + "][" + msg + "][" << time(nullptr) << "][" + filename + "][" << line << "]" << std::endl;
}

工具类

工具类模块中存放着四个工具类:

  • 时间工具类:包含了毫秒级时间戳的获取的方法
  • 路径工具类:包含了对不同文件添加后缀和拼接临时文件路径的方法
  • 文件工具类:包含了对文件读写、判断文件是否存在等文件操作方法
  • 字符串工具类:包含了对字符串进行切割等操作字符串的方法

编译运行模块

介绍

该模块负责编译运行oj_server上传过来的代码,并将结果返回给oj_server。oj_server会向编译服务器发送json串,格式如下:

code:用户代码
input:用户自己提交的代码的输入
cpu_limit:时间限制
mem_limit:内存限制{"code":"xxx","input":"xxx","cpu_limit":"xxx","mem_limit":"xxx"
}

编译服务器需要将代码提取出来,并进行编译,结果以json串格式返回,如下:

status:代码运行状态码
reason:原因
stderr:代码运行完报错信息
stdout:代码运行完的结果{"status":"xxx","reason":"xxx","stderr":"xxx","stdout":"xxx"
}

        编译服务器是基于第三方库cpp-httplib进行搭建的,需要注意的是,编译此库需要用安装新版本gcc,需要是7以上即可。compile_server注册了两种请求方式——/check_net和/compile_run,oj_server可以通过请求/check_net,根据响应来判断compile_server是否上线,可以给上线主机发起/compile_run请求对代码进行编译,并将结果响应给oj_server

int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage:\n\t" << argv[0] << " port" << std::endl;return -1;}Server svr;// 用来给oj_server检测网络是否通畅svr.Get("/check_net", [](const Request &req, Response &rep){ rep.set_content("ok", "text/html;charset=utf-8"); });// 注册POST方法svr.Post("/compile_run", [](const Request &req, Response &rep){std::string in_json = req.body;std::string out_json;CompileRun::Start(in_json, out_json);rep.set_content(out_json, "application/json;charset=utf-8"); });LOG(INFO, "begin listen");svr.listen("0.0.0.0", atoi(argv[1]));
}

简单的描述框图:

编译

用户的代码可以写入到文件中,并保存在我们项目设置的temp目录下。对应每一个用户的代码的文件,我们都需要给它设置一个唯一的文件名,这个文件名我们通过毫秒级时间戳+原子性递增id生成唯一的一个文件名

毫秒级时间戳获取方法:可以通过gettimeofday这个函数先获取到当前时间信息,从struct timeval这个结构体中提取,如下

int gettimeofday(struct timeval *tv, struct timezone *tz);struct timeval {time_t      tv_sec;     /* seconds */suseconds_t tv_usec;    /* microseconds */
};

我们可以将tv_sec除以1000,tv_usec乘以1000,二者都转为毫秒,再相加,这样就可以得到当前的毫秒级时间戳

单单靠一个毫秒级时间戳还不能够完全保证唯一性,所以这里再拼接一个原子性递增id,这里使用atomic_uint,从0开始递增,这样即便时间戳相同,id也不是相同的,这样就保证了文件名的唯一性,实现如下:

static std::string GetMsTimeStamp()
{struct timeval tv;gettimeofday(&tv, nullptr);return std::to_string(tv.tv_sec * 1000 + (int)(tv.tv_usec / 1000));
}static std::string UniqueFilename()
{// 毫秒级时间戳+原子性递增得出唯一文件名static std::atomic_uint id(0);++id;return TimeUtil::GetMsTimeStamp() + "_" + std::to_string(id);
}

获取到了唯一的文件名之后,就可以给不同的文件添加不同的后缀,我们的项目有这么几个文件:

// 编译 如果编译成功,会生成可执行,编译失败,错误信息会被记录到xxx_x.compile_err文件中
xxx_x.cc
xxx_x.compile_err
xxx_x.exe
// 运行
xxx_x.stdin
xxx_x.stdout
xxx_x.stderr
        static bool Compile(const std::string &filename){// 要编译的文件放在了temp目录下// filename.exe filename.cc filename.errumask(0);  //重置文件描述符的权限int err_fd = open(PathUtil::CompileError(filename).c_str(), O_CREAT | O_WRONLY, 0644);if (err_fd < 0){LOG(ERROR, "open error file fail");exit(1);}pid_t id = fork();if (id == 0){// child// 1. 打开错误文件,没有就创建dup2(err_fd, 2);// 程序替换,编译代码 g++ -o target.ext src.cc -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(filename).c_str(), PathUtil::Src(filename).c_str(), "-std=c++11", "-D", "COMPILE_RUN", nullptr);// 失败才会走到这一步LOG(ERROR, "compile execlp fail");exit(2);}else if (id < 0){// errorclose(err_fd);LOG(ERROR, "compile fork fail");return false;}// parentwaitpid(id, nullptr, 0);close(err_fd);// 判断exe文件是否存在 是否编译成功if (FileUtil::FileIsExists(PathUtil::Exe(filename))){LOG(INFO, "file: " + filename + " 编译成功");return true;}LOG(ERROR, "file: " + filename + " 编译失败");return false;}

编译开始,我们可以打开xxx_x.compile_err,并对标准错误进行重定向,如果编译错误,那么错误信息会被写入到该文件中,编译成功,该文件将为空。这个项目我们通过创建子进程并进行程序替换的方式来编译源文件,编译完成之后,我们只需要让父进程检查temp目录下是否存在可执行程序文件,如果有则说明编译成功,否则编译失败。

运行

编译成功后,就要开始对可执行程序进行执行了,执行之前,需要打开三个文件,也就是上面谈到的xxx_x.stdin、xxx_x.stdout和
xxx_x.stderr三个文件,并将标准输入、标准输出和标准错误分别重定向到三个文件中。执行可执行程序的方式和上面的一样,也是通过创建子进程并进行程序替换的方式运行可执行程序,通过退出码分析出运行结果。

我们这个项目对每道题题目的代码运行时间和内存大小都有限制,所以我们执行可执行程序之前我们需要对内存和时间进行限制,这里使用setrlimit系统函数来进行设置,接口如下:

int setrlimit(int resource, const struct rlimit *rlim);

struct rlimit结构体(描述软硬限制),原型如下:

struct rlimit {rlim_t rlim_cur;rlim_t rlim_max;
};

这里我们需要设置的两个参数分别是RLIMIT_ASRLIMIT_CPU,如下:

RLIMIT_AS  // 进程的最大虚内存空间,字节为单位。
RLIMIT_CPU // 最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这            一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,            核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。

这里我们将二者的硬限制都设置为无穷大RLIM_INFINITY,软限制设置为题目要求的,具体代码如下:

static void SetProcLimit(int cpu_limit, int mem_limit)
{struct rlimit climit;climit.rlim_cur = cpu_limit;climit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &climit);struct rlimit mlimit;mlimit.rlim_cur = mem_limit * 1024; // 转为kbmlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mlimit);
}

父进程需要分析运行结果,如果waitpid的返回值小于0,说明父进程等待失败,也是运行错误,否则分析status,如果是正常退出,我们可以提取出退出码分析,如果是异常退出,此时我们能够知道子进程是被信号所杀,这时,我们只需要提取出信号即可

         static int Run(const std::string &filename, int cpu_limit, int mem_limit){            std::string execute = PathUtil::Exe(filename);std::string stdin = PathUtil::Stdin(filename);std::string stdout = PathUtil::Stdout(filename);std::string stderr = PathUtil::Stderr(filename);//生成对应的文件用来存储对应的数据// 打开三个文件umask(0);int in_fd = open(stdin.c_str(), O_CREAT | O_WRONLY, 0644);int out_fd = open(stdout.c_str(), O_CREAT | O_WRONLY, 0644);int err_fd = open(stderr.c_str(), O_CREAT | O_WRONLY, 0644);if (in_fd < 0 || out_fd < 0 || err_fd < 0)//万一失败,就得报错{LOG(ERROR, "open std file error");return -1;}pid_t id = fork();if (id < 0){close(in_fd);close(out_fd);close(err_fd);LOG(ERROR, "run fork error");return -1;}else if (id == 0){// child// 进行文件描述符的重定向dup2(in_fd, 0);dup2(out_fd, 1);dup2(err_fd, 2);// 对cpu和内存资源进行限制SetProcLimit(cpu_limit, mem_limit);execl(execute.c_str(), execute.c_str(), nullptr);// 程序替换失败exit(1);}// fatherint status = 0;int ret = waitpid(id, &status, 0);close(in_fd);close(out_fd);close(err_fd);int sig = 0; // 检验是否是被信号所杀if (ret > 0){if (WIFEXITED(status)) //检测进程的终止状态,判断子进程是否正常终止{// 正常退出int exit_code = WEXITSTATUS(status);if (exit_code == 0){LOG(INFO, "run success");}else if (exit_code == 1){LOG(ERROR, "run execlp fail");return -1;}else{LOG(ERROR, "unknow status code:" + std::to_string(exit_code));return -1;}}else{// 异常退出sig = status & 0x7f;LOG(WARNING, "sig: " + std::to_string(sig));}}else{// 等待失败LOG(ERROR, "run wait fail");return -1;}return sig; // 返回收到的信号 正常是0 异常时一个信号}

综合编译和运行结果进行分析,对返回json串进行设置:

  1. 如果编译失败,或编译成功运行失败,我只需要设置status、reason两个个字段
  2. 如果编译运行成功,我们还需要设置stdout和stderr两个字段

编译和运行结合起来

因为编译加上运行可能会有多种情况,如果把这些在编译模块的主函数里面进行结合的话,代码会相当的冗长,难分,为了方便,创建一个compile_run函数来进行进行多种情况的判断。

 static void Start(const std::string &in_json, std::string &out_json){Json::Value in_value;Json::Reader reader;// 对json串进行反序列化reader.parse(in_json, in_value); // 将json串进行转化,把里面的数据给in_valuestd::string code = in_value["code"].asString();   //代码std::string input = in_value["input"].asString(); //输入int cpu_limit = in_value["cpu_limit"].asInt();    //时间限制int mem_limit = in_value["mem_limit"].asInt();    //空间限制//从in_value中获取我们需要的相关数据Json::Value out_value;std::string filename; // 生成的唯一文件名int status_code = 0;  // 状态码int res = 0;if (code.size() == 0) {// 提交空代码status_code = -1;goto END;}filename = FileUtil::UniqueFilename();// 将代码写入文件中if (!FileUtil::WriteFile(PathUtil::Src(filename), code)) //将代码写进filename.cc文件中{// 未知错误:写文件失败status_code = -2;goto END;}// 编译代码if (!Compiler::Compile(filename)){// 代码编译失败status_code = -3;goto END;}// 执行代码res = Runner::Run(filename, cpu_limit, mem_limit);if (res < 0){// 运行时未知错误status_code = -2;}else{//负数  代码有问题// 0    正常运行// 1-31 被信号终止status_code = res;}END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(filename, status_code);if (status_code == 0){out_value["stdout"] = FileUtil::ReadFile(PathUtil::Stdout(filename));out_value["stderr"] = FileUtil::ReadFile(PathUtil::Stderr(filename));}else if (status_code > 0){out_value["stderr"] = FileUtil::ReadFile(PathUtil::Stderr(filename));}Json::FastWriter writer;out_json = writer.write(out_value);// 清除临时文件RemoveTempFile(filename);LOG(INFO, "临时文件已清除");}

编译运行完之后记得需要将相关文件进行删除,不然后台会保留过多无意义的临时文件。

        static void RemoveTempFile(const std::string &filename){std::string src = PathUtil::Src(filename);if (FileUtil::FileIsExists(src))unlink(src.c_str());std::string exe = PathUtil::Exe(filename);if (FileUtil::FileIsExists(exe))unlink(exe.c_str());std::string compile_err = PathUtil::CompileError(filename);if (FileUtil::FileIsExists(compile_err))unlink(compile_err.c_str());std::string in = PathUtil::Stdin(filename);if (FileUtil::FileIsExists(in))unlink(in.c_str());std::string out = PathUtil::Stdout(filename);if (FileUtil::FileIsExists(out))unlink(out.c_str());std::string err = PathUtil::Stderr(filename);if (FileUtil::FileIsExists(err))unlink(err.c_str());}

上面就是整体后端代码的实现,

业务逻辑模块

介绍

        该模块是整个项目业务逻辑的核心,包括用户登录注册、题目获取、与数据库进行数据交互、网页渲染以及协调编译服务器的负载均衡,同时该模块也会用到会话模块和数据库模块,进行用户会话管理、数据管理。综合这些利用第三方库cpp-httplib结合MVC模式框架搭建一个oj服务器,该服务器注册了很多Get和Post请求方法,供前端页面发起ajax请求进行前后端数据交互,及时更新前端页面


int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage:\n\t" << argv[0] << " port" << std::endl;return -1;}// 控制器Control ctrl;pthread_t tid;pthread_create(&tid, nullptr, check, &ctrl);// 会话std::shared_ptr<AllSessionInfo> all_sess(new AllSessionInfo);std::shared_ptr<Server> svr(new Server);std::shared_ptr<UserManage> manager(new UserManage);svr->Get(R"(/all_questions)", [&ctrl](const Request &req, Response &rep){//LOG(INFO, "get questions request");std::string html;ctrl.GetAllQuestionsListHtml(html);rep.set_content(html, "text/html;charset=utf-8"); });svr->Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &rep){ //LOG(INFO, "get one question request");std::string number = req.matches[1];std::string html;ctrl.GetOneQuestionByNumberHtml(number, html);rep.set_content(html, "text/html;charset=utf-8"); });svr->Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &rep){ //LOG(INFO, "get one judge request");std::string number = req.matches[1];std::string out_json;ctrl.Judge(number, req.body, out_json); rep.set_content(out_json, "application/json;charset=utf-8"); });// 注册/******* json* user 账户名* password 密码* ******/svr->Post("/register", [&ctrl](const Request &req, Response &rep){LOG(INFO, "get a register request");std::string out_json;ctrl.Register(req.body, out_json);rep.set_content(out_json, "application/json;charset=utf-8"); });// 登录svr->Post("/sign", [&ctrl, &all_sess, &manager](const Request &req, Response &rep){LOG(INFO, "get a sign request");std::string out_json;int user_id = ctrl.SignIn(req.body, "user", out_json);std::string tmp;if (user_id > 0){Session sess(req.body, user_id, "user");std::string session_id = sess.GetSessionId();tmp = "JSESSION=" + session_id;all_sess->SetSessionInfo(session_id, sess);// 将用户添加到管理manager->AddUserToSet(user_id, 1);}rep.set_header("Set-Cookie", tmp.c_str());// 返回cookierep.set_content(out_json, "application/json;charset=utf-8"); });svr->Get("/GetUserId", [&all_sess](const Request &req, Response &rep){//1.会话校验Json::Value resp_json;resp_json["id"] = all_sess->CheckSessionInfo(req);std::string out_json;Json::FastWriter writer;out_json = writer.write(resp_json); rep.set_content(out_json, "application/json;charset=utf-8"); });svr->Post("/GetUserName", [&all_sess, &ctrl](const Request &req, Response &rep){LOG(INFO, "get a get username request");Json::Value in_json;Json::Reader reader;reader.parse(req.body, in_json);std::string out_json;int id = in_json["id"].asInt();std::string strId = in_json["strId"].asString();std::string table = in_json["table"].asString();Json::Value out_value;out_value["username"] = ctrl.GetUserName(id, strId, table);Json::FastWriter writer;out_json = writer.write(out_value); rep.set_content(out_json, "application/json;charset=utf-8"); });// 修改密码svr->Post("/forget", [&ctrl](const Request &req, Response &rep){LOG(INFO, "get a forget password request");std::string out_json;ctrl.Forget(req.body, out_json);rep.set_content(out_json, "application/json;charset=utf-8"); });// 管理员登录svr->Post("/administrator", [&ctrl, &all_sess, &manager](const Request &req, Response &rep){LOG(INFO, "get a administrator sign request");std::string out_json;int administrator_id = ctrl.SignIn(req.body, "administrators", out_json);std::string tmp;if (administrator_id > 0){Session sess(req.body, administrator_id, "administrator");std::string session_id = sess.GetSessionId();tmp = "JSESSION=" + session_id;all_sess->SetSessionInfo(session_id, sess);// 将用户添加到管理manager->AddUserToSet(administrator_id);}rep.set_header("Set-Cookie", tmp.c_str());// 返回cookierep.set_content(out_json, "application/json;charset=utf-8"); });svr->Post("/add_question", [&ctrl](const Request &req, Response &rep){LOG(INFO, "get a add question request");std::string out_json;ctrl.AddQuestion(req.body, out_json);rep.set_content(out_json, "application/json;charset=utf-8"); });svr->set_base_dir("wwwroot");svr->listen("0.0.0.0", atoi(argv[1]));return 0;
}

MVC模式框架

模型(Model)

Model负责与数据库进行交互,听取控制器的调用,往数据库中插入数据或从数据库中获取数据并让View将请求结果返回给用户

插入数据的几种情形:

  • 用户注册
  • 管理员添加题目

查询数据的几种情形:

  • 用户获取题目信息
  • 获取用户信息

题目设计:

  1. 我们项目题目的属性有这么几个:编号、标题、难度、时间限制、内存限制、题目描述、头文件、用户显示代码、测试代码,后序我们需要对header、body和tail这三个部分进行拼接,形成一份新的代码,再提交给编译服务器
  2. 形成的数据库表结构如下:

struct Question
{std::string show_num; // 显示编号std::string num;      // 题目编号std::string title;    // 题目标题std::string level;    // 题目难度等级int cpu_limit;        // 题目时间限制 单位 sint mem_limit;        // 题目内存限制 单板 bytestd::string desc;     // 题目描述std::string header;   // 用户需要用到的头文件std::string body;     // 显示给用户的代码std::string tail;     // 用来测试用户的代码
};

接口设计:

  1. 接口主要包括加载配置、单个题目的获取、全部题目的获取、添加题目和用户数据相关操作
  2. 加载配置主要是将show_num和num建立起映射关系,show_num这个属性是给用户页面显示的,不直接用number显示给用户的原因是:number在数据库中是自增长的,且每次添加题目,其number是从最大的number+1开始增长,在不删除题目的情况下,number是连续的,如果中途删除了某个题目,后序number这个序列就不会是连续的,中间会断开,如:1、2、3、4,删除了3,再增加一个题目,number是5,这时number序列就是1、2、4、5,这样就不是连续的,所以用number显示给用户不太好,设计一个show_num是连续的,并与number建立好映射关系,这样就比较友好
  3. 单个题目的获取和多个题目的获取,主要是查询数据库,获取到的数据可以交付给View
  4. 添加题目需要往数据库中插入数据,同时更新show_num和number的映射关系
  5. 用户数据操作结合数据库操作一起完成
视图(View)

View负责将Model提供的数据以某种方式呈现给用户,这个项目主要是网页界面。View会使用google的开源库ctemplate进行网页渲染,以这种方式将数据呈现给用户

ctemplate的获取

可以在GitHub的国内镜像网站中获取,速度会比较快,链接如下(里面有详细的安装方法说明):

https://hub.fastgit.xyz/OlafvdSpek/ctemplate

ctemplate的简单用法

  1. {{变量名}}:把它放入我们的网页中,该部分会被替换成我们字典中添加的值,使用如:{{number}}、{{show_num}}
  2. {{#片断名}}:片断在数据字典中表现为一个子字典,字典是可以分级的,根字典下面有多级子字典。片断可以处理条件判断和循环,循环的结束{{/片段名}}
  3. TemplateDictionary:可以创建字典
  4. SetValue:可以往字典中添加模板
  5. AddSectionDictionary:可以往字典中添加子字典
  6. GetTemplate和Expand:两个接口可以获取到扩展之后的模板

接口设计:

这里对题目列表和单个题目两张网页进行渲染,将数据添加到网页中,实现如下:

namespace ns_view
{using namespace ns_model;using namespace ns_view;using namespace ctemplate;const std::string template_path = "wwwroot/template_html/";const std::string user_path = "wwwroot/";class View{public:void ExpandAllQuestionsHtml(const std::vector<Question> qs, std::string &outhtml){std::string src_html = template_path + "all_questions.html";// 创建数据字典TemplateDictionary root("all_questions");for (auto &q : qs){// 往root添加子字典TemplateDictionary *sub = root.AddSectionDictionary("questions_list");sub->SetValue("number", q.num);sub->SetValue("show_num", q.show_num);sub->SetValue("title", q.title);sub->SetValue("level", q.level);}// 获取要渲染的网页  不做删除任何符号的动作Template *tpl = Template::GetTemplate(src_html, DO_NOT_STRIP);// 开始渲染tpl->Expand(&outhtml, &root);}void ExpandOneQuestioHtml(Question &q, std::string &outhtml){std::string src_html = template_path + "question.html";// 创建数据字典TemplateDictionary root("question");root.SetValue("show_num", q.show_num);root.SetValue("title", q.title);root.SetValue("level", q.level);root.SetValue("desc", q.desc);root.SetValue("pre_code", q.body);// 获取要渲染的网页  不做删除任何符号的动作Template *tpl = Template::GetTemplate(src_html, DO_NOT_STRIP);// 开始渲染tpl->Expand(&outhtml, &root);}};
}
控制器(Controller)

Controller是整个项目业务逻辑的控制器负责协调model和view一起完成业务。

负载均衡设计

控制器的核心还包括了一个负载均衡的小模块,帮助控制器根据主机负载选择编译服务器,负载均衡的设计框架如下:

    const std::string service = "./conf/service_machine.conf";// 负载均衡模块class LoadBlance{public:LoadBlance(){assert(LoadConf()); // 加载配置文件}bool LoadConf(){std::ifstream in(service, std::ifstream::in);if (!in.is_open()){LOG(FATAL, "加载主机配置文件失败");return false;}std::string line;while (getline(in, line)){std::vector<std::string> tokens;StringUtil::Spilit(line, tokens, ":");if (tokens.size() != 2){LOG(WARNING, "切分" + line + "失败");return false;}Machine m;m.ip = tokens[0];m.port = std::stoi(tokens[1]);m.status = OFFLINE;     // 默认都是下线m.mtx = new std::mutex; // 记得释放m.id = _machines.size();_machines.push_back(std::move(m));}in.close();LOG(INFO, "加载主机配置文件成功");return true;}int AutoChoose(Machine *&machine){_mtx.lock();if (_count == 0){LOG(FATAL, "所有主机全部下线,请及时核查原因");_mtx.unlock();return -1;}int id = 0;int min_load = INT_MAX;for (int i = 0; i < _machines.size(); ++i){// 当前主机下线就选择另一台主机if (_machines[i].status == OFFLINE)continue;int load = _machines[i].GetLoad();std::cout << "load " << i << ": " << load <<  std::endl;if (load < min_load){min_load = load;id = i;}}_mtx.unlock();machine = &_machines[id];return id;}void Online(int id){_mtx.lock();_machines[id].status = ONLINE;LOG(INFO, "主机" + std::to_string(id) + "已经上线 详情" + _machines[id].ip + ":" + std::to_string(_machines[id].port));_count++;_mtx.unlock();}void OfflineMachine(int id){_mtx.lock();_machines[id].status = OFFLINE;_count--;_machines[id].ResetLoad();_mtx.unlock();}void ShowMachines(){_mtx.lock();std::cout << "-------------------online-------------------" << std::endl;for (auto &m : _machines){if (m.status == ONLINE)std::cout << m.id << " ";}std::cout << std::endl;std::cout << "-------------------offline-------------------" << std::endl;for (auto &m : _machines){if (m.status == OFFLINE)std::cout << m.id << " ";}std::cout << std::endl;_mtx.unlock();}public:std::vector<Machine> _machines; // 可以提供服务的所有主机,下标充当主机idint _count = 0;                 // 在线主机数std::mutex _mtx;};

主机设计: 主机的属性应该有状态(上线、下线),当前负载,主机id,主机ip,主机绑定端口号,如下:

    enum status{ONLINE,OFFLINE};class Machine{public:// 增加主机负载void IncLoad(){if (mtx)mtx->lock();++load;if (mtx)mtx->unlock();}// 减少主机负载void DecLoad(){if (mtx)mtx->lock();--load;if (mtx)mtx->unlock();}void ResetLoad(){if (mtx)mtx->lock();load = 0;if (mtx)mtx->unlock();}// 获取主机负载uint64_t GetLoad(){uint64_t curload;if (mtx)mtx->lock();curload = load;if (mtx)mtx->unlock();return curload;}~Machine(){// ...}public:int id; // 主机idstd::string ip;int port;enum status status = OFFLINE;uint64_t load = 0; // 负载std::mutex *mtx = nullptr;};

加载配置文件: 我们的配置文件中存放着我们需要用到的主机信息,每行存放一台主机的信息,格式如:ip:port,如下:

127.0.0.1:8082
127.0.0.1:8083
127.0.0.1:8084

通过读取文件,并对每一行进行分析,将主机信息存放到vector容器中

根据负载选择主机: 我们需要遍历所有上线的主机,选出负载最小的那一台主机,如果当前上线主机上为0,则打印出提示信息,发起警告,如果选择主机成功,我们可以返回主机id,具体操作如下:

        int AutoChoose(Machine *&machine){_mtx.lock();if (_count == 0){LOG(FATAL, "所有主机全部下线,请及时核查原因");_mtx.unlock();return -1;}int id = 0;int min_load = INT_MAX;for (int i = 0; i < _machines.size(); ++i){// 当前主机下线就选择另一台主机if (_machines[i].status == OFFLINE)continue;int load = _machines[i].GetLoad();std::cout << "load " << i << ": " << load <<  std::endl;if (load < min_load){min_load = load;id = i;}}_mtx.unlock();machine = &_machines[id];return id;}

主机上线: 我们需要能够设计一个接口来更改status这个字段,如果主机上线了,我们就把status字段改为ONLINE,否则改为OFFLINE,如何检测主机是否上线呢?还记得我们前面在编译服务器中注册的一个Post /chech_net用来检测网络通畅请求方法吗,我们可以在oj_sever启动时开辟一个线程,该线程会不停地给所有状态为OFFLINE的主机发起请求,如果得到了响应,那么说明,该主机已经上线,我们就可以把该主机的status字段改成ONLINE,表示主机已经上线,检测方法如下:

void *check(void *arg)
{sleep(1);pthread_detach(pthread_self());Control *ctrl = (Control *)arg;LoadBlance *loadblance = &ctrl->_lb;while (1){std::vector<Machine> machines = loadblance->_machines;for (auto &machine : machines){if (machine.status == OFFLINE){Client clt(machine.ip, machine.port);if (auto res = clt.Post("/check_net", "check", "text/plain;charset=utf-8")){// 得到响应,证明对端主机上线loadblance->Online(machine.id);}}}}
}

主机上线了。我们需要对负载均衡模块中的上线主机上count进行加1的操作:

        void Online(int id){_mtx.lock();_machines[id].status = ONLINE;LOG(INFO, "主机" + std::to_string(id) + "已经上线 详情" + _machines[id].ip + ":" + std::to_string(_machines[id].port));_count++;_mtx.unlock();}

主机下线: 如果自己下线,我们只需要对count进行减1的操作,并对主机的负载进行重置:

        void OfflineMachine(int id){_mtx.lock();_machines[id].status = OFFLINE;_count--;_machines[id].ResetLoad();_mtx.unlock();}
判题

步骤:

对传入json串进行反序列化,获取题目id

Json::Value in_value;
Json::Reader reader;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString();

根据题目id获取测试代码,并拼接上用户提交的代码,填充code,code需要用header、body和tail三种进行拼接

Question q;
_model.GetOneQuestionByNumber(number, q);
in_value["code"] = q.header + code + q.tail;
in_value["cpu_limit"] = q.cpu_limit;
in_value["mem_limit"] = q.mem_limit;Json::FastWriter writer;
std::string compile_json = writer.write(in_value);

选择一台负载最小的主机,调用负载均衡里面的AutoChoose接口完成

选择主机成功,向该主机发起http请求,将json传过去,响应得到json之后再返回给前端

会话模块

用户登录成功之后,服务器会针对该用户创建一个会话,并保存在服务器这一端,同时服务器会根据用户的身份、账号和密码再利用MD5哈希算法生成唯一的Session ID,并通过Cookie返回给浏览器。

class Session
{
public:Session(){}Session(const std::string &in_json, int id, const std::string &identity){}bool SumMd5(){}//获取会话idstd::string &GetSessionId(){}public:std::string _session_id; //当前会话的会话idstd::string _real_str;   //用来生成会话id的原生字符串std::string _identity;int _id;
};

MD5接口如下:

int MD5_Init(MD5_CTX *c);// 初始化MD5码
int MD5_Update(MD5_CTX *c, const void *data, unsigned long len);// 更新获取MD5码
int MD5_Final(unsigned char *md, MD5_CTX *c);// 生成的16字节MD5码放在md中

实现如下: 其中_str是由用户的账号名+密码+身份组成

bool SumMd5()
{MD5_CTX ctx;// 1.初始化MD5_Init(&ctx);// 2.更新MD5if (MD5_Update(&ctx, _str.c_str(), _str.size()) != 1){return false;}// 3.取出MD5unsigned char md5[16] = {0};if (MD5_Final(md5, &ctx) != 1){return false;}char tmp[3] = {0};char buf[32] = {0};// 将md5码转为16进制,进行输出for (int i = 0; i < 16; i++){snprintf(tmp, sizeof(tmp) - 1, "%02x", md5[i]);strncat(buf, tmp, 2);}_session_id = buf;return true;
}

其中Session ID会返回给浏览器,单个会话会保存在服务器中。用户在获取每一张网页页面,前端页面会发起异步ajax请求,请求获取用户id,后端收到请求需要进行会话校验,如果校验失败,则返回一个小于0的用户ID,不允许用户获取页面,同时提示用户进行登录,如果成功则给用户返回一个大于0的用户ID,并给用户显示页面,且可以如下:

var user_id = -1;
var user_name = "";function CheckUser() {console.log(user_id);if (user_id > 0) {$(".nav_bar .last_li").text(user_name);return;}// 发起ajax请求获取用户id$.ajax({url: "/GetUserId",method: "Get",dataType: 'json',contentType: 'application/json;charset=utf-8',success: function (data) {console.log(data);if (data.id > 0) {user_id = data.id;GetUsername();console.log(user_name);$(".nav_bar .last_li").text(user_name);// $(".nav_bar .last_li").attr("href", "#");} else {alert("请先进行登录");window.location.href = "/signin.html";}},});
}

数据库模块
数据库模块主要与MVC模式框架中的Model进行数据交互。该模块模块主要使用MySQL C Connect连接数据库,对项目数据进行存放,该项目主要有三张表:oj_questions、user和administrators,分别存放题目信息,用户信息和管理员信息,如下:

数据库模块代码框架:

namespace ns_database
{class DataBase{public:DataBase(){//初始化mysql操作句柄_mfp = mysql_init(nullptr);assert(ConnectMysql());pthread_mutex_init(&_mtx, nullptr);}~DataBase(){//关闭连接mysql_close(_mfp);pthread_mutex_destroy(&_mtx);}bool ConnectMysql(){if (nullptr == mysql_real_connect(_mfp, "127.0.0.1", "oj_client", "125000", "oj", 3306, nullptr, 0)){LOG(FATAL, "连接数据库失败");return false; // 给开发人员看}mysql_set_character_set(_mfp, "utf8");LOG(INFO, "连接数据库成功");return true;}std::string GetUserName(int id, const std::string &strId, const std::string &table){std::string sql = "select user from " + table + " where " + strId + " = ";sql += std::to_string(id) + ";";// std::cout << sql << std::endl;int ret = mysql_query(_mfp, sql.c_str());if (ret != 0){LOG(WARNING, "SQL执行失败: " + std::string(mysql_error(_mfp)));LOG(WARINNG, sql + " 执行失败");return ""; // 给开发人员看}// 提取结果MYSQL_RES *result = mysql_store_result(_mfp);// 分析结果int row = mysql_num_rows(result);if (row == 0){LOG(ERROR, "查询结果为空,SQL语句: " + sql);LOG(ERROR, "无结果");return "";}MYSQL_ROW line = mysql_fetch_row(result);std::cout << "line[0]" << " " << line[0] << std::endl;std::cout << "line[1]" << " " << line[1] << std::endl;std::cout << "line[2]" << " " << line[2] << std::endl;free(result);return line[2];}void AddQuestion(const std::string &in_json, std::string &out_json){// 1.获取用户信息Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);std::string title = in_value["title"].asString();std::string level = in_value["level"].asString();std::string desc = in_value["desc"].asString();std::string header = in_value["header"].asString();std::string body = in_value["body"].asString();std::string tail = in_value["tail"].asString();std::string cpu_limit = in_value["cpu"].asString();std::string mem_limit = in_value["mem"].asString();std::string sql = "insert into oj_questions(title, level, `desc`, `header`, `body`, `tail`, cpu_limit, mem_limit) values('";sql += title + "', '";sql += level + "', '";sql += desc + "', '";sql += header + "', '";sql += body + "', '";sql += tail + "', ";sql += cpu_limit + ", ";sql += mem_limit + ");";// LOG(INFO, sql);Json::Value out_value;if (Insert(sql)){LOG(INFO, "题目添加成功");out_value["result"] = "success";}else{LOG(INFO, "题目添加失败");out_value["result"] = "fail";}Json::FastWriter writer;out_json = writer.write(out_value);}bool QueryMysql(const std::string &sql, MYSQL_RES *&result){pthread_mutex_lock(&_mtx);int ret = mysql_query(_mfp, sql.c_str());if (ret != 0){LOG(WARINNG, sql + " 执行失败");perror("执行失败");return false;}// 提取结果result = mysql_store_result(_mfp);pthread_mutex_unlock(&_mtx);return true;}bool Insert(const std::string &sql){int ret = mysql_query(_mfp, sql.c_str());if (ret != 0){LOG(WARINNG, sql + " 执行失败");LOG(WARINNG, "MySQL 错误: " + std::string(mysql_error(_mfp)));return false; // 给开发人员看}return true;}int Select(const std::string &sql, const std::string &password, std::string &out){// std::cout << sql << std::endl;int ret = mysql_query(_mfp, sql.c_str());if (ret != 0){LOG(WARINNG, sql + " 执行失败");return -1; // 给开发人员看}// 提取结果MYSQL_RES *result = mysql_store_result(_mfp);// 分析结果int row = mysql_num_rows(result);if (row == 0){out = "1";return -2;}MYSQL_ROW line = mysql_fetch_row(result);if (line[1] != password){out = "2";return -3;}out = "0";free(result);return atoi(line[0]);}bool Update(const std::string &sql, std::string &out){// std::cout << sql << std::endl;int ret = mysql_query(_mfp, sql.c_str());if (ret != 0){LOG(WARINNG, sql + " 执行失败");return false; // 给开发人员看}out = "0";return true;}private:MYSQL *_mfp;pthread_mutex_t _mtx;};
}

项目总结 


问题与解决

如何检测编译主机是否上线?通过给编译主机注册一个检测网络畅通的请求方法,oj_server可以单独开一个线程不停地给状态为OFFLINE的主机发起/check_net请求,以此判断网络是否畅通,然后修改主机状态
用户代码提交过快,会导致后端数据库频繁请求,导致数据库报错:Operation now in progress。解决:前端界面通过js控制按钮点击事件,每1s才能够点击一次,后端数据库模块对该sql执行进行加锁,二者结合有效解决问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/770153.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用html做一个2048小游戏

下载地址: https://pan.xunlei.com/s/VNtiF13HxmmE4gglflvS1BUhA1?pwdvjrt# 提取码&#xff1a;vjrt”

Linux文件系统 底层原理

linux文件、目录、Inode inode负责文件的元数据和数据存储&#xff0c;文件存储块负责实际数据的存储&#xff0c;而目录文件维护文件名和inode之间的联系。 1. 用户空间到内核空间 首先&#xff0c;当用户程序请求打开一个文件时&#xff08;例如使用open系统调用&#xff09…

Excel 打开后提示:MicrosoftExcel无法计算某个公式。在打开的工作簿中有一个循环引用...

目录预览 一、问题描述二、原因分析三、解决方案四、参考链接 一、问题描述 MicrosoftExcel无法计算某个公式。在打开的工作簿中有一个循环引用&#xff0c;但无法列出导致循环的引I用。请尝试编辑上次输入的公式&#xff0c;或利用“撤消”命令删除该公式&#xff0c;如下图&…

微服务(基础篇-003-Nacos)

目录 Nacos注册中心&#xff08;1&#xff09; 认识和安装Nacos&#xff08;1.1&#xff09; Nacos快速入门&#xff08;1.2&#xff09; 服务注册到Nacos(1.2.1) Nacos服务分级存储模型&#xff08;1.3&#xff09; 配置集群&#xff08;1.3.1&#xff09; 根据集群修改…

如何用Airtest脚本连接无线Android设备?

之前我们已经详细介绍过如何用AirtestIDE无线连接Android设备&#xff0c;它的关键点在于&#xff0c;需要先 adb connect 一次&#xff0c;才能点击 connect 按钮无线连接上该设备&#xff1a; 但是有很多同学&#xff0c;在使用纯Airtest脚本的形式连接无线设备时&#xff0c…

K8s-网络原理-下篇

引言 本文是《深入剖析 K8s》的学习笔记&#xff0c;相关图片和案例可从https://github.com/WeiXiao-Hyy/k8s_example中获取&#xff0c;欢迎Star! K8s 的网络隔离: NetWorkPolicy K8s 如何考虑容器之间网络的“隔离” -> NetWorkPolicy 以下是一个 NetWorkPolicy 的定义…

Android开发 --- Android12外部存储权限问题

1.问题 Android12使用如下权限&#xff0c;将不会获得读写文件的权限 <uses-permission android:name"android.permission.WRITE_EXTERNAL_STORAGE" /> 2.解决 if (!Environment.isExternalStorageManager()) {Intent intent new Intent(Settings.ACTION_M…

将数据转换成xml格式的文档并下载

现在有一个实体类对象的集合&#xff0c;需要将它们转换为xml文档&#xff0c;xml文档就是标签集合的嵌套&#xff0c;例如一个学生类&#xff0c;有姓名、年龄等&#xff0c;需要转换成一下效果&#xff1a; <student><age>14</age><name>张三</na…

Java学习Day3

一道简单练习题&#xff0c;对编号进行随机排名 import java.util.Random; import java.util.Scanner;public class Main {public static void rand(int[] a,int n){Random rnew Random();for (int i0;i<n;i){int rrr.nextInt(n);int tempa[i];a[i]a[rr];a[rr]temp;}}publ…

时间戳的转换-unix时间戳转换为utc时间(python实现)

import datetimetimestamp = 1711358882# 将时间戳转换为UTC时间 utc_time = datetime.datetime.utcfromtimestamp(timestamp)# 格式化并输出时间 formatted_time = utc_time.strftime(%Y-%m-%d %H:%M:%S) print(formatted_time)同样:UTC如何转换为unix时间戳 from datetime …

如何利用python 把一个表格某列数据和另外一个表格某列匹配 类似Excel VLOOKUP功能

环境: python3.8.10 Excel2016 Win10专业版 问题描述: 如何利用python 把一个表格某列数据和另外一个表格某列匹配 类似Excel VLOOKUP功能 先排除两表A列空白单元格,然后匹配x1表格和x2表格他们的A列,把x1表格中A列A1-A810范围对应的B列B1-B810数据,匹配填充到x2范围…

C语言程序与设计——预处理命令

宏 在C语言中宏有三种形式: 定义符号常量定义傻瓜表达式定义代码段 在使用宏的过程中需要注意的是&#xff0c;宏的作用仅仅是在预处理阶段对代码进行替换&#xff0c;而非进行运算&#xff0c;所以在使用时&#xff0c;如果出现了我们预期之外的结果&#xff0c;很有可能是宏…

Spring IoC DI(1)

IoC & DI入门 Spring 通过前面的学习, 我们知道了Spring是一个开源框架, 它让我们的开发更加简单. 它支持广泛的应用场景, 有着活跃且庞大的社区, 这就是Spring能够长久不衰的原因. 但是这个概念还是比较抽象. 可以用更具体的话描述Spring, 那就是: Spring是包含了众多…

Jakarta项目介绍

概述 在升级Spring Boot到3.0版本以后&#xff0c;或升级Spring到6.0版本以上&#xff0c;会发现应用编译失败或启动失败等问题。 经过排查不难得知&#xff0c;Spring 6或Spring Boot 3&#xff08;实际上依赖于Spring 6&#xff09;不再支持javax.开头的一系列依赖包&#…

人工智能的迷惑行为:AI世界的隐秘角落

人工智能迷惑行为大赏 在当今数字化时代&#xff0c;人工智能技术的飞速发展给我们的生活带来了诸多便利和可能性&#xff0c;但同时也伴随着一些令人困惑的现象和行为。本文将深入探讨人工智能的迷惑行为&#xff0c;揭示AI世界中的隐秘角落&#xff0c;让我们一同探寻这个充…

《深入浅出LLM 》(二):大模型基础知识

&#x1f389;AI学习星球推荐&#xff1a; GoAI的学习社区 知识星球是一个致力于提供《机器学习 | 深度学习 | CV | NLP | 大模型 | 多模态 | AIGC 》各个最新AI方向综述、论文等成体系的学习资料&#xff0c;配有全面而有深度的专栏内容&#xff0c;包括不限于 前沿论文解读、…

CPU缓存行及伪共享

CPU Cache概述 随着CPU的频率不断提升&#xff0c;而内存的访问速度却没有质的突破&#xff0c;为了弥补访问内存的速度慢&#xff0c;充分发挥CPU的计算资源&#xff0c;提高CPU整体吞吐量&#xff0c;在CPU与内存之间引入了一级Cache。随着热点数据体积越来越大&#xff0c;…

代码随想录算法训练营第三十二天 | 122.买卖股票的最佳时机II ,55. 跳跃游戏 , 45.跳跃游戏II

贪心&#xff1a;只要把每一个上升区间都吃到手&#xff0c;就能一直赚 class Solution { public:int maxProfit(vector<int>& prices) {int res 0;for(int i 1;i< prices.size();i){int diff prices[i] - prices[i-1];if(prices[i] > prices[i-1]){res d…

蓝桥杯练习题总结(二)dfs题、飞机降落、全球变暖

目录 一、飞机降落 二、全球变暖 初始化和输入 确定岛屿 DFS搜索判断岛屿是否会被淹没 计算被淹没的岛屿数量 三、军训排队 一、飞机降落 问题描述&#xff1a; N架飞机准备降落到某个只有一条跑道的机场。其中第 i 架飞机在 时刻到达机场上空&#xff0c;到达时它的剩余…