【c++实战项目】负载均衡式在线OJ
主页:醋溜马桶圈-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_err
dup2(_compile_err, 2);
// 子进程:调用编译器,完成对代码的编译工作
// g++ -o target src -std=c++11
execlp("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: // 6
desc = "超出内存限制";
break;
case SIGXCPU: // 24
desc = "运行超时";
break;
case SIGFPE: // 8
desc = "浮点数溢出错误";
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_json
Json::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: // 6
desc = "超出内存限制";
break;
case SIGXCPU: // 24
desc = "运行超时";
break;
case SIGFPE: // 8
desc = "浮点数溢出错误";
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_json
Json::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 string
in_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库是阻塞式多线程的 使用原生线程库pthread cpp-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-httplib
Server 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-httplib
if (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:value
return 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::split
boost::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和view oj_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 看到用户提交的源代码 input
Json::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_json
if (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_json
void 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 看到用户提交的源代码 input
Json::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_json
if (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 ./configure sudo 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.获取被渲染的html
ctemplate::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.获取被渲染的html
ctemplate::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; // 编译服务的ip
int _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; // 在线主机id
std::vector<int> _offline; // 离线主机id
std::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.轮询+hash
int 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.html
editor.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=0 2.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_server
int 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;
}
// 设置链接的编码格式 utf8
mysql_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 ../;