# load_balance_oj
**Repository Path**: yyx_dev/load_balance_oj
## Basic Information
- **Project Name**: load_balance_oj
- **Description**: 负载均衡式在线OJ
- **Primary Language**: C++
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2022-08-29
- **Last Updated**: 2025-03-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
> 项目博客地址:https://blog.csdn.net/yourfriendyo/article/details/127456708
>
> 以下是项目文档简介
# 负载均衡式在线OJ
## 1. 项目技术和开发环境
### 项目技术
- C++ STL 标准库
- Boost 库
- cpp-httplib 第三方开源网络库
- ctemplate 第三方开源前端网页渲染库
- jsoncpp 第三方开源序列化、反序列化库
- 负载均衡设计
- 多进程、多线程
- MySQL C Connect
- html /css/js/jquery/ajax
### 开发环境
- centos 7 服务器
- vim / gcc(g++) / makefile
## 2. 结构设计和实现思路
我们的项目核心是如下三个模块:
| 目录 | 介绍 |
| ---------------- | ------------------------------------------------------------ |
| `comm` | 公共模块,存放公用的代码如一些工具类 |
| `compile_server` | 编译模块,编译运行远端提交的代码 |
| `oj_server` | 服务模块,提供题目列表、题目查看、题目编写,实现反向代理负载均衡的功能 |
> 在线判题方面我们只实现类似牛客、力扣等网站的题目列表和在线编程的功能。
项目目录结构大致如下:
```shell
OnlineJudge/
├── comm
│ ├── httplib.h -> /home/yyx/depts/cpp-httplib/httplib.h
│ ├── log.hpp
│ └── util.hpp
├── compile_server
│ ├── compiler.hpp
│ ├── compile_run.hpp
│ ├── compile_server.cc
│ ├── Makefile
│ ├── runner.hpp
│ └── temp
├── Makefile
└── oj_server
├── conf
│ └── server.conf
├── Makefile
├── oj_controller.hpp
├── oj_model_file_version.hpp
├── oj_model.hpp
├── oj_server.cc
├── oj_view.hpp
├── questions_all
│ ├── 1
│ │ └── #...
│ ├── 2
│ │ └── #...
│ └── questions.conf
├── template_html
│ ├── all_questions.html
│ └── one_question.html
└── wwwroot
└── index.html
```
### 2.1 日志模块
```cpp
#pragma once
#include
#include
#include "util.hpp"
namespace NS_Log
{
using namespace NS_Util;
// log level
enum
{
INFO,
DEBUG,
WARNING,
ERROR,
FATAL
};
// LOG() << "xxxxxx";
std::ostream& Log(const std::string& level, const std::string& file_name, int line)
{
std::string message = "[" + level + "]"; // 添加日志等级
message += "[" + file_name + "]"; // 获取报错文件
message += "[" + std::to_string(line) + "]"; // 获取报错行号
message += "[" + TimeUtil::GetTimeStamp() + "]"; // 获取日志时间
std::cout << message; // 存入缓冲区,不刷新待填充报错信息
return std::cout;
}
#define LOG(level) Log(#level, __FILE__, __LINE__)
}
```
### 2.2 编译运行模块
#### 编译模块
```cpp
static bool Compile(const std::string& file_name)
{
pid_t pid = fork();
if (pid == 0) /* child */
{
int fd = open(PathUtil::Err(file_name).c_str(), O_CREAT | O_WRONLY, 644);
if (fd < 0)
//...
dup2(err_fd, 2); // 将报错信息重定向至.stderr文件中
//g++ -o 123.exe 123.cpp -std=c++11
execlp(
"g++",
"-o",
PathUtil::Exe(file_name).c_str(),
PathUtil::Src(file_name).c_str(),
"-std=c++11",
nullptr
);
}
else if (pid > 0) /* parent */
waitpid(pid, nullptr, 0);
//...
else /* pid < 0 error */
//...
}
```
#### 运行模块
运行功能由`runner.hpp`提供。
运行模块不需要考虑代码运行的结果是否正确,只关心程序是否正常退出。程序的退出信息交给上层模块处理。
其次我们要控制程序的输入输出,
- 标准输入:不作处理
- 标准输出:一般是程序运行的结果
- 标准错误:运行时错误信息
```cpp
static int Run(const std::string& file_name)
{
pid_t pid = fork();
if (pid == 0) /* child */
{
//重定向标准输出输入错误
dup2(sin_fd, 0);
dup2(sout_fd, 1);
dup2(serr_fd, 2);
SetProcLimit(cpu_limit, mem_limit); // 资源约束
// ./tmp/code.out
execl(exe_file.c_str(), exe_file.c_str());
}
else if (pid > 0) /* parent */
{
int status = 0;
waitpid(pid, &status, 0);
return status & 0x7F; // 返回程序的退出信息,具体情况交给oj模块处理
}
else /* error */
//...
}
```
##### 限制时空复杂度
通过`setrlimit()`限制程序的资源的占用大小。如CPU时间,占用空间大小等。分别通过发出`24)SIGCPU`和`6)SIGABRT`信号终止程序。
故我们可以在运行子进程之前设置资源约束,在子进程运行结束后也可以通过父进程等待的返回值,确定子进程是否是异常退出以及是否收到第几位信号。
```cpp
// 设置子进程运行占用资源的大小
static void SetProcLimit(int cpu_limit, int mem_limit)
{
struct rlimit cpu_rl;
cpu_rl.rlim_max = RLIM_INFINITY;
cpu_rl.rlim_cur = cpu_limit;
setrlimit(RLIMIT_CPU, &cpu_rl);
//...
}
```
#### 编译运行模块
`compile_run.hpp`实现编译和运行功能。
要能够适配用户需求,定制通信协议字段,正确调用编译和运行模块。
```cpp
/***************************************************************
* 输入json串:
* code : 用户提交代码
* input : 用户提交代码的标准输入内容,不作处理以待扩展
* cpu_lim : 时间复杂度
* mem_lim : 空间复杂度
* {"code": "#include...", "input": "", "cpu_lim": "1", "mem_lim": "10240"}
*
* 输出json串:
* 必填:
* status : 状态码
* reason : 结果原因
* 选填:
* stdout : 用户提交代码的运行正确结果
* stderr : 用户提交代码的运行错误结果
* {"status": "0", "reason": "", "stdout": "", "stdin": ""}
***************************************************************/
static void Start(const std::string& in_json, std::string* out_json)
{
// 反序列化
Json::Value in_value;
Json::Reader reader;
reader.parse(in_json, in_value); // 解析in_json
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_lim"].asInt();
int mem_limit = in_value["mem_lim"].asInt();
int status_code = 0;
int ret_code = 0;
std::string file_name;
// 生成源文件并写入
file_name = FileUtil::UniqueFileName();
if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
{
status_code = -2; // 文件写入错误
goto ERROR;
}
// 编译源文件
if (!Compiler::Compile(file_name))
// 运行可执行文件
ret_code = Runner::Run(file_name, cpu_limit, mem_limit);
ERROR:
//...
Json::StyledWriter writer;
*out_json = writer.write(out_value);
FileUtil::DeleteTempFile(file_name); // 清理临时文件
}
```
#### 服务模块
编译服务随时可能被多人请求,必须保证上传调源文件名唯一。下面代码仅用来测试功能是否正确。
```cpp
//客户端通过HTTP协议向编译服务上传一个json串,编译模块发送回一个json串
int main()
{
std::string in_json;
std::string out_json;
Json::Value in_value;
Json::StyledWriter writer;
in_json = writer.write(in_value);
CompileAndRun::Start(in_json, &out_json);
return 0;
}
```
我们使用`cpp-httplib`来将编译服务模块打包成网络服务,`cpp-httplib`要求`gcc/g++`版本必须高于`7`。我们使用`scl`工具集安装:
```shell
$ sudo yum install centos-release-scl scl-utils-build # 安装scl yum源
$ sudo yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++ # 安装scl gcc版本工具集
$ ls /opt/rh/ # 查看安装工具集
$ scl enable devtoolset-9 bash # 启动工具集
$ gcc -v
```
```cpp
int main()
{
Server svr;
// 提供编译运行服务
svr.Post("/compile_and_run", [](const Request& req, Response& rsp)
{
std::string in_json = req.body;
std::string out_json;
if (!in_json.empty())
{
CompileAndRun::Start(in_json, &out_json);
rsp.set_content(out_json, "application/json; charset=utf-8");
}
}
);
svr.listen("0.0.0.0", 8080); // 启动http服务
return 0;
}
```
如上代码就是我们的网络编译服务,我们采用 Posman 进行测试。
至此,我们的编译运行服务模块就完成了。
> 为什么将编译运行模块独立成一个服务呢?
编译运行服务比较耗时耗资源,危险系数比较高,独立出来便于进行分布部署在多台主机。
### 2.3 OJ服务模块
OJ服务本质就是建立一个小型的在线判题网站。我们只提供最基本的网站功能:
1. 首页获取:使用题库列表页面充当首页;
2. 代码编辑:
3. 代码提交:对应编译和运行模块。
我们采用MVC结构来设计OJ服务模块,何为MVC呢?
| 概念 | 含义 |
| ---- | ----------------------------------------------------- |
| `M` | `Model`,进行数据交互的模块,比如对题库进行增删改查。 |
| `V` | `View`,拿到数据后,进行构建和渲染网页。 |
| `C` | `Control`,控制数据交互等,就是我们的核心业务逻辑。 |
#### 服务路由模块
用户请求的服务不同,我们就要进行不同的工作,所以我们首先要实现服务路由的功能。
```cpp
// 服务路由功能
Server svr;
/* 获取题库列表 */
svr.Get("/problem_set", [](const Request& req, Response& rsp));
/* 根据题号获取题目内容 */
// /problems/10 正则表达式匹配
svr.Get(R"(/problems/(\d+))", [](const Request& req, Response& rsp));
/* 提交代码,使用编译运行服务 */
svr.Get(R"(/judge/(\d+))", [](const Request& req, Response& rsp));
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
```
#### 题目题库设计
题目具有如下属性:题目的编号,题目的标题,题目的难度,题目的题干,题目的时间空间要求,测试用例等等。
我们建立两张表,一张是题库列表,第二张表存放题目的描述、题目的预设置代码`header.cpp`和测试用例代码`tail.cpp`等等。
```cpp
// header.cpp
#include
#include
#include
#include