北京谷歌seo公司十堰seo优化分析
web/
2025/10/3 19:13:40/
文章来源:
北京谷歌seo公司,十堰seo优化分析,哪个微信公众号有a ,怎样在手机做自己的网站6负载均衡式——Online Judge#x1f60e; 前言#x1f64c;Online Judge 项目一、项目介绍二、项目技术栈三、项目使用环境四、项目宏观框架五、项目后端服务实现过程1、comm模块设计1.1 Log.hpp实现1.2 Util.hpp实现 2、compiler_server 模块设计2.1compile.hpp文件代码编写… 负载均衡式——Online Judge 前言Online Judge 项目一、项目介绍二、项目技术栈三、项目使用环境四、项目宏观框架五、项目后端服务实现过程1、comm模块设计1.1 Log.hpp实现1.2 Util.hpp实现 2、compiler_server 模块设计2.1compile.hpp文件代码编写2.2 runner.hpp文件代码编写2.3 compile_runing.hpp文件代码编写2.4 compile_server.cc文件代码编写 3、设计文件版题库4、oj_server 模块设计——基于MVC4.1 初步使用一下cpp-httplib。4.2 oj_model.hpp文件代码编写4.3 oj_control.hpp文件代码编写4.4 oj_view.hpp文件代码编写4.5 oj_server.cc文件代码编写 六、项目前端页面实现过程七、 项目演示过程八、项目的扩展方向 总结撒花 博客昵称博客小梦 最喜欢的座右铭全神贯注的上吧 作者简介一名热爱C/C算法等技术、喜爱运动、热爱K歌、敢于追梦的小博主 博主小留言哈喽各位CSDN的uu们我是你的博客好友小梦希望我的文章可以给您带来一定的帮助话不多说文章推上欢迎大家在评论区唠嗑指正觉得好的话别忘了一键三连哦 前言 哈喽各位友友们我今天又学到了很多有趣的知识现在迫不及待的想和大家分享一下我仅已此文手把手带领大家实现负载均衡式——Online Judge 项目~ 都是精华内容可不要错过哟 Online Judge 项目
一、项目介绍 在线OJ系统是一个能够自动对用户提交的程序源代码进行编译、执行和评测的在线平台。Online Judge项目实现的主要意义是为了帮助我们更好地适应工作和学习中的编程需求。Online Judge系统具有丰富的题库和实时反馈功能可以帮助我们更好地进行编程练习和测试。 此外负载均衡在线oj系统还可以进行项目的扩展具有方便的使用和活跃的社区互动等优势可以提高我们的学习效果和团队协作能力。 学习编程技术的小伙伴我相信一定是做过在线OJ的。而本项目就是借鉴于LeetCode和牛客网等各大知名的刷题网站提供的在线OJ功能实现自己的一个在线OJ系统。
二、项目技术栈
所⽤技术栈:
C STL 标准库Boost 准标准库应用于本项目实现字符串切割的功能需求。cpp-httplib 第三⽅开源⽹络库ctemplate 第三⽅开源前端⽹⻚渲染库jsoncpp 第三⽅开源序列化、反序列化库负载均衡设计多进程、多线程Ace前端在线编辑器了解html/css/js/jquery/ajax 了解
三、项目使用环境
Centos7云服务器vscode
四、项目宏观框架
我们的项⽬核⼼是三个模块
comm :公共模块compile_server :编译与运⾏模块oj_server :获取题⽬列表查看题⽬编写题⽬界⾯负载均衡其他功能 leetcode 结构只实现类似 leetcode 的题⽬列表在线编程功能
五、项目后端服务实现过程
1、comm模块设计
我们将所有功能模块所要使用的共性功能封装成comm模块从而实现复用提高项目整体的开发效率。
1.1 Log.hpp实现
Log.hpp实现的核心点分析
在C语言和C中__ FILE __和 __ LINE __ 是两个非常有用的预定义宏predefined macros。它们主要用于调试目的帮助开发者追踪错误发生的具体位置。 __ FILE __ 是一个字符串常量表示当前源代码文件的名称。这个宏在编译时被自动替换为包含它的源文件名。 __ LINE __ 是一个十进制整型常量表示当前源代码中的行号。这个宏在编译时自动替换为它在源代码中的行号。单 # 作用表示将该变量转换成对应的字符串表示。例如我们的enum菜单中1 对应 DEBUG则会将1 转化为DEBUG。#define 设计的一个LOG宏函数当调用宏函数时会在调用的地方进行替换成Log函数。
功能代码的整体实现 namespace ns_log
{using namespace ns_util;// 日志等级enum{INFOR 0,DEBUG,WARNING,ERROR,FATAL};inline std::ostream Log(const std::string level, const std::string file, int line){// 日志等级信息std::string logmessage [;logmessage level;logmessage ];// 日志出错在哪个文件名的信息logmessage [;logmessage file;logmessage ];// 日志出错在哪一行的信息logmessage [;logmessage std::to_string(line);logmessage ];// 日志打印的时间信息logmessage [;logmessage TimeUtil::GetTimeStamp();logmessage ];//将上述信息加载到cout内部的缓冲区中std::cout logmessage;return std::cout;}
// 单#表示将该变量转换成对应的字符串表示
#define LOG(level) Log(#level, __FILE__, __LINE__)
}1.2 Util.hpp实现
Util.hpp 实现的核心点分析 获取时间信息方法本项目使用了系统调用gettimeofday函数来获取对应的时间信息。 参数类型的解析 路径 具体文件名字形成方法编译时需要有的临时文件构建源文件路径后缀的完整文件名例如1234 - . . /temp/1234.cpp 对于文件一些操作方法 这里设计了判断一个可执行文件是否存在、形成唯一文件名、读文件、写文件方法。 其中判断可执行文件是否存在使用了系统调用接口stat。该接口可以获得一个文件的属性。我们都知道文件 内容 属性。如果属性存在那么这个文件也是一定存在的。 形成唯一文件名我们采用的是毫秒级时间戳 唯一的递增id相结合形成一个相对唯一的文件名。由于存在多个执行流进入该函数造成多执行流并发访问的问题。我们使用atomic_uint 类型进行唯一的递增id的设计保证它是唯一的。 字符串切割方法 我们使用了Boost准标准库中的split函数接口实现了字符串切割。可以将字符串按照指定的分隔符拆分成多个子字符串并将这些子字符串存储在指定的容器中。第一个参数是用于保存分割后结果的容器通常使用 std::vector std::string 类型。第二个参数是需要被分割的原始字符串。第三个参数是分隔符可以是一个字符或一组字符。这个参数决定了如何分割源字符串。boost::split 还提供了一个可选的第四个参数 boost::token_compress_on当连续出现多个分隔符时可以用来合并这些分隔符。
功能代码的整体实现 namespace ns_util
{class TimeUtil{public:// 获取1970年1月1日至今的秒数 --- 时间戳static std::string GetTimeStamp(){struct timeval Tvl;gettimeofday(Tvl, nullptr);return std::to_string(Tvl.tv_sec);}static std::string GetMsLevelTimestamp(){struct timeval Tvl;gettimeofday(Tvl, nullptr);return std::to_string(Tvl.tv_sec * 1000 Tvl.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 pathname temp_path;pathname file_name;pathname suffix;return pathname;}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_error);}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);}static std::string Stderr(const std::string file_name){return AddSuffix(file_name, .stderr);}};class FileUtil{public:static bool IsFileExists(const std::string path_name){struct stat st;// stat:获得一个文件的属性 如果文件存在 ---- 属性也存在if (stat(path_name.c_str(), st) 0)return true;elsereturn false;}// 形成唯一的文件名static std::string UniqFileName(){static std::atomic_uint id(0);id;std::string ms TimeUtil::GetMsLevelTimestamp();std::string uniq_id std::to_string(id);return ms _ uniq_id;}static bool WriteFile(const std::string path_name, const std::string content){std::ofstream out(path_name);if (!out.is_open()){return false;}out.write(content.c_str(), content.size());out.close();return true;}static bool ReadFile(const std::string path_name, std::string *content, bool keep false){content-clear();std::ifstream in(path_name);if (!in.is_open()){return false;}std::string line;while (std::getline(in, line)){*(content) line;*(content) keep ? \n : ;}in.close();return true;}};class StringUtil{public:static void SpileString(const std::string target, std::vectorstd::string *out, const std::string sep){boost::split((*out), target, boost::is_any_of(sep), boost::algorithm::token_compress_on);}};
}2、compiler_server 模块设计 提供的服务编译并运⾏代码得到格式化的相关的结果
2.1compile.hpp文件代码编写
compile.hpp代码编写重难点分析
采用多进程系统编程技术利用系统调用fork函数进行创建子进程帮助我们完成编译服务父进程等待子进程的退出并回收子进程的相关资源并继续执行主程序判断子进程编译执行的结果。 fork()函数的接口信息 由于子进程的pcb会继承父进程许多的信息文件描述符也会被拷贝。因此我们可以在子进程中使用系统调用dup2接口将stderr2进行重定向到我们指定的的临时文件中这样就可以实现将编译错误信息记录在相应的临时文件中。 如何让我们的子进程去进行编译程序呢这里使用到了进程程序替换。本项目使用的是exelp接口。父进程使用waitpid方法等待子进程的退出并将他回收。这是必须要做的。如果父进程不等待子进程会系统会产生僵尸进程导致系统资源不断地变少。父进程调用Util.hpp中的文件操作方法判断子进程编译运行的情况。调用Log.hpp的LOG宏函数打印日志信息有助于查看程序运行的信息排查错误。
功能代码的整体实现 // 实现编译功能
namespace ns_compile
{using namespace ns_util;using namespace ns_log;class Compiler{public:Compiler(){}// 根据给的文件名进行编译static bool Compile(const std::string file_name){pid_t id fork();if (id 0){LOG(ERROR) 内部错误创建⼦进程失败 \n;return false;}else if (id 0){// 创建编译错误文件并进行重定向umask(0);int _stderr open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_stderr 0){LOG(WARNING) 没有成功形成CompilerError⽂件 \n;exit(1);}LOG(INFOR) 成功形成CompilerError⽂件 \n;dup2(_stderr, 2);// 子进程去进行完成编译工作// 使用exec进行进程程序替换调用编译器// 编译的时候我们会产生很多临时文件// 需要以nullptr结尾// 注意有些函数接口是需要c语言风格的字符串string 进行转换。execlp(g, g, -o, PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), -stdc11, -D,COMPILER_ONLINE,nullptr);LOG(ERROR) 启动编译器g失败可能是参数错误 \n;exit(-1);}else{// 父进程pid_t rid waitpid(id, nullptr, 0);// 对于子进程是否完成编译成功进行判断。if (FileUtil::IsFileExists(PathUtil::Exe(file_name))){// 编译成功LOG(INFO) PathUtil::Src(file_name) 编译成功! \n;return true;}}// 编译不成功LOG(ERROR) 编译失败没有形成可执⾏程序 \n;return false;}~Compiler(){}private:};}2.2 runner.hpp文件代码编写
runner.hpp代码编写重难点分析
1程序运行
代码跑完结果正确代码跑完结果不正确代码没跑完异常了
2一个程序在默认启动的时候
标准输入: 我们这里不处理标准输出: 程序运行完成输出结果是什么标准错误: 运行时错误信息
3资源条件的约束 防止用户上传恶意代码从而形成恶意可执行。我们需要对这个进行排查。
我们再刷LeetCode时会发现每一道题目都有时间和空间的约束。我们通过下面代码就能够对我们的运行程序进行资源的限制
使用setrlimit系统调用接口实现。 结构体的解析和内部属性。rlimit_cur:表示当前设置的资源数量rlimit_max表示最大设置的资源数量。第二个参数是设置软限制和硬限制。非特权进程os需要对其进行受限我们在该项目编写时都是非特权的设置rlim_max为无穷即可,rlim_cur设置为我们想要的值。 资源限制的代码设计 static void SetProcLimit(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; //转化成为KBsetrlimit(RLIMIT_AS, mem_rlimit);}我们这里只是从时间和内存这两个方面进行排查也可以进行扩展只是比较复杂。好的在线oj因为每一个不同的题目要求不一样因此无法进行统一。 Run需要考虑代码跑完结果正确与否吗我们无需考虑结果正确与否是由我们的测试用例决定的我们只考虑是否正确运行完毕
功能代码的整体实现 namespace ns_runner
{using namespace ns_util;using namespace ns_log;class Runner{public:Runner() {}~Runner() {}public://提供设置进程占用资源大小的接口static void SetProcLimit(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; //转化成为KBsetrlimit(RLIMIT_AS, mem_rlimit);}static int Run(const std::string file_name, int cpu_limit, int mem_limit){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);SetProcLimit(cpu_limit, mem_limit);execl(_execute.c_str(), _execute.c_str(), nullptr);exit(1);}else{close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status 0;waitpid(pid, status, 0);// 程序运行异常一定是因为因为收到了信号LOG(INFO) 运行完毕, info: (status 0x7F) \n; return status 0x7F;}}};
}2.3 compile_runing.hpp文件代码编写
compile_runing.hpp代码编写重难点分析
先做一个宏观上的设计 通过compile_runing.hpp模块的编写使我们的编译和运行模块相结合起来。通过传入的json字符串进行反序列化初始化我们的json对象然后获取需要的信息。形成源文件、对源文件编译形成可执行、对可执行文件运行。再把运行得到的信息填充到json对象中经过序列化形成json字符串然后通过out_json带出去。每一次运行后成功后都会产生很多的临时文件我们可以设计一个函数对这些生成的临时文件进行一个清理。
功能代码的整体实现 namespace ns_compile_run
{using namespace ns_compile;using namespace ns_runner;class compile_run{public:compile_run(){};~compile_run(){};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 _exe PathUtil::Exe(file_name);if(FileUtil::IsFileExists(_exe)) unlink(_exe.c_str());std::string _compileError PathUtil::CompilerError(file_name);if(FileUtil::IsFileExists(_compileError)) unlink(_compileError.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 StatusToStr(int status, const std::string file_name){std::string desc;switch (status){case 0:desc 编译运行成功;break;case -1:desc 提交的代码是空;break;case -2:desc 未知错误;break;case -3:FileUtil::ReadFile(PathUtil::CompilerError(file_name), desc, true);break;case SIGABRT: // 6desc 内存超过范围;break;case SIGXCPU: // 24desc CPU使用超时;break;case SIGFPE: // 8desc 浮点数溢出;break;default:desc 未知: std::to_string(status);break;}return desc;}static void Start(const std::string in_json, std::string *out_json){//序列化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 mem_limit in_value[mem_limit].asInt();int cr_status 1;int c_status 1;int r_status 1;std::string file_name;int run_result;if (code.size() 0){// 代码为空cr_status -1;goto END;}file_name FileUtil::UniqFileName();// 将获得的代码形成源文件Srcif (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){// 写入失败了,发生未知错误cr_status -2;goto END;}// 写入成功// 对Src文件进行编译if (!Compiler::Compile(file_name)){// 编译失败cr_status -3;goto END;}// 编译成功,形成可执行// 运行run_result Runner::Run(file_name, cpu_limit, mem_limit);if (run_result 0){// run失败(异常)cr_status run_result;}else if (run_result 0){// run发送内部错误,未知错误到导致cr_status -2;}else{// run完毕cr_status 0;}END:// 对每一个编译运行情况进行统一的差错处理Json::Value out_value;out_value[status] cr_status;out_value[reason] StatusToStr(cr_status, file_name);if (cr_status 0){// 编译运行的整个过程都是成功的std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), _stdout, true);out_value[stdout] _stdout;std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), _stderr, true);out_value[stderr] _stderr;}Json::FastWriter Write;*out_json Write.write(out_value);RemoveTempFile(file_name);}};
}2.4 compile_server.cc文件代码编写
各个文件间的调用逻辑 compile_server.cc代码编写重难点分析
接⼊cpp-httplib:header-only只需要将.h拷⻉到项⽬中即可直接使⽤cpp-httplib:需要使⽤⾼版本的gcc建议是gcc 7,8,9 [如果没有升级cpp-httplib要么就是编译报错要么就是运⾏出错]cpp-httplib 阻塞式多线程的⼀个⽹络http库测试的时候可以采⽤postman进⾏测试我们使用的Post方法将编译链接服务打包形成网络服务。我们使用lambda表达式。这样使得代码更加简洁。我们获取客户端的数据并将运行的结果返回给客户。让我们的服务器一直处于监听状态一旦访问便可以进行服务。
功能代码的整体实现
#include compile_runing.hpp
#include ../comm/httplib.h
using namespace ns_compile_run;
using namespace httplib;
void Usage(std::string proc)
{std::cerr Usage: \n\t proc port std::endl;
}int main(int argc, char* argv[])
{if(argc ! 2){Usage(argv[0]);return 0;}Server svr;svr.Post(/compile_and_run,[](const Request req, Response resp){std::string in_json req.body;std::string out_json;if(!in_json.empty()){compile_run::Start(in_json, out_json);resp.set_content(out_json, application/json;charsetutf-8);}});svr.listen(0.0.0.0, atoi(argv[1]));
}3、设计文件版题库 题⽬的编号题⽬的标题题⽬的难度题⽬的描述题⾯时间要求(内部处理)空间要求(内部处理) 两批⽂件构成
第⼀个questions.list : 题⽬列表不需要题⽬的内容第⼆个题⽬的描述题⽬的预设置代码(header.cpp), 测试⽤例代码(tail.cpp)
这两个内容是通过题⽬的编号产⽣关联的。 当⽤⼾提交⾃⼰的代码的时候header.cpp
#include iostream
#include string
#include vector
#include map
#include algorithm
using namespace std;
class Solution{
public:bool isPalindrome(int x){//将你的代码写在下⾯//code//code//code//code//codereturn true;}
};OJ不是只把上⾯的代码提交给compile_and_run ⽽是要拼接上该题号对应的测试⽤例 tail.cpp。
#include iostream
#include string
#include vector
#include map
#include algorithm
using namespace std;
class Solution{
public:bool isPalindrome(int x){//将你的代码写在下⾯//code//code//code//code//codereturn true;}
};最终提交给编译服务主机的代码是
#include iostream
#include string
#include vector
#include map
#include algorithm
using namespace std;
class Solution{
public:bool isPalindrome(int x){//将你的代码写在下⾯//code//code//code//code//codereturn true;}
};
// 下⾯的代码仅仅是为了让我们设计测试⽤例的时候不要报错。
// 我们不想让编译器编译的时候保留它⽽是裁剪掉g -D COMPILER_ONLINE
#ifndef COMPILER_ONLINE
#include header.cpp
#endifvoid Test1()
{bool ret Solution().PalindromeNumber(121);if (ret){cout 通过用例1, 测试121通过 ... OK! endl;}else{std::cout 没有通过用例1, 测试的值是: 121 std::endl;}
}void Test2()
{bool ret Solution().PalindromeNumber(-10);if (!ret){cout 通过用例2, 测试-10通过 ... OK! endl;}else{std::cout 没有通过用例2, 测试的值是: -10 std::endl;}
}int main()
{Test1();Test2();
}
4、oj_server 模块设计——基于MVC
4.1 初步使用一下cpp-httplib。
功能代码的整体实现
#include iostream
#include ../comm/httplib.h
//进行服务路由功能
using namespace httplib;
int main()
{Server svr;//获取所有题目svr.Get(/all_questions,[](const Request Req, Response Resp){Resp.set_content(这是所有题目列表,text/plain;charsetutf-8);});//用户根据题号获取特定的一道题svr.Get(R(/question/(\d)),[](const Request Req, Response Resp){std::string number Req.matches[1];Resp.set_content(这是指定的一道题: number,text/plain;charsetutf-8);});//用户提交代码使用我们的判题功能。svr.Get(R(/judge/(\d)),[](const Request Req, Response Resp){std::string number Req.matches[1];Resp.set_content(这是指定题目的判题功能: number, text/plain;charsetutf-8);});svr.set_base_dir(./wwwroot);svr.listen(0.0.0.0,8080);return 0;
}同级目录下创建wwwroot文件夹。里面存放首页网页。
功能代码的整体实现
!DOCTYPE html
html langen
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title这是题目列表/title
/head
body/body
/html效果 4.2 oj_model.hpp文件代码编写 compile_server.cc代码编写重难点分析
oj_model.hpp文件主要是用于数据交互我们的数据就是题目list的相关信息。首先就是将我们的题目列表的信息从文件加载到内存中和读取每一道题目的题目描述、预设值函数头代码、对应的测试用例代码。形成每一个question。 question的设计 提供获取一个题目的问题细节方法和获取所有题目问题细节的方法。
功能代码的整体实现
class Model{public:Model(){assert(LoadQuestionsList(questions_list));};~Model(){};bool LoadQuestionsList(const string QuestionsList){//首先打开题目信息文件ifstream in(QuestionsList);if(!in.is_open()){//打开文件失败LOG(FATAL) 加载题库失败,请检查是否存在题库文件 \n;return false;}//打开成功string line;while(getline(in,line)){vectorstring lines;// 1 判断回⽂数 简单 1 30000//对于获得每一行进行切割StringUtil::SpileString(line,lines, );if(lines.size() ! 5){LOG(WARNING) 加载部分题目失败,请检查文件格式 \n;}Question q;q.number lines[0];q.title lines[1];q.dif_level lines[2];q.cpu_limit stoi(lines[3]);q.mem_limit stoi(lines[4]);std::string path question_path;path q.number;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(INFOR) 加载题库...成功! \n;in.close();return true;}bool GetAllQuestions(vectorQuestion* out){if(questions.size() 0){//没有题目数据/或者加载题目数据失败LOG(ERROR) 用户获取题库失败 \n; return false;}//有题目数据信息for(auto q : questions){out-push_back(q.second);}return true;}bool GetOneQuestion(const 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;}private://题目问题细节unordered_mapstring, Question questions;};4.3 oj_control.hpp文件代码编写
oj_control.hpp代码编写重难点分析
先描述在组织我们对于每一台进行编译服务的主机进行描述。 string _ip; // 提供编译服务主机的ipint _port; // 提供编译服务主机的端口号uint64_t _load; // 主机的负载均衡系数mutex *_mtx; // C的mutex是禁止拷贝的,所以这里定义指针具体的方法实现我们使用的网络库是基于多线程的因此当很多网络服务到来时可能有多个执行流进入同一台主机从而对于该主机的负载情况进行修改因此必须加锁进行保护。 // 获取主机的负载情况uint64_t Load(){uint64_t load 0;if (_mtx)_mtx-lock();load _load;if (_mtx)_mtx-unlock();return load;}// 增加主机的负载void IncLoad(){if (_mtx)_mtx-lock();_load;if (_mtx)_mtx-unlock();}// 清除主机的负载void ClearLoad(){if (_mtx)_mtx-lock();_load 0;if (_mtx)_mtx-unlock();}// 减少主机的负载void DecLoad(){if (_mtx)_mtx-lock();_load--;if (_mtx)_mtx-unlock();}负载均衡设计 首先先将主机列表配置文件进行加载并将他添加至在线主机列表和主机列表中。 具体代码实现 bool LoadConf(const string machine_conf){ifstream in(machine_conf);if (!in.is_open()){// 没有成功打开文件LOG(FATAL) 打开machine_conf出错 \n;return false;}// 打开文件成功string line;while (getline(in, line)){vectorstring tokens;StringUtil::SpileString(line, tokens, :);if (tokens.size() ! 2){LOG(WARNING) machine_conf文件,格式有误 \n;continue;}// 成功切分Machine m;m._ip tokens[0];m._port stoi(tokens[1]);m._load 0;m._mtx new mutex();Online.push_back(Machines.size());Machines.push_back(m);}LOG(INFOR) 成功加载machine_conf文件 \n;in.close(); // 记得关闭文件return true;}智能选择主机方法设计选择一个在线列表中负载系数最低的主机进行处理当前的网络服务。需要加锁保护。 具体实现代码 bool SmartChoice(int *id, Machine **m){_mtx.lock();int OnlineNum Online.size();if (OnlineNum 0){LOG(FATAL) 所有主机都离线了,请运维的老铁过来看看 \n;_mtx.unlock();return false;}*id Online[0];*m Machines[Online[0]];uint64_t min_load Machines[Online[0]].Load();for (int i 1; i OnlineNum; i){uint64_t cur_load Machines[Online[i]].Load();if (cur_load min_load){min_load cur_load;*id Online[i];*m Machines[Online[i]];}}_mtx.unlock();return true;}
当一台主机离线后我们需要对它的负载系数进行一个清0操作。如果不清零可能他离线后它的负载系数还是一个比较高的值当让它再次上线后就无法调用到这台主机了。需要加锁保护 具体实现代码 // 将指定的一台主机进行离线void OfflineMachines(int witch){_mtx.lock();for (auto iter Online.begin(); iter ! Online.end(); iter){if (*iter witch){// 清除主机的负载系数Machines[witch].ClearLoad();Online.erase(iter);Offline.push_back(witch);break; // 直接break,迭代器失效不影响}}_mtx.unlock();}
一键上线功能设计。当我们所有的主机都挂掉了我们需要将主机重新上线进行编译服务。需要加锁保护。 主要实现代码 void OnlineMachines(){_mtx.lock();Online.insert(Online.end(), Offline.begin(), Offline.end());cout 所有主机都上线啦 endl;_mtx.unlock();}Control结构体的设计 Model _model; // 提供后台数据View _view; // 提供html渲染功能LoadBalance _load_balance; // 核心负载均衡器
调用获取所有题目列表的信息方法并调用oj_view模块中的形成网页的方法形成一个题目列表的网页。 具体实现代码 bool AllQuestions(string *html){vectorQuestion all;bool ret true;if (_model.GetAllQuestions(all)){// 对题目进行排序sort(all.begin(), all.end(), [](const Question q1, const Question q2){ return q1.number q2.number; });// 获取题目数据成功// 形成网页_view.AllExpandHtml(all, html);}else{*html 获取题⽬失败, 形成题⽬列表失败;ret false;// 获取题目信息失败}return ret;}调用获取所有题目列表的信息方法并调用oj_view模块中的形成网页的方法形成指定题目的网页。 具体实现代码 bool Onequestion(const string number, string *html){Question q;bool ret true;if (_model.GetOneQuestion(number, q)){// 获取指定题目信息成功// 形成网页_view.OneExpandHtml(q, html);}else{*html 指定题⽬: number 不存在!;ret false;}return ret;}提供外部调用一键上线方法的接口 void RecoveryMachine(){_load_balance.OnlineMachines();}设计题目的Judge服务方法。首先进行反序列化和序列化操作形成需要编译运行的json字符串。 in_json进⾏反序列化得到用户提交的代码。重新拼接⽤⼾代码测试⽤例代码形成新的代码选择负载最低的主机(差错处理) 规则: ⼀直选择直到主机可⽤否则就是全部挂掉然后发起http请求得到结果。http的状态码是200,才表明是成功的将结果赋值给out_json 具体代码实现 void Judge(const string number, const string in_json, string *out_json){// 0. 根据题⽬编号直接拿到对应的题⽬细节Question q;_model.GetOneQuestion(number, q);Json::Reader reader;Json::Value in_value;reader.parse(in_json, in_value);string code in_value[code].asString();Json::Value compile_value;compile_value[input] in_value[input].asString(); // 注意asString()和asCString()接口。compile_value[code] code q.tail;compile_value[cpu_limit] q.cpu_limit;compile_value[mem_limit] q.mem_limit;Json::FastWriter writer;string compile_string writer.write(compile_value);while (true){int id 0;Machine *m nullptr;if (!_load_balance.SmartChoice(id, m)){LOG(ERROR) 失败 \n;break;}Client cli(m-_ip, m-_port);m-IncLoad();LOG(INFOR) 选出编译服务主机,详情: m-_ip : m-_port 负载系数: m-Load() \n;if (auto res cli.Post(/compile_and_run, compile_string, application/json;charsetutf-8)){LOG(INFOR) 然后发起http请求 \n;if (res-status 200){*out_json res-body;m-DecLoad();LOG(INFOR) 请求和编译服务成功... \n;break;}m-DecLoad();}else // 请求失败{LOG(ERROR) 当前请求的主机id: id 详情: m-_ip : m-_port 可能已经离线 \n;_load_balance.OfflineMachines(id);_load_balance.ShowMachines();}}}4.4 oj_view.hpp文件代码编写
oj_view.hpp代码编写重难点分析 本质其实就是完成四部曲的编写即可
形成路径形成数据字典获取被渲染的html的对象完成渲染 将所有题目列表信息形成网页 void AllExpandHtml(std::vectorQuestion all, std::string* html){std::string src_path template_path all_questions.html;ctemplate::TemplateDictionary root(all_questions);for(const auto q : all){ctemplate::TemplateDictionary* sub root.AddSectionDictionary(question_list);sub-SetValue(number,q.number);sub-SetValue(title,q.title);sub-SetValue(dif_level,q.dif_level);}ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_path,ctemplate::DO_NOT_STRIP);tpl-Expand(html,root);}将特定的题目细节形成网页
void OneExpandHtml(Question q, std::string* html){std::string src_path template_path one_question.html;ctemplate::TemplateDictionary root(one_question);root.SetValue(number,q.number);root.SetValue(title,q.title);root.SetValue(dif_level,q.dif_level);root.SetValue(desc,q.desc);root.SetValue(header,q.header);ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_path,ctemplate::DO_NOT_STRIP);tpl-Expand(html,root);}4.5 oj_server.cc文件代码编写
调用Get方法实现网络服务。获取所有题目、用户根据题号获取特定的一道题、用户提交代码使用我们的判题功能。(1. 每道题的测试用例 2. compile_and_run)。 //进行服务路由功能
using namespace httplib;
using namespace ns_control;static Control* ctl_ptr nullptr;void Recovery(int signum)
{ctl_ptr-RecoveryMachine();
}int main()
{signal(SIGQUIT,Recovery);Server svr;Control ctl;ctl_ptr ctl;svr.Get(/all_questions,[ctl](const Request Req, Response Resp){std::string html;ctl.AllQuestions(html);Resp.set_content(html,text/html;charsetutf-8);});svr.Get(R(/question/(\d)),[ctl](const Request Req, Response Resp){std::string number Req.matches[1];std::string html;ctl.Onequestion(number,html);//ctl.Question(number,html);Resp.set_content(html,text/html;charsetutf-8);});svr.Post(R(/judge/(\d)),[ctl](const Request Req, Response Resp){std::string number Req.matches[1];std::string in_json Req.body;std::string out_json;ctl.Judge(number,in_json,out_json);Resp.set_content(out_json, application/json;charsetutf-8);});svr.set_base_dir(./wwwroot);svr.listen(0.0.0.0,8080);return 0;
}六、项目前端页面实现过程 我主要是做后端开发的项目前端没有那么重要只是了解一下前端的一些基础语法和调用简单的接口大家了解一下即可。这里就不做详细的分析啦大家想要前端页面的代码可以直接到我的gitee里面copy即可~ 本项目实现了一个简单版的网页前端。一共有三个网页
简单的主页网页所有题目列表信息的网页特定题目细节信息和用户在线OJ的网页。
效果展示 前端主网页 题目列表网页 我这里只录入两道题目作为测试~ 指定题目细节和在线OJ网页
七、 项目演示过程 首先启动我们的三台编译服务的主机 启动我们的oj_server主机负责将用户提交的代码传输到编译服务主机。 3. 前端主网页 题目列表网页 我这里只录入两道题目作为测试~ 指定题目细节和在线OJ网页 进行编译和提交代码 我们可以看到在线oj的基本功能都是可以跑通的。 测试一下常见的错误看能否让用户看到错误信息。 可以看到能够返回错误信息。 八、项目的扩展方向 项目目前实现了查看题库和对题目进行在线OJ的功能。本项目还有很多扩展的地方。
基于注册和登陆的录题功能业务扩展⾃⼰写⼀个论坛接⼊到在线OJ中即便是编译服务在其他机器上也其实是不太安全的可以将编译服务部署在docker⽬前后端compiler的服务我们使⽤的是http⽅式请求(仅仅是因为简单)但是也可以将我们的compiler服务设计成为远程过程调⽤用rest_rpc替换我们的httplib功能上更完善⼀下判断⼀道题⽬正确之后⾃动下⼀道题⽬题库目前只实现文件版的可以实现成用数据库来存储题库。其他
总结撒花 本篇文章旨在分享的是负载均衡式——Online Judge项目的详细设计过程。希望大家通过阅读此文有所收获 如果我写的有什么不好之处请在文章下方给出你宝贵的意见。如果觉得我写的好的话请点个赞赞和关注哦~
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/86374.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!