负载均衡的在线OJ项目
- 所用技术与开发环境
- 项目的宏观结构
- compile_server模块
- 测试Compile_server模块
- Comm模块编写
- Oj_Server模块编写
- 前端的设计
- 顶层makefile设计
- 本项目的亮点
- 项目扩展方向
-
- [1. 功能扩展](#1. 功能扩展)
- [2. 性能优化](#2. 性能优化)
- [3. 安全性增强](#3. 安全性增强)
- [4. 运维与监控](#4. 运维与监控)
本项目旨在实现一个负载均衡式的在线OJ系统。码云直达
所用技术与开发环境
所用技术:
C++ STL
标准库。Boost
标准库。cpp-httplib
开源轻量级网络库。Ctemplate
网页渲染库。jsoncpp
库对结构化数据进行序列化、反序列化。- 多进程、多线程。
MYSQL C++ connect
。- ace在线编辑器的简单使用(前端,简单使用,参考AI)。
html/css/js/jquery/ajax
(前端,简单使用,使用AI生成)。
开发环境:
ubuntu 24.04
云服务器。VScode
。mysql workbench
。
项目的宏观结构
本项目主要分为三个核心模块:
Comm
公共模块:主要包含一些头文件,这些头文件中的方法是在很多模块中都会使用的。compile_server
模块:提供编译运行的网络服务。oj_server
模块:提供获取题库、获取单个题目列表、负载均衡的选择后端编译主机等功能,后续详细介绍。
我们的项目要实现的最重要的功能:
- 主要的业务逻辑 :让用户能像
leetcode
等在线OJ平台一样查看题目列表、在线编写代码并提交代码,并获取结果的功能。
我们项目的整体结构:

项目编写思路
- 先完成
compile_server
编译运行服务的编写,编写完通过测试工具测试。 - 再编写OJ_Server模块的编写 ,完成
MYSQL
版本的OJ_Server
。 - 前端编写。
- 顶层
makefile
编写,实现一键编译项目。 - 结项与项目扩展思路。
compile_server模块
compiler模块设计与编写

compile.hpp
:
c++
#pragma once
#include<string>
#include<unistd.h>
#include <fcntl.h>
#include"../Comm/Util.hpp"
#include"../Comm/Log.hpp"
#include <sys/stat.h>
#include <sys/wait.h>
#include<iostream>
namespace Compile_ns
{
using namespace util_ns;
using namespace Log_ns;
class Compiler
{
public:
Compiler()
{}
~Compiler()
{}
static bool Compile(const std::string& filename)
{
pid_t pid = fork();
if(pid < 0)
{
//打印错误信息
LOG(ERROR) << "内部错误,进程创建失败" << "\n";
return false;
}
else if(pid == 0)
{
//子进程
//新建一个文件,用来存编译时的错误信息(如果有)
umask(0);
int _stderr = open(Path_Util::Compile_Error_Path(filename).c_str(),O_CREAT | O_WRONLY,0644);
if(_stderr < 0)
{
//文件打开失败了,无法继续执行
LOG(WARNNING) << "Compile_Error file 创建失败" << "\n";
exit(1);
}
//重定向标准错误流,让它将内容写到我们的文件中
dup2(_stderr,2);
//可以开始执行源代码了
//g++ -o target src -std-c++11
execlp("g++","g++","-o",Path_Util::Exe_Path(filename).c_str(),Path_Util::Src_Path(filename).c_str(),"-std=c++11","-D","COMPILER_ONLINE",nullptr);
//内部错误
LOG(ERROR) << "启动编译器失败,可能是参数传错了" << "\n";
exit(2);
}
else if(pid > 0)
{
//父进程
waitpid(pid,nullptr,0);//阻塞式的等待子进程编译完代码
//判断是否编译成功,就看exe文件是否生成了
if(!File_Util::Is_Exist_File(Path_Util::Exe_Path(filename)))//编译失败
{
LOG(ERROR) << "编译失败,没有形成可执行文件" << "\n";
return false;
}
}
//走到这里一定编译成功了
LOG(INFO) << "编译成功" << "\n";
return true;
}
};
};
编译服务,程序流程图:

Runner模块设计与编写
该模块主要是负责运行编译成功后的可执行程序,所以如果用户提交的代码编译失败,是不会运行这个模块的相关函数的。

Runner.hpp
:
c++
#pragma once
#include<string>
#include<fcntl.h>
#include<unistd.h>
#include"../Comm/Util.hpp"
#include"../Comm/Log.hpp"
#include<sys/wait.h>
#include <sys/resource.h>
namespace run_ns
{
using namespace Log_ns;
using namespace util_ns;
class Runner
{
private:
/* data */
public:
Runner(/* args */);
~Runner();
static void SetProcLimit(int _cpu_rlim,int _mem_rlim)//_mem_rlim的单位是kB,_cpu_rlim是s
{
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_cur = _cpu_rlim;
cpu_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU,&cpu_rlimit);
struct rlimit mem_rlimit;
mem_rlimit.rlim_cur = _mem_rlim*1024;//1KB
mem_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS,&mem_rlimit);
}
static int Run(const std::string& file_name,int cpu_rlim,int mem_rlim)
{
//1.创建错误文件、输出文件、输入文件
umask(0);
int _stderr = open(Path_Util::Stderr_Path(file_name).c_str(),O_CREAT | O_WRONLY,0644);
int _stdout = open(Path_Util::Stdout_Path(file_name).c_str(),O_CREAT | O_WRONLY,0644);
int _stdin = open(Path_Util::Stdin_Path(file_name).c_str(),O_CREAT | O_RDONLY,0644);
if(_stderr < 0 || _stdin < 0 || _stdout < 0)
{
LOG(ERROR) << "运行时打开标准文件失败" << "\n";
return -1;
}
int pid = fork();
if(pid < 0)
{
LOG(ERROR) << "创建进程失败" << "\n";
::close(_stderr);
::close(_stdin);
::close(_stdout);
return -2;
}
if(pid == 0)
{
//设置当前进程的CPU时间和内存占用上限
SetProcLimit(cpu_rlim,mem_rlim);
//重定向标准输入、错误、输出到对应的文件中
dup2(_stderr,2);
dup2(_stdout,1);
dup2(_stdin,0);
//子进程
execl(Path_Util::Exe_Path(file_name).c_str(),Path_Util::Exe_Path(file_name).c_str(),nullptr);
//走到这里说明execl执行失败了
LOG(ERROR) << "可执行程序执行失败,可能是参数传错了" << "\n";
exit(1);
}
else
{
//父进程
int status;
waitpid(pid,&status,0);
::close(_stderr);
::close(_stdin);
::close(_stdout);
LOG(INFO) << "运行完成,INFO: " << (status & 0x7f) << "\n";
return (status & 0x7f);//返回信号码
}
}
};
}
细节
-
一般的在线oj平台给每道题都会设置超时时间和内存上限值,但是可能不会展示给用户,这两个功能我们的项目中也有体现,只需要调用系统调用
setrlimit
就可以设置程序执行的最大cpu
时间和使用内存,如果一但超过这个值,OS就会给进程发送信号,终止它。 -
打开输入、输出、错误等临时文件放在主进程中,是为了给上层报告错误,如果放在子进程中,不好返回,放在主进程中,这样子进程就只会出现运行错误了,会返回信号码(一般程序替换不会执行失败,除非参数错误,这些我们不考虑,打印日志就行,不必返回给用户)。
setrlimit系统调用函数
c
int setrlimit(int resource, const struct rlimit *rlim);//设置
int getrlimit(int resource, struct rlimit *rlim);//获取
struct rlimit {
rlim_t rlim_cur; // 软限制
rlim_t rlim_max; // 硬限制
};
-
函数功能:用于设置每个进程资源限制。它允许你控制诸如最大文件大小、最大CPU时间、最大堆栈大小等资源的使用量。
-
软限制与硬限制:
rlim_max
:硬限制只有超级用户才能够设置,这是所有进程的软限制可以被设置的最大值。rlim_cur
(软限制):这是当前生效的限制值。如果当前进程尝试分配超过此值的资源,则该分配操作将失败,还会收到相关的信号,导致进程被终止或操作失败(如malloc
申请内存失败返回NULL
)。
-
函数参数:
-
int resource
:指定你要查询或修改的资源类型。- RLIMIT_CPU : 最大CPU时间。如果进程超过了这个限制,则会收到
SIGXCPU
信号。默认情况下,这会导致进程终止。可以通过设置更宽松的限制来改变这一行为,或者处理该信号以执行其他操作。 - RLIMIT_FSIZE : 文件大小限制。若进程试图超过此限制,则会产生
SIGXFSZ
信号。通常,这也意味着相关操作失败,并可能记录错误信息。 - RLIMIT_STACK , RLIMIT_AS , RLIMIT_CORE 等:对于这些限制,如果进程尝试超出其软限制,结果取决于具体的资源和操作。例如,增加栈空间失败可能导致程序崩溃;而对核心转储文件大小的限制不会直接产生信号,但会影响核心转储的行为。
RLIMIT_AS
则是对进程地址空间的大小做限制。
- RLIMIT_CPU : 最大CPU时间。如果进程超过了这个限制,则会收到
-
const struct rlimit *rlim
:输入型参数,用于添加当前进程对某个资源的软限制、硬限制。我们通常将硬限制设置为RLIM_INFINITY
,这是一个宏表示无限大。防止默认的硬限制比我们的软限制还小,影响预期结果。
-
程序流程图

Compile_and_run模块设计与编写
这个模块主要将我们之前写的编译、运行模块整合在一起,给外部提供一个简单的接口,让它可以不不关注我们这个编译运行模块的细节。
- 外部只需要给我们提供用户提交来的完整代码(
json
串的形式),最终我们这个模块帮它执行编译运行后,也给外部返回一个特定格式的json
串。
Compile_and_run.hpp
:
c
#pragma once
#include "Compile.hpp"
#include "Runner.hpp"
#include <jsoncpp/json/json.h>
namespace compile_and_run_ns
{
using namespace Compile_ns;
using namespace run_ns;
class CompileAndRun
{
public:
/**
*去除此时编译运行形成的临时文件
**/
static void RemoveTempFile(const std::string &file_name)
{
// src / exe /stderr/stdout/stdin/compile_err
std::string SrcPath = Path_Util::Src_Path(file_name);
if (File_Util::Is_Exist_File(SrcPath.c_str()))
unlink(SrcPath.c_str());
std::string ExePath = Path_Util::Exe_Path(file_name);
if (File_Util::Is_Exist_File(ExePath.c_str()))
unlink(ExePath.c_str());
std::string StderrPath = Path_Util::Stderr_Path(file_name);
if (File_Util::Is_Exist_File(StderrPath.c_str()))
unlink(StderrPath.c_str());
std::string StdoutPath = Path_Util::Stdout_Path(file_name);
if (File_Util::Is_Exist_File(StdoutPath.c_str()))
unlink(StdoutPath.c_str());
std::string StdinPath = Path_Util::Stdin_Path(file_name);
if (File_Util::Is_Exist_File(StdinPath.c_str()))
unlink(StdinPath.c_str());
std::string CompileErrPath = Path_Util::Compile_Error_Path(file_name);
if (File_Util::Is_Exist_File(CompileErrPath.c_str()))
unlink(CompileErrPath.c_str());
}
/**
* code < 0 -> 内部错误/代码为空
* code == 0 -> 代码执行无问题
* code > 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:
File_Util::ReadFile(Path_Util::Compile_Error_Path(file_name), &desc, true);
break;
case SIGABRT: // 6
desc = "内存超过范围";
break;
case SIGXCPU: // 24
desc = "CPU使用超时";
break;
case SIGFPE: // 8
desc = "浮点数溢出";
break;
default:
desc = "未知错误" + std::to_string(code);
break;
}
return desc;
}
/**
*
* 输入:
* [code]:
* [cpu_limit]
* [mem.limit]
* [input],留出来但我们不做处理
* 输出:
* 必填字段:
* [statuscode]:状态码
* [reason]:状态码对应的原因
* 选填:
* [stderr]:错误原因
* [stdout]:代码的输出结果
*/
static void Start(const std::string &json_in, std::string *json_out)
{
// 反序列化
Json::Value in_value;
Json::Reader reader;
reader.parse(json_in,in_value);
std::string code = in_value["code"].asCString();
std::string input = in_value["input"].asCString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
std::string _file_name; // 形成唯一的文件名
std::string _stderr;
std::string _stdout;
std::string reason;
Json::Value out_value;
int status_code = 0;
int run_result = 0;
if (code.size() == 0)
{
status_code = -1; // 代码为空
goto END;
}
_file_name = File_Util::UniqueFileName();
// 将code写入到code.cpp文件中
if (!File_Util::WriteFile(Path_Util::Src_Path(_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 = 0; // 运行成功了
}
else if (run_result < 0)
{
status_code = -2; // 未知错误
}
else if (run_result > 0)
{
// 运行错误
status_code = run_result;
}
END:
reason = CodeToDesc(status_code, _file_name);
if (status_code == 0)
{
// 运行没有问题
File_Util::ReadFile(Path_Util::Stdout_Path(_file_name), &_stdout, true);
File_Util::ReadFile(Path_Util::Stderr_Path(_file_name), &_stderr, true);
out_value["stdout"] = _stdout;
out_value["stderr"] = _stderr;
}
out_value["code"] = status_code;
out_value["reason"] = reason;
Json::StyledWriter Writer;
(*json_out) = Writer.write(out_value);
RemoveTempFile(_file_name);
}
};
};
程序流程图

Compile_server.cc的实现
Compile_server.cc
文件主要对Compile_and_run
模块的功能做一个调用,然后以网络通信的形式给外部提供出去。
我们采用httplib
库来快速构建网络服务,httplib
是一个轻量级的C++11 HTTP/HTTPS
库,由 Yoshito Umaoka 开发。它旨在简化在C++中进行HTTP
请求和响应处理的过程。httplib
支持同步通信,适用于需要简单而高效地与HTTP服务器交互的应用场景。
- 创建
httplib::server
对象。 - 添加一个
POST
方法到容器中,客户端访问这个路径就会直接路由到这个方法中执行相关操作。 - 调用
http::Server
对象的listen
函数, bindip
地址和port
,并开始提供网络服务(死循环监听外部连接)。
c++
#include "Compile_and_run.hpp"
#include "../Comm/httplib.h"
using namespace compile_and_run_ns;
using namespace httplib;
void Usage(char *proc)
{
std::cout << "Usage:" << proc << "port" << std::endl;
exit(1);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
}
Server svr;
svr.Post("/Compile_Run", [](const Request &req, Response &response)
{
std::string json_in = req.body;
std::string json_out;
if(!json_in.empty())
{
CompileAndRun::Start(json_in,&json_out);
response.set_content(json_out,"application/json; charset=UTF-8");
}
else
{
response.set_content("请求错误","text/html; charset=UTF-8");
}
});
svr.listen("0.0.0.0",atoi(argv[1]));
return 0;
}
测试Compile_server模块
我们采用postman
工具来进行Compile_server
模块的测试:
postman
是windows
上的一个测试工具,它可以向部署在公网上的服务发送http
请求报文(支持设置请求方法、请求体、请求头),并拿到响应报文。它是图形化界面,使用起来十分方便快捷。
-
选择
POST
方法,按照我们在Compile_server
模块精心设计好的json
字符串的格式构建(key
值一定要对的上),json
字符串中必须有以下几个k-v
:code
:你想提交给Compile_server
模块的代码。cpu_limit
:代码执行的最大cpu
时间。单位1/smem_limit
:代码可以申请的虚拟内存的上限。单位1/KBinput
:这是选填字段,内容可以为空,但必须要有,否则Compile_server
模块在进行反序列化的时候就会出错。
-
填写要发送的
json
串: -
填写服务的
url
后,一键发送得到编译运行结果:
至此我们的后端服务的Compile_server
模块编写、测试完成。
Comm模块编写
代码
Log.hpp
:提供日志服务,显示日志的行号、文件名、时间戳、日志等级、基本信息。
c++
#pragma once
#include<iostream>
#include<string>
#include"Util.hpp"
namespace Log_ns
{
using namespace util_ns;
enum{
INFO = 1,
DEBUG,
WARNNING,
ERROR,
FATAL
};
inline std::ostream& Log(const std::string& level,const std::string& filename,int line)
{
std::string message;
//添加日志水平
message += "[";
message += level;
message += "]";
//添加文件名
message += "[";
message += filename;
message += "]";
//添加文件的行号
message += "[";
message += std::to_string(line);
message += "]";
//添加时间戳信息
message += "[";
message += Time_Util::GetTimeStamp();
message += "]";
//将message加入到缓冲区中
std::cout << message;//先不要将内容从缓冲区中刷新出来
return std::cout;
}
#define LOG(level) Log(#level,__FILE__,__LINE__)
};
Util.hpp
:提供一些常用的方法,放在特定的工具类中,以静态成员函数的方式存在,让外部无需创建对象可以直接使用:
c++
#pragma once
#include <string>
#include <sys/stat.h>
#include <sys/time.h>
#include <atomic>
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>
namespace util_ns
{
const static std::string temp = "./temp/";
class Time_Util
{
public:
static std::string GetTimeStamp()
{
struct timeval tv;
gettimeofday(&tv, nullptr);
return std::to_string(tv.tv_sec);
}
static std::string GetMsTimeStamp() // 得到毫秒级的时间戳
{
struct timeval tv;
gettimeofday(&tv, nullptr); // 不关心时区
return std::to_string(tv.tv_sec * 1000 + tv.tv_usec / 1000);
}
};
class Path_Util
{
private:
static std::string Spilt_Path(const std::string &filename, const std::string &suffic)
{
return temp + filename + suffic;
}
public:
static std::string Compile_Error_Path(const std::string &filename)
{
return Spilt_Path(filename, ".CompileError");
}
static std::string Src_Path(const std::string &filename)
{
return Spilt_Path(filename, ".cpp");
}
static std::string Exe_Path(const std::string &filename)
{
return Spilt_Path(filename, ".exe");
}
static std::string Stderr_Path(const std::string &filename)
{
return Spilt_Path(filename, ".stderr");
}
static std::string Stdin_Path(const std::string &filename)
{
return Spilt_Path(filename, ".stdin");
}
static std::string Stdout_Path(const std::string &filename)
{
return Spilt_Path(filename, ".stdout");
}
};
class File_Util
{
public:
static bool Is_Exist_File(const std::string &file_name)
{
struct stat st;
if (stat(file_name.c_str(), &st) == 0)
{
// 成功获取文件属性,说明该文件是存在的
return true;
}
return false;
}
/**
* 毫秒级的时间戳+原子计数器保证文件名的唯一性
*/
static std::string UniqueFileName()
{
static std::atomic<int> id(0);
std::string res = Time_Util::GetMsTimeStamp();
res += ("_" + std::to_string(id));
id++;
return res;
}
/**
*写入文件->将内容输出进文件
*/
static bool WriteFile(const std::string &file_name, const std::string &content)
{
std::ofstream out(file_name);
if (!out.is_open())
{
return false;
}
out.write(content.c_str(), content.size());
out.close();
return true;
}
/*
*从文件中读取内容->将文件中的内容输入到用户缓冲区
*
*/
static bool ReadFile(const std::string &file_name, std::string *content, bool keep)
{
(*content).clear();
std::ifstream in(file_name);
if (!in.is_open())
{
return false;
}
std::string line;
while (std::getline(in, line))
{
(*content) += line;
(*content) += keep ? "\n" : "";
}
return true;
}
};
class Spilt_Util
{
public:
static void Split(const std::string &str, std::vector<std::string> *out, const std::string &seq)
{
// 调用boost库,并分割str,存入out
boost::split(*out, str, boost::is_any_of(seq), boost::algorithm::token_compress_on);
}
};
}
stat函数介绍
c
int stat(const char *restrict pathname,
struct stat *restrict statbuf);
- 函数功能 :得到以
pathname
为路径的文件的属性的信息。 - 函数参数 :
const char *restrict pathname
:输入型参数,指定你要查看属性的文件的路径。struct stat *restrict statbuf
:输出型参数,可以传nullptr
,如果你不关心文件的属性信息的话。
- 返回值:获取成功返回0,获取失败返回-1。
我们使用这个函数,主要用来判读某个文件是否存在,例如在编译后,调用它通过判断可执行文件是否生成来知道是否编译成功/在使用unlink
函数删除一个文件前也需要调用这个函数,保证代码的健壮性。
unlink函数介绍
c
int unlink(const char *pathname);
- 函数功能 :如果没有进程打开这个文件 && 这个文件是最后一个硬链接,这个文件的内容才会被删除,空间被系统回收。注 :这个函数不能删除目录文件,若需要可以调用
rmdir
函数。
gettimeofday与time函数介绍
这两个函数似乎都可以获取时间戳(时间戳 (timestamp)指的是自 1970年1月1日 00:00:00 UTC(称为Unix纪元或Epoch)以来经过的秒数)。
示例程序:
c++
#include<iostream>
#include<time.h>
#include <sys/time.h>
int main()
{
time_t t1 = time(nullptr);
std::cout << t1 << std::endl;
struct timeval tv;
gettimeofday(&tv,nullptr);
std::cout << "tv.sec " << tv.tv_sec << " tv.usec: " << tv.tv_usec << std::endl;
return 0;
}
运行结果:

两者的区别:
-
头文件不同 ,
time
在头文件time.h
中声明,而gettimeofday
在头文件sys/time.h
中声明。 -
精度不同 :这是两者最大的区别,
time()
只提供了秒级别的精度,而gettimeofday
提供微妙级的精度。 -
输出型参数不同 :两者都可以通过传参拿到当前时间戳,但是
gettimeofday
的是一个描述时间的结构体(包括秒、微秒),但是time
只是一个简单的time_t
的整数。cstruct timeval { time_t tv_sec; /* Seconds */ suseconds_t tv_usec; /* Microseconds */ };
-
返回值不同 :
time
的返回值也是一个time_t
的整数,表示当前时间戳,给用户提供两种方式获取时间戳,当参数为nullptr
时就可以通过返回值来获取,而gettimeofday
的返回值则是0/-1,用来标识函数是否执行成功。
boost::split函数介绍
boost
库是对标准库的补充,它有很多标准库未提供的功能,它是一个开源的库,很多标准库在经过一段时间的成熟和发展后,被标准库所采纳,如智能指针。
boost::split 函数是Boost库的一部分,具体来说,它位于 Boost.StringAlgorithms
库中。
c
template <typename SequenceSequenceT, typename RangeT, typename PredicateT>
SequenceSequenceT& split(SequenceSequenceT& Result, const RangeT& Input, PredicateT Pred, token_compress_mode_type eCompress = token_compress_off);
- 函数功能 :这个函数用于将一个字符串按照指定的分隔符分割成多个子字符串,并将这些子字符串存储在一个容器中(如 std::vector
<std::string>
)。 - 函数参数 :
SequenceSequenceT& Result
:用来存放分割后的子字符串的容器。const RangeT& Input
:要被分割的源字符串。PredicateT
:谓词(predicate),定义了如何识别分隔符。可以是一个字符或一组字符,也可以是满足特定条件的判断函数。- 可选参数,控制是否压缩连续的分隔符,默认为
token_compress_off
,即不压缩;如果设置为token_compress_on
,则会忽略连续出现的分隔符之间的空隙。
- 返回值:该函数的返回值也是返回一个容器,该容器中存储切割后的字符串。
- 头文件 :
<boost/algorithm/string.hpp>
示例函数:
c++
#include<iostream>
#include<time.h>
#include<boost/algorithm/string.hpp>
#include<vector>
int main()
{
std::string src = "每天,都要,吃饭,对吗???????????你";
std::vector<std::string> res1;
std::vector<std::string> res2;
res2 = boost::split(res1,src,boost::is_any_of("?,"));
std::cout << "res1: ";
for(auto& s:res1)
std::cout << s << " ";
std::cout << "\n";
std::cout << "res2: ";
for(auto& s:res2)
std::cout << s << " ";
std::cout << "\n";
return 0;
}
注意 :is_any_of
是boost
库中的一个函数,这个函数的作用是生成一个谓词对象,它用来检查一个字符是否属于给定的一组字符。
运行结果:

这是为压缩的结果,压缩后,想让boost:spilt
函数有压缩效果,最后一个参数需要传token_compress_on
,表示开启压缩,默认是off
:


生成唯一文件名的方法介绍(原子计数器+毫秒级的时间戳)
采用两种方式保证文件名的唯一性:
- 原子计数器 :生成一个静态的原子计数器变量,每次使用之后
++
,C++11
的atomic
类提供类似功能,它对数据做访问、修改操作都是原子的。 - 毫秒级时间戳 :获取当前时间戳,调用
gettimeofday
函数,拿到毫秒级的时间戳。
c
static std::string UniqueFileName()
{
static std::atomic<int> id(0);
std::string res = Time_Util::GetMsTimeStamp();
res += ("_" + std::to_string(id));
id++;
return res;
}
Oj_Server模块编写
Oj_Server模块的功能
- 向客户端(浏览器)提供网络服务,浏览器给客户端发送请求后,
Oj_Server
处理。 - 如果客户端请求的是获取所有题目和获取单个题目,就无需和编译服务交互,直接返回渲染后的
html
。 - 如果用户提交代码,
Oj_Server
需要负载均衡的选择编译服务的主机,让其提供编译运行的服务,最后Oj_Server
模块将Compile_Server
服务返回的结果直接返回给客户端。
基于MVC结构的Oj_Server模块的设计
我们的Oj_Server
模块主要分为以下三个小的模块:
Oj_control
模块:控制模块,里面实现直接为客户端提供服务的方法,我们Oj_Server
模块的核心业务逻辑在这个模块中,包括获取题库、获取单个题目、判题功能,未来可能还会增加登录、注册的功能。Oj_Model
模块:这个模块主要与数据打交道,例如题目的数据(题目编号、题目的难度、题目的描述、题目的给用户的代码、测试用例代码)等,我们将题目相关的信息都存在数据库中,后面可能加入与用户相关的数据,也放在这个模块中。所以这个模块就主要负责与数据进行交互。Oj_View
模块:这个模块主要和前端相关,它主要负责网页渲染 的功能,当Oj_Model
模块取出题目数据后,由这个模块将题目的数据渲染成一张html
的网页后返回给客户端,我们的网页是服务器动态生成的(利用已经准备好的模板)。
代码实现
Oj_control.hpp
c++
#pragma once
#include<iostream>
#include"../Comm/Log.hpp"
#include"../Comm/Util.hpp"
#include<string>
#include<vector>
#include"OJ_Model.hpp"
#include"Oj_View.hpp"
#include<jsoncpp/json/json.h>
#include<mutex>
#include<fstream>
#include<assert.h>
#include"../Comm/httplib.h"
#include<algorithm>
//核心控制模块
namespace control_ns
{
using namespace model_ns;
using namespace httplib;
using namespace view_ns;
using namespace util_ns;
using namespace httplib;
class HostMachine
{
public:
std::string _ip;//编译运行服务主机的ip地址
int _port;//端口号
uint64_t _load;//负载情况
std::mutex* _mutex;
public:
HostMachine():_load(0),_mutex(nullptr){}
~HostMachine(){}
public:
void IncLoad()
{
_mutex->lock();
++_load;
_mutex->unlock();
}
void DecLoad()
{
_mutex->lock();
--_load;
_mutex->unlock();
}
void ReLoad()
{
_mutex->lock();
_load = 0;
_mutex->unlock();
}
int GetLoad()
{
uint64_t load = 0;
_mutex->lock();
load = _load;
_mutex->unlock();
return load;
}
};
static const std::string hostpath = "./Config/compile_service_host.conf";
class LoadBalance
{
private:
std::vector<HostMachine> _host;//所有的主机
std::vector<int> _online;//所有在线的主机
std::vector<int> _offline;//所有离线的主机
std::mutex _mut;//锁,保护当前类中的数据安全
public:
LoadBalance()
{
assert(LoadConfig());
}
~LoadBalance()
{
}
public:
bool LoadConfig()
{
//0.导入配置文件进入内存
std::ifstream ifs(hostpath);
//1.判断是否正确打开文件
if(!ifs.is_open())
{
LOG(FATAL) << "编译服务配置打开失败,请检查路径是否设置正确..." << "\n";
return false;
}
//2.开始按行读取内容,同时配置主机
std::string line;
while(std::getline(ifs,line))
{
std::vector<std::string> res;
Spilt_Util::Split(line,&res,":");
if(res.size() != 2)
{
LOG(WARNNING) << "此行配置出错..." << "\n";
continue;
}
HostMachine machine;
machine._ip = res[0];
machine._port = atoi(res[1].c_str());
machine._mutex = new std::mutex;
//将主机添加到_host中
_online.emplace_back(_host.size());
// std::cout << "_onlinesize" << _online.size() << std::endl;
_host.emplace_back(machine);
}
LOG(INFO) << "导入主机配置成功..." << "\n";
return true;
}
//id,hm都是输出型参数
bool SmartChoice(int* id,HostMachine** hm)
{
_mut.lock();//加锁保护在线主机、离线主机列表等公共数据
int onlinenum = _online.size();
if(onlinenum == 0)
{
_mut.unlock();
LOG(FATAL) << "所有后端编译主机的已经离线,服务无法继续..." << "\n";
return false;
}
//负载均衡算法
//1.随机数+hash
//2.轮询+hash->我们选择这种
(*id) = _online[0];
(*hm) = &_host[*id];
uint64_t min_load = _host[_online[0]].GetLoad();
for(int i = 1;i < _online.size();++i)
{
uint64_t cur_load = _host[_online[i]].GetLoad();
if(cur_load < min_load)
{
min_load = cur_load;
(*id) = _online[i];
(*hm) = &_host[*id];
}
}
_mut.unlock();
return true;
}
void OfflineHost(int which)
{
_mut.lock();
for(auto iter = _online.begin();iter != _online.end();++iter)
{
if((*iter) == which)
{
_host[which].ReLoad();
_online.erase(iter);
_offline.emplace_back(which);
break;
}
}
_mut.unlock();
}
void OnlineHost()
{
_mut.lock();
//将所有的离线主机中的全加入在线主机中
_online.insert(_online.end(),_offline.begin(),_offline.end());
_offline.erase(_offline.begin(),_offline.end());
_mut.unlock();
LOG(INFO) << "所有主机都上线了" << "\n";
}
void ShowHostlist()
{
_mut.lock();
//打印在线主机列表
std::cout << "在线主机列表: ";
for(int i = 0;i < _online.size();++i)
{
std::cout << _online[i] << " ";
}
std::cout << "\n";
std::cout << "离线主机列表: ";
for(int i = 0;i < _offline.size();++i)
{
std::cout << _offline[i] << " ";
}
std::cout << "\n";
_mut.unlock();
}
};
class Control
{
private:
Model _model;
View _view;
LoadBalance _LoadBalance;
public:
Control(){}
~Control(){}
public:
void RecoveryMachine()
{
_LoadBalance.OnlineHost();
}
//返回给外层回调一个包含所有题目的html文件,要经过网页渲染,View中的功能
bool AllQuestions(std::string* html)
{
bool ret;
//将所有的题目的题目列表信息放入我们的question_v中
std::vector<Question*> question_v;
if(_model.GetAllQuestions(&question_v))
{
sort(question_v.begin(),question_v.end(),[](Question* a,Question*b)->bool{
return atoi(a->number.c_str()) < atoi(b->number.c_str());
});//升序排序
_view.AllRenderHtml(question_v,html);
ret = true;
}
else
{
*html = "获取所有题目列表失败";
ret = false;
}
return true;
}
bool OneQuestion(const std::string& number,std::string* html)
{
Question* question;
bool ret;
if(_model.GetOneQuestion(number,&question))
{
LOG(INFO) << "获取题目成功,number is " << number << "\n";
_view.OneRenderHtml(question,html);
}
else
{
LOG(INFO) << "获取题目失败,number is " << number << "\n";
ret = false;
*html = "获取题目失败,number is " + number;
}
return ret;
}
void Judge(const std::string& number,const std::string& in_json,std::string * out_json)
{
//0.根据题目编号,拿到题目细节
Question* question;
_model.GetOneQuestion(number,&question);
//1.1将用户提交的in_json,解析出来 -- 反序列化
Json::Value in_Value;
Json::Reader reader;
reader.parse(in_json,in_Value);
//1.1拿到结构化数据
std::string submit_code = in_Value["code"].asCString();//用户提交的代码
std::string input = in_Value["input"].asCString();
//2.构建给后端编译服务的请求json串
//2.1将拿到用户提交的代码和测试用例拼接在一起
std::string code = submit_code+"\n"+ question->tail;
Json::Value out_Value;
//2.2构建json数据
out_Value["code"] = code;
out_Value["input"] = input;
out_Value["cpu_limit"] = question->cpu_limit;
out_Value["mem_limit"] = question->mem_limit;
Json::FastWriter writer;
//2.3反序列化,得到发送给compile_run服务的json串
std::string compile_json = writer.write(out_Value);
//3.负载均衡式的选择主机
while(true)
{
int id = 0;
HostMachine* host;
//3.1负载均衡式的获取主机
if(!_LoadBalance.SmartChoice(&id,&host))
{
LOG(ERROR) << "主机都未上线,id is " << id << "\n";
break;
}
//走到这里,一定获取到了一个在线主机
// 开始发送http post请求
//3.2创建httplib client
Client client(host->_ip,host->_port);
host->IncLoad();
//设置client的超时时间
client.set_read_timeout(std::chrono::seconds(20));//20s超时
client.set_connection_timeout(std::chrono::seconds(20));
LOG(INFO) << "当前请求的主机,id: " << id << " 主机详情: " << host->_ip << ":" << host->_port << "\n";
//发送post请求向目标主机
auto res = client.Post("/Compile_Run",compile_json,"application/json;charset=utf-8");
if(res)
{
if(res->status == 200)//响应成功
{
host->DecLoad();
(*out_json) = res->body;
LOG(INFO) << "编译运行成功" << "\n";
break;
}
(*out_json) = res->body;
host->DecLoad();
LOG(INFO) << "请求成功" << "\n";
break;
}
else
{
LOG(ERROR) << "请求编译运行服务失败,可能已经离线当前主机id: " << id << " 主机详情: " << host->_ip << ":" << host->_port << "\n";
_LoadBalance.OfflineHost(id);
_LoadBalance.ShowHostlist();
}
}
}
};
}
Oj_view.hpp
:
c++
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include"OJ_Model.hpp"
#include<ctemplate/template.h>
namespace view_ns
{
using namespace model_ns;
using namespace ctemplate;
const static std::string comm_path = "./Template_html/";
class View//帮助我们完成网页的渲染工作
{
public:
View(){}
~View(){}
public:
void AllRenderHtml(std::vector<Question*> question_arr,std::string* html)
{
//1.创建字典
TemplateDictionary root("AllQuestionList");
//2.往字典中插入数据
for(int i = 0;i < question_arr.size();++i)
{
TemplateDictionary* sub = root.AddSectionDictionary("OneQuestionList");
sub->SetValue("number",question_arr[i]->number);
sub->SetValue("title",question_arr[i]->title);
sub->SetValue("star",question_arr[i]->level);
}
//3.加载模板html
Template* tpl = Template::GetTemplate(comm_path+"All_Question.html",DO_NOT_STRIP);
//4.渲染模板
tpl->Expand(html,&root);
for(auto& ptr:question_arr)
delete ptr;
}
void OneRenderHtml(Question* question,std::string* html)
{
LOG(INFO) << "开始渲染网页了..." << "\n";
//1.创建字典
TemplateDictionary root("OneQuestion");
//2.设置变量的值
root.SetValue("desc",question->desc);
root.SetValue("star",question->level);
root.SetValue("pre_code",question->header);
root.SetValue("number",question->number);
root.SetValue("title",question->title);
//3.加载模板文件
Template* tpl = Template::GetTemplate(comm_path+"One_Question.html",DO_NOT_STRIP);
//4.将模板文件渲染
tpl->Expand(html,&root);
delete question;
}
};
}
Oj_Model1.hpp
:
c++
#pragma once
#include <string>
#include <vector>
#include <assert.h>
#include <fstream>
#include "../Comm/Log.hpp"
#include "../Comm/Util.hpp"
#include <unordered_map>
#include "include/mysql/jdbc.h"
namespace model_ns
{
using namespace Log_ns;
using namespace util_ns;
struct Question
{
std::string number; // 题目编号
std::string title; // 题目名
std::string level; // 题目难度
int cpu_limit;
int mem_limit;
std::string desc; // 题目的内容/描述
std::string header; // 给用户的编写代码区域
std::string tail; // 测试用例,最终要与用户返回的代码拼接在一起,交给后台的编译运行服务
};
const static std::string path = "./Questions/Questions.list"; // 题目列表文件路径
const static std::string comm = "./Questions/"; // 文件的公共目录
const static int port = 3306;
const static std::string schem = "oj";//哪个数据库
const static std::string hostname = "127.0.0.1";//ip地址/url
const static std::string passward = "123456";//ip地址/url
const static std::string username = "oj_client";//ip地址/url
const static std::string tablename = "questions";
// 1.给Control提供对题库增删查改的功能
class Model
{
private:
std::unordered_map<std::string, Question *> questions;
public:
Model()
{
}
~Model() {}
public:
bool GetQuery(const std::string &sql, std::vector<Question *> *out)
{
LOG(INFO) << "开始连接数据库了... " << "\n";
// 1.连接数据库
sql::mysql::MySQL_Driver *driver;
sql::Connection *con;
sql::ConnectOptionsMap connection_detail;
sql::Statement* st;
sql::ResultSet* res;
connection_detail[OPT_PORT] = port;
connection_detail[OPT_SCHEMA] = schem;
connection_detail[OPT_HOSTNAME] = hostname;
connection_detail[OPT_PASSWORD] = passward;
connection_detail[OPT_USERNAME] = username;
connection_detail[OPT_CHARSET_NAME] = "utf8mb4";//客户端和服务器之间的所有通信(双向)。
connection_detail[OPT_RECONNECT] = true;//设置该连接的编码格式
//初始化driver
driver = sql::mysql::get_mysql_driver_instance();
if(!(con = driver->connect(connection_detail)))
{
LOG(FATAL) << "连接数据库失败,检查参数..." << "\n";
return false;
}
LOG(INFO) << "连接数据库成功..." << "\n";
st = con->createStatement();
assert(st);
res = st->executeQuery(sql);
assert(res);
while(res->next())
{
LOG(INFO) << "拿到一行数据了" << "\n";
Question* question = new Question;
question->number = res->getString("number");
question->desc = res->getString("question_desc");
question->header = res->getString("header");
question->tail = res->getString("tail");
question->level = res->getString("star");
question->mem_limit = res->getInt("mem_limit");
question->cpu_limit = res->getInt("cpu_limit");
question->title = res->getString("title");
(*out).emplace_back(question);
}
delete st;
delete con;
delete res;
return true;
}
// 读取所有题目的内容
bool GetAllQuestions(std::vector<Question *> *out)
{
std::string sql = "select * from ";
sql += tablename;
return GetQuery(sql,out);
}
// 获取一个题目的信息
bool GetOneQuestion(const std::string &number, Question **question)
{
std::string sql = "select * from ";
sql += tablename;
sql += " where number=";
sql += number;
std::vector<Question*> res;
if(GetQuery(sql,&res))
{
*question = res[0];
return true;
}
return false;
}
};
}
Oj_Server.cc
:
c++
#include <iostream>
#include <string>
#include "../Comm/httplib.h"
#include "OJ_Control.hpp"
#include<signal.h>
using namespace httplib;
using namespace control_ns;
Control* controlptr;
void HandlerSignal(int signal)
{
controlptr->RecoveryMachine();
}
int main()
{
signal(SIGQUIT,HandlerSignal);//ctrl /
Server svr;
Control control;
controlptr = &control;
// 1.可以看到首页
// 2.可以请求所有题目列表 -GET
// 3.可以看到详细的单个题目 -GET
// 4.提供判题功能 -POST
svr.Get("/All_Questions", [&control](const Request &req, Response &resp)
{
std::string html;
control.AllQuestions(&html);
resp.set_content(html,"text/html; charset=UTF-8");
});
svr.Get(R"(/Question/(\d+))", [&control](const Request &req, Response &resp)// 正则表达式要加括号,括号表示定义捕获组
{
std::string number = req.matches[1]; // 下标[1]存储的是第一个匹配的结果
std::string html;
control.OneQuestion(number, &html);
resp.set_content(html, "text/html; charset=UTF-8");
});
svr.Post(R"(/judge/(\d+))", [&control](const Request &req, Response &resp){
std::string number = req.matches[1];
std::string out_json;
std::string in_json = req.body;
if(!in_json.empty())
{
control.Judge(number,in_json,&out_json);
}
if(!out_json.empty())
resp.set_content(out_json,"application/json; charset=UTF-8");
});
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
return 0;
}
程序流程图
-
当客户端请求
url
为/All_Questions
的服务:补:调用
model
模块中的GetAllQuestions
的程序流程图:C++连接/操作
mysql
不是我们项目的重点,不做过多介绍。补:调用
oj_view
模块中的AllRenderHtml
函数流程图: -
当客户端请求
url
为Question/(\d+)
的服务: -
当客户端请求
url
为judge/(\d+)
的服务:
负载均衡功能介绍
下面是我们负载均衡相关的代码:
c
class HostMachine
{
public:
std::string _ip;//编译运行服务主机的ip地址
int _port;//端口号
uint64_t _load;//负载情况
std::mutex* _mutex;
public:
HostMachine():_load(0),_mutex(nullptr){}
~HostMachine(){}
public:
void IncLoad()
{
_mutex->lock();
++_load;
_mutex->unlock();
}
void DecLoad()
{
_mutex->lock();
--_load;
_mutex->unlock();
}
void ReLoad()
{
_mutex->lock();
_load = 0;
_mutex->unlock();
}
int GetLoad()
{
uint64_t load = 0;
_mutex->lock();
load = _load;
_mutex->unlock();
return load;
}
};
static const std::string hostpath = "./Config/compile_service_host.conf";
class LoadBalance
{
private:
std::vector<HostMachine> _host;//所有的主机
std::vector<int> _online;//所有在线的主机
std::vector<int> _offline;//所有离线的主机
std::mutex _mut;//锁,保护当前类中的数据安全
public:
LoadBalance()
{
assert(LoadConfig());
}
~LoadBalance()
{
}
public:
bool LoadConfig()
{
//0.导入配置文件进入内存
std::ifstream ifs(hostpath);
//1.判断是否正确打开文件
if(!ifs.is_open())
{
LOG(FATAL) << "编译服务配置打开失败,请检查路径是否设置正确..." << "\n";
return false;
}
//2.开始按行读取内容,同时配置主机
std::string line;
while(std::getline(ifs,line))
{
std::vector<std::string> res;
Spilt_Util::Split(line,&res,":");
if(res.size() != 2)
{
LOG(WARNNING) << "此行配置出错..." << "\n";
continue;
}
HostMachine machine;
machine._ip = res[0];
machine._port = atoi(res[1].c_str());
machine._mutex = new std::mutex;
//将主机添加到_host中
_online.emplace_back(_host.size());
// std::cout << "_onlinesize" << _online.size() << std::endl;
_host.emplace_back(machine);
}
LOG(INFO) << "导入主机配置成功..." << "\n";
return true;
}
//id,hm都是输出型参数
bool SmartChoice(int* id,HostMachine** hm)
{
_mut.lock();//加锁保护在线主机、离线主机列表等公共数据
int onlinenum = _online.size();
if(onlinenum == 0)
{
_mut.unlock();
LOG(FATAL) << "所有后端编译主机的已经离线,服务无法继续..." << "\n";
return false;
}
//负载均衡算法
//1.随机数+hash
//2.轮询+hash->我们选择这种
(*id) = _online[0];
(*hm) = &_host[*id];
uint64_t min_load = _host[_online[0]].GetLoad();
for(int i = 1;i < _online.size();++i)
{
uint64_t cur_load = _host[_online[i]].GetLoad();
if(cur_load < min_load)
{
min_load = cur_load;
(*id) = _online[i];
(*hm) = &_host[*id];
}
}
_mut.unlock();
return true;
}
void OfflineHost(int which)
{
_mut.lock();
for(auto iter = _online.begin();iter != _online.end();++iter)
{
if((*iter) == which)
{
_host[which].ReLoad();
_online.erase(iter);
_offline.emplace_back(which);
break;
}
}
_mut.unlock();
}
void OnlineHost()
{
_mut.lock();
//将所有的离线主机中的全加入在线主机中
_online.insert(_online.end(),_offline.begin(),_offline.end());
_offline.erase(_offline.begin(),_offline.end());
_mut.unlock();
LOG(INFO) << "所有主机都上线了" << "\n";
}
void ShowHostlist()
{
_mut.lock();
//打印在线主机列表
std::cout << "在线主机列表: ";
for(int i = 0;i < _online.size();++i)
{
std::cout << _online[i] << " ";
}
std::cout << "\n";
std::cout << "离线主机列表: ";
for(int i = 0;i < _offline.size();++i)
{
std::cout << _offline[i] << " ";
}
std::cout << "\n";
_mut.unlock();
}
};
负载均衡模块主要有两个类:
HostMachine类
:描述一台提供编译网络服务的主机,它有如下字段:std::string _ip
:主机部署的ip
地址。int _port
:主机服务进程对应的端口号。uint64_t _load
:主机当前的负载情况(正在给多少客户端提供编译运行服务)。std::mutex* _mutex
:锁,用于保持线程安全,httplib
内部是多线程的,可能有多个线程同时对同一主机的_load
进行操作,不加锁就会有数据不一致的情况。
LoadBalance类
:对主机进行管理,包括管理在线主机列表、离线主机列表,创建LoadBalance
对象时,同时会将配置文件加载到内存中,这个时候就会创建HostMachine
对象,使用vector
存储它,同时将下标作为id
添加进在线主机列表:std::vector<HostMachine> _host
:维护所有的主机对象。std::vector<int> _online
:维护在线主机的id(对应在_host
中的下标)。std::vector<int> _offline
:所有离线的主机的id。std::mutex _mut
:锁,保护当前类中的数据安全,当前类的_online
、_offline
可能会有多个线程对其进行add/del
,不加锁保护,就会出现数据不一致的情况。
SmartChoice
方法介绍:
c
bool SmartChoice(int *id, Machine **m)
- 函数功能:负载均衡的选择当前负载最小的主机。
- 函数参数 :
int* id
:输出型参数,获得当前选择的主机的id
。Machine **m
:输出型参数,获得当前选择主机的地址。
- 函数返回值 :
bool
值,true
表示选择成功,反之失败。
函数流程图:

因为是互斥锁,只能有一个线程同时获取,上锁之后由于其它会对online
数组的值有影响的操作都会申请这个互斥锁,所以在上述方法中加锁就可以保证一定不会出现数组越界的情况。
一键上线功能介绍
有时候我们的编译服务可能因为网络的原因离线了,在Oj_Server
服务中难道需要重启启动吗?并不需要,我们利用信号机制就可以做到一键上线所有主机 ,上线所有主机的过程就是将所有离线主机的id
都添加到在线主机列表,并清空离线主机列表中的内容:
c++
"Oj_Control.hpp":
class LoadBalance
{
public:
void OnlineHost()
{
_mut.lock();
//将所有的离线主机中的全加入在线主机中
_online.insert(_online.end(),_offline.begin(),_offline.end());
_offline.erase(_offline.begin(),_offline.end());
_mut.unlock();
LOG(INFO) << "所有主机都上线了" << "\n";
}
}
class Control
{
public:
void RecoveryMachine()
{
_LoadBalance.OnlineHost();
}
}
"Oj_Server.cc":
#include<signal.h>
using namespace control_ns;
Control* controlptr;
void HandlerSignal(int signal)
{
controlptr->RecoveryMachine();
}
int main()
{
signal(SIGQUIT,HandlerSignal);//ctrl /
...
}
测试Oj_Server模块
现在我们没有前端页面,只能借助Postman
工具进行功能的基本测试。
-
测试获取所有题目的功能:
-
测试获取单个题目的详细信息的功能:
-
测试判题功能:
这里我们是随便输入的代码肯定会报编译错误。
前端的设计
-
主页:
html<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>在线OJ平台</title> <style> /* 全局样式 */ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; color: #333; } /* 页头 */ header { background-color: #2c3e50; color: white; padding: 40px 20px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } header h1 { font-size: 36px; margin-bottom: 10px; } header p { font-size: 18px; color: #ecf0f1; } /* 导航栏 */ nav { background-color: #34495e; overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } nav a { float: left; display: block; color: white; text-align: center; padding: 14px 20px; text-decoration: none; transition: background-color 0.3s ease; } nav a:hover { background-color: #3b536b; } /* 内容区域 */ .container { padding: 30px; max-width: 800px; margin: 0 auto; background-color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; margin-top: 30px; margin-bottom: 80px; /* 避免内容被页脚遮挡 */ } .container h2 { color: #2c3e50; font-size: 28px; margin-bottom: 20px; } .container p { font-size: 16px; line-height: 1.6; color: #555; } .container ul { list-style-type: none; padding: 0; } .container ul li { background-color: #f8f9fa; margin: 10px 0; padding: 10px; border-radius: 4px; font-size: 16px; color: #333; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } /* 页脚 */ footer { background-color: #34495e; color: white; text-align: center; padding: 15px; position: fixed; bottom: 0; width: 100%; box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); } footer p { margin: 0; font-size: 14px; } </style> </head> <body> <!-- 页头 --> <header> <h1>在线OJ平台</h1> <p>欢迎来到在线编程评测平台</p> </header> <!-- 导航栏 --> <nav> <a href="/">首页</a> <a href="/All_Questions">题库</a> <a href="/">竞赛</a> <a href="/">排名</a> <a href="/">关于</a> </nav> <!-- 内容区域 --> <div class="container"> <h2>平台简介</h2> <p>在线OJ平台是一个提供编程题目和在线评测的系统,支持多种编程语言,帮助用户提升编程能力。</p> <h2>最新动态</h2> <ul> <li>新增了C++题目。</li> <li>2025年秋季编程竞赛即将开始。</li> <li>优化了评测系统的性能。</li> </ul> </div> <!-- 页脚 --> <footer> <p>© 2025 在线OJ平台. 保留所有权利.</p> </footer> </body> </html>
-
获取所有题的模板
html
: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>在线OJ平台-题目列表</title> <style> /* 起手式, 100%保证我们的样式设置可以不受默认影响 */ * { /* 消除网页的默认外边距 */ margin: 0px; /* 消除网页的默认内边距 */ padding: 0px; box-sizing: border-box; } html, body { width: 100%; height: 100%; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5; } .container .navbar { width: 100%; height: 60px; background-color: #2c3e50; /* 给父级标签设置overflow,取消后续float带来的影响 */ overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .container .navbar a { /* 设置a标签是行内块元素,允许你设置宽度 */ display: inline-block; /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */ width: 100px; /* 设置字体颜色 */ color: #ecf0f1; /* 设置字体的大小 */ font-size: 16px; /* 设置文字的高度和导航栏一样的高度 */ line-height: 60px; /* 去掉a标签的下划线 */ text-decoration: none; /* 设置a标签中的文字居中 */ text-align: center; transition: background-color 0.3s ease; } /* 设置鼠标事件 */ .container .navbar a:hover { background-color: #34495e; } .container .navbar .login { float: right; } .container .question_list { padding-top: 50px; width: 800px; height: 100%; margin: 0px auto; text-align: center; } .container .question_list table { width: 100%; font-size: 16px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin-top: 30px; background-color: white; border-collapse: collapse; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; overflow: hidden; } .container .question_list h1 { color: #2c3e50; font-size: 32px; margin-bottom: 20px; } .container .question_list table th, .container .question_list table td { padding: 15px; text-align: center; } .container .question_list table th { background-color: #34495e; color: white; font-weight: bold; } .container .question_list table tr:nth-child(even) { background-color: #f8f9fa; } .container .question_list table tr:hover { background-color: #e9ecef; } .container .question_list table .item a { text-decoration: none; color: #2c3e50; transition: color 0.3s ease; } .container .question_list table .item a:hover { color: #3498db; text-decoration: underline; } .container .footer { width: 100%; height: 60px; text-align: center; line-height: 60px; color: #7f8c8d; margin-top: 30px; background-color: #ecf0f1; box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); } .container .footer h4 { font-size: 14px; font-weight: normal; } </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> </div> <div class="question_list"> <h1>在线OJ题目列表</h1> <table> <tr> <th class="item">编号</th> <th class="item">标题</th> <th class="item">难度</th> </tr> {{#OneQuestionList}} <tr> <td class="item">{{number}}</td> <td class="item"><a href="/Question/{{number}}">{{title}}</a></td> <td class="item">{{star}}</td> </tr> {{/OneQuestionList}} </table> </div> <div class="footer"> <h4>@小镇敲码人</h4> </div> </div> </body> </html>
-
单个题目的html模板:
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插件 --> <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; box-sizing: border-box; } html, body { width: 100%; height: 100%; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5; } .container .navbar { width: 100%; height: 60px; background-color: #2c3e50; overflow: hidden; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .container .navbar a { display: inline-block; width: 100px; color: white; font-size: 16px; line-height: 60px; text-decoration: none; text-align: center; transition: background-color 0.3s ease; } .container .navbar a:hover { background-color: #34495e; } .container .navbar .login { float: right; } .container .part1 { width: 100%; height: 600px; overflow: hidden; margin-top: 20px; } .container .part1 .left_desc { width: 50%; height: 600px; float: left; overflow: scroll; background-color: white; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; margin-right: 10px; } .container .part1 .left_desc h3 { color: #2c3e50; font-size: 24px; margin-bottom: 15px; } .container .part1 .left_desc pre { font-size: 16px; line-height: 1.6; color: #555; white-space: pre-wrap; word-wrap: break-word; } .container .part1 .right_code { width: calc(50% - 10px); height: 600px; float: right; background-color: white; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; } .container .part1 .right_code .ace_editor { height: 600px; border-radius: 8px; } .container .part2 { width: 100%; overflow: hidden; margin-top: 20px; } .container .part2 .result { width: calc(100% - 140px); float: left; background-color: white; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border-radius: 8px; margin-right: 10px; } .container .part2 .result pre { font-size: 16px; color: #333; } .container .part2 .btn-submit { width: 120px; height: 50px; font-size: 16px; float: right; background-color: #26bb9c; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; } .container .part2 .btn-submit:hover { background-color: #1f9c81; } </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> </div> <!-- 左右呈现,题目描述和预设代码 --> <div class="part1"> <div class="left_desc"> <h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3> <pre>{{desc}}</pre> </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"></div> <button class="btn-submit" onclick="submit()">提交代码</button> </div> </div> <script> // 初始化ACE编辑器 var editor = ace.edit("code"); editor.setTheme("ace/theme/monokai"); editor.session.setMode("ace/mode/c_cpp"); editor.setFontSize(16); editor.getSession().setTabSize(4); editor.setReadOnly(false); // 启用提示菜单 ace.require("ace/ext/language_tools"); editor.setOptions({ enableBasicAutocompletion: true, enableSnippets: true, enableLiveAutocompletion: true }); function submit() { // 收集代码和题号 var code = editor.getSession().getValue(); var number = $("#number").text(); var judge_url = "/judge/" + number; // 发起AJAX请求 $.ajax({ method: 'POST', url: judge_url, dataType: 'json', contentType: 'application/json;charset=utf-8', data: JSON.stringify({ 'code': code, 'input': '' }), success: function (data) { show_result(data); } }); } function show_result(data) { 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); } } </script> </body> </html>
顶层makefile设计
一键编译:
makefile
.PHONY:all
all:
@cd Compile_Run;\
make;\
cd -;\
cd Oj_Server;\
make;\
cd -;
.PHONY:output
output:
@mkdir -p output/Compile_Server;\
mkdir -p output/Oj_Server;\
cp -rf Compile_Run/Compile_Server output/Compile_Server;\
cp -rf Compile_Run/temp output/Compile_Server;\
cp -rf Oj_Server/Oj_Server output/Oj_Server;\
cp -rf Oj_Server/Template_html output/Oj_Server;\
cp -rf Oj_Server/lib output/Oj_Server;\
cp -rf Oj_Server/include output/Oj_Server;\
cp -rf Oj_Server/Config output/Oj_Server;\
cp -rf Oj_Server/wwwroot output/Oj_Server;
.PHONY:clean
clean:
@cd Compile_Run;\
make clean;\
cd -;\
cd Oj_Server;\
make clean;\
cd -;\
rm -rf output;
本项目的亮点
-
技术选型与模块化设计:
-
核心技术栈:
项目采用 C++ 作为后端核心语言,结合
cpp-httplib
实现轻量级网络服务,利用Ctemplate
进行动态网页渲染,jsoncpp
处理数据序列化与反序列化,技术选型高效且轻量。
优势:高性能、低延迟,适合处理高并发编译请求。 -
模块化设计:
系统分为
Comm
(公共模块)、compile_server
(编译服务)、oj_server
(前端与负载均衡)三大模块,职责清晰。
亮点:- 通过
compile_server
独立处理编译运行,实现服务解耦。 oj_server
采用 MVC 模式(Model-View-Control),提升代码可维护性。
- 通过
-
-
负载均衡与容错机制
-
动态负载均衡 :
基于主机负载的智能选择算法,优先分配低负载节点,避免单点故障。
创新点:支持一键上线所有离线主机(通过信号机制),提升服务可用性。
-
错误隔离与恢复 :
自动检测编译服务节点故障,将其移出在线列表,保障服务稳定性。
-
-
安全与资源控制
-
代码沙箱隔离 :
使用
setrlimit
限制进程资源(CPU 时间、内存),防止恶意代码耗尽系统资源。亮点 :通过信号机制(如
SIGXCPU
)强制终止超限进程,确保系统安全。 -
临时文件管理 :
编译生成的临时文件(源码、可执行文件)在运行后自动清理,避免磁盘空间泄漏。
-
-
前端与用户体验
-
动态网页渲染:
使用
Ctemplate
模板引擎动态生成题目列表和详情页。亮点:集成 ACE 编辑器,提供代码高亮、自动补全功能,用户体验接近主流 OJ 平台。
-
异步判题反馈 :
前端通过
AJAX
异步提交代码并实时显示编译结果,减少页面刷新。
-
项目扩展方向
1. 功能扩展
- 多语言支持 :
扩展compile_server
,支持 Python、Java 等语言的编译/解释运行,需适配不同语言的沙箱配置。 - 用户系统与竞赛功能 :
- 增加用户注册/登录模块,集成 JWT 鉴权。
- 支持创建编程竞赛,实时排名榜(基于 Redis 缓存提升性能)。
- 题目管理与测试用例自动化 :
- 开发管理员后台,支持题目上传、测试用例批量导入。
- 实现自动化测试框架,验证题目正确性。
2. 性能优化
- 分布式编译集群 :
将compile_server
部署为多节点集群,结合ZooKeeper
实现服务发现与动态扩缩容。 - 结果缓存与异步队列 :
- 使用 Redis 缓存高频题目的编译结果,减少重复计算。
- 引入 RabbitMQ 异步处理判题请求,削峰填谷。
3. 安全性增强
- 代码沙箱强化 :
- 使用 Docker 或 Linux Namespace 实现进程级隔离,彻底防止恶意代码逃逸。
- 静态代码分析(如 Clang 静态检查器),拦截危险系统调用(如
fork
)。
- 流量控制与防刷 :
集成 Nginx 限流模块,防止高频提交攻击。
4. 运维与监控
- Prometheus + Grafana 监控 :
采集编译节点负载、请求耗时等指标,实现可视化监控与告警。 - 日志聚合 :
使用 ELK(Elasticsearch + Logstash + Kibana)集中管理日志,便于故障排查。