【项目设计】 负载均衡在线OJ系统
🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
文章目录
- 一、项目介绍
- 项目技术栈和开发环境
- 二、项目的宏观结构
- 三、compile_server模块
- ①日志模块开发,Util工具类,供所以模块使用
- ②编译功能开发(compiler模块)
- 拼接路径工具类
- 检测编译是否成功
- 编译出错
- compiler编译模块核心逻辑实现
- ③运行功能开发(runner模块)
- 资源限制(CPU占用,内存)
- runner模块核心逻辑以及实现
- ④ 编译运行模块开发(compile_run模块)
- 引入jsoncpp
- compile_run模块 明确步骤
- 差错处理
- 独特文件名的形成
- 读写文件接口
- 清理所有临时文件
- compile_run模块的核心逻辑与实现
- 设计测试用例对compile_run模块进行测试
- 引入cpp-httplib第三方网络库
- gcc升级
- compile_run打包成网络服务
- ⑤使用Postman对打包成为网络后的c_r模块进行综合测试
- 四、oj_server模块
- ①oj_server模块结构设计,MVC架构模式
- ②oj_server的功能路由
- ③ version1: 建立文件版的题库
- 文件结构
- 给用户预设的代码header
- tail.cpp测试用例部分
- ③version2 MySQL版本
- 创建用户并赋权
- 使用MySQL_WorkBench创建表结构
- 在MySQL_WorkBench当中进行录题
- 编写MySQL版本的model模块
- ④model模块
- 使用boost准标准库当中的split进行字符串分割
- 按行读取配置文件形成Question对象
- ⑤controller模块
- controller模块整体结构
- 引入ctemplate模板渲染库测试基本功能
- ⑥judge模块(负载均衡)
- 编写负载均衡器
- 离线和上线
- ⑦使用Postman进行oj_server的综合测试
- ⑧view模块整体代码结构(前端的东西,不是重点)
- index
- all_questions
- one_question
- 五、最终效果
- 六、项目结项与扩展
- 七、顶层makefile发布项目
- 项目源码
一、项目介绍
项目相关背景
在线oj系统是一种在线评测系统,用于评测程序员提交的代码。它可以模拟各种编程语言的运行环境,对程序进行编译、运行和测试,并根据测试结果给出评分和反馈。可以帮助程序员提高编程能力和解决问题的能力,同时也可以帮助教师和企业筛选优秀的人才。在线oj系统的背景可以追溯到20世纪80年代,当时已经有一些类似的系统出现,但是随着互联网的发展和计算机技术的进步,在线oj系统得到了广泛的应用和发展。目前,国内外已经有很多知名的在线oj系统,例如LeetCode、Codeforces、TopCoder等。
我们对于leetcode,newcode这些一定不陌生
我们选择在线oj系统项目的原因是因为它可以帮助我们提高编程能力和解决问题的能力,同时也可以帮助我们更好地适应工作和学习中的编程需求。在线oj系统具有丰富的题库和实时反馈功能,可以帮助我们更好地进行编程练习和测试。 此外,负载均衡在线oj系统还可以进行项目的扩展,具有方便的使用和活跃的社区互动等优势,可以提高我们的学习效果和团队协作能力。
项目技术栈和开发环境
技术栈:
- C++ STL 标准库
- Boost准标准库,主要应用split字符串切割
- cpp-httplib 第三方开源网络库
- ctemplate 第三方开源前端网页渲染库
- jsoncpp 第三方开源序列化、反序列化库
- 负载均衡设计
- 多线程、多进程
- MySQL C connect
- Ace前端在线编辑器(了解)
- html/css/js/jquery/ajax(了解)
开发环境:
- Centos 7云服务器
- vscode
- MySQL 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.hpp"namespace 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
}
解释说明:
- 其中 __FILE__和__LINE__是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_name:1234 后续我们自己拼接路径,后缀等// 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(_stderr<0){LOG(WARNING) << "没有成功形成stderr临时文件" << std::endl;exit(1);}//重定向标准错误到我们形成的标准错误临时文件,dup2(oldfd,newfd);dup2(_stderr,2);//我要把old的文件描述符放到new的文件描述符位置//g++打印错误信息到stderr当中,就会重定向到我们的标准错误临时文件// 子进程调用编译器,完成对代码的编译工作// 进程程序替换exec系列函数// g++ -o target src -std=c++11//因为我们选用的带p的进程程序替换函数,所以g++可以在环境变量中找到execlp("g++","g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-std=c++11","-D","COMPILER_ONLINE",nullptr);//未替换成功,直接终止LOG(ERROR)<<"启动编译器g++失败,可能是参数错误"<<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号文件描述符,分别对应stdin,stdout,stderr。我们为了方便我们运行的自测输入(我们这里暂时不支持),运行结果,运行错误结果等的查看与返回给用户。我们需要把这三个文件描述符进行重定向
//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.h>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);}};
另外,如果超过了资源限制,则会被信号中断,比如超出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.hpp"namespace 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 version
jsoncpp的简单使用
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::cout<<str<<std::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 -std=c++11 -ljsoncpp
compile_run模块 明确步骤
明确步骤:
- 1.把被人通过网络传给我们的json string 反序列化,取出里面规定好的内容(这是我们定制的协议,我们 规定里面需要有code代码,input自测输入(目前不支持),cpu_limit占用时间限制,mem_limit占用空间限制)
- 2.生成独特的文件名,不能和其他的起冲突,这个文件名就用来后面生成本次提供编译运行服务的临时文件。
- 3.生成一份源文件程序,把code代码放进去
- 4.正确调用compiler和runner模块的接口进行处理(编译运行)
- 5.结果发回给对端
差错处理
我们可以把其他的错误的错误码用负数表示,与信号做区分
但是每一次我们都要去构建 status,reasons,序列化,甚至还有选填字段,那么这样写下来,显而易见的就知道很臃肿。我们就看能不能想办法把这块代码统一做下处理。
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.h>namespace 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/g++
- cpp-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",[](const Request& req,Response&resp){});//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",[](const Request&req,Response&resp){resp.set_content("hello httplib,你好httplib","text/plain;charset utf-8");//第二个参数就是这个内容的content-type,我们这是纯文本,字符编码utf8
});
上面的content-type就是内容的形式,比如纯文本text/plain
,比如html 的类型text/html
,json串的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.h"using 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", [](const Request &req, Response &resp){//用户请求的服务,正文是我们想要的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;charset=utf-8");//什么内容,内容是什么格式} }); // 客户端将来采用Post的方式// 这个listen就等于是启动网络服务了std::cout<<argv[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.h"using 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](const Request &req, Response &resp) { // lambda表达式想用父作用域的变量,引用捕捉一下// 我想返回的是一张包含所有题目的html网页std::string html;ctrl.AllQuestions(&html);resp.set_content(html, "text/html;charset=utf-8"); // 测试,给个响应就行// 要从后端的model获取(和数据交互的模块去获取)}); // 获取什么资源,然后回调// 用户要根据题目编号获取题目内容,编辑代码// /questions/100 -> 正则匹配//\d+是正则表达式 \d代表匹配数字,+代表匹配一个或多个,那么这就可以把题号全部读出来// R"()",raw string 保持字符串内容的原貌,不用做相关的转义svr.Get(R"(/questions/(\d+))", [&ctrl](const Request &req, Response &resp) { // 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;charset=utf-8");});// 用户提交代码,判题(依靠compile_server功能)(1.每道题的测试用例 2.compile_and_run)svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp) { //\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;charset=utf-8");//给用户响应//resp.set_content("指定题目的判题" + number, "text/plain;charset=utf-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\100
,question就放到了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.题号 number
- 2.标题 title
- 3.难度 star
- 4.描述 desc
- 5.时间要求 cpu_limit
- 6.空间要求 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()
{vector<int> 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()
{vector<int> 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 '对应题目的最大开辟的内存空间'
) ENGINE=INNODB , CHARSET=UTF8;
注意desc,header和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占用时间限制(s)int 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, vector<Question> *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 i=0;i<rows;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(vector<Question> *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;vector<Question> 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占用时间限制(s)int mem_limit; // 空间限制(KB)std::string desc; // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例,需要和header拼接,形成完整代码再compile_server};
选择用unordered_map<string,Question>
的结构体来存储生成的Question,建立题目(字符串)与Question的映射。
使用boost准标准库当中的split进行字符串分割
class StringUtil{public:/*** str:输入性参数,要切分的字符串* target:输出型参数,保存并返回切分完毕的结果* sep:separator分隔符*/static void SplitString(const std::string &str,std::vector<std::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::vector<std::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模板当中的Key,Value就是待填入的值 - 3.然后进行html模板的获取
ctemplate::Template * tpl = cctemplate::Template::GetTemplate(html模板路径,是否保持原貌)
;保持原貌的话就传入参数ctemplate::DO_NOT_STRIP
- 4.调用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::vector<Machine> machines; // 可以提供compile_server的所有的主机std::vector<int> online; // 所有在线的主机std::vector<int> 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::vector<std::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(); // 最好搞成RAII,C++中就有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) << in_json << "\nnumber:" << number << std::endl;// 0.根据题目编号,直接拿到对应的题目细节,很简单,因为我们有和数据交互的model模块struct Question q;model_.GetOneQuestion(number, &q);// 1.对in_json反序列化 ,因为in_json当中的code只有header,不全// 而且我们还需要里面的题目的id,input数据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 (!load_balance_.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;charset=utf-8"))// 请求的服务,请求的参数,请求的content-type{// res指的是result,里面有response,和err 请求是否成功是判断res里面有没有response,err就是一堆的枚举常量// 发起了之后,服务端处理完成之后会返回给我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;// 请求完毕,减少负载。这里没有必要,因为一旦我们将主机离线,负载会清零load_balance_.OfflineMachine(id); // 根据id来进行主机的离线load_balance_.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].load=0;//负载清零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 lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ZPXD个人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>
<body><div class="container"><!-- 导航栏 ,功能不实现,但是给个导航栏--><div class="navbar"><a href="#">首页</a><!-- a标签是超链接 --><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><!-- 网页的内容 --><div class="content"><h1 class="font_">欢迎来到我的OnlineJudge平台</h1><p class="font_">这是我个人独立发开的一个oj平台</p><a class="font_" href="/all_questions">点击我开始编程了!!</a></div></div><div class="container2"><div class="footer"><h4>@猪皮兄弟</h4></div></div>
</body>
</html>
all_questions
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.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>
<body><div class="container"><!-- 导航栏 ,功能不实现,但是给个导航栏--><div class="navbar"><a href="/">首页</a><!-- a标签是超链接 --><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><div class="questions_list"><h1>OnlineJudge题目列表</h1><table><!-- <tr></tr>代表一行 TableRow<th></th>代表表头 TableHead<td></td>代表数据框,就是一行中的一个表格 TableData --><tr><th class ="item">编号</th><th class ="item">标题</th><th class ="item">难度</th></tr>{{#questions_list}}<tr><td class ="item">{{number}}</td><td class ="item"><a href="/questions/{{number}}">{{title}}</a></td><!-- 虽然访问的是/question/{{number}}这个资源 --><!-- 但是会由我们的功能路由去进行路由,最终是渲染的我们的one_question.html --><td class ="item">{{star}}</td></tr>{{/questions_list}}</table></div></div><div class="container2"><div class="footer"><h4>@猪皮兄弟</h4></div></div>
</body>
</html>
one_question
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{{number}}.{{title}}</title><!-- 1.判断回文数 --><!-- 引入ACE CDN,CDN是用来帮我们进行网络加速的,类似于云服务 --><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"charset="utf-8"></script><!-- 引入另一个CDN,语言识别 --><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"charset="utf-8"></script><!-- 引入jquery CDN --><script src="http://code.jquery.com/jquery-2.1.1.min.js"></script><style>* {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></head><body><div class="container"><!-- 导航栏 ,功能不实现,但是给个导航栏--><div class="navbar"><a href="/">首页</a><!-- a标签是超链接 --><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><!-- 左右的结构 --><div class="part1"><div class="left_desc"><h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3><!-- 三级标题 --><pre>{{desc}}</pre><!-- p是段落,放题目描述 --></div><div class="right_code"><pre id="code" class="ace_editor"><textarea class="ace_text-input">{{pre_code}}</textarea></pre></div></div><!-- 交互模块!!!! --><!-- 提交并且得到结果并显示 --><div class="part2"><!-- 结果查看,后面使用jquery进行标签的插入 --><div class="result"></div><!-- 提交按钮 --><button class="btn_submit" onclick="submit()">保存提交</button></div></div><!-- <textarea name="code" id="" cols="120" rows="30">{{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);//请求哪个资源,我们后台进行路由GET,POST//2.构建需要的json串,并向后台发起请求基于http的json请求(json串)$.ajax({method: "Post",//请求方法url: judge_url,//想请求什么资源dataType: 'json',//告知服务端我需要说明格式contentType: 'application/json;charset=utf-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中就是我们发布后给别人的文件,别人就可以直接使用
项目源码
Gitee:https://gitee.com/zhu-pi/zhupi-linux/tree/master/OnlineJudge
相关文章:

【项目设计】 负载均衡在线OJ系统
🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸 文章目录 一、项目介绍项目技术栈和开发环境 二、项目的宏观结构三、compile_server模块①日志模块开发,Util工具类,供所以模…...

【服务器】无公网IP,异地远程连接威联通NAS
Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员,2024届电子信息研究生 目录 前言 1. 威联通安装cpolar内网穿透 2. 内网穿透 2.1 创建隧道 2.2 测试公网远程访问 3. 配置固定二级子域名 3.1 保留二级子域名 3.2 配置二级子域名 4. 使用固定二级子…...

在中国,年收入20W是什么水平?答案扎心了(文末附最新招聘)
最近关于“年薪20万算什么水平?”冲上了热搜。对此,许多网友纷纷表示自己的看法,有的认为这个收入属于中高收入人群了。 因为按照最近某招聘网站发布的《中国企业招聘薪酬报告》来看,今年一季度38城企业平均招聘薪酬为10101元&…...

navicat连接oracle报错 ORA-28547
报错 原因 Navicat自带的oci.dll并不支持oracle11g 具体操作 1. 先用idea连接oracle,查看oracle版本 select * from v$version; 2. 去官网下载 Instant Client 地址: Oracle Instant Client Downloads 下载 选择对应的版本(下载时&#x…...

量化指标WR:弱的确是弱,但是老Q会魔改啊!
WR指标是一个极其简单的指标,跟我们前边讲过的KDJ有着千丝万缕的联系。原本不打算讲这个指标的,但是有粉丝一直想了解一下,那今天老Q就再专门说一下。 顺便把KDJ那篇文章就提到的魔改思路给大家实现一下——毕竟,WR这种指标,不魔改一下实在是坑人啊。 文末附魔改公式。 …...

生物信息学知识点
生物信息学知识点 1. 序列比对:1.1 基本概念:1.2 全局比对和局部比对:1.3 空位罚分的改进:1.4 同源性和相似性:1.5 相似性矩阵:1.5.1 PAM:1.5.2 BLOSUM: 2. BLAST算法:2.…...

14.贪心算法
一、算法内容 1.简介 贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择,而不考虑后续可能造成的影响。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。 贪心算法不是对所有问题都能得到整体最优…...

你知道营销人为什么要讲洞察吗?
用户洞察,是制定品牌和产品战略的基础,基于深刻的用户洞察,才能谈价值发现,目标规划,产品设计,全方位运营等。 可以这么说,没有洞察就没有营销,因为你的营销策略不能凭空想象&#…...

Neovim-配置教程
环境:Ubuntu 20.04 宿主机:windows (windows terminal)WSL2 NVIM:v0.10.0-dev 配置Neovim 需要保证流畅的github环境(以便于快速拉取插件),可以使用代理或是配置Github SSH key 本文…...

Windows管理内存的3种方式——堆、虚拟内存、共享内存
一、操作系统管理内存概述 在 Windows 操作系统中,每个进程都被分配了 4GB 的虚拟地址空间,这被称为进程的虚拟地址空间。虚拟地址空间提供了一个抽象的地址空间,使得每个进程都可以认为它拥有自己的独立内存空间。这个虚拟地址空间被分为两…...

PCM/FM解调原理与Matlab算法仿真
调制的作用是将调制信息的频谱从低频搬移到高频,以适合信道传输。关于调制的原理,在上一节中已经讲过了。在这一节中,主要讲解FM的解调原理。与调制相对应的是在接收端需要解调过程将调制信息复原,所以解调是影响通信系统性能的重要技术。 解调方法按照是否需要载波恢复的…...

我的『1024』创作纪念日
目录 ◐机缘 ◑收获 ◐日常 ◑成就 ◐憧憬 记得,2020年07月22日我撰写了第1篇技术博客:《遗传算法实例解析》在这平凡的一天,我赋予了它不平凡的意义也许是立志成为一名专业T作者、也许是记录一段刚实践的经验但在那一刻,我已…...

Python ---> 衍生的数据技术
我的个人博客主页:如果’真能转义1️⃣说1️⃣的博客主页 关于Python基本语法学习---->可以参考我的这篇博客:《我在VScode学Python》 随着人工智能技术的发展,挖掘和分析商业运用大数据已经成为一种推动应用, 推动社会发展起着…...

【27】linux进阶——rpm软件包的管理
大家好,这里是天亮之前ict,本人网络工程大三在读小学生,拥有锐捷的ie和红帽的ce认证。每天更新一个linux进阶的小知识,希望能提高自己的技术的同时,也可以帮助到大家 另外其它专栏请关注: 锐捷数通实验&…...

HTTP第六讲——键入网址再按下回车,后面究竟发生了什么?
使用 IP 地址访问 Web 服务器 首先我们运行 www 目录下的“start”批处理程序,启动本机的 OpenResty 服务器,启动后可以用“list”批处理确认服务是否正常运行。 然后我们打开 Wireshark,选择“HTTP TCP port(80)”过滤器,再鼠标…...

layui目录和项目引入
1.目录结构如下 ├─css //css目录 │ │─modules //模块css目录(一般如果模块相对较大,我们会单独提取,比如下面三个:) │ │ ├─laydate │ │ ├─layer │ │ └─layim │ └─layui.css //核心样式文件…...

Ubuntu22.04 将EFI启动分区迁移到另一块硬盘
机器上有两块硬盘, 一块已经安装了Win10, 另一块新装Ubuntu22.04, 在新硬盘上划分分区的时候, 有分出256M给 BOOT EFI, 但是安装的时候没注意, 启动分区不知道怎的跑到 Windows 所在的硬盘上了 记录一下将 /boot/efi 分区迁移至 Ubuntu 所在硬盘, 并创建 Grub 的记录. 预留的…...

只要学会这些AI工具,一个人就是一家营销咨询公司
本教程收集于:AIGC从入门到精通教程 只要学会这些AI工具,一个人就是一家营销咨询公司 随着AI工具的不断涌现,您只需掌握市面上热门的AI工具,便可独自开展营销咨询公司。通过一系列AI工具,您可以为企业提供全案服务,收获丰厚回报。 例如,在协助一家美妆初创公司出海时,…...

[离散数学] 函数
文章目录 函数判断函数的条件复合函数复合函数的性质 逆函数 函数 判断函数的条件 dom F A ⇔ \Leftrightarrow ⇔所有x 都有 F(x)与之对应 有唯一的与其对应 < x , y > ∈ f ∧ < y , z > ∈ f ⇒ y z <x,y>\in f \land <y,z…...

好家伙,又一份牛逼笔记面世了...
最近网传的一些裁员的消息,搞的人心惶惶。已经拿到大厂offer的码友来问我:大厂还能去,去了会不会被裁。 还在学习的网友来问我:现在还要冲互联网么? 我是认为大家不用恐慌吧,该看啥看啥,该学啥…...

基于nodejs+vue3 的高仿网易云音乐
大家好,我是小寻,欢迎大家关注我的公众号:工具优选,加入前端、java群聊哦! 今天给大家分享一个超高水准的项目:基于nodejsvue3研发的高仿网易云音乐,项目内容出自寻码网! 技术栈&a…...

MySQL数据库用户管理以及数据库用户授权
一、数据库用户管理 1、新建用户 CREATE USER 用户名来源地址 [IDENTIFIED BY [PASSWORD] 密码]; ---------------------------------------------------------------------------------------------------------- 用户名:指定将创建的用户名 来源地址:…...

全面分析生物技术的优缺点以及应用场景
一、 引言 生物识别技术具有不可撤销性、高度便利性和较低错误率等优势,在安全领域中也备受瞩目。然而,对于生物识别技术在应对安全挑战方面的可靠性和有效性,但争议并未被完全解决 二、生物识别技术的介绍 所谓生物识别技术就是,…...

OpenAI是什么?
OpenAI是一家人工智能技术公司,成立于2015年,总部位于美国旧金山。它的创始人包括埃隆马斯克等多名知名人士,公司的目标是推进人工智能技术的发展,同时确保人工智能的发展不会对人类造成负面影响。 OpenAI在研究和开发各种人工智能…...

量子计算——新兴领域的前沿技术
随着人类社会文明的不断进步,计算技术也在不断发展。传统计算机在过去的几十年中快速发展,计算速度、存储能力等方面发生了天翻地覆的变化。但随着大数据、人工智能、区块链等新兴领域的迅速崛起,传统计算机的发展似乎面临了瓶颈。在这样的背…...

.Net平台下OpenGL绘制图形(1)(VS2019,Winform,C#)
1、介绍 OpenGL(英语:Open Graphics Library,译名:开放图形库或者“开放式图形库”)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口(API)。这个接口由近350个不同的函数调用组成…...

Casso的创作纪念日
机缘 注册CSDN的时候才刚上大学,到现在使用CSDN已经四年了,距发布第一篇文章却只刚过去一百多天,刚看到这个提醒消息的时候只感慨时间过得真快,自己也在慢慢成长着,当初刚开始学习的时候,查资料用得最多的就…...

Bernhard‘s Talk on Towards Causal NLP 笔记
因果学习系列笔记 这是我的 GitHub 因果学习笔记仓库 https://github.com/xin007-kong/ryCausalLearning,欢迎 star🤩 讲者是 Bernhard Schlkopf talk 链接:(41) Bernhard Schoelkopf | Towards Causal NLP | KeynoteEMNLP 2021 Causal Infer…...

ES6模块化规范
在没有ES6模块化规范前,有像AMD、CMD这样的浏览器模块化规范,还有像CommonJS这样的服务端模块化规范。 2015年,JS终于推出了官方的模块化规范,为了统一各种规范,我们简称ES6 模块化。 ES6目前作为JS的内置模块化系统&a…...

红黑树下岗,内核新数据结构上场:maple tree!
在外界看来,Linux 内核的内部似乎变化很少,尤其是像内存管理子系统(memory-management subsystem)这样的子系统。然而,开发人员时常需要更换内部接口来解决某些长期存在的问题。比如,其中一个问题就是用来保…...