项目主要内容:1.在线OJ;2.负载均衡
三大模块:
项目实现:该项目采用通过负载均衡算法(轮询检测)可以使得多个服务器协同处理大量的提交请求和编译请求从而提高用户的体验和服务的稳定性。项目总体为三部分:
1. comm: 公共模块,包含一些通用方。
2. CompileServer: 编译与运行模块,为 oj_server提供编译运行
3. OjServer: 基于MVC结构的OJ服务设计,获取题目列表,查看题目编写题目界面,负载均衡(智能选择)
最终形成两个可执行程序:1.编译服务器;2.在线OJ服务器;两者通过网络套接字通信。这将编译模块部署在服务器后端的多台机器上。OJ_server部署一台。OJ_server会负载均衡地去选择后端的编译服务,让我们能够以集群处理能力的方式去对外输出我们的在线OJ服务。
所用技术与开发环境
所用技术:
C++ STL 标准库 Boost 准标准库(字符串切割)
cpp-httplib第三方开源网络库
ctemplate 第三方开源前端网页渲染库
jsoncpp 第三方开源序列化、反序列化库
负载均衡设计 多进程
Ace前端在线编辑器(了解)
html/css/js/jquery/ajax(了解)
开发环境:
Centos 7云服务器 /vscode
项目简介
后端
当前端有人提交之后
前端界面
我们的项目首页
本项目的主体架构为
|-- Comm
| |-- httplib.h
| |-- log.hpp
| `-- util.hpp
|-- CompileServer
| |-- Compile.hpp
| |-- CompileRun.hpp
| |-- CompileServer
| |-- CompileServer.cc
| |-- Makefile
| |-- Runner.hpp
| `-- temp
|-- OjServer
| |-- conf
| | `-- ServerMachine.conf
| |-- Makefile
| |-- OjServer
| |-- OjServer.cc
| |-- OjServer_control.hpp
| |-- OjServer_model.hpp
| |-- OjServer_view.hpp
| |-- Questions
| | |-- 1
| | | |-- desc.txt
| | | |-- header.hpp
| | | `-- tail.hpp
| | |-- 2
| | | |-- desc.txt
| | | |-- header.hpp
| | | `-- tail.hpp
| | `-- questions.list
| |-- TemplateHtml
| | |-- AllQuestions.html
| | `-- AppiontQuestion.html
| `-- wwwroot
| `-- index.html
|-- README.md
Comm文件中
- util.hpp中封装了我们常用的一些方法,诸如字符串分割,生成唯一文件名,判断文件是否存在...
- log.hpp中提供了日志打印服务,打印的信息包含的有:时间,所在文件名,所在文件行,以及我们可以自定义的传入一些数据。
- 提供一些网络服务。
CompileServer文件中
- Compile.hpp中主要完成的是编译模块
- Runner.hpp中主要完成的是运行模块
- CompileRun.hpp中主要是将上述的两个模块的接口集中起来,以便统一调用。
- temp文件夹:由于我们在进行OJ题目的判定时,会出现以下几种情况:
- 当我们的代码提交上来之后,我们会首先生成一个后缀为.cpp的源文件,然后经过编译生成一个.exe的文件。我们的代码编译运行不成功,这时我们会生成一个.compileerr的后缀的文件
- 我们的代码编译运行成功但是又出现了问题,会产生3个衍生文件。标准输出文件后缀为.stdout,标准输入文件后缀为.stdin,标准错误文件后缀为.stderr。
当然由于我们系统的需要,我们这些文件必定是临时的,当我们本次代码的相关操作完成之后,我们就需要清理掉相关的文件,这些方法在我们的Comm文件中util提供了相关的方法。
在 OjServer中
- conf文件中,放置的是我们的编译服务器的相关信息,其实就是编译服务器的ip以及端口号。
- 在OjServer这个目录下我们完成项目前中后的中端核心模块oj_server模块,该模块我们设计的文件有oj_server.cc,这个文件引入网络模块,给用户提供首页、给用户提供题目列表、给用户提供某道题的做题界面和把用户提交的json串发送给后端compile模块进行编译运行,返回结果给用户,还有model、view和control,就是对应的MVC。
- model实现跟数据进行交换,就是把文件中的题目数据或数据库中的题目加载到内存管理起来,使用文件版model我们就要在该模块目录下创建一个子目录questions用来存储所有题目的数据,在questions目录下为每一道题都创建一个目录,题目编号就是目录名称,在该目录下就是有三个文件分别是题目的描述desc.txt、题目的头文件就是渲染到用户编写界面的代码接口,题目的尾文件tail.cpp就是包含我们的main和所有的测试用例。
- desc.txt
- header.hpp
- tail.hpp
我们代码编译的时候将我们的head和tail进行整合形成我们待编译的.cpp文件。
- view就是拿到数据后进行网页构建,数据渲染的,我们在oj_server目录下创建一个子目录template_html,该子目录下创建一个questionlist.html用来构建题目列表,设置好结构用到ctemplate第三方开源渲染库,把题目编号、名称和难度都渲染到questionlist.html中最终效果就成了用户看到的题目列表。同样的构建用户选定题目后的编写界面question.html 我们也把特定题号的题目数据渲染到该界面,形成用户看到的编辑界面。
- control模块就是oj_server端的核心模块,引入负载均衡模块,把编译服务启动多个,在不同的机器上运行,oj_server端请求编译端的编译服务,会选择当前在运行的编译机器中负载最轻的机器发起http请求。同时在该文件中我们调用model中的方法加载所有题目数据,调用view中的方法,根据用户请求构建网页,还有实现判断功能,有oj_server.cc来调用。
- TemplateHtml这里的是我们待渲染的模板,AllQuestions.html,此为我们的题目列表,AppiontQuestion.html为我们的单个题目页面
由于我们采用了负载均衡设计,所以注定了OJ服务会被大量访问,对于核心代码:智能选择,就是选择当前负载最少的机器进行后续操作。负载均衡算法采用轮询检测。代码如下
// id: 输出型参数
// m : 输出型参数
bool SmartChoice(int *id, ServerMachine **m)
{
_mtx.lock();
int online_num = _online.size();
if(online_num == 0)
{
LOG(FATAL) << "后端编译主机全部离线,请注意!!!!"<<"\n";
_mtx.unlock();
return false;
}
//寻找负载最小主机
*id = _online[0];
uint64_t min_load = _machines[_online[0]]._load;
(*m) = &_machines[_online[0]];
for(int i = 1; i < online_num; ++i)
{
uint64_t curr_load = _machines[_online[i]]._load;
if(min_load > curr_load)
{
min_load = _machines[_online[i]]._load;
(*id) = _online[i];
(*m) = &_machines[_online[i]];
}
}
_mtx.unlock();
return true;
}
其次面对大量的编译运行请求时,我们的后端不可避免会产生大量的文件,那么如何避免产生重复的文件名,我们采用 通过毫秒级别的时间戳和一个自增的id(原子的)来生成唯一文件名
static std::string UniqueFileName()
{
static std::atomic<int> id(0);
id++;
std::string message = TimeUtil::GetTimemillisecond();
return message+"_"+ std::to_string(id);
}
我们的日志hpp
#pragma once
#include <iostream>
#include <string>
#include "util.hpp"
namespace ns_log
{
using namespace ns_util;
enum
{
INFO,
DEBUG,
WARING,
ERROR,
FATAL
};
inline std::ostream &Log(const std::string& level, const std::string& file_name, const int line)
{
//日志文件
std::string message;
message += "[";
message += level;
message += "]";
//文件名
message += "[";
message += file_name;
message += "]";
//行数
message += "[";
message += std::to_string(line);
message += "]";
//时间
message += "[";
message += std::to_string(TimeUtil::GetTimeStamp());
message += "]";
//先将信息缓存在cout流中
std::cout << message;
return std::cout;
}
#define LOG(level) Log(#level, __FILE__, __LINE__)
}
我们的conf中的配置文件