打开网页出现网站建设中文化企业官方网站开发方案书

当前位置: 首页 > news >正文

打开网页出现网站建设中,文化企业官方网站开发方案书,.net 网站开发权限设计,上海网站搭建平台公司#x1f9f8;#x1f9f8;#x1f9f8;各位大佬大家好#xff0c;我是猪皮兄弟#x1f9f8;#x1f9f8;#x1f9f8; 文章目录 一、项目介绍项目技术栈和开发环境 二、项目的宏观结构三、compile_server模块①日志模块开发#xff0c;Util工具类#xff0c;供所以模…各位大佬大家好我是猪皮兄弟 文章目录 一、项目介绍项目技术栈和开发环境 二、项目的宏观结构三、compile_server模块①日志模块开发Util工具类供所以模块使用②编译功能开发(compiler模块)拼接路径工具类检测编译是否成功编译出错compiler编译模块核心逻辑实现 ③运行功能开发runner模块资源限制CPU占用内存runner模块核心逻辑以及实现 ④ 编译运行模块开发(compile_run模块)引入jsoncppcompile_run模块 明确步骤差错处理独特文件名的形成读写文件接口清理所有临时文件compile_run模块的核心逻辑与实现设计测试用例对compile_run模块进行测试引入cpp-httplib第三方网络库gcc升级compile_run打包成网络服务 ⑤使用Postman对打包成为网络后的c_r模块进行综合测试 四、oj_server模块①oj_server模块结构设计MVC架构模式②oj_server的功能路由③ version1: 建立文件版的题库文件结构给用户预设的代码headertail.cpp测试用例部分 ③version2 MySQL版本创建用户并赋权使用MySQL_WorkBench创建表结构在MySQL_WorkBench当中进行录题编写MySQL版本的model模块 ④model模块使用boost准标准库当中的split进行字符串分割按行读取配置文件形成Question对象 ⑤controller模块controller模块整体结构引入ctemplate模板渲染库测试基本功能 ⑥judge模块负载均衡编写负载均衡器离线和上线 ⑦使用Postman进行oj_server的综合测试⑧view模块整体代码结构前端的东西不是重点indexall_questionsone_question 五、最终效果六、项目结项与扩展七、顶层makefile发布项目项目源码 一、项目介绍 项目相关背景 在线oj系统是一种在线评测系统用于评测程序员提交的代码。它可以模拟各种编程语言的运行环境对程序进行编译、运行和测试并根据测试结果给出评分和反馈。可以帮助程序员提高编程能力和解决问题的能力同时也可以帮助教师和企业筛选优秀的人才。在线oj系统的背景可以追溯到20世纪80年代当时已经有一些类似的系统出现但是随着互联网的发展和计算机技术的进步在线oj系统得到了广泛的应用和发展。目前国内外已经有很多知名的在线oj系统例如LeetCode、Codeforces、TopCoder等。 我们对于leetcodenewcode这些一定不陌生 我们选择在线oj系统项目的原因是因为它可以帮助我们提高编程能力和解决问题的能力同时也可以帮助我们更好地适应工作和学习中的编程需求。在线oj系统具有丰富的题库和实时反馈功能可以帮助我们更好地进行编程练习和测试。 此外负载均衡在线oj系统还可以进行项目的扩展具有方便的使用和活跃的社区互动等优势可以提高我们的学习效果和团队协作能力。 项目技术栈和开发环境 技术栈 C STL 标准库Boost准标准库主要应用split字符串切割cpp-httplib 第三方开源网络库ctemplate 第三方开源前端网页渲染库jsoncpp 第三方开源序列化、反序列化库负载均衡设计多线程、多进程MySQL C connectAce前端在线编辑器(了解)html/css/js/jquery/ajax(了解) 开发环境 Centos 7云服务器vscodeMySQL workbench 二、项目的宏观结构 我们的项目核心是三个模块 ①comm公共模块httplib网络服务log日志信息util工具类集合等 ②compile_server编译运行模块(以网络的形式访问compile_server请求编译运行服务) ③oj_server基于MVC架构模式的服务器主要负责负载均衡、用户交互以及数据访问 运行过程梳理 客服端(网页)向oj_server服务器发起请求oj_server通过cpp-httplib打包的server进行功能路由如获取题目列表获取单道题目以及代码的提交。然后oj_server负载均衡式的选择compile_server进行编译运行。结果返回给用户。 三、compile_server模块 compile_server模块主要的工作是进行编译运行。通过cpp-httplib打包成服务器然后进行功能路由 ①日志模块开发Util工具类供所以模块使用 日志我们想提供 日志等级打印日志的文件名称报错行添加日志的时间日志信息开放性输出 开放性输出就是说我们可以在后面输出自己想输出的东西比如LOG(DEBUG)我想输出的东西std::endl; #pragma once#include iostream #include string #include util.hppnamespace ns_log {using namespace ns_util;enum{// 日志等级 0-4INFO, // 常规的只是一些提示信息DEBUG, // 调试日志WARNING, // 告警不影响后续使用// 一般碰到ERROR或者FATAL这样的错误就需要有人来运维了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 message; // 不要std::endl进行刷新因为换行就会刷新缓冲区return std::cout; // 返回一个流式缓冲区上面的信息写到一个缓冲区当种}// LOG(INFO)message\n; # \n进行缓冲区的刷新#define LOG(level) Log(#level, FILE, LINE)// LOG中的level是枚举 0-4 log中的#level就可以把宏参以字符串的方式传参 比如INFO对应的enum是0而#level就是INFO }解释说明: 其中 FILELINE是C语言中的两个宏获得文件名称和获得行数。#define LOG(level) log(#level,FILE,LINE);这个宏当中#level的作用是直接转化成字符串的形式比如DEBUG对应的枚举是1那么我们只传DEBUG的话在预编译阶段就会替换成1但是我们传入#level的话他就会认为是字符串DEBUG; ②编译功能开发(compiler模块) 编译模块的整体结构如下。
首先我们想要提供编译服务那么急需要去调用编译器。在Linux当中我们知道对进程操作可以有进程创建、进程终止、进程等待、进程程序替换那么我们就需要去进程程序替换成g来对用户提交的代码进行编译 进程程序替换 通过 man 2 exec 我们可以看到操作系统给我们提供的用于程序替换的接口exec系列函数 接口说明猪皮兄弟进程控制.csdn 带l的我们可以认为是需要传入一串参数比如说g -o test test.cc,需要以NULL/nullptr结尾带v的我们可以认为是需要数组去进行传递也就是把我们上面的一串参数先放入数组再进行调用带p的可以认为是环境变量也就是说系统已经认识了该程序无序我们传入相对/绝对地址而不带p是需要我们传入的。 我们今天选择的是execlp最符合我们的调用execlp的调用方式execlp(g,g,-o,test,test.cc,nullptr); 第一个g代表的是在环境变量当中去找 拼接路径工具类 在客户提交代码之后要形成一些文件比如源文件编译之后形成可执行文件编译错误的话要形成编译错误文件。 所以这时候需要一些方法来对这些文件进行构建我们把这些构建后缀的方法放到comm模块的Util类当中 //comm模块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.cppstatic std::string Src(const std::string file_name){return AddSuffix(file_name, .cpp);}// 构建可执行程序的完整路径后缀名// 1234 - ./temp/1234.exestatic std::string Exe(const std::string file_name){return AddSuffix(file_name, .exe);}// 构建该程序对应的标准错误的完整路径后缀名// 1234 - ./temp/1234.stderrstatic std::string CompilerError(const std::string file_name){return AddSuffix(file_name, .compile_error);}};检测编译是否成功 我们编译是否成功只有一个标准就是是否形成可执行文件 第一种方式r读方式打开文件如果失败了说明不存在这种方式太简单粗暴第二种方式使用系统调用接口stat检测文件属性。 stat的第二个参数是一个输出型参数是一个系统提供的结构体类型结构如下 判断文件是否存在逻辑 class FileUtil{public:static bool IsFileExists(const std::string file_path){// stat成功0被返回失败-1返回struct stat st;if (stat(file_path.c_str(), st) 0){// 获取属性成功说明文件依旧存在return true;}return false;}};编译出错 编译出错g会向标准错误流里面打印错误信息所以我们就要形成一个文件也就是编译错误文件xxx.compiler_error让标准错误文件描述符进行重定向到该文件如果编译出错就可以在这个文件当中看见错误原因。 形成路径 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.stderrstatic std::string CompilerError(const std::string file_name){return AddSuffix(file_name, .compile_error);}};compiler编译模块核心逻辑实现 我们需要对标准错误文件描述符重定向这里采用的是系统调用dup2的方式 编译模块核心逻辑 namespace ns_compiler {// 引入路径拼接功能using namespace ns_util;class Compiler{public:Compiler(){}~Compiler(){}// 返回值 编译成功true 否则:false// 输入参数编译的文件名// file_name1234 后续我们自己拼接路径后缀等// 1234 - ./temp/1234.cpp// 1234 - ./temp/1234.exe// 1234 - ./temp/1234.compile_errorstatic bool Compile(const std::string file_name) // 编译{pid_t pid fork();if (pid 0){LOG(ERROR) 内部错误创建子进程失败 std::endl;return false;}else if (pid 0){umask(0);int _stderr open(PathUtil::CompilerError(file_name).c_str(),O_CREAT | O_WRONLY,0644);//打开标准错误临时文件出错就向其中写入错误信息if(_stderr0){LOG(WARNING) 没有成功形成stderr临时文件 std::endl;exit(1);}//重定向标准错误到我们形成的标准错误临时文件dup2(oldfd,newfd);dup2(_stderr,2);//我要把old的文件描述符放到new的文件描述符位置//g打印错误信息到stderr当中就会重定向到我们的标准错误临时文件// 子进程调用编译器完成对代码的编译工作// 进程程序替换exec系列函数// g -o target src -stdc11//因为我们选用的带p的进程程序替换函数所以g可以在环境变量中找到execlp(g,g, -o, PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), -stdc11,-D,COMPILER_ONLINE,nullptr);//未替换成功直接终止LOG(ERROR)启动编译器g失败可能是参数错误std::endl;exit(2);}else{ waitpid(pid,nullptr,0);//进程等待等pid进程退出结果等待方式(这里S是阻塞等待)//编译是否成功,标准就是可执行文件是否存在if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){LOG(INFO)PathUtil::Src(file_name)编译成功std::endl;return true;}}LOG(ERROR)编译失败没有形成可执行程序std::endl;return false;}private:}; }③运行功能开发runner模块 编译完成之后如果成功则会生成可执行程序我们现在是想办法把程序run起来。 程序运行1.代码跑完结果正确2.代码跑完结果不正确3.代码没跑完异常了进程等待中的status就可以直到 前8位是退出吗 低8位是核心转储 后面7位是进程信号信号为0则退出码有效不为0则退出码无效。核心转储需要自己开启并且核心转储是存储核心错误信息但是运行模块Run我们是不需要考虑结果正确与否结果正确与否是由测试用例决定的。但是跑错了是要报错的。错误又分为编译错误和运行错误运行错误才是在runner模块里该出现的进程起来之后默认会打开三个文件描述符分别是0,1,2号文件描述符分别对应stdinstdoutstderr。我们为了方便我们运行的自测输入(我们这里暂时不支持)运行结果运行错误结果等的查看与返回给用户。我们需要把这三个文件描述符进行重定向 //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); // 置权限掩码为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);//文件重定向打开了才能重定向打开了才有对应的fd dup2(_stdin_fd, 0); dup2(_stdout_fd, 1); dup2(_stderr_fd, 2);资源限制CPU占用内存 我们在leetcode做题的时候通常会发现出现 CPU占用时间超限内存超限等其实就是给执行这个运行服务的进程进行了资源的限制 对进程做资源限制这里只针对CPU占用时间和内存占用 对进程做资源限制我们需要调用 setrlimit 的系统调用来完成 其中RLIMIT_AS最大给这个进程的虚拟地址用字节来衡量 RLIMIT_CPU就代表CPU占用时间的限制 而我们看到还有一个对应的struct rlimit结构体第一个是软件限制第二个是硬件限制硬件一般设成无穷的不加约束 (无限INFINITY)
其实就是设置对应的struct rlimit结构体然后调用setrlimit进行设置就可以了具体操作如下 #include sys/time.h#include sys/resource.hclass Runner{public:Runner(){}~Runner(){}public://提供设置进程占用资源大小的接口static void SetProcLimit(int _cpu_limit,int _mem_limit){//设置CPU时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur _cpu_limit;cpu_rlimit.rlim_max RLIM_INFINITY;//无限setrlimit(RLIMIT_CPU,cpu_rlimit);//设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_cur _mem_limit * 1024;//传过来是以KB为单位我们这里是以B为单位mem_rlimit.rlim_max RLIM_INFINITY;//无限setrlimit(RLIMIT_AS,mem_rlimit);}};另外如果超过了资源限制则会被信号中断比如超出CPU占用时间限制则会被SIGXCPU(CPU time limit exceeded)信号打断内存使用超限就会被SIGABRT打断(abort signal)。分别对应24号信号和6号信号
如果我们想测试是否是这两个信号则我们可以对1-31号进行signal自定义捕捉(0号信号32,33号信号是不存在的34号及以后是实时信号我们不管。)然后捕捉到了就进打印出来看看。 runner模块核心逻辑以及实现 //runner模块 #pragma once#include iostream #include string #include unistd.h #include sys/types.h #include sys/stat.h #include fcntl.h #include sys/wait.h #include sys/time.h #include sys/resource.h#include ../comm/log.hpp #include ../comm/util.hppnamespace ns_runner {using namespace ns_log;using namespace ns_util;class Runner{public:Runner(){}~Runner(){}public://提供设置进程占用资源大小的接口static void SetProcLimit(int _cpu_limit,int _mem_limit){//设置CPU时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur _cpu_limit;cpu_rlimit.rlim_max RLIM_INFINITY;//无限setrlimit(RLIMIT_CPU,cpu_rlimit);//设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_cur _mem_limit * 1024;//传过来是以KB为单位我们这里是以B为单位mem_rlimit.rlim_max RLIM_INFINITY;//无限setrlimit(RLIMIT_AS,mem_rlimit);}// 运行和我们之前一样指明文件名即可不需要带路径我们可以自动补全全在temp目录下/*** 返回值如果是0,证明程序异常了退出时收到了信号返回值就是对应的信号编号* 返回值0标明是正常运行完成结果是什么我们不关心结果保存到了对应的临时标准输出文件当中* 返回值0表名是内部错误* cpu_limit: 程序运行的时候可以使用的最大CPU资源上限* mem_limit: 程序运行的时候可以使用的最大内存大小(KB)/static int Run(const std::string file_name,int cpu_limit ,int mem_limit)// bool也是可以的不过为了让它适应各种场景把它设为int{/** 程序运行* 1.代码跑完结果正确* 2.代码跑完结果不正确* 3.代码没跑完异常了** //进程等待中的status就可以直到 前8位是退出吗 低8位是核心转储 后面7位是进程信号* //信号为0则退出码有效不为0则退出码无效。核心转储需要自己开启并且核心转储是存储核心错误信息** 但是运行模块Run我们是不需要考虑结果正确与否* 结果正确与否是由测试用例决定的。但是跑错了是要报错的。** 知道是哪个可执行程序* 一个程序在默认启动的时候* 标准输入我们今天不考虑用户自测由oj平台帮我们去做* 标准输出程序运行完成输出结果是什么* 标准错误运行的时候的错误信息** 错误又分为编译错误和运行错误运行错误才是在runner模块里该出现的/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); // 置权限掩码为0int _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) 运行时打开标准文件失败 std::endl;return -1; // 代表打开文件失败}pid_t pid fork();if (pid 0){LOG(ERROR) 运行时创建子进程失败 std::endl; // 服务器压力太大close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);return -2; // 代表创建子进程失败}else if (pid 0){ //现在对应的是子进程我们要对子进程的资源做限制//我们今天重点关心的是运行时长和资源占用 RLIMIT_AS RLIMIT_CPUSetProcLimit(cpu_limit,mem_limit);// 进行重定向子进程随便执行执行结果一定会输出到打开的文件当中dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2);// 我们现在有的是路径而不是像g那样的环境变量当中有的所以我们选择使用execlexecl(_execute.c_str() /路径/, _execute.c_str(), nullptr /如何执行的可变参数以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) std::endl;return status 0x7F; // 有异常返回值一定是0的值没有异常返回值就是0}}}; }④ 编译运行模块开发(compile_run模块) 现在就应该编写compile_run模块去进行组合。compile_run模块就需要去适配用户请求定制通信协议字段然后逐次完善功能正确的调用compile和runner 引入jsoncpp 未来在使用compile_server服务的时候是以网络的形式请求的我们要求客户端需要给我们发一个json string供我们解析。等待我们运行完成之后结果也会以json string的方式发送给client或者oj_server服务器 jsoncpp是一个开源的第三方库用于序列化和反序列化序列化和反序列化的原因在于我们要屏蔽机器大小端和结构体内存对齐等问题。 jsoncpp的安装 $ sudo yum install -y jsoncpp-devel [sudo] password for zhupi:****** Loaded plugins: aliases, auto-update-debuginfo, fastestmirror, protectbase Repository epel is listed more than once in the configuration Loading mirror speeds from cached hostfile

  • base: mirrors.aliyun.com

  • epel-debuginfo: mirrors.tuna.tsinghua.edu.cn

  • extras: mirrors.aliyun.com

  • updates: mirrors.aliyun.com 0 packages excluded due to repository protections Package jsoncpp-devel-0.10.5-2.el7.x86_64 already installed and latest versionjsoncpp的简单使用 jsoncpp最简单的用法就是创建一个Value类型的万能对象然后以KV的方式进行序列化然后给对方对端接收到之后进行read反序列化 #include json/jsoncpp/json.h #include string int main() {Json::Value root;root[code] mycode;//KV的形式读的时候就可以通过key读出valueroot[user] zhupi;root[age] 19;//序列化Json::StyleWriter writer;//不止一种序列化的类区别就在于形成的json string不同std::string str writer.write(root);//str就是序列化之后的结果 std::coutstrstd::endl; }//假设对端接收到了一个json string void jsonTest(const std::string in_json) {Json::Value in_value;Json::Reader reader;//反序列化对象reader.parse(in_json,in_value);//把in_json反序列化到in_value当中std::string code in_value[code].asString(); // 当成字符串std::string user in_value[user].asString(); std::string age in_value[age].asString(); //就得到了对端发给我的结果 }需要注意的是因为我们使用了jsoncpp他是一个第三方库在编译的时候我们需要给g一些选项g -o test test.cc -stdc11 -ljsoncpp compile_run模块 明确步骤 明确步骤 1.把被人通过网络传给我们的json string 反序列化取出里面规定好的内容这是我们定制的协议我们 规定里面需要有code代码input自测输入(目前不支持)cpu_limit占用时间限制mem_limit占用空间限制2.生成独特的文件名不能和其他的起冲突这个文件名就用来后面生成本次提供编译运行服务的临时文件。3.生成一份源文件程序把code代码放进去4.正确调用compiler和runner模块的接口进行处理(编译运行)5.结果发回给对端 差错处理 我们可以把其他的错误的错误码用负数表示与信号做区分
    但是每一次我们都要去构建 statusreasons序列化甚至还有选填字段那么这样写下来显而易见的就知道很臃肿。我们就看能不能想办法把这块代码统一做下处理。 int status_code 0;Json::Value out_value;int run_result 0;std::string file_name; // 需要内部形成的唯一文件名// goto语句中是不能定义变量的所以我们把定义全部放在前面if (code.size() 0){status_code -1; // 代码为空goto END;}// 我们这里要设计一个函数使他们有独特的文件名不能重复(无目录无后缀)file_name FileUtil::UniqFileName(); // 在FileUtil类当中// 然后把读到的code写到源文件当中,我们只需要一个文件名去写编译的时候会自动写上后缀if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) // 形成src文件(这里有路径和后缀了){status_code -2; // 未知错误goto END;}if (!Compiler::Compile(file_name)){status_code -3; // 代码编译时发生了错误goto END;}run_result Runner::Run(file_name, cpu_limit, mem_limit);if (run_result 0){// 服务器内部错误status_code -2; // 服务器内部错误给用户显示成未知错误}else if (run_result 0){// 程序运行错误被信号终止status_code run_result;}else{// 运行成功结果就在stdout文件当中status_code 0;}END: // END标签,用来gotoout_value[status] status_code;out_value[reason] CodeToDesc(status_code, file_name);if (status_code 0) // 说明整个过程全部成功{// 全部正确开始填充std::string _stdout;// 我们这次必须带上\n要不然打印出来就是一行内容这就是我们设计的好处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;}我们使用的是goto语句去处理对于到了END标签我们把对于status_code转化为reason的任务全交给CodeToDesc(int code)这个函数去做 进行错误描述的转化。 如果全部成功编译运行成功那么status_code就会是0. 处理status和reason外我们还要添加选填的stdout和stderr字段进out_value 最后进行序列化让输出型参数out_json带出 CodeToDesc(int code) static std::string CodeToDesc(int code, const std::string file_name){// 待完善std::string desc;switch (code){case 0:desc 编译运行成功;break;case -1:desc 提交的代码是空;break;case -2:desc 未知错误;break;case -3:// desc 代码编译的时候发生了错误;FileUtil::ReadFile(PathUtil::CompilerError(file_name), desc, true);break;case SIGABRT: // 6desc 内存超限;break;case SIGXCPU: // 24desc CPU占用超时;break;case SIGFPE: // 8 floating-point execptiondesc 浮点数溢出错误;break;default:desc 未知 std::to_string(code);break;}return desc;}独特文件名的形成 我们采用毫秒级时间戳和原子性的唯一值来保证形成的文件名的唯一性。 或者我们用mutex互斥锁去进行计数也是一样的。 获得时间戳 我们可以使用gettimeofday来获得时间戳 它需要的是一个struct timeval的结构体 第二个成员就是我们需要的成员毫秒级时间戳 class TimeUtil{public:static std::string GetTimeStamp(){struct timeval _time;gettimeofday(_time, nullptr);return std::to_string(_time.tv_sec);}static std::string GetTimeMs()// 获取毫秒级别的时间戳而在我们的timeval结构体当中第二个成员就是表示的微秒// s - ms *1000 us - ms /1000// 因为s级别的时间戳跨度太长了{struct timeval _time;gettimeofday(_time, nullptr); // 第二个参数我们不管return std::to_string(_time.tv_sec * 1000 _time.tv_usec / 1000);}};原子性的递增数 C中是有原子类的 我们就可以使用std::automic_uint id(0);来产生一个原子数 唯一文件名 class FileUtil{public:static std::string UniqFileName(){std::string ms TimeUtil::GetTimeMs(); // 获得毫秒级的时间戳// 架不住有时候客户请求同时到来所以我们还需要一个原子性的递增唯一数来确保文件的唯一性// 1.mutex加锁 2.C当中的原子数高并发内存池计时的时候用过// 定义一个原子性递增的计数器初始化为0// 定义成静态避免每次进这个函数都会重新定义在函数当中定义static就是每次都不会被重新定义static std::atomic_uint id(0);id;//原子操作std::string uniq_id std::to_string(id);return ms . uniq_id;//中间可以加上一个点来进行区分}};读写文件接口 class FileUtil{static bool WriteFile(const std::string target, const std::string content){std::ofstream out(target);//写谁模式(我们不用管ofstream默认就是输出的)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是不保存行分隔符的比如abcd\n,读上来只有abcd//但是有些时候我们是需要保留行分隔符\n的,比如特定的一些格式使用getline之后就变了。//getline重载了强制类型转换所以while可以直接判断while(std::getline(in,line))//从哪个流当中读读到哪儿{(*content) line; (content) ((keep) ? \n : );//我们自动添加如果需要保留(keep是true的话)}in.close();return true;}};getline()的注意事项 1.getline()不会保留换行符比如1234\n它只会读上来1234所以如果在后面我们想换行的话需要自己添加在读文件的参数中第三个参数设为true自己写getline逻辑的话就要自己判断填不填加2.getline进行了返回类型的重载导致while()可以对getline的返回值做正确与否的判断 清理所有临时文件 我们会在执行过程中产生多少个临时文件的数目是不确定。但是有哪些类型我们是知道的上面都说过 一共有六个 .cpp.exe.compile_error.stdin.stdout.stderr 我们只需要判断文件存不存在FileUtil::IsFileExists()来判断再进行删除就可以了unlink()函数 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 _compiler_error PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(_compiler_error))unlink(_compiler_error.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());} compile_run模块的核心逻辑与实现 #pragma once#include compiler.hpp #include runner.hpp #include jsoncpp/json/json.h #include ../comm/log.hpp #include ../comm/util.hpp #include signal.h #include unistd.hnamespace ns_compile_and_run {using namespace ns_util;using namespace ns_log;using namespace ns_compiler;using namespace ns_runner;class CompileAndRun{public:static std::string CodeToDesc(int code, const std::string file_name){// 待完善std::string desc;switch (code){case 0:desc 编译运行成功;break;case -1:desc 提交的代码是空;break;case -2:desc 未知错误;break;case -3:// desc 代码编译的时候发生了错误;FileUtil::ReadFile(PathUtil::CompilerError(file_name), desc, true);break;case SIGABRT: // 6desc 内存超限;break;case SIGXCPU: // 24desc CPU占用超时;break;case SIGFPE: // 8 floating-point execptiondesc 浮点数溢出错误;break;default:desc 未知 std::to_string(code);break;}return desc;}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 _compiler_error PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(_compiler_error))unlink(_compiler_error.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());}/** 输入参数* 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){// 首先反序列化Json::Value in_value;Json::Reader reader;// parse叫做解析解析哪个字符串到哪个Value的对象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();// input我们暂时是不管的是用户提交的测试用例int status_code 0;Json::Value out_value;int run_result 0;std::string file_name; // 需要内部形成的唯一文件名// goto语句中是不能定义变量的所以我们把定义全部放在前面if (code.size() 0){status_code -1; // 代码为空goto END;}// 我们这里要设计一个函数使他们有独特的文件名不能重复(无目录无后缀)file_name FileUtil::UniqFileName(); // 在FileUtil类当中// 然后把读到的code写到源文件当中,我们只需要一个文件名去写编译的时候会自动写上后缀if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) // 形成src文件(这里有路径和后缀了){status_code -2; // 未知错误goto END;}if (!Compiler::Compile(file_name)){status_code -3; // 代码编译时发生了错误goto END;}run_result Runner::Run(file_name, cpu_limit, mem_limit);if (run_result 0){// 服务器内部错误status_code -2; // 服务器内部错误给用户显示成未知错误}else if (run_result 0){// 程序运行错误被信号终止status_code run_result;}else{// 运行成功结果就在stdout文件当中status_code 0;}END: // END标签,用来gotoout_value[status] status_code;out_value[reason] CodeToDesc(status_code, file_name);if (status_code 0) // 说明整个过程全部成功{// 全部正确开始填充std::string _stdout;// 我们这次必须带上\n要不然打印出来就是一行内容这就是我们设计的好处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::StyledWriter writer;*out_json writer.write(out_value);// 清理掉所有文件因为是临时的RemoveTempFile(file_name);}}; }设计测试用例对compile_run模块进行测试 测试这种复杂程序的时候一定要单元化的测试不要等最后代码全写完了才来测试像这样的话代码根本跑不通。 compile_run需要对端传入一个json串我们这里本地的构建一个但是实际上是oj_server服务器负载均衡选择后通过http传过来的。 这里用到了一个R()的语法这是C的语法意思是Row String 原生字符串的意思他就是说括号里面的东西保持原貌不要和其他东西进行匹配主要就是因为里面的双引号和字符串的双引号会匹配冲突一些东西 但是呢R()当中其实会屏蔽\n也就是把\n也给转义成\n所以我们在写row string的时候我们需要如下的方式去写 运行结果 形成的临时文件 资源限制测试 CPU占用超时 内存超限
    浮点数溢出错误 编译出错 引入cpp-httplib第三方网络库 我们自己写网络套接字来进行通信也是可以的不过太麻烦了我们直接使用开源第三方库cpp-httplib 进行cpp-httplib的安装后cpp-httplib是header only的也就是说把它里面的.h拷贝到项目中就可以直接完成。如果你想的话你也可以拷贝到系统目录下比如/usr/include/但是不推荐 需要注意的是 cpp-httplib的使用需要使用高版本的gcc/gcpp-httplib是阻塞式的多线程http网络库因为里面使用了原生线程库所以在编译的时候需要带上选项-lpthread gcc升级 首先我们通过gcc -v来查看当前gcc的版本cpp-httplib的使用要求gcc的版本在7.x.x以上这里有两种方法进行gcc的升级 ①在vscode上我们可以通过以下命令中的其中一个对gcc进行升级这样的升级方式只在本次登录有效vscode被关掉或者断开连接下一次需要重新升级不然使用了cpp-httplib编译的时候就会报错。

    安装工具集 scl

    sudo yum install centos-release-scl scl-utils-build scl enable devtoolset-7 bash scl enable devtoolset-8 bash scl enable devtoolset-9 bash②在云服务器上我们就可以通过上面的方法进行单次的升级还有下面的方法进行永久升级(修改配置文件~/.bash_profile)。 compile_run打包成网络服务 刚刚已经测试过我们已经能在本地进行编译运行的服务了。接下来我们使用cpp-httplib将我们的compile_run模块打包成网络服务。 我们这里是想把它构成服务器那么在cpp-httplib中的做法就是 1.构建服务器对象2.进行功能路由通过访问资源和回调函数的方式3.启动服务器等待链接(tcp的方式) #include ../comm/httplib.h … int main() {//1.构建服务器对象Server svr;//2.功能路由(资源相对路径lambda表达式)svr.Get(/hello,{});//3.启动服务器svr.listen(0.0.0.0,8080);//指定IP地址和PORT端口号return 0; }解释: Get的意思是对放用Get方法来请求资源。Get和Post的区别就是提交参数的位置不同而已Get回显到url通过url进行提参Post通过请求正文提交参数。Get成员函数的参数当中第一个就是需要的资源是哪个如果对端申请的是这个资源也就是说将来url是http://101.43.231.47:8080/hello通过Get方法请求这样的形式就会被捕捉到然后调用第二个参数是回调函数这里使用lambda表达式。Request和Response就是httplib给我们提供的类型可以填特定的成员进行通信httplib会自动帮我们发送和接收0.0.0.0就代表的是任意地址对应INADDR_ANY就是说只要是发给这个端口任何IP都可以被接收到因为有可能服务器不止一个网卡所以服务器一般都是这样设置 比如用户提交上来的代码就在Request的body当中method就是请求的方法path就是请求的路径(也就是请求我的什么资源)然后header就是请求报头等等。 这些都是部分截图Request和Response类远不止如此
    svr.Get(hello,{resp.set_content(hello httplib,你好httplib,text/plain;charset utf-8);//第二个参数就是这个内容的content-type我们这是纯文本字符编码utf8 });上面的content-type就是内容的形式比如纯文本text/plain比如html 的类型text/htmljson串的content-type就是application/json 这样的转化表在网上是可以搜到的
    有些时候你在进行编译的时候编译器会给你报一个fatal的错误就是vscode占用资源过多了OS直接终止掉了把vscode重启一下就行。 后续呢用户请求到来时在lambda表达式中调用compile_run的接口去进行编译运行拿到一个out_json的字符串装的就时是编译运行的结果。然后直接resp.set_content(out_json,content-type)就可以了然后httplib自动帮我们响应给用户 利用httplib将compile_run服务打包成网络服务我们需要的服务是compile_run //compile_server.cc #include compile_run.hpp #include ../comm/httplib.husing namespace ns_compile_and_run; using namespace httplib;void Usage(std::string proc) {std::cerr Usage: \n\t proc port std::endl; }//./compile_server port int main(int argc, char *argv[]) {if (argc ! 2){Usage(argv[0]);return 1;}Server svr; // 定义服务器对象// compile_and_run打包成网络服务svr.Post(/compile_and_run, {//用户请求的服务正文是我们想要的json stringstd::string in_json req.body;//代码std::string out_json;//返回的结果if(!in_json.empty()){CompileAndRun::Start(in_json,out_json);//编译运行然后结果就在out_json当中resp.set_content(out_json,application/json;charsetutf-8);//什么内容内容是什么格式} }); // 客户端将来采用Post的方式// 这个listen就等于是启动网络服务了std::coutargv[1]std::endl;svr.listen(0.0.0.0, atoi(argv[1])); // 哪个ip哪个端口提供服务选项默认return 0; }⑤使用Postman对打包成为网络后的c_r模块进行综合测试 Postman是一个可以用来发送网络请求的工具 我们的服务器打包成为了Post方法的一个功能路由所以我们拿Postman构建一个方法为Post的申请给compile_server服务器 设置Postman的这些东西 然后构建我们的请求
    发送之后我们可以看到结果
    测试CPU占用超时 测试空间申请超限
    都非常成功。至此呢就有了可以对外提供编译运行服务的服务器了只要你用Post方法申请/compile_and_run我们现在完成的就是最右边的compile_server一个一个的小框框 然后呢我们的compile_server作为一个多平台多主机部署。甚至在一台主机上部署多个服务这样的情况我们默认端口8080就不行了。我们需要引入命令行参数将端口暴露 int main(int argc,char*argv[]) {…. }然后我们后续就可以 ./compile_server 8081 ./compile_server 8082就可以了。可以在不同的主机上进行部署也可以在同一台主机部署多个。 四、oj_server模块 ①oj_server模块结构设计MVC架构模式 我们以及有了能够给我们提供网络服务的编译运行服务器了现在需要实现oj_server oj_server说白了就是一个网站。oj_server的功能如下 1.获取首页2.获取题目列表3.获取单道题目并提供编辑功能4.提交判题功能背后依靠的就是提供编译运行服务的服务器 我们想采用的是基于MVC的一种架构模式 MVC M model 与数据交互的模块V view 视图指用户界面就是用来与用户进行交互的模块C controller 控制器核心的业务逻辑都在这里实现合理调配model和view模块。 oj_server承担的就是负载均衡式的去调用后端的一个个编译服务然后展现给用户所以oj_server更靠近用户。 ②oj_server的功能路由 我们设计的oj_server一共能提供给用户的是3个功能路由 1.题目列表的功能路由2.单道题目的功能路由3.提交代码进行判题的功能路由 而至于首页就直接写了不用去功能路由 #include oj_controller.hpp #include iostream #include signal.h #include ../comm/httplib.husing namespace httplib; using namespace ns_controller;static Controller *ctrl_ptr nullptr;void Recovery(int signo) {ctrl_ptr-RecoveryMachine(); }int main(int argc, char argv[2]) {signal(SIGQUIT,Recovery);// 用户请求的服务路由功能Server svr;Controller ctrl; // 当用户请求时就直接调用controller当中的方法交互数据model也被controller包含在内ctrl_ptr ctrl;// 获取所有的题目列表svr.Get(/all_questions, ctrl { // lambda表达式想用父作用域的变量引用捕捉一下// 我想返回的是一张包含所有题目的html网页std::string html;ctrl.AllQuestions(html);resp.set_content(html, text/html;charsetutf-8); // 测试给个响应就行// 要从后端的model获取和数据交互的模块去获取}); // 获取什么资源然后回调// 用户要根据题目编号获取题目内容编辑代码// /questions/100 - 正则匹配//\d是正则表达式 \d代表匹配数字代表匹配一个或多个那么这就可以把题号全部读出来// R(),raw string 保持字符串内容的原貌不用做相关的转义svr.Get(R(/questions/(\d)), ctrl { // C当中的raw string原生字符串std::string number req.matches[1];std::string html;ctrl.Question(number, html);//获得一道题的html// req中有一个成员是matches他是把资源请求分段放在了里面我们拿的数字的位置就是matches[1]resp.set_content(html,text/html;charsetutf-8);});// 用户提交代码判题(依靠compile_server功能)1.每道题的测试用例 2.compile_and_runsvr.Post(R(/judge/(\d)), ctrl { //\d正则表达式std::string number req.matches[1];//matches分割请求资源//植入controller判题std::string result_json;ctrl.Judge(number,req.body,result_json);//传过来的是提交的代码,然后获得了out_json执行结果返回给用户通过responseresp.set_content(result_json,application/json;charsetutf-8);//给用户响应//resp.set_content(指定题目的判题 number, text/plain;charsetutf-8);});// 设置Web根目录svr.set_base_dir(./wwwroot);// 启动服务器svr.listen(0.0.0.0, 8080); // 就固定成8080来提供服务// 在这里就固定了虽然也可以像compile_server那样可以暴露出去今天我们就写死return 0; }解释 1.set_base_dir其实是提供给首页的我们的url如果是http://101.43.231.47/的话就代表想要的资源是/这个其实就代表的是访问的我们的web根目录我们命名为wwwroot而一般这样的访问代表首页我们会在web根目录下放置一个index.html供用户访问 2.R()上面以及说过了就是row string保持()中字符串原貌。 3.然后(\d)代表的是正则表达式代表有多少就匹配多少\d是匹配数字 4.上面使用到了Request当中的mathes对象其实matches对象就是将我们的资源申请做了切分比如说\question\100question就放到了matches[0]100就放到了matches[1]当中。
    我们提供了三个功能路由就分别对应三种资源申请 http://101.43.231.47/all_questions http://101.43.231.47/questions/题号 http://101.43.231.47/judge/题号
    ③ version1: 建立文件版的题库 首先我们的题目需要的东西有 1.题号 number2.标题 title3.难度 star4.描述 desc5.时间要求 cpu_limit6.空间要求 mem_limit 文件结构 在oj_server目录下我们需要一个questions目录对题目的所有东西进行存储。 而我们需要一个questions.list配置文件来读取所有题目(我们打算将题目构建成一个Question对象) 然后更具体的东西比如题目的描述预设给用户的代码测试用例单独放在一个目录里 在questions.list配置文件中的存储方式 我们并不需要存储题目描述我们可以通过对应的题号找到题目对应细节目录下的题目描述如上面的文件结构我们就可以找到对应题目的desc.txt 给用户预设的代码header 我们想要的效果是这样的在代码编辑窗口我们是给用户预设了一部分代码的。 这些代码就放在了header.cpp当中。 未来用户提交代码之后我们不是直接将这部分代码直接交给compile_server进行编译运行。因为代码不全compile_server只提供编译运行服务。只提交这部分代码的话是一定报错的。 tail.cpp测试用例部分 所以我们需要给header.cpp中的代码进行合并进行合并的代码就放在tail.cpp当中 所谓测试用例其实就是把你在代码编辑框中的代码提交上来然后和另外一个代码进行合并。这个代码里差的就是对你写的那部分函数。所以两个合在一起才形成了完整的一个程序。 tail.cpp的样子如下 #ifndef COMPILER_ONLINE #include header.cpp #endifvoid Test1() {vectorint v {1, 2, 3, 4, 5, 6};int max Solution().Max(v); // 匿名对象来完成方法的调用if (max 6){std::cout Test 1 … OK! std::endl;}else{// 这些可以不显示但是我需要方便我调试std::cout Test 1 … Failed std::endl;} }void Test2() {vectorint v {-1, -2, -3, -4, -5, -6};int max Solution().Max(v);if (max -1){std::cout Test2 … OK! std::endl;}else{std::cout Test2 … Failed! std::endl;} }int main() {Test1(); // 测试用例1Test2(); // 测试用例2return 0; }解释 条件编译的原因是这部分代码因为缺少用户提交的那部分函数所以我们在编译oj_server的时候是会报错的因为少了函数跑不了可以理解。所以我们需要加一个条件编译让这个.cc文件知道我们有该函数不要报错。这个条件编译到时候我们再通过给其他方式去掉我们可以在调用g的时候加选项比如我们上面的宏是 COMPILER_ONLINE那么到时候我们直接 gcc … -D COMPILER_ONLIEN就可以去掉了。-D选项就是在命令行进行宏定义的方式 ③version2 MySQL版本 首先我们要创建一个用户并给他赋权以便我们进行连接和其他库的隐藏。 创建用户并赋权 创建可以远程登录的用户 Create user oj_client% identified by 密码; //%就是在任意地点登录MySQL默认是只允许localhost登录的。 //这就给它设置了可以远程登录的能力建立数据库 oj create database oj; 赋权赋权就是让该用户只能看见一些想让他看见的东西比如我只让他看见oj这个数据库 grant all on oj.
    to oj_client%; 这里会出现一些错误都整理好了请点击☞解决MySQL赋权… 使用MySQL_WorkBench创建表结构 在MySQL当中我们就不需要像文件那样分很多个模块存储了都直接存一起 MySQL的表结构 CREATE TABLE IF NOT EXISTS oj_questions (number INT PRIMARY KEY AUTO_INCREMENT COMMENT 题目的编号,title VARCHAR(128) NOT NULL COMMENT 题目的标题,star VARCHAR(8) NOT NULL COMMENT 题目的难度,desc TEXT NOT NULL COMMENT 题目的描述,header TEXT NOT NULL COMMENT 对应题目的预设代码,tail TEXT NOT NULL COMMENT 对应题目的测试用例代码,cpu_limit INT DEFAULT 1 COMMENT 对应题目的超时时间,mem_limit INT DEFAULT 50000 COMMENT 对应题目的最大开辟的内存空间 ) ENGINEINNODB , CHARSETUTF8;注意descheader和tail可能一个varchar不够所以用tex大文本来进行存储 在MySQL_WorkBench当中进行录题 编写MySQL版本的model模块 首先我们梳理一下在文件版当中我们model模块是要完成以下几个部分 1.我们进行文件题库的读取生成unordered_map string,Question的容器2.提供获得所有题目的接口3.提供获得单道题目的接口 那么我们现在MySQL版本的model模块就是 1.创建Question对象因为要给别人返回这个对象2.连接数据库进行读取读取好了之后构建Question给别人返回就完了 而MySQL当中也需要搜题目列表和单道题目。 如果是需要题目列表那么MySQL会通过你传过来的sql语句进行搜索因为搜索出来的是多行所以搜索出来的会放进一个特定的地方结构是MYSQL_RES然后我们以二维数组的方式去读取就可以了。如果是单道同样的也是放入MYSQL_RES,只不过我们的Question只构建一次 读取MySQ流程 1.我们需要定义一个MySQL句柄MYSQL * my mysql_init(nullptr); 2.然后进行MySQL的链接mysql_real_connect(my,ip,port,db…..)需要传入的参数如下。 3.修改字符编码 mysql_set_character_set(my,utf8) 4.然后已经找到了数据库连接其实是连接的数据库然后通过传入的sql去进行查询 mysql_query(my,sal); 5.查询好的东西都放在一个特定的结构里面叫做MYSQL_RES我们通过mysql_num_rows和mysql_num_fields去进行该结构中数据的行和列的数目。 6.循环进行读取数据构建Question对象 7.MYSQL_ROW row mysql_fetch_row(res)就是拿到了一行,res是MYSQL_RES的对象res是通过 MYSQL_RES *res mysql_store_result(my)拿到的 8.通过数组的方式读取 比如 code row[1] 9.释放MYSQL_RES结构关掉MYSQL句柄 free(res); mysql_close(my);
    namespace ns_model {using namespace std;using namespace ns_log;using namespace ns_util;struct Question{std::string number; // 题目编号唯一std::string title; // 题目标题std::string star; // 题目难度简单 中等 困难std::string desc; // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例需要和header拼接形成完整代码再compile_serverint cpu_limit; // CPU占用时间限制sint mem_limit; // 空间限制KB};const std::string oj_questions oj_questions; // 表名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 unsigned int port 3306;//MySQL默认端口class Model{public:Model(){}bool QueryMySQL(const std::string sql, vectorQuestion *out){//这里面访问数据库构建Question结构体或者数组返回给调用者。//主要就是访问数据库调用官方给的第三方库MYSQL *my mysql_init(nullptr);//创建MySQL句柄//连接数据库if(nullptr mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0))//句柄主机用户密码数据库端口是否使用域间套接字选项{LOG(FATAL)连接数据库失败std::endl;return false;}mysql_set_character_set(my,utf8);//一定要设置编码格式要不然会出先乱码问题(默认的应该是拉丁1)LOG(INFO)连接数据库成功std::endl;//访问数据库执行sql语句if( 0 ! mysql_query(my,sql.c_str()))//mysql进行查询{LOG(WARNING)sql execute error!std::endl;//sql执行失败return false;}//调用完之后我们这里就有了只不过它帮我们存储好的我们调用特定的方法去取这些数据//提取结果结果给我们放到特定的结构当中了MYSQL_RES *res mysql_store_result(my);//store叫做存储这就把保存结果的特定结构拿到了//分析访问结果//获得行数和列数我们还没开始用就觉得多半是数据存储字符串的形式int rows mysql_num_rows(res);//特定结果的行数int cols mysql_num_fields(res);//列数fields是领域的意思//提取数据for(int i0;irows;i){MYSQL_ROW row mysql_fetch_row(res);//拿出一行struct Question q;//然后构成一个或多个Question返回q.number row[0];//拿到这一行的第j列q.title row[1];q.star row[2];q.desc row[3];q.header row[4];q.tail row[5];q.cpu_limit stoi(row[6]);q.mem_limit stoi(row[7]);out-push_back(q);//多少行mysql表就会有多少个}//释放结果空间free(res);//关掉mysql连接mysql_close(my);return true;}bool GetAllQuestions(vectorQuestion *out){std::string sql select * from ;sql oj_questions;return QueryMySQL(sql, out); // 所有题目全部拿到返回给controller进行其他操作}bool GetOneQuestion(const std::string number, Question *q){bool res false;std::string sql select * from ;sql oj_questions;sql where number;sql number;vectorQuestion result; // 只是转化一下满足调用接口的参数要求if (QueryMySQL(sql, result)){if (result.size() 1){q result[0];res true;}}return res;}~Model(){}};④model模块 model模块主要是用来和数据交互的对外提供访问数据的接口 我们在model模块当中因为我们的数据就是题目所以一上来我们就要把题目读出来。 我们会有一个Question类用它来描述该题目的信息 struct Question{std::string number; // 题目编号唯一std::string title; // 题目标题std::string star; // 题目难度简单 中等 困难int cpu_limit; // CPU占用时间限制sint mem_limit; // 空间限制KBstd::string desc; // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例需要和header拼接形成完整代码再compile_server};选择用unordered_mapstring,Question的结构体来存储生成的Question建立题目(字符串)与Question的映射。 使用boost准标准库当中的split进行字符串分割 class StringUtil{public:/** str:输入性参数要切分的字符串* target:输出型参数保存并返回切分完毕的结果* sep:separator分隔符/static void SplitString(const std::string str,std::vectorstd::string target,std::string sep){//使用C准标准库boost 当中的split进行字符串分割boost::split((*target),str,boost::is_any_of(sep),boost::algorithm::token_compress_on);//is_any_of代表sep分隔符字符串当中的任意一个字符都能用来分割//token_compress_on代表我是否需要进行压缩//调用这个接口就自动的帮我们完成了字符串切分}};按行读取配置文件形成Question对象 1.用C的文件流的方式创建ifstream对象打开文件流2.使用getline进行按行读取getline的注意事项上面以及说过不再重复3.使用字符串工具类中封装好的函数进行字符串切割放入tokens数组4.利用该数组进行Question结构体的创建 bool LoadQuestionList(const std::string questino_list){// 加载配置文件 questions/questions.list 题目编号对应目录下的文件// 比如200道题其实就是加载了200个Key值和200个Question对象// 我们按行读取配置文件可以用FileUtil当中的ifstream in(question_list);if (!in.is_open()){LOG(FATAL) 加载题库失败请检查是否存在题库文件 std::endl; // 题目列表这个配置文件都加载不进来那所有的都跑不动所以是致命的return false;}// 读取getline注意①不会保留换行符②重载了强制类型转换使得while可以判断成功与否std::string line;while (getline(in, line)){// 我们要按顺序进行切分编号标题难度时间限制空间限制std::vectorstd::string tokens;StringUtil::SplitString(line, tokens, ); // 按空格分割到target当中// 是按顺序切分的,按照我们questions.list配置文件的要求它必须是被分成几份// 如1 判断回文数 简单 1 30000if (tokens.size() ! 5){LOG(WARNING) 加载部分题目失败请检查文件格式 std::endl; // 因为只是这道题出问题不是很影响所以WARNINGcontinue; // 这一行配置我们就不能要}// 构建Question对象Question q;q.number tokens[0];q.title tokens[1];q.star tokens[2];q.cpu_limit stoi(tokens[3]);q.mem_limit stoi(tokens[4]); // 或者转成c_str()使用atoistd::string path question_path;path q.number; // q.number是字符串的方式呈现的path /; // 题号目录路径FileUtil::ReadFile(path desc.txt, (q.desc), true); // 读取描述文件FileUtil::ReadFile(path header.cpp, (q.header), true); // 读取header.cppFileUtil::ReadFile(path tail.cpp, (q.tail), true); // 读取tail.cpp// 我们是要保持原貌的不然特别丑所以true// 然后插入questions 题号Question// questions.insert(make_pair(q.number,q));questions.insert({q.number, q}); // 使用列表初始化}LOG(INFO) 加载题录…成功! std::endl;in.close();return true;}⑤controller模块 controller模块整体结构 Controller模块是MVC架构模式当中的C主要负责核心逻辑的编写。 比如model模块和view模块的调用将来都是在controller模块
    我们以及有了功能路由但是如果向访问到页面就需要用到view模块(前端页面)和model模块(数据获取)。所以功能路由一定是通过创建controller对象去进行调用。controller的类当中就会合理的调用model模块还要view模块就会有一个渲染好的html显示给用户 引入ctemplate模板渲染库测试基本功能 首先百度搜索ctemplate的教程进行ctemplate库的安装 ctemplate的渲染方法 1.需要体现准备好html模板 2.使用ctemplate::TemplateDirection这个类型构建一个模板对应的数据字典这个字典是Key-Value的形式。Key就是html模板当中的KeyValue就是待填入的值3.然后进行html模板的获取 ctemplate::Template * tpl cctemplate::Template::GetTemplate(html模板路径是否保持原貌);保持原貌的话就传入参数ctemplate::DO_NOT_STRIP4.调用Template对象的成员方法Expand进行渲染 // 测试ctemplate 小demo int main() {std::string in_html ./test.html;// 我们要处理的网页,也就是说我们测试的demo就直接放在test.cc的同级目录下std::string value 猪皮兄弟;// 形成模板字典ctemplate::TemplateDictionary root(test); // 这就相当于这个字典对象的名字叫做testroot.SetValue(key, value); // SetValue给模板字典设置值进去// 获取被渲染网页对象ctemplate::Template *tpl ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP);//DO_NOT_STRIP是保持网页htmp原貌strip是剥夺的意思// 添加模板字典到网页中进行合并std::string out_html;tpl-Expand(out_html, root);// 完成了渲染std::cout out_html std::endl;return 0; }⑥judge模块负载均衡 用户在编辑器中编写的代码提交给oj_server之后oj_server是需要做负载均衡的也就是选择负载最少的主机进行访问
    那么我们就在controller增加一个判题的功能。当客户端把代码提交上来之后judge模块就要进行主机的选择然后序列化成compile_server需要的json串发过去。不要忘记需要拼接测试用例 现在看来用户提交的json串有三部分构成 1.首先需要题目的id让我们可以进行测试用例的拼接2.code这个就是用户编辑的那部分代码3.input其实是可以有自测输入的不过我们今天不支持反正也不难 收到json串的code之后judge模块就会根据读取配置文件建立好的unordered_map来找到对应的题目细节然后拿到题目对应的测试用例进行拼接 下一步我们就是把拼接好的代码需要发给compile_server服务器进行编译运行了 那么有哪些主机可以供我们选择呢我们又怎么去选择负载最低的呢 所以我们就需要给一个配置文件里面配置的就是主机的信息比如IP端口然后我们还需要再oj_server当中维护对应主机的负载情况以便我们进行选择。 Machine类 // 提供compile_server服务的主机class Machine{public:std::string ip; // 编译服务的IPint port; // 编译服务的端口uint64_t load; // 编译服务的负载情况std::mutex mtx; // mutex是禁止拷贝Machine管理到容器当中是一定会发生拷贝的所以我们用指针来管理mutexpublic:Machine(): ip(), port(0), load(0), mtx(nullptr){}~Machine(){}public:void IncLoad() // increase,提升主机负载{if (mtx)mtx-lock();load;if (mtx)mtx-unlock();}void DecLoad() // decrease,减少主机负载{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;} // 获取负载的时候避免这个machine被下线加锁};因为一旦连接我拼接完之后就要对主机进行选择所以这里是要加锁包的为了负载均衡我们维护的有load我们要选择load最小的去进行服务。 加锁也可以用系统当中的pthread_mutex_xxx等pthread库中的内容 也可以用C当中的mutex库需要注意的是C当中所有的锁都是防拷贝的。 因为创建出来的Machine对象要管理到某个容器当中所以一定会发生拷贝所以我们直接定义mutex对象是会报错的我们这里需要用指针的方式去用锁 负载均衡函数 const std::string service_machine ./conf/service_machine.conf;// 负载均衡模块class LoadBalance{private:// 此时我就想从配置文件当中把所有的主机读上来IP端口// 把可以提供compile_server的Machine对象放入容器。// 每一台主机都有自己的下标我们用它充当当前主机的idstd::vectorMachine machines; // 可以提供compile_server的所有的主机std::vectorint online; // 所有在线的主机std::vectorint offline; // 所有离线主机的idstd::mutex mtx; // 这个只有一个就不用用指针了public:LoadBalance(){assert(LoadConf(service_machine));LOG(INFO) 加载 service_machine 配置文件成功 std::endl;}~LoadBalance(){}bool LoadConf(const std::string machine_conf) // 传入配置文件路径{// 读取部署主机配置文件ifstream in(machine_conf); // 打开文件流if (!in.is_open()){LOG(FATAL) 加载: machine_conf 失败 std::endl;return false;}// 开始读取并构建Machine对象放入vector形成machinesstd::string line;while (std::getline(in, line)) // getline 1.不会保留换行符2.重载了强制类型转换所以可用while进行判断{// 101.43.231.47:8081Machine m;// int i line.find(:);// machine.ip line.substr(0, i - 0);// machine.port stoi(line.substr(i));std::vectorstd::string tokens;StringUtil::SplitString(line, tokens, :); // 以冒号分割进vif (tokens.size() ! 2){LOG(WARNING) 切分 line 失败 std::endl;continue; // 失败切下一个}m.ip tokens[0];m.port stoi(tokens[1]); // 转为intm.load 0;m.mtx new std::mutex(); // 一定要是指针C的所有mutex防拷贝push到容器当中是需要拷贝的必须用指针来使用锁line.clear();// 一开始全部都是Online在后面使用的时候再去下线我们push的是下标,所以使用machines.size()来进行pushonline.push_back(machines.size());machines.push_back(m);}in.close();return true;}/** id:输出型参数选择到了哪个主机* m:输出型参数选择的主机的对象只是我想直接访问不想再去通过下标访问machines*/bool ItelligentChoice(int *id, Machine **m) // 智能选择,负载均衡我们想拿的是二级指针{ // 我现在要访问compile_server了给我智能选择// 通过machines来进行选择通过它的负载load,而选择machines的时候因为要操作load所以加锁保护临界资源mtx.lock(); // 加锁保护// 选择服务器负载均衡。// 负载均衡的算法1.随机数法2.轮询随机选择最小的load绝对负载均衡我们选择这种方案int online_num online.size();if (online_num 0){// 所有主机离线mtx.unlock(); // 最好搞成RAIIC中就有LockGuard能够RAIILOG(FATAL) 所有的后端编译运行主机已经离线请运维的老铁尽快查看 std::endl;return false;}// 通过遍历的方式找到负载最小的机器*id online[0];*m machines[online[0]];uint64_t min_load machines[online[0]].Load(); // online和offline存在是machines的下标for (int i 1; i online_num; i){uint64_t curr_load machines[online[i]].Load();if (min_load curr_load){min_load curr_load;*id online[i]; // 选择的主机是对应的machines的哪台online中是下标的映射*m machines[online[i]]; // 我要拿的是这台主机的地址}}mtx.unlock();return true;}};编写负载均衡器 过程明确 1.获得该题目对应的Question结构体2.解析客户端发来的json串进行代码拼接和构建新的compile_server需要的json串3.负载均衡主机智能选择4.选择到负载最小的主机使用cpp-httplib生成客户端进行数据发送。5.会自动的帮我们发送请求并接收结果然后我们就拿到了结果对结果进行分析发送给用户 cpp-httplib 构建Client端的方法 1.Client cli(IP,Port);2.cli.Post/Get.(请求的资源发送什么数据content-type)3.自动接收结果第二步的方法的返回值是一个result结构体里面就有Response结构的成员返回的东西都填到body里面了的包括执行结果等等 //ctroller的Judge成员函数void Judge(const std::string number, const std::string in_json, std::string *out_json) // 给我json串我judge之后返回结果json串{//LOG(DEBUG) injson \nnumber: number std::endl;// 0.根据题目编号直接拿到对应的题目细节很简单因为我们有和数据交互的model模块struct Question q;model.GetOneQuestion(number, q);// 1.对in_json反序列化 ,因为in_json当中的code只有header不全// 而且我们还需要里面的题目的idinput数据Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value); // 将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 \n q.tail; // 需要把编译宏给去掉compile_value[cpu_limit] q.cpu_limit;compile_value[mem_limit] q.mem_limit; // 以上四个字段就是compile_server需要的Json::FastWriter writer;std::string compile_string writer.write(compile_value);// 3.负载均衡,选择负载最低的主机(前面的数据准备工作已经做好) 这里要做各种差错处理// 规则一直选择直到主机可用否则就是全部下线挂掉不需要让客户知道while (true) // 必须选择主机并编译运行成功返回的Response中状态码是200表示成功才行{int id 0;Machine *m nullptr;if (!loadbalance.ItelligentChoice(id, m)){break; // 所有主机都挂掉了}// 4.发起http请求请求compile_server服务Client cli(m-ip, m-port); // 构建客户端m-IncLoad(); // 请求主机增加负载LOG(INFO) 选择主机成功主机id: id 详情: m-ip : m-port 当前主机的负载是: m-Load() std::endl;if (auto res cli.Post(/compile_and_run, compile_string, application/json;charsetutf-8))// 请求的服务,请求的参数,请求的content-type{// res指的是result里面有response和err 请求是否成功是判断res里面有没有responseerr就是一堆的枚举常量// 发起了之后服务端处理完成之后会返回给我Response// 5.返回用户json串json串的发送和接收由cli和svr自动完成if (res-status 200) // 这样请求才算完全成功{*out_json res-body; // 拿到编译的结果m-DecLoad(); // 请求完毕减少负载LOG(INFO) 请求编译和运行服务成功… std::endl;break;}// 访问到了但是结果是不对的那么就会while(true)回来重新再选择主机m-DecLoad();}else{// 请求失败LOG(ERROR) 当前选择的主机id: id 详情: m-ip : m-port 可能已经离线 std::endl;// 请求完毕减少负载。这里没有必要因为一旦我们将主机离线负载会清零loadbalance.OfflineMachine(id); // 根据id来进行主机的离线loadbalance.ShowMachines(); // 仅仅是为了调试}}}离线和上线 如果一台主机请求失败我们就应该让该主机离线 如果是上线就是说如果运维把主机弄好了重新启动了我们就进行上线可以让他重新被选择。我们这里粗暴一点如果所有主机都离线了统一上线。 而上线和离线呢我们其实均衡调度LoadBalance模块有online和offliine两个数组进行machines机器下标的存储。上线就是把下标加到online数组能够被选择嘛离线就是从online数组移到offline数组。 void OfflineMachine(int which) // 请求不成功将这台主机offline{// 离线的时候可能有人正在ItelligentChoice所以加锁mtx.lock();for (auto iter online.begin(); iter ! online.end(); iter){if (*iter which){ // 找到该目标主机在Online保存的下标,移到Offline当中machines[which].load0;//负载清零online.erase(iter);// offline.push_back(iter);//迭代器失效offline.push_back(which);break; // 有break的存在我们这里就不用迭代器失效的问题只会erase一次不然的话就要去更新迭代器}}mtx.unlock();}void OnlineMachine()//上线所有的{// 当所有主机都离线的时候我们统一上线offline的移到online// 所以我们要有一些检测机制来检测全部离线mtx.lock();online offline;//vector的深拷贝//online.insert(online.end(),offline.begin(),offline.end());offline.clear();//offline.erase(offline.begin(),offline.end());mtx.unlock();LOG(INFO)所有的主机又上线啦std::endl;}⑦使用Postman进行oj_server的综合测试 首先我们启动三个 compile_server服务 分别是./compile_server 8081 ./compile_server 8082 ./compile_server 8083 这就可以支持oj_server对我们进行负载均衡调度的选择 我们这里测试的主要是oj_server的判题Judge功能我们对于oj_server需要访问的资源是/judge/number ,比如 http://101.43.231.47/judge/1;判断1号题目是否正确 我们看到这里的状态码是-3从我们之前对于状态码的描述来看负数就是编译错误。 然后找到错误是因为测试用例当中有一段空定义我们忘记去掉。 那么我们就改一下g的选项就可以了。 然后重新编译运行 负载均衡的话因为Postman只能一次一次发我们没办法测试 我们得等到后面能通过网页提交代码的时候才测试得了。 我们挂掉主机
    ⑧view模块整体代码结构前端的东西不是重点 由上可知View类应该需要的是两个接口AllExpandHtml和Expand 从名字就可以看出来AllExpandHtml用于获得题目列表的html形成Expand就用于单道题目的html形成 因为这个前端的东西对于我们来说不是很重要我也只进行了一些了解后面就直接粘代码了。前端的东西涉及html/css/js/jquery/ajax等等我们在用户编辑代码的部分引入了Ace在线编辑器可以在后端调用Ace的方法直接拿到用户编辑的内容 下面的html代码就是html模板我们根据上面的方法对模板里面的内容用ctemplate库进行渲染即可 对于前后端交互就是前端给了一个按钮点击按钮后设的有onclick的属性然后响应事件会触发后面给定的函数js该函数就完成调用Ace在线编辑器提供的方法拿到用户提交代码构成json串发给oj_server进行处理。然后后面过程走完了我返回给你结果你进行显示 index !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0titleZPXD个人oj系统/title!– 这张网页的整体样式 –style/
    选中所有标签,消除内外边距 / {/* 消除网页的默认外边距 /margin:0px;/ 消除网页的默认内边距这俩100%保证我们的样式设置不受默认影响 /padding:0px;}/ html和body标签都是按照100%进行 /html,body{width: 100%;height: 100%;}.container .navbar{width: 100%;height: 50px;background-color: black;/ 给父级标签设置overflow /overflow:hidden;}.container .navbar a{/ 设置成行内块元素 /display: inline-block;/ 设置a标签的宽度 /width: 80px;/ 设置字体颜色 /color: white;/ 设置大小 /font-size: large;/ 设置文字的高度和导航栏一样的高度 /line-height: 50px;/ 去掉下划线 /text-decoration: none;/ 设置文字居中 /text-align: center;}/ 设置鼠标事件 /.container .navbar a:hover{background-color: green;}.container .navbar .login{float: right;}.container .content{/ 设置标签的宽度px是像素点的意思 /width: 800px;/ 背景色 // background-color:#ccc; // content整体居中上下0px像素点左右auto居中 /margin: 0px auto;/ 设置content在container当中也居中 /text-align: center;/ 设置上外边距 /margin-top: 200px;}.container .content .font_{/ 设置标签为块级元素独占一行可以设置高度宽度等属性 /display: block;/ 设置每个文字的上外边距 /margin-top: 20px;/ 去掉下划线 /text-decoration: none;/ 设置字体大小 // font-size:larger; /}.container2 .footer{margin-top: 400px;width: 100%;height: 50px;background-color: black;text-align: center;color: white;}/style /head bodydiv classcontainer!– 导航栏 功能不实现但是给个导航栏–div classnavbara href#首页/a!– a标签是超链接 –a href/all_questions题库/aa href#竞赛/aa href#讨论/aa href#求职/aa classlogin href#登录/a/div!– 网页的内容 –div classcontenth1 classfont_欢迎来到我的OnlineJudge平台/h1p classfont这是我个人独立发开的一个oj平台/pa classfont href/all_questions点击我开始编程了!!/a/div/divdiv classcontainer2div classfooterh4猪皮兄弟/h4/div/div /body /htmlall_questions !DOCTYPE html html langen headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0!– 这是网页标题在最上面的框框中 –title在线OJ-题目列表/title style/ 选中所有标签,消除内外边距 / {/* 消除网页的默认外边距 /margin:0px;/ 消除网页的默认内边距这俩100%保证我们的样式设置不受默认影响 /padding:0px;}/ html和body标签都是按照100%进行 /html,body{width: 100%;height: 100%;}.container .navbar{width: 100%;height: 50px;background-color: black;/ 给父级标签设置overflow /overflow:hidden;}.container .navbar a{/ 设置成行内块元素 /display: inline-block;/ 设置a标签的宽度 /width: 80px;/ 设置字体颜色 /color: white;/ 设置大小 /font-size: large;/ 设置文字的高度和导航栏一样的高度 /line-height: 50px;/ 去掉下划线 /text-decoration: none;/ 设置文字居中 /text-align: center;}/ 设置鼠标事件 /.container .navbar a:hover{background-color: green;}.container .navbar .login{float: right;}.container .questions_list {padding-top: 50px;width: 800px;height: 100%;margin: 0px auto;/ background-color: #ccc; /text-align: center;}.container .questions_list table{width: 100%;font-size: large;font-family: Lucida Sans, Lucida Sans Regular, Lucida Grande, Lucida Sans Unicode, Geneva, Verdana, sans-serif;margin-top: 50px;background-color: rgb(243,248,246);}.container .questions_list h1{color: green;}/ .是用来查类的标签比如table不用加. /.container .questions_list table .item{width: 100px;height: 40px;font-size: large;font-family: Times New Roman, Times, serif;}.container .questions_list table .item a{text-decoration: none;color: black;}.container .questions_list table .item a:hover{color:blue;font-size: larger;}.container .footer{width: 100%;height: 50px;text-align: center;line-height: 50px;color:#ccc;margin-top: 15px;}.container2 .footer{margin-top: 50px;width: 100%;height: 50px;background-color: black;text-align: center;color: white;}/style /head bodydiv classcontainer!– 导航栏 功能不实现但是给个导航栏–div classnavbara href/首页/a!– a标签是超链接 –a href/all_questions题库/aa href#竞赛/aa href#讨论/aa href#求职/aa classlogin href#登录/a/divdiv classquestions_listh1OnlineJudge题目列表/h1table!– tr/tr代表一行 TableRowth/th代表表头 TableHeadtd/td代表数据框就是一行中的一个表格 TableData –trth class item编号/thth class item标题/thth class item难度/th/tr{{#questions_list}}trtd class item{{number}}/tdtd class itema href/questions/{{number}}{{title}}/a/td!– 虽然访问的是/question/{{number}}这个资源 –!– 但是会由我们的功能路由去进行路由最终是渲染的我们的one_question.html –td class item{{star}}/td/tr{{/questions_list}}/table/div/divdiv classcontainer2div classfooterh4猪皮兄弟/h4/div/div /body /htmlone_question !DOCTYPE html html langenheadmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0title{{number}}.{{title}}/title!– 1.判断回文数 –!– 引入ACE CDN,CDN是用来帮我们进行网络加速的类似于云服务 –script srchttps://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js typetext/javascriptcharsetutf-8/script!– 引入另一个CDN语言识别 –script srchttps://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js typetext/javascriptcharsetutf-8/script!– 引入jquery CDN –script srchttp://code.jquery.com/jquery-2.1.1.min.js/scriptstyle {margin: 0;padding: 0;}html,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow /overflow: hidden;}.container .navbar a {/ 设置成行内块元素 /display: inline-block;/ 设置a标签的宽度 /width: 80px;/ 设置字体颜色 /color: white;/ 设置大小 /font-size: large;/ 设置文字的高度和导航栏一样的高度 /line-height: 50px;/ 去掉下划线 /text-decoration: none;/ 设置文字居中 /text-align: center;}/ 设置鼠标事件 /.container .navbar a:hover {background-color: green;}.container .navbar .login {float: right;}.container .part1 {/ 宽度铺满 /width: 100%;/ 高度600像素 /height: 600px;}.container .part1 .left_desc {width: 50%;height: 600px;float: left;/ 添加滚动条 /overflow: scroll;}.container .part1 .left_desc h3 {padding-top: 10px;padding-left: 10px;}.container .part1 .left_desc pre {padding-top: 10px;padding-left: 10px;font-size: medium;font-family: Gill Sans, Gill Sans MT, Calibri, Trebuchet MS, sans-serif;}.container .part1 .right_code {width: 50%;height: 600px;float: right;}.container .part1 .right_code .ace_editor {height: 600px;}.container .part2 {width: 100%;overflow: hidden;}.container .part2 .result {width: 300px;float: left;}.container .part2 .btn_submit {width: 120px;height: 50px;font-size: large;float: right;background-color: #26bb9c;color: #fff;/ 给按钮带上圆角 */border-radius: 1ch;border: #26bb9c solid 0px;margin-top: 10px;margin-right: 10px;}.container .part2 button:hover {color: green;}.container .part2 .result {margin-top: 15px;margin-left: 15px;}.container .part2 .result pre{font-size: large; }/style/headbodydiv classcontainer!– 导航栏 功能不实现但是给个导航栏–div classnavbara href/首页/a!– a标签是超链接 –a href/all_questions题库/aa href#竞赛/aa href#讨论/aa href#求职/aa classlogin href#登录/a/div!– 左右的结构 –div classpart1div classleftdesch3span idnumber{{number}}/span.{{title}}{{star}}/h3!– 三级标题 –pre{{desc}}/pre!– p是段落放题目描述 –/divdiv classright_codepre idcode classace_editortextarea classace_text-input{{pre_code}}/textarea/pre/div/div!– 交互模块 –!– 提交并且得到结果并显示 –div classpart2!– 结果查看后面使用jquery进行标签的插入 –div classresult/div!– 提交按钮 –button classbtn_submit onclicksubmit()保存提交/button/div/div!– textarea namecode id cols120 rows30{{pre_code}}/textarea –!– 这是文本编辑框要放我们预设的代码 pre_code –script//初始化对象editor ace.edit(code);//设置风格和语言更多风格和语言请到github上相应目录查看// 主题大全http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.htmleditor.setTheme(ace/theme/monokai);editor.session.setMode(ace/mode/c_cpp);// 字体大小editor.setFontSize(16);// 设置默认制表符的大小:editor.getSession().setTabSize(4);// 设置只读true时只读用于展示代码editor.setReadOnly(false);// 启用提示菜单ace.require(ace/ext/language_tools);editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true});function submit() {//alert(嘿嘿);//console.log(哈哈);//1.收集当前页面的有关数据1.题号2.代码var code editor.getSession().getValue();//console.log(code);var number \((.container .part1 .left_desc h3 #number).text();//#是id选择器//console.log(number);var judge_url /judge/ number;//console.log(judge_url);//请求哪个资源我们后台进行路由GETPOST//2.构建需要的json串并向后台发起请求基于http的json请求json串\).ajax({method: Post,//请求方法url: judge_url,//想请求什么资源dataType: json,//告知服务端我需要说明格式contentType: application/json;charsetutf-8,//我给你的是什么格式data: JSON.stringify({code: code,input: }),success: function (data) {//成功的时候执行回调匿名函数//成功得到结果写到data当中//console.log(data);show_result(data);}});//3.得到结果我们解析结果并显示到result中function show_result(data) {//里面肯定是json串那么里面的字段我们怎么拿出来呢//status reason 等等//console.log(data.status);//console.log(data.reason);//√//拿到结果标签var result_div \((.container .part2 .result);result_div.empty();//清空上一次的运行结果//拿到结果的状态码和原因var _status data.status;var _reason data.reason;var reason_lable \)(p, {text: _reason});reason_lable.appendTo(result_div);if (status 0) {//请求成功但是结果是否正确看测试用例的结果 var _stdout data.stdout;var _stderr data.stderr;var stdout_lable \((pre, {text: _stdout});var stderr_lable \)(pre, {text: _stderr});stdout_lable.appendTo(result_div);stderr_lable.appendTo(result_div);}else {//编译运行出错我们只显示reason,do nothing}}}/script /body/html五、最终效果 六、项目结项与扩展 项目亮点 STL标准库的使用Boost准标准库split字符串切割cpp-httplib的使用ctemplate进行网页渲染jsoncpp进行序列化和反序列化负载均衡的设计多线程多进程锁的使用ACE插件在线编辑器前端html/js/css/jquery/ajax的使用等 项目扩展思路 1.基于注册和登录的录题功能2.业务扩展比如我们保留出来的论坛竞赛求职等接入到我们的在线oj当中3.即便是便器服务在其他机器上也其实不太安全。可以将编译服务部署自docker上。一旦挂掉不会 影响操作系统4.目前compiler编译运行服务使用的是http方式请求因为简单我们也可以把它设计成远程过程调用RPC可以用它来替换我们的httplib的内容5.其他 七、顶层makefile发布项目 我们的项目写好了之后给别人用不是把代码给别人的而是只需要把可执行文件和运行该程序需要的配置文件给用户就可以了。 对于顶层makefile我们想完成3个任务 1.一键编译2.一键发布3.一件清除 .PHONY:all all:cd compile_server;\make;\cd -;\cd oj_server;\make;\cd -;.PHONY:output output:mkdir -p output/compile_server;\mkdir -p output/oj_server;\cp -rf compile_server/compile_server output/compile_server;\cp -rf compile_server/temp output/compile_server/;\cp -rf oj_server/conf output/oj_server/;\cp -rf oj_server/include output/oj_server/;\cp -rf oj_server/lib output/oj_server/;\cp -rf oj_server/questions output/oj_server/;\cp -rf oj_server/template_html output/oj_server/;\cp -rf oj_server/wwwroot output/oj_server/;\cp -rf oj_server/oj_server output/oj_server/;.PHNOY:clean clean:cd compile_server;\make clean;\cd -;\cd oj_server;\make clean;\cd -;\rm -rf output;\除了有转移的意思还有续航的意思代表这一坨东西都是一起的。然后就是说在执行的时候不要显示这部分内容默默执行就可以了output中就是我们发布后给别人的文件别人就可以直接使用 项目源码 Giteehttps://gitee.com/zhu-pi/zhupi-linux/tree/master/OnlineJudge