> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:能自己实现负载均衡式在线 OJ。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:实战项目_დ旧言~的博客-CSDN博客
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
一、整体框架
总结:
先运行 oj_server,用来提供前端页面(前端页面向后端发送http请求),后端运行多个 compile_server 编译主机,oj_server 接收到请求后把请求再转发给 compile_server,compile_server经过编译运行后把结果响应回去,最终反映到前端页面上。
本项目四个核心模块构成:
- 公共模块:存放各个模块都能用到的公共文件。
- 编译与运行模块(可视为服务端):适配用户请求,负责完成基于网络请求式的编译并运行服务。
- OJ相关模块(可视为客户端):完成连接数据库、获取题目列表,查看题目、编写题目界面,负载均衡等后端核心业务逻辑。
- 项目发布模块:只包含整合后的二进制可执行编译运行文件与OJ系统网页文件,专供用户使用。
二、所用技术和开发环境
- C++ STL:标准库.
- Boost:准标准库(字符串切割).
- cpp-httplib:第三⽅开源⽹络库.
- ctemplate:第三⽅开源前端网页渲染库.
- jsoncpp:第三⽅开源序列化、反序列化库.
- 负载均衡设计.
- 多进程、多线程.
- MySQL C connect.
- Ace前端在线编辑器(了解).
- html/css/js/jquery/ajax (了解).
三、编写思路
- 先编写compile_server.
- 再编写oj_server.
- 基于文件版的负载均衡式在线 Oj.
- 前端网页设计.
- 基于MySQL版的负载均衡式在线 Oj.
四、compile_server 服务设计
提供的服务:
- 编译并运行代码,得到格式化的相关结果。
4.1、第一个功能: Compile 编译
compiler.hpp文件:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include "../comm/utils.hpp"
#include "../comm/log.hpp"
// 只进行编译
namespace ns_compiler
{
using namespace ns_utils;
using namespace ns_log;
class Compiler
{
public:
Compiler() {}
~Compiler() {}
// 返回值,编译成功:true,否则:false
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 errfd = open(PathUtil::Stderr(filename).c_str(), O_CREAT | O_WRONLY, 0644);
// std::cout << "debug ===== " << std::endl;
if (errfd < 0)
{
LOG(ERROR) << "内部错误,打开文件失败" << "\n";
exit(1);
}
// 重定向标准错误到errfd,以至于打印到显示器的信息,打印到Stderr文件
dup2(errfd, 2);
// g++ -o 可执行文件 源文件 -std=c++11
execlp("g++", "g++", "-o", PathUtil::Exe(filename).c_str(), PathUtil::Src(filename).c_str(), "-std=c++11", nullptr /*不要忘记*/);
exit(2); // 执行完就退出
}
else
{
// 父进程
waitpid(pid, nullptr, 0);
// 判断可执行文件在不在,如果在,就说明编译成功,否则失败
if (FileUtil::IsExistFile(PathUtil::Exe(filename).c_str()))
{
LOG(INFO) << PathUtil::Src(filename) << " 编译成功" << "\n";
return true;
}
}
LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";
return false;
}
};
}
Log.hpp文件:
#pragma once
#include <iostream>
#include "utils.hpp"
namespace ns_log
{
using namespace ns_utils;
// 日志等级
enum
{
INFO, // 就是整数
DEBUG,
WARNING,
ERROR,
FATAL
};
// 频繁使用,使用内联函数
inline std::ostream &Log(const std::string &level, const std::string &filename, int line)
{
std::string message = "[";
message += level + "]";
message += "[";
message += filename + "]";
message += "[";
message += std::to_string(line) + "]";
message += "[";
message += TimeUtil::GetCurrentTime() + "]";
std::cout << message;
return std::cout;
}
// LOG(INFO) << "message"; // 开放式日志
#define LOG(level) Log(#level, __FILE__, __LINE__)
}
4.2、第二个功能: Run 运行
test_setrlimit文件:测试使用限制资源函数
#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{
std::cout << "sig : " << sig << std::endl;
exit(-1);
}
int main()
{
for (int i = 1; i < 32; ++i)
{
signal(i, handler);
}
// // 限制cpu使用时间
// struct rlimit r_cpu;
// r_cpu.rlim_cur = 1; // 1秒
// r_cpu.rlim_max = RLIM_INFINITY;
// setrlimit(RLIMIT_CPU, &r_cpu);
// while (1)
// ;
// 限制开辟空间大小
struct rlimit r_mem;
r_mem.rlim_cur = 1024 * 1024 * 40; // 40M
r_mem.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &r_mem);
int count = 0;
while (1)
{
int *p = new int[1024 * 1024];
++count;
std::cout << "count : " << count << std::endl;
sleep(1);
}
return 0;
}
这里测试这两个终止信号:
# cpu时间限制信号:24
xp2@Xpccccc:~/Items/Load_Balancing_Online_Judging$ ./a.out
sig : 24
# 申请内存限制信号:6
xp2@Xpccccc:~/Items/Load_Balancing_Online_Judging$ ./a.out
count : 1
count : 2
count : 3
count : 4
count : 5
count : 6
count : 7
count : 8
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
sig : 6
Aborted (core dumped)
# 信号列表
xp2@Xpccccc:~/Items/Load_Balancing_Online_Judging$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
runner.hpp文件:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "../comm/log.hpp"
#include "../comm/utils.hpp"
namespace ns_runner
{
using namespace ns_log;
using namespace ns_utils;
class Runner
{
public:
static void SetRlimit(int cpu_limit, int mem_limit)
{
// 限制cpu使用时间
struct rlimit r_cpu;
r_cpu.rlim_cur = cpu_limit; // 1秒
r_cpu.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU, &r_cpu);
// 限制开辟空间大小
struct rlimit r_mem;
r_mem.rlim_cur = mem_limit * 1024; // KB
r_mem.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &r_mem);
}
public:
// 信号 ,0表示运行成功;-1表示内部错误,其他就是对应的信号值
/*
filename: 文件名,不带后缀
cpu_limit: cpu限制使用时间(秒)
mem_limit: 内存开辟限制(KB)
*/
static int runner(std::string &filename, int cpu_limit, int mem_limit)
{
/*
因为运行可执行文件,需要保存运行结果
运行结果:1.执行失败 2.执行成功
1.执行失败 把错误信息放在.stderr文件中
2.执行成功 把执行结果放在.stdout文件中
这里加一个可扩展的模块,比如,对于用户,可能会自己添加测试用例,那么他输入的用例需要放到.stdin文件中
*/
// 这里执行对应的文件,我们不关心他运行后的结果,只关心它运行成功或失败
umask(0);
int _stdin_fd = open(PathUtil::Stdin(filename).c_str(), O_CREAT | O_WRONLY, 0644);
int _stdout_fd = open(PathUtil::Stdout(filename).c_str(), O_CREAT | O_WRONLY, 0644);
int _stderr_fd = open(PathUtil::Stderr(filename).c_str(), O_CREAT | O_WRONLY, 0644);
// 必须得先保证能打开这些文件,如果打不开,后续动作将没有意义(读取不了文件,对比不了结果)
if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0)
{
LOG(FATAL) << "打开文件失败" << "\n";
return -1; // 代表打开文件失败
}
pid_t pid = fork();
if (pid < 0)
{
::close(_stdin_fd);
::close(_stdout_fd);
::close(_stderr_fd);
LOG(FATAL) << " 内部错误,创建子进程错误" << "\n";
return -2; // 代码创建子进程失败
}
else if (pid == 0)
{
// 重定向
dup2(_stdin_fd, 0);
dup2(_stdout_fd, 1);
dup2(_stderr_fd, 2);
// 设置限制
SetRlimit(cpu_limit, mem_limit); // 这里出问题也是使用信号
execl(PathUtil::Exe(filename).c_str() /*文件路径*/, PathUtil::Exe(filename).c_str() /*执行文件*/, nullptr);
exit(1);
}
else
{
::close(_stdin_fd);
::close(_stdout_fd);
::close(_stderr_fd);
int status = 0;
waitpid(pid, &status, 0);
return status & 0x7f; // 信号 ,0表示运行成功;-1表示内部错误,其他就是对应的信号值
}
return -3; // 执行错误
}
};
}
4.3、第三个功能: Compile_Run 编译并运行
图解:
compile_run.hpp文件:
#pragma once
#include <jsoncpp/json/json.h>
#include "compiler.hpp"
#include "runner.hpp"
#include