银川公司做网站,单位网站建设维护情况报告,wordpress怎么给别人建站,网站建设的功能描述TOC
目录 项目介绍
开发环境
主要技术
项目实现
公共模块
日志
工具类
编译运行模块
介绍
编译
运行
编译和运行结合起来
业务逻辑模块
介绍
MVC模式框架
模型#xff08;Model#xff09;
视图#xff08;View)
控制器#xff08;Controller#xff09…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模式框架ajaxMySQL 项目实现
公共模块
日志
为了方便后期编码调试和项目演示这里设计了一个日志打印函数日志打印的格式如下 日志的五个级别
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_runoj_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;charsetutf-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;charsetutf-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除以1000tv_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.stderrstatic 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 -stdc11execlp(g, g, -o, PathUtil::Exe(filename).c_str(), PathUtil::Src(filename).c_str(), -stdc11, -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_AS和RLIMIT_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串进行设置
如果编译失败或编译成功运行失败我只需要设置status、reason两个个字段如果编译运行成功我们还需要设置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_ptrAllSessionInfo all_sess(new AllSessionInfo);std::shared_ptrServer svr(new Server);std::shared_ptrUserManage 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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-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;charsetutf-8); });svr-set_base_dir(wwwroot);svr-listen(0.0.0.0, atoi(argv[1]));return 0;
}MVC模式框架
模型Model
Model负责与数据库进行交互听取控制器的调用往数据库中插入数据或从数据库中获取数据并让View将请求结果返回给用户
插入数据的几种情形
用户注册管理员添加题目
查询数据的几种情形
用户获取题目信息获取用户信息
题目设计
我们项目题目的属性有这么几个编号、标题、难度、时间限制、内存限制、题目描述、头文件、用户显示代码、测试代码后序我们需要对header、body和tail这三个部分进行拼接形成一份新的代码再提交给编译服务器形成的数据库表结构如下 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; // 用来测试用户的代码
};
接口设计
接口主要包括加载配置、单个题目的获取、全部题目的获取、添加题目和用户数据相关操作加载配置主要是将show_num和num建立起映射关系show_num这个属性是给用户页面显示的不直接用number显示给用户的原因是number在数据库中是自增长的且每次添加题目其number是从最大的number1开始增长在不删除题目的情况下number是连续的如果中途删除了某个题目后序number这个序列就不会是连续的中间会断开如1、2、3、4删除了3再增加一个题目number是5这时number序列就是1、2、4、5这样就不是连续的所以用number显示给用户不太好设计一个show_num是连续的并与number建立好映射关系这样就比较友好单个题目的获取和多个题目的获取主要是查询数据库获取到的数据可以交付给View添加题目需要往数据库中插入数据同时更新show_num和number的映射关系用户数据操作结合数据库操作一起完成
视图View)
View负责将Model提供的数据以某种方式呈现给用户这个项目主要是网页界面。View会使用google的开源库ctemplate进行网页渲染以这种方式将数据呈现给用户
ctemplate的获取
可以在GitHub的国内镜像网站中获取速度会比较快链接如下里面有详细的安装方法说明
https://hub.fastgit.xyz/OlafvdSpek/ctemplatectemplate的简单用法
{{变量名}}把它放入我们的网页中该部分会被替换成我们字典中添加的值使用如{{number}}、{{show_num}}{{#片断名}}片断在数据字典中表现为一个子字典字典是可以分级的根字典下面有多级子字典。片断可以处理条件判断和循环循环的结束{{/片段名}}TemplateDictionary可以创建字典SetValue可以往字典中添加模板AddSectionDictionary可以往字典中添加子字典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::vectorQuestion 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::vectorstd::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::vectorMachine _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::vectorMachine 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;charsetutf-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获取测试代码并拼接上用户提交的代码填充codecode需要用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;charsetutf-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执行进行加锁二者结合有效解决问题