主页:醋溜马桶圈-CSDN博客
专栏:实战项目_醋溜马桶圈的博客-CSDN博客
gitee:mnxcc (mnxcc) - Gitee.com
项目源码
文件版:OnlineJudge_file: 负载均衡在线OJ项目基于文件版本
数据库版:mnxcc/OnlineJudge_MySQL
目录
1.项目目标
2.技术与开发环境
2.1 技术
2.2 开发环境
3.项目宏观结构
编辑
3.1 设计思路
4.compiler_server设计
4.1 compiler
4.1.1 Compile()
4.2 runner
4.2.1 Run()
4.2.2 Run超时/内存超限
1.超时退出
编辑
2.内存超限退出
编辑
3.终止进程
编辑
4.进程资源限制函数SetProcRlimit()
4.3 compile_run
4.3.1 jsoncpp
4.3.2 Start()
4.3.3 获取唯一文件名
4.3.4 将code写入.cpp文件
4.3.5 读取文件
4.3.6 测试用例
4.3.7 移除临时文件
4.3.4 形成网络服务
编辑
4.3.4.1 测试get服务
编辑
4.3.4.2 测试post请求
编辑
4.3.5 compile_server.cc
5.基于MVC结构的oj服务
5.1 oj_server.cc
5.1.1 测试
5.1.2 注意事项
5.2 文件版题目设计
编辑
5.2.1 oj_model设计
5.2.1.1 class Model
5.2.1.2 加载题目列表函数
5.2.1.3 SplitString设计-使用boost实现
5.2.2 oj_control设计
5.2.2.1 class Contril 设计
5.2.2.2 AllQuestions()设计
5.2.2.3 Question()设计
5.2.2.4 Judge()设计
5.2.3 oj_view设计
5.2.3.1 ctemplate库安装
编辑
5.2.3.2 测试ctemplate
5.2.3.3 渲染题目列表页
5.2.3.4 渲染题目描述页
5.2.4 class Machine设计
5.2.5 class LoadBalance设计
6.前端页面设计
6.1 丐版的页面-首页
6.2 所有题目列表的页面
6.3 OJ指定题目页面-代码提交
6.3.1 ACE在线编辑器
6.3.2 前后端交互jquery
6.3.2.1 通过ajax向后端发起请求
6.3.3 onequestion.html代码
7.基于数据库的版本
7.1 数据库设计
7.1.1mysql workbench创建表结构
7.1.1.1 oj_questions
7.1.2 数据库连接和设计问题
7.2 oj_model重新设计
7.2.1 引入三方库
7.2.2 oj_model代码
8.项目扩展
9.顶层makefile
1.项目目标
前端:题目列表 (自动录题) 在线OJ 报错提醒
后端:oj_server和compile_server服务器 负载均衡 数据库+文件
2.技术与开发环境
2.1 技术
- C++ STL标准库
- Boost准标准库(字符串切割)
- cpp-httplib第三方开源网络库
- ctemplate第三方开源前端网页渲染库
- jsoncpp第三方开源序列化、反序列化库
- 负载均衡设计算法
- 多进程、多线程
- MySQL C connect (数据库版本 文件版本)
- Ace前端在线编辑器
- html/css/js/jquery/ajax
2.2 开发环境
- Ubuntu 云服务器
- vscode
- MySQL Workbench
3.项目宏观结构
类似LeetCode模式:题目列表+在线编程功能
3.1 设计思路
(1) compile_server
(2) oj_server
(3) 基于文件的在线OJ
(4) 前端的页面设计
(5) 基于MySQL的在线OJ
4.compiler_server设计
编译并运行代码,得到格式化的相关结果
4.1 compiler
4.1.1 Compile()
Compiler::Compile()核心代码:编译功能// 编译成功则生成可执行文件 返回true 否则返回false (只判断编译是否成功)static bool Compile(const std::string &file_name){pid_t pid = fork();if (pid < 0){LOG(ERROR) << "内部错误,创建子进程失败" << "\n";return false;}else if (pid == 0){umask(0);int _compile_err = open(PathUtil::CompilerErr(file_name).c_str(), O_CREAT | O_WRONLY, 0644); // 以创建|读写方式打开file.stderr文件if (_compile_err < 0){LOG(WARNING) << "没有成功形成compile_err文件" << "\n";exit(1);}// 重定向标准错误到_compile_errdup2(_compile_err, 2);// 子进程:调用编译器,完成对代码的编译工作// g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr);// 程序替换并不影响进程的文件描述符表LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";exit(2);}else{// 父进程:等待子进程 等待失败直接返回false 等待成功再判断是否生成可执行程序(是否编译成功)waitpid(pid, nullptr, 0);if (FileUtil::IsFileExist(PathUtil::Exe(file_name))){LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";return true;}}LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";return false;}
4.2 runner
4.2.1 Run()
Runner::Run()核心代码:运行功能/********* 返回值>0:程序异常,返回退出信号* 返回值==0:程序运行成功,结果保存到stdout* 返回值<0:内部错误 文件错误 进程创建错误* cpu_rlimit:运行时消耗cpu资源的上限* mem_rlimit:运行时消耗内存资源的上限 以KB为单位********/static int Run(const std::string &file_name, int cpu_limit, int mem_limit){const std::string _execute = PathUtil::Exe(file_name);const std::string _stdin = PathUtil::Stdin(file_name);const std::string _stdout = PathUtil::Stdout(file_name);const std::string _stderr = PathUtil::Stderr(file_name);umask(0);int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_WRONLY, 0644);int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){LOG(ERROR) << "运行时打开标准文件失败" << "\n";return -1; // 表示文件打开或创建失败}pid_t pid = fork();if (pid < 0){LOG(ERROR) << "创建子进程失败" << "\n";// 子进程创建失败close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);return -2;}else if (pid == 0){// 子进程 运行dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2);SetProcRlimit(cpu_limit, mem_limit);execl(_execute.c_str(), _execute.c_str(), nullptr);exit(1);}else{// 父进程 等待close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status;waitpid(pid, &status, 0);LOG(INFO) << PathUtil::Exe(file_name) << " 运行完毕,退出信号:" << (status & 0x7F) << "\n";return status & 0x7F;// 通过退出信号判断运行成功与否// 成功 status==0// 失败 status!=0}
4.2.2 Run超时/内存超限
使用setrlimit函数
1.超时退出
struct rlimit rt;rt.rlim_cur=1;//软限制为1秒rt.rlim_max=RLIM_INFINITY;//硬限制为无穷setrlimit(RLIMIT_CPU,&rt);//CPU的运行时间限制while(1);
2.内存超限退出
struct rlimit rt;rt.rlim_cur = 1024 * 1024 * 40; // 软限制为40MB空间rt.rlim_max = RLIM_INFINITY; // 硬限制为无穷setrlimit(RLIMIT_AS, &rt); // 虚拟内存大小限制int count = 0;while (1){int *p = new int[1024 * 1024]; // 一次申请1MB的空间count++;std::cout << "size: " << count << std::endl;sleep(1);}
3.终止进程
资源不足,os通过信号终止进程
void handler(int signo)
{std::cout << "signo : " << signo << std::endl;exit(1);
}for (int i = 0; i < 31; i++)signal(i,handler);
// 为所有信号都注册了 handler 函数,当收到 SIGXCPU 信号时,handler 函数会被调用,输出信号编号,然后进程终止
4.进程资源限制函数SetProcRlimit()
static void SetProcRlimit(int _cpu_limit, int _mem_limit){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;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}
4.3 compile_run
核心功能:编译运行
//适配用户请求 定制通信协议字段
//正确调用compile and run
//形成唯一文件名
// 整合服务
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>namespace ns_compile_and_run
{using namespace ns_compiler;using namespace ns_runner;class CompileAndRun{public:// 移除临时文件 使用unlink系统调用static void RemoveTempFile(const std::string &file_name){// 移除.cpp文件std::string _src = PathUtil::Src(file_name);if (FileUtil::IsFileExist(_src))unlink(_src.c_str());// 移除.compile_err文件std::string _compile_err = PathUtil::CompilerErr(file_name);if (FileUtil::IsFileExist(_compile_err))unlink(_compile_err.c_str());// 移除.exe文件std::string _exe = PathUtil::Exe(file_name);if (FileUtil::IsFileExist(_exe))unlink(_exe.c_str());// 移除.stdin文件std::string _stdin = PathUtil::Stdin(file_name);if (FileUtil::IsFileExist(_stdin))unlink(_stdin.c_str());// 移除.stdout文件std::string _stdout = PathUtil::Stdout(file_name);if (FileUtil::IsFileExist(_stdout))unlink(_stdout.c_str());// 移除.stderr文件std::string _stderr = PathUtil::Stderr(file_name);if (FileUtil::IsFileExist(_stderr))unlink(_stderr.c_str());}// 将错误码转化成错误码描述/***** >0* <0* =0****/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::CompilerErr(file_name), &desc, true);break;case SIGABRT: // 6desc = "超出内存限制";break;case SIGXCPU: // 24desc = "运行超时";break;case SIGFPE: // 8desc = "浮点数溢出错误";break;default:desc = "debug:" + std::to_string(code);break;}return desc;}/******** 输入:一个json串* code:用户提交的代码* input:用户给自己提交的代码的对应的输入(扩展自测用例)* cpu_limit:时间要求* mem_limit:空间要求** 输出:一个json串* 必填:* status:状态码* reason:请求结果 成功/失败* 选填:* stdout:运行完的结果* stderr:运行完的错误结果*******/// in_json :{"code":"#include...","input":" ","cpu_limit":"1","mem_limit":"1024"};// out_json :{"status":"0","reason":"",["stdout":"","stderr":""]};static void Start(const std::string &in_json, std::string *out_json){// 将传入的in_put 反序列化得到kv值 使用jsoncpp库Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);std::string code = in_value["code"].asString();std::string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int mem_limit = in_value["mem_limit"].asInt();Json::Value out_value;std::string file_name = "";int status_code = 0;int run_result = 0;if (code.size() == 0){status_code = -1; // 代码为空goto END;}// 形成的文件名具有唯一性 没有后缀和路径file_name = FileUtil::UniqFileName();// 形成临时文件src文件 把code写道file.cpp中if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){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{status_code = 0; // 运行成功}END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);if (status_code == 0){// 运行成功std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}// 序列化 返回 out_jsonJson::StyledWriter writer;*out_json = writer.write(out_value);// 移除临时文件RemoveTempFile(file_name);}};
}
4.3.1 jsoncpp
sudo apt-get install libjsoncpp-dev
问题:序列化后的string汉字变为乱码
#include <jsoncpp/json/json.h>
int main()
{// 序列化:将结构化的数据转化成一个字符串 方便在网络中传输// Value是一个Json的中间类,可以填充KV值Json::Value root;root["code"] = "mycode";root["name"] = "dc";root["age"] = "20";// Json::StyledWriter writer;Json::FastWriter writer;std::string str = writer.write(root);std::cout << str << std::endl;return 0;
}
// 编译的时候需要链接jsoncpp库 —ljsoncpp
序列化:kv->string
反序列化:string->kv
4.3.2 Start()
CompileAndRun::Start()核心功能:整合编译运行服务
判断六种情况 序列化给 *out_json 1.code为空 -1 reason 2.code写入.cpp失败 -2 reason 3.编译失败 -3 reason 4.内部错误非运行时错误(包含于运行函数) -2 reason 5.收到信号运行失败 signal reason 5.运行成功 0 reason stdout stderr(判断运行结果)
class CompileAndRun{public:// 将错误码转化成错误码描述/***** >0* <0* =0****/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::CompilerErr(file_name), &desc, true);break;case SIGABRT: // 6desc = "超出内存限制";break;case SIGXCPU: // 24desc = "运行超时";break;case SIGFPE: // 8desc = "浮点数溢出错误";break;default:desc = "debug:" + std::to_string(code);break;}return desc;}/******** 输入:一个json串* code:用户提交的代码* input:用户给自己提交的代码的对应的输入(扩展自测用例)* cpu_limit:时间要求* mem_limit:空间要求** 输出:一个json串* 必填:* status:状态码* reason:请求结果 成功/失败* 选填:* stdout:运行完的结果* stderr:运行完的错误结果*******/// in_json :{"code":"#include...","input":" ","cpu_limit":"1","mem_limit":"1024"};// out_json :{"status":"0","reason":"",["stdout":"","stderr":""]};static void Start(const std::string &in_json, std::string *out_json){// 将传入的in_put 反序列化得到kv值 使用jsoncpp库Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);std::string code = in_value["code"].asString();std::string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int mem_limit = in_value["mem_limit"].asInt();Json::Value out_value;std::string file_name = "";int status_code = 0;int run_result = 0;if (code.size() == 0){status_code = -1; // 代码为空goto END;}// 形成的文件名具有唯一性 没有后缀和路径file_name = FileUtil::UniqFileName();// 形成临时文件src文件 把code写道file.cpp中if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){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{status_code = 0; // 运行成功}END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);if (status_code == 0){// 运行成功std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}// 序列化 返回 out_jsonJson::StyledWriter writer;*out_json = writer.write(out_value);}};
4.3.3 获取唯一文件名
通过毫秒级时间戳+原子性递增得到唯一文件名
// 毫秒级时间戳static std::string GetTimeMs(){struct timeval _time;gettimeofday(&_time, nullptr); // 时间,时区return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);}// 形成唯一文件名static std::string UniqFileName(){// 毫秒级时间戳+原子性递增static std::atomic_uint id(0);id++;std::string ms = TimeUtil::GetTimeMs();std::string uniq_id = std::to_string(id);return ms + "_" + uniq_id;}
4.3.4 将code写入.cpp文件
// 形成临时文件src文件 把code写道file.cpp中static bool WriteFile(const std::string &target, const std::string &code){std::ofstream out(target);if (!out.is_open()){return false;}out.write(code.c_str(), code.size());out.close();}
4.3.5 读取文件
// 读取目标文件内容// 输出到输出型参数content里面static bool ReadFile(const std::string &target, std::string *content, bool keep){(*content).clear();std::ifstream in(target);if (!in.is_open()){return false;}std::string line;// getline不保存行分割符,有些时候需要保留\n// getline内部重载了强制类型转换while (getline(in, line)) // 按行读取 把in里面的内容读到line{(*content) += line;(*content) += (keep ? "\n" : "");}in.close();return true;}
4.3.6 测试用例
// 基于网络请求式的服务
#include "compile_run.hpp"
#include <jsoncpp/json/json.h>
using namespace ns_compile_and_run;
int main()
{// 将编译服务打包成一个网络服务 // cpp-httplib// 通过http 让client 上传一个json string// in_json :{"code":"#include...","input":" ","cpu_limit":"1","mem_limit":"1024"};// out_json :{"status":"0","reason":"",["stdout":"","stderr":""]};std::string in_json;Json::Value in_value;//R"()" raw stringin_value["code"] = R"(#include<iostream>int main(){std::cout << "你好" << std::endl;int a=10;a/=0;return 0;})";in_value["input"] = "";in_value["cpu_limit"] = 1;in_value["mem_limit"] = 20*1024;// 序列化Json::StyledWriter writer;in_json = writer.write(in_value);// 反序列化std::cout << in_json << std::endl;std::string out_json;CompileAndRun::Start(in_json, &out_json);std::cout << out_json << std::endl;return 0;
}
4.3.7 移除临时文件
// 移除临时文件 使用unlink系统调用static void RemoveTempFile(const std::string &file_name){// 移除.cpp文件std::string _src = PathUtil::Src(file_name);if (FileUtil::IsFileExist(_src))unlink(_src.c_str());// 移除.compile_err文件std::string _compile_err = PathUtil::CompilerErr(file_name);if (FileUtil::IsFileExist(_compile_err))unlink(_compile_err.c_str());// 移除.exe文件std::string _exe = PathUtil::Exe(file_name);if (FileUtil::IsFileExist(_exe))unlink(_exe.c_str());// 移除.stdin文件std::string _stdin = PathUtil::Stdin(file_name);if (FileUtil::IsFileExist(_stdin))unlink(_stdin.c_str());// 移除.stdout文件std::string _stdout = PathUtil::Stdout(file_name);if (FileUtil::IsFileExist(_stdout))unlink(_stdout.c_str());// 移除.stderr文件std::string _stderr = PathUtil::Stderr(file_name);if (FileUtil::IsFileExist(_stderr))unlink(_stderr.c_str());}
4.3.4 形成网络服务
将编译并运行内容打包成网络服务
接入cpp—httplib库 header-only 1.到gitee搜索cpp-httplib库,选择合适的版本下载压缩包到本地 2.解压到项目文件夹下的third_part文件夹 3.项目中#include <httplib.h>就行 4.cpp-httplib库是阻塞式多线程的 使用原生线程库pthreadcpp-httplib需要使用高版本的gcc 需要升级gcc gcc-v 11.4.0 cpp-httplib-v0.16.0 git clone https://gitee.com/magicor/cpp-httplib.git
通过firewalld命令置防火墙规则
4.3.4.1 测试get服务
#include "compile_run.hpp"
#include <jsoncpp/json/json.h>
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;int main()
{// 将编译服务打包成一个网络服务// cpp-httplibServer svr;svr.Get("/hello",[](const Request&req,Response&resp){resp.set_content("hello httplib,你好 httplib","text/plain;charset=utf-8");//基本测试});// svr.set_base_dir("./wwwroot");//测试网页svr.listen("0.0.0.0",8080);//启动 http 服务return 0;
}
4.3.4.2 测试post请求
//测试的时候可以采用postman测试 //百度:postman官网下载安装
4.3.5 compile_server.cc
// 基于网络请求式的服务
#include "compile_run.hpp"
#include <jsoncpp/json/json.h>
#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;// 使用说明
}
int main(int argc, char *argv[])
{// 将编译服务打包成一个网络服务// cpp-httplibif (argc != 2){Usage(argv[0]);return 1;}Server svr;// svr.Get("/hello",[](const Request&req,Response&resp){// resp.set_content("hello httplib,你好 httplib","text/plain;charset=utf-8");// //get基本测试// });svr.Post("/compile_and_run", [](const Request &req, Response &resp){//测试通过post请求 客户端发送的req.body in_json串 Start后 把out_json写到resp中返回给用户std::string in_json=req.body;std::string out_json;if(!in_json.empty()){CompileAndRun::Start(in_json,&out_json);resp.set_content(out_json,"application/json;charset=utf-8");} });// svr.set_base_dir("./wwwroot");//测试网页// svr.listen("0.0.0.0",8080);//启动 http 服务svr.listen("0.0.0.0", atoi(argv[1])); // 启动 http 服务return 0;
}
5.基于MVC结构的oj服务
本质:建立一个小型网站
1.获取首页 以题目列表充当首页 2.编辑区域页面 3.提交判题功能(编译并运行-compile_server)M:model 通常是和数据交互的模块,比如对题库的增删改查(文件/MySQL) V:view 通常是拿到数据后,要渲染网页内容,展示给用户(浏览器) C:control 控制器,就是核心业务逻辑 MVC结构:数据 逻辑 页面 分离
5.1 oj_server.cc
5.1.1 测试
// 展示给用户 负载均衡式地调用compile_server
#include <iostream>
#include "../comm/httplib.h"using namespace httplib;int main()
{Server svr;// 1.获取题目列表svr.Get("/all_questions", [](const Request &req, Response &resp){ resp.set_content("这是所有题目的列表", "text/plain; charset=utf-8"); });// 2.用户根据题目编号,获取题目内容// R"()" 原始字符串raw string 保持字符串的原貌 不用自己做转义// /question/100 -> 正则匹配svr.Get(R"(/question/(\d+))", [](const Request &req, Response &resp){ std::string number=req.matches[1];resp.set_content("这是一个指定题目:"+number, "text/plain; charset=utf-8"); });// 3.用户提交代码,使用判题功能(1.测试用例2.compile_and_run)svr.Get(R"(/judge/(\d+))", [](const Request &req, Response &resp){ std::string number=req.matches[1];resp.set_content("指定题目的判题:"+number, "text/plain; charset=utf-8"); });svr.set_base_dir("./wwwroot");// 默认的首页svr.listen("0.0.0.0", 8080);return 0;
}
5.1.2 注意事项
5.2 文件版题目设计
1.题目编号
2.题目标题
3.题目难度
4.题目描述
5.时间要求(不暴露)
6.空间要求(不暴露)1.questions.list题目列表(题目编号 标题 难度)
2.题目的描述 编辑区域(预设值代码 header.cpp) 测试用例代码(tail.cpp)
通过题目编号产生管理关联
5.2.1 oj_model设计
5.2.1.1 class Model
Model中有一个unordered_map<>存储题目编号和题目信息的映射关系
// 操作数据
// 根据题目list文件 加载所有的题目信息到内存中
// 主要用来和数据进行交互,对外提供访问数据的接口
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <cassert>
#include <fstream>
#include <cstdlib>
#include "../comm/log.hpp"
#include "../comm/util.hpp"namespace ns_model
{// using namespace std;using namespace ns_util;using namespace ns_log;struct Question{std::string number; // 题号 唯一std::string title; // 题目的标题std::string star; // 题目的难度 简单 中等 困难int cpu_limit; // 时间要求(s)int mem_limit; // 空间要求(KB)std::string desc; // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例,需要和header拼接形成完整代码code 发送给compile_server};class Model{private:// 题号(string) : 题目细节(Question)std::unordered_map<std::string, Question> questions;public:std::string _questions_list = "./questions/questions.list";std::string _questions_path = "./questions/";Model(){assert(LoadQuestionsList(_questions_list));}bool LoadQuestionsList(const std::string &questions_list){// 加载配置文件 ./questions/question.list+题目编号文件std::ifstream in(questions_list);if (!in.is_open()){LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";return false;}std::string line;while (getline(in, line)) // 按行获取{std::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, " ");if (tokens.size() != 5){LOG(WARNING) << "部分题目加载失败,请检查文件格式" << "\n";continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.mem_limit = atoi(tokens[4].c_str());std::string _path = _questions_path;_path += q.number;_path += "/"; // 形成./questions/1/路径FileUtil::ReadFile(_path + "desc.txt", &(q.desc), true);FileUtil::ReadFile(_path + "header.cpp", &(q.header), true);FileUtil::ReadFile(_path + "tail.cpp", &(q.tail), true);questions.insert({q.number, q});}LOG(INFO) << "加载题库...成功!" << "\n";in.close();return true;}bool GetAllQuestions(std::vector<Question> *out){if (questions.empty()){LOG(ERROR) << "用户获取题库失败" << "\n";return false;}for (const auto &e : questions)out->push_back(e.second); // first:key second:valuereturn true;}// 根据题目编号拿到一个题目bool GetOneQuestion(const std::string &number, Question *q){const auto &iter = questions.find(number);if (iter == questions.end()){LOG(ERROR) << "用户获取题目失败,题目编号: " << number << "\n";return false;}(*q) = iter->second;return true;}~Model(){}};
}
5.2.1.2 加载题目列表函数
bool LoadQuestionsList(const std::string &questions_list){// 加载配置文件 ./questions/question.list+题目编号文件std::ifstream in(questions_list);if (!in.is_open()){LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";return false;}std::string line;while (getline(in, line)) // 按行获取{std::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, " ");if (tokens.size() != 5){LOG(WARNING) << "部分题目加载失败,请检查文件格式" << "\n";continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.mem_limit = atoi(tokens[4].c_str());std::string _path = _questions_path;_path += q.number;_path += "/"; // 形成./questions/1/路径FileUtil::ReadFile(_path + "desc.txt", &(q.desc), true);FileUtil::ReadFile(_path + "header.cpp", &(q.header), true);FileUtil::ReadFile(_path + "tail.cpp", &(q.tail), true);questions.insert({q.number, q});}LOG(INFO) << "加载题库...成功!" << "\n";in.close();return true;}
5.2.1.3 SplitString设计-使用boost实现
boost 测试
#include <iostream>
#include <vector>
#include <boost/algorithm/string.hpp>
int main()
{std::vector<std::string> token;const std::string str = "1 回文数 简单 1 30000";boost::split(token, str, boost::is_any_of(" "), boost::algorithm::token_compress_on);for (auto &iter : token)std::cout << iter << std::endl;return 0;
}
static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string &seq)
{//boost::splitboost::split((*target),str,boost::is_any_of(seq),boost::algorithm::token_compress_on);
}
5.2.2 oj_control设计
5.2.2.1 class Contril 设计
oj_control连通model和viewoj_control中有model对象和view对象题目列表部分 定义一个vector<Question>存放题目列表 通过model.GetAllQuestions()获取题目信息写到vector中 通过view.AllExpandHtml渲染指定题目部分 定义一个Question对象存放题目信息 通过model.GetOneQuestion()获取题目信息写道q中 通过view.OneExpandHtml渲染
// 核心业务逻辑控制器class Control{private:Model _model; // 操作数据类View _view; // 渲染网页类LoadBalance _load_balance; // 负载均衡类public:Control(){}~Control(){}bool AllQuestions(std::string *html){bool flag = true;std::vector<struct Question> all;if (_model.GetAllQuestions(&all)){// 获取所有题目信息成功,将题目数据构建成网页_view.AllExpandHtml(all, html);}else{*html = "获取题目失败,形成题目列表失败";flag = false;}return flag;}bool Question(const std::string &number, std::string *html){bool flag = true;struct Question q;if (_model.GetOneQuestion(number, &q)){// 获取指定题目信息成功,将题目数据构建成网页_view.OneExpandHtml(q, html);}else{*html = "指定题目: " + number + "不存在!";flag = false;}return flag;}void Judge(const std::string &number, const std::string in_json, std::string *out_json){// 0.根据题号,直接得到题目细节struct Question q;_model.GetOneQuestion(number, &q);// 1.in_json进行反序列化,得到题目的id 看到用户提交的源代码 inputJson::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);// 2.重新拼接用户代码+测试用例,形成新的代码Json::Value compile_value;Json::FastWriter writer;compile_value["input"] = in_value["input"];compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;std::string code = in_value["code"].asString();compile_value["code"] = code + "\n" + q.tail;std::string compile_string = writer.write(compile_value);// 3.选择负载最低的主机(差错处理)// 一直选择 直到主机可用 否则就是全部离线while (true){Machine *m = nullptr;int id = 0;if (!_load_balance.SmartChoice(&id, &m)){break;}LOG(INFO) << "选择主机主机id:" << id << "详情:" << m->_ip << ":" << m->_port << "\n";// 4.发起http请求 得到结果Client cli(m->_ip, m->_port); // 作为客户端访问指定的编译服务主机m->IncLoad();if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){// 5.将结果赋值给out_jsonif (res->status == 200) // 请求完全成功{*out_json = res->body;m->DecLoad();LOG(INFO) << "请求编译和运行服务成功" << "\n";break;}m->DecLoad();}else{// 请求失败LOG(ERROR) << "当前请求的主机id:" << id << "详情" << m->_ip << ":" << m->_port << " 离线\n";_load_balance.OfflineMachine(id);_load_balance.ShowMachines();}}}};
5.2.2.2 AllQuestions()设计
题目列表部分 定义一个vector<Question>存放题目列表 通过model.GetAllQuestions()获取题目信息写到vector中 通过view.AllExpandHtml渲染
bool AllQuestions(std::string *html){bool flag = true;std::vector<struct Question> all;if (_model.GetAllQuestions(&all)){// 获取所有题目信息成功,将题目数据构建成网页_view.AllExpandHtml(all, html);}else{*html = "获取题目失败,形成题目列表失败";flag = false;}return flag;}
5.2.2.3 Question()设计
指定题目部分 定义一个Question对象存放题目信息 通过model.GetOneQuestion()获取题目信息写道q中 通过view.OneExpandHtml渲染
bool Question(const std::string &number, std::string *html){bool flag = true;struct Question q;if (_model.GetOneQuestion(number, &q)){// 获取指定题目信息成功,将题目数据构建成网页_view.OneExpandHtml(q, html);}else{*html = "指定题目: " + number + "不存在!";flag = false;}return flag;}
5.2.2.4 Judge()设计
// 1.in_json进行反序列化,得到题目的id 看到用户提交的源代码 input
// 2.重新拼接用户代码+测试用例,形成新的代码
// 3.选择负载最低的主机,发起http请求 得到结果
// 4.将结果复制给out_jsonvoid Judge(const std::string &number, const std::string in_json, std::string *out_json){// LOG(DEBUG) << in_json << "\nnumber:" << number << "\n";// 0.根据题号,直接得到题目细节struct Question q;_model.GetOneQuestion(number, &q);// 1.in_json进行反序列化,得到题目的id 看到用户提交的源代码 inputJson::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);// 2.重新拼接用户代码+测试用例,形成新的代码Json::Value compile_value;Json::FastWriter writer;compile_value["input"] = in_value["input"];compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;std::string code = in_value["code"].asString();compile_value["code"] = code + q.tail;std::string compile_string = writer.write(compile_value);// 3.选择负载最低的主机(差错处理)// 一直选择 直到主机可用 否则就是全部离线while (true){Machine *m = nullptr;int id = 0;if (!_load_balance.SmartChoice(&id, &m)){break;}// 4.发起http请求 得到结果Client cli(m->_ip, m->_port); // 作为客户端访问指定的编译服务主机m->IncLoad();LOG(INFO) << "选择主机主机id:" << id << " 详情:" << m->_ip << ":" << m->_port << " 当前主机的负载:" << m->Load() << "\n";if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){// 5.将结果赋值给out_jsonif (res->status == 200) // 请求完全成功{*out_json = res->body;m->DecLoad();LOG(INFO) << "请求编译和运行服务成功" << "\n";break;}m->DecLoad();}else{// 请求失败LOG(ERROR) << "当前请求的主机id:" << id << " 详情" << m->_ip << ":" << m->_port << " 离线\n";_load_balance.OfflineMachine(id);_load_balance.ShowMachines();}}}
5.2.3 oj_view设计
5.2.3.1 ctemplate库安装
git clone https://gitee.com/src-oepkgs/ctemplate.git sudo apt-get install libctemplate-dev使用:./autogen.sh./configuresudo make install
5.2.3.2 测试ctemplate
#include <iostream>
#include <string>
#include <ctemplate/template.h>int main()
{std::string in_html = "./test.html";std::string value = "比特";// 形成字典数据ctemplate::TemplateDictionary root("test"); // unordered_map<> test;root.SetValue("key", value);// 获取被渲染网页对象ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP); // 保持网页原貌// 添加字典数据到网页中std::string out_html;tpl->Expand(&out_html, &root);// 完成渲染std::cout << out_html << std::endl;return 0;
}
5.2.3.3 渲染题目列表页
AllExpandHtml()
void AllExpandHtml(const std::vector<struct Question> &questions, std::string *html){// 题目编号 题目标题 题目难度// 表格// 1.形成路径std::string src_html = template_path + "all_questions.html";// 2.形成字典ctemplate::TemplateDictionary root("all_questions");for (const auto &q : questions){ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("questions_list");sub->SetValue("number", q.number);sub->SetValue("title", q.title);sub->SetValue("star", q.star);}// 3.获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4.开始完成渲染tpl->Expand(html, &root);}
5.2.3.4 渲染题目描述页
OneExpandHtml()
void OneExpandHtml(const struct Question &q, std::string *html){// 1.形成路径std::string src_html = template_path + "one_question.html";// 2.形成字典ctemplate::TemplateDictionary root("one_question");root.SetValue("number", q.number);root.SetValue("title", q.title);root.SetValue("star", q.star);root.SetValue("desc", q.desc);root.SetValue("pre_code", q.header);// 3.获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4.开始完成渲染tpl->Expand(html, &root);}
5.2.4 class Machine设计
class Machine{public: // 暴露出来不用设置set和get方法std::string _ip; // 编译服务的ipint _port; // 编译服务的端口uint64_t _load; // 编译服务的负载情况std::mutex *_mtx; // mutex是禁止拷贝的,使用指针public:Machine(): _ip(""), _port(0), _load(0), _mtx(nullptr){}~Machine(){}// 清空负载void ResetLoad(){_mtx->lock();_load = 0;_mtx->unlock();}// 增加负载void IncLoad(){if (_mtx)_mtx->lock();++_load;if (_mtx)_mtx->unlock();}// 减少负载void DecLoad(){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;}};
5.2.5 class LoadBalance设计
const std::string service_machine = "./conf/service_machine.conf";// 负载均衡模块class LoadBalance{private:std::vector<Machine> _machines; // 每一台主机都有下标 充当主机的编号std::vector<int> _online; // 在线主机idstd::vector<int> _offline; // 离线主机idstd::mutex _mtx; // 保证Load Balance的安全public:LoadBalance(){assert(LoadConf(service_machine)); // 初始化的时候就加载LOG(INFO) << "加载" << service_machine << "成功" << "\n";}~LoadBalance(){}public:bool LoadConf(const std::string &machines_conf){std::ifstream in(machines_conf);if (!in.is_open()){LOG(FATAL) << "配置文件" << machines_conf << "加载失败" << "\n";return false;}std::string line;while (getline(in, line)){std::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, ":");if (tokens.size() != 2)LOG(WARNING) << "配置文件不完整" << "\n";Machine m;m._ip = tokens[0];m._port = atoi(tokens[1].c_str());m._load = 0;m._mtx = new std::mutex();_online.push_back(_machines.size()); // 默认全部上线_machines.push_back(m);}in.close();return true;}bool SmartChoice(int *id, Machine **m){// 智能选择// 1.使用选择好的主机(更新该主机的负载)// 2.可能需要离线该主机_mtx.lock();// 负载均衡算法// 1.随机数+hash// 2.轮询+hashint online_num = _online.size();if (online_num == 0){_mtx.unlock();LOG(FATAL) << "所有的后端编译主机已离线" << "\n";return false;}// 通过遍历的方式找到负载最小的主机*id = _online[0];*m = &_machines[_online[0]];uint64_t min_load = _machines[_online[0]].Load();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];*m = &_machines[_online[i]];}}_mtx.unlock();return true;}void OnlineMachine(){// 统一上线}void OfflineMachine(int id){_mtx.lock();for (auto iter = _online.begin(); iter != _online.end(); iter++){if (*iter == id){// 找到需要离线的主机_offline.push_back(*iter);_online.erase(iter);break;}}_mtx.unlock();}// 查看所有的主机列表void ShowMachines(){_mtx.lock();std::cout << "当前在线主机列表:";for (auto &id : _online)std::cout << id << " ";std::cout << std::endl;std::cout << "当前离线主机列表:";for (auto &id : _offline)std::cout << id << " ";std::cout << std::endl;_mtx.unlock();}};
6.前端页面设计
编写页面:html+css+js
6.1 丐版的页面-首页
1.选中标签 2.设置样式
<!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>杜超的个人刷题网站</title><style>/* 起手式,100%保证我们的样式设置不受默认影响 */* {/* 消除网页的默认外边距 */margin: 0px;/* 消除网页的默认内边距 */padding: 0px;}html,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow,取消后续浮动带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签式行内块元素,允许设置宽度 */display: inline-block;/* 设置a标签的宽度 a标签默认行内元素 无法设置宽度*/width: 80px;/* 设置字体颜色 */color: white;/* 设置字体大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: gray;}.container .navbar .register {float: right;}.container .navbar .login {float: right;}.container .content {/* 设置标签的宽度 */width: 800px;/* 设置背景元素 *//* background-color: #ccc; *//* 整体居中 */margin: 0px auto;/* 设置文字居中 */text-align: center;/* 设置上外边距 */margin-top: 200px;}.container .content .font_ {/* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */display: block;/* 设置字体的样式 */margin-top: 20px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置字体大小 *//* font-size: larger; */}</style>
</head><body><div class="container"><!-- 导航栏 部分功能暂未实现--><div class="navbar"><a href="#">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a><a class="register" 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>
</body></html>
6.2 所有题目列表的页面
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>题目列表页</title><style>/* 起手式,100%保证我们的样式设置不受默认影响 */* {/* 消除网页的默认外边距 */margin: 0px;/* 消除网页的默认内边距 */padding: 0px;}html,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow,取消后续浮动带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签式行内块元素,允许设置宽度 */display: inline-block;/* 设置a标签的宽度 a标签默认行内元素 无法设置宽度*/width: 80px;/* 设置字体颜色 */color: white;/* 设置字体大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: gray;}.container .navbar .register {float: right;}.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(236, 246, 237);}.container .questions_list h1 {color: green;}.container .questions_list table .item {width: 100px;height: 40px;/* padding-top: 10px;padding-bottom: 10px; */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;/* background-color: #ccc; */line-height: 50px;color: black;margin-top: 15px;}</style>
</head><body><div class="container"><!-- 导航栏 部分功能暂未实现--><div class="navbar"><a href="#">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a><a class="register" href="#">注册</a></div><div class="questions_list"><h1>OnlineJudge题目列表</h1><table><tr><td class="item">题号</td><td class="item">题目</td><td class="item">难度</td></tr>{{#questions_list}}<tr><td class="item">{{number}}</td><td class="item"><a href="/question/{{number}}">{{title}}</a></td><td class="item">{{star}}</td></tr>{{/questions_list}}</table></div><div class="footer"><h4>@超子</h4></div></div></body></html>
6.3 OJ指定题目页面-代码提交
6.3.1 ACE在线编辑器
<!-- 引入ACE插件 --><!-- 官网链接:https://ace.c9.io/ --><!-- CDN链接:https://cdnjs.com/libraries/ace --><!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 --><!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ --><!-- 引入ACE CDN --><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"charset="utf-8"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"
6.3.2 前后端交互jquery
<!-- 引入jquery CDN --><script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
6.3.2.1 通过ajax向后端发起请求
$.ajax({method: 'Post', // 向后端发起请求的方式url: judge_url, // 向后端指定的url发起请求dataType: 'json', // 告知server,我需要什么格式contentType: 'application/json;charset=utf-8', // 告知server,我给你的是什么格式data: JSON.stringify({'code': code,'input': ''}),success: function (data) {//成功得到结果// console.log(data);show_result(data);}});
6.3.3 onequestion.html代码
<!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><!-- 引入ACE插件 --><!-- 官网链接:https://ace.c9.io/ --><!-- CDN链接:https://cdnjs.com/libraries/ace --><!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 --><!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ --><!-- 引入ACE CDN --><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"charset="utf-8"></script><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,取消后续float带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素,允许你设置宽度 */display: inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */width: 80px;/* 设置字体颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签中的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: gray;}/* 设置鼠标事件 */.container .part2 .btn-submit:hover {background-color: rgb(195, 224, 218);}.container .navbar .register {float: right;}.container .navbar .login {float: right;}.container .part1 {width: 100%;height: 600px;overflow: hidden;}.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%;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: 0px;margin-top: 10px;margin-right: 10px;}.container .part2 button:hover {color: green;}.container .part2 .result {margin-top: 15px;margin-left: 15px;font-size: large;}.container .part2 .result pre {font-size: large;}</style>
</head><body><div class="container"><!-- 导航栏, 部分功能不实现--><div class="navbar"><a href="#">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a><a class="register" href="#">注册</a></div><!-- 左右呈现,题目描述和预设代码 --><div class="part1"><div class="left_desc"><h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3><!-- 保持文本原貌 --><pre>{{desc}}</pre><!-- <textarea name="code" cols="30" rows="10">{{pre_code}}</textarea> --></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"><div class="result"><p>结果</p></div><button class="btn-submit" onclick="submit()">提交代码</button></div></div><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("嘿嘿!");// 1. 收集当前页面的有关数据, 1. 题号 2.代码var code = editor.getSession().getValue();// console.log(code);var number = $(".container .part1 .left_desc h3 #number").text();// console.log(number);var judge_url = "/judge/" + number;// console.log(judge_url);// 2. 构建json,并通过ajax向后台发起基于http的json请求$.ajax({method: 'Post', // 向后端发起请求的方式url: judge_url, // 向后端指定的url发起请求dataType: 'json', // 告知server,我需要什么格式contentType: 'application/json;charset=utf-8', // 告知server,我给你的是什么格式data: JSON.stringify({'code': code,'input': ''}),success: function (data) {//成功得到结果// console.log(data);show_result(data);}});// 3. 得到结果,解析并显示到 result中function show_result(data) {// console.log(data.status);// console.log(data.reason);// 拿到result结果标签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 {// 编译运行出错,do nothing}}}</script>
</body></html>
7.基于数据库的版本
1.在数据库中设计可以远程的登录的用户,并给他赋权oj_client 2.设计表结构 数据库 oj 表 oj_questions 3.开始编码连接访问数据库引入第三方库oj_sever是基于mvc模式的 只有oj_model和数据交互计划一张表,结束所有的需求
7.1 数据库设计
create 用户 oj_client
赋权给oj_client
远程连接 修改配置文件 连接地址
7.1.1mysql workbench创建表结构
mysql workbench通过远程连接数据库创建表结构
https://dev.mysql.com/downloads/workbench/
7.1.1.1 oj_questions
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 default charset=utf8mb4;
预设一道题目
7.1.2 数据库连接和设计问题
在项目实现过程中出现了数据库连接失败的问题 解决办法:1.ssl连接存储 禁用 vim /etc/mysql/mysql.conf.d/mysqld.conf ->添加ssl=02.MySQL8之前的版本中加密规则是`mysql_native_password`,而在MySQL8之后,加密规则是`caching_sha2_password` mysql版本为mysql Ver 8.0.40-0ubuntu0.22.04.1 for Linux on x86_64 ((Ubuntu)) 修正为mysql_native_password
7.2 oj_model重新设计
7.2.1 引入三方库
引入第三方库MySQL :: Download MySQL Connector/C (Archived Versions) 不使用mysql自带的开发包
mysql-connector-c-6.1.11-linux-glibc2.12-x86_64
如果mysql不带开发包,按照下面的操作
1.cd /etc/ld.conf.so.d/ 2.touch oj_search.conf 3.vim oj_search.conf -> /home/dc/OnlineJudge_mysql/oj_server/lib 4.sudo ldconfig 5.makefile -> -I./include -L./lib -lmysqlclient
页面乱码 需要考虑链接编码 utf8
7.2.2 oj_model代码
// 操作数据
// 根据题目list文件 加载所有的题目信息到内存中
// 主要用来和数据进行交互,对外提供访问数据的接口
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <cassert>
#include <fstream>
#include <cstdlib>#include "include/mysql.h"
#include "../comm/log.hpp"
#include "../comm/util.hpp"namespace ns_model
{// using namespace std;using namespace ns_util;using namespace ns_log;struct Question{std::string number; // 题号 唯一std::string title; // 题目的标题std::string star; // 题目的难度 简单 中等 困难std::string desc; // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例,需要和header拼接形成完整代码code 发送给compile_serverint cpu_limit; // 时间要求(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 = "041116Dc!";const std::string db = "oj";const int port = 3306;class Model{public:Model(){}bool QueryMySql(std::string &sql, std::vector<struct Question> *out){// 创建一个mysql句柄MYSQL *my = mysql_init(nullptr);// 连接数据库if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0)){LOG(FATAL) << "连接数据库失败!" << "\n";return false;}// 设置链接的编码格式 utf8mysql_set_character_set(my, "utf8");LOG(INFO) << "连接数据库成功" << "\n";// 执行sql语句if (0 != mysql_query(my, sql.c_str())){// 访问失败LOG(WARNING) << sql << "execute error!" << "\n";return false;}// 提取结果MYSQL_RES *res = mysql_store_result(my);// 分析结果// 1.获得行数int rows = mysql_num_rows(res);// 2.获得列数// int cols = mysql_num_fields(res);struct Question q;// 获取题目信息for (int i = 0; i < rows; i++){MYSQL_ROW row = mysql_fetch_row(res);q.number = row[0];q.title = row[1];q.star = row[2];q.desc = row[3];q.header = row[4];q.tail = row[5];q.cpu_limit = atoi(row[6]);q.mem_limit = atoi(row[7]);out->push_back(q);}// 释放结果空间free(res);// 关闭mysq连接mysql_close(my);return true;}bool GetAllQuestions(std::vector<Question> *out){std::string sql = "select * from ";sql += oj_questions;return QueryMySql(sql, out);}// 根据题目编号拿到一个题目bool GetOneQuestion(const std::string &number, Question *q){bool res = false;std::string sql = "select * from ";sql += oj_questions;sql += " where number=";sql += number;std::vector<struct Question> result;if (QueryMySql(sql, &result)){if (result.size() == 1){*q = result[0];res = true;}}return res;}~Model(){}};
}
8.项目扩展
项目扩展的思路
1.基于注册和登录的录题功能 2.业务扩展 论坛接入在线OJ 3.docker 可以将编译服务部署在docker上 4.目前的后端服务是使用http方式请求 是因为简单 可以设计成远程过程调用 使用reset_rpc替换httplib 5.功能完善 判题正确后跳转下一题 6.navbar中的功能
9.顶层makefile
项目的整体编译和清理
.PHONY:all
all:@cd compile_server/;\make;\cd ../;\cd oj_server/;\make;cd ../;.PHONY:clean
clean:@cd compile_server/;\make clean;\cd ../;\cd oj_server/;\make clean;\cd ../;