当前位置: 首页 > news >正文

C++ -- 负载均衡式在线OJ (一)

一、项目宏观结构

1.项目功能

本项目的功能为一个在线的OJ,实现类似leetcode的题目列表、在线提交、编译、运行等功能。

2.项目结构

该项目一共三个模块:

  • comm : 公共模块
  • compile_server : 编译与运行模块
  • oj_server : 获取题目列表,查看题目编写题目界面,负载均衡,其他功能

代码由客户端编写完成后,上传到服务端oj_server,由oj_server根据compile_server的负载情况选择相应的服务,来进行代码的编译与运行,结果再由oj_server返回给客户端,是基于BS模式(浏览器(客户端)-服务端)编写的。

在这里插入图片描述

二、comm公共模块

1.log.hpp

日志,我们想提供

  • 日志等级
  • 打印日志的文件名称
  • 报错行
  • 添加日志的时间
  • 日志信息
  • 开放性输出

注意: 开放性输出就是说我们可以在后面输出自己想输出的东西,比如LOG(DEBUG)<<“我想输出的东西”<<std::endl;

#pragma once#include <iostream>
#include <string>
#include "util.hpp"namespace ns_log
{using namespace ns_util;enum{// 日志等级  0-4INFO,    // 常规的,只是一些提示信息DEBUG,   // 调试日志WARNING, // 告警,不影响后续使用// 一般碰到ERROR或者FATAL这样的错误,就需要有人来运维了ERROR,   // 错误,用户的请求不能继续了FATAL    // 整个系统就用不了了};// LOG() << "message"  我们想进行日志打印的方式,是一个开放式的日志功能inline std::ostream &Log(const std::string &level, const std::string &file_name, int line) // 打印日志的函数{// 添加日志等级std::string message = "[";message += level;message += "]";// 添加报错文件名称message += "[";message += file_name;message += "]";// 添加报错行message += "[";message += std::to_string(line); // 整数转字符串message += "]";// 日志一般都有它的时间,就是这个日志是上面时候打的// 添加日志时间戳message += "[";message += TimeUtil::GetTimeStamp(); // 整数转字符串message += "]";// cout 本质 内部是包含缓冲区的std::cout << message; // 不要std::endl进行刷新,因为换行就会刷新缓冲区return std::cout;     // 返回一个流式缓冲区,上面的信息写到一个缓冲区当种}// LOG(INFO)<<"message"<<"\n"; # \n进行缓冲区的刷新#define LOG(level) Log(#level, __FILE__, __LINE__)
}

注意:

  • 其中 __FILE__和__LINE__是C语言中的两个宏,获得文件名称和获得行数。
  • #define LOG(level) log(#level,FILE,LINE);这个宏当中,#level的作用是,直接转化成字符串的形式,比如DEBUG对应的枚举是1,那么我们只传DEBUG的话,在预编译阶段就会替换成1,但是我们传入#level的话,他就会认为是字符 串"DEBUG";

2. util.hpp

先编写compile_server模块的compiler.hpp

编译模块的整体结构如下:
在这里插入图片描述

首先,我们想要提供编译服务,那么急需要去调用编译器。在Linux当中,我们知道对进程操作可以有进程创建、进程终止、进程等待、进程程序替换,那么我们就需要去进程程序替换成g++来对用户提交的代码进行编译

  • 带l的我们可以认为是需要传入一串参数,比如说g++ -o test test.cc,需要以NULL/nullptr结尾
  • 带v的我们可以认为是需要数组去进行传递,也就是把我们上面的一串参数,先放入数组再进行调用
  • 带p的可以认为是环境变量,也就是说系统已经认识了该程序,无序我们传入相对/绝对地址,而不带p是需要我们传入的。

注意:我们今天选择的是execlp,最符合我们的调用,execlp的调用方式:execlp(“g++”,“g++”,“-o”,“test”,“test.cc”,nullptr); ;(第一个g++代表的是在环境变量当中去找)

进程程序替换

在这里插入图片描述

util.hpp 接路径工具类

在客户提交代码之后,要形成一些文件,比如源文件,编译之后形成可执行文件,编译错误的话要形成编译错误文件。

所以,这时候需要一些方法来对这些文件进行构建,我们把这些构建后缀的方法放到comm模块的Util类当中

namespace ns_util
{const std::string temp_path = "./temp/";class PathUtil{public:static std::string AddSuffix(const std::string &file_name, const std::string &suffix){std::string path_name = temp_path;path_name += file_name;path_name += suffix;return path_name;}// 编译时需要有的临时文件// 构建源文件+后缀的完整文件名// 1234 -> ./temp/1234.cppstatic std::string Src(const std::string &file_name){return AddSuffix(file_name, ".cpp");}// 构建可执行程序的完整路径+后缀名static std::string Exe(const std::string &file_name){return AddSuffix(file_name, ".exe");}static std::string CompilerError(const std::string &file_name){return AddSuffix(file_name, ".compiler_error");}// 运行时需要的临时文件static std::string Stdin(const std::string &file_name){return AddSuffix(file_name, ".stdin");}static std::string Stdout(const std::string &file_name){return AddSuffix(file_name, ".stdout");}// 构建该程序对应的标准错误的完整路径+后缀名static std::string Stderr(const std::string &file_name){return AddSuffix(file_name, ".stderr");}};}

检测编译是否成功

我们编译是否成功只有一个标准,就是是否形成可执行文件

  • 第一种方式:r读方式打开文件,如果失败了,说明不存在,这种方式太简单粗暴-
  • 第二种方式:使用系统调用接口stat检测文件属性。

在这里插入图片描述

注意:stat的第二个参数是一个输出型参数,是一个系统提供的结构体类型。

namespace ns_util
{class FileUtil{public:static bool IsFileExists(const std::string &path_name){struct stat st;// stat成功,0被返回,失败-1返回if (stat(path_name.c_str(), &st) == 0){// 获取属性成功,文件已经存在return true;}return false;}}

编译出错

编译出错,g++会向标准错误流里面打印错误信息,所以我们就要形成一个文件,也就是编译错误文件xxx.compiler_error,让标准错误文件描述符进行重定向到该文件,如果编译出错,就可以在这个文件当中看见错误原因。

namespace ns_util
{const std::string temp_path = "./temp/";class PathUtil{public:static std::string AddSuffix(const std::string &file_name, const std::string &suffix){std::string path_name = temp_path;path_name += file_name;path_name += suffix;return path_name;}static std::string CompilerError(const std::string &file_name){return AddSuffix(file_name, ".compiler_error");}};
}

compiler编译模块核心逻辑实现

在这里插入图片描述

编译模块核心逻辑 compile_server模块的compiler.hpp

//只负责进行代码的编译
namespace ns_compiler
{//引用路径拼接功能using namespace ns_util;using namespace ns_log;class Compiler{public:Compiler(){}~Compiler(){}//返回值:编译成功true,编译失败false//输入参数:编译的文件名//1234.cpp -> ./temp/1234.cpp//1234 -> ./temp/1234.exe//1234 -> ./temp/1234.stderrstatic bool Compile(const std::string &file_name){pid_t pid = fork();if(pid < 0){LOG(ERROR) << "内部错误,创建子进程失败" << "\n";return false;}else if(pid == 0){   int _stderr = open(PathUtil::Stderr(file_name).c_str(),O_CREAT | O_WRONLY,0644);if(_stderr < 0){LOG(WARNING) << "没有成功形成stderr文件" << "\n";exit(1);}//重定向标准错误到_stderrdup2(_stderr,2);//程序替换,并不影响进程的文件描述符表//子进程:执行调用编译器完成对代码的编译工作//g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE","-std=c++11",  nullptr/*不要忘记*/);LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";exit(2);}else{waitpid(pid,nullptr,0);//编译是否成功?就看有没有形成对应的可执行程序if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){LOG(INFO) << PathUtil::Exe(file_name) << "编译成功" << "\n";return true;}}LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";return false;}};
}

三、compile_server模块

1. 运行功能开发(runner模块)

编译完成之后,如果成功,则会生成可执行程序,我们现在是想办法把程序run起来。

  程序运行1.代码跑完,结果正确2.代码跑完,结果不正确3.代码没跑完,异常了进程等待中的status就可以直到 前8位是退出吗 低8位是核心转储 后面7位是进程信号信号为0,则退出码有效,不为0,则退出码无效。核心转储需要自己开启,并且核心转储是存储核心错误信息但是运行模块,Run,我们是不需要考虑结果正确与否结果正确与否是由测试用例决定的。但是跑错了是要报错的。错误又分为编译错误和运行错误,运行错误才是在runner模块里该出现的

进程起来之后,默认会打开三个文件描述符,分别是0,1,2号文件描述符,分别对应stdin,stdout,stderr。我们为了方便我们运行的自测输入(我们这里暂时不支持),运行结果,运行错误结果等的查看与返回给用户。我们需要把这三个文件描述符进行重定向

//file_name为传入的文件名参数。文件分文件名和文件后缀
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0); // 置权限掩码为0//打开文件
int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 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);//文件重定向(打开了才能重定向,打开了才有对应的fd)
dup2(_stdin_fd, 0);
dup2(_stdout_fd, 1);
dup2(_stderr_fd, 2);

资源限制(CPU占用,内存)

我们在leetcode做题的时候通常会发现出现 CPU占用时间超限,内存超限等,其实就是给执行这个运行服务的进程进行了资源的限制

对进程做资源限制,我们需要调用 setrlimit 的系统调用来完成:
在这里插入图片描述

注意:

  • RLIMIT_AS最大给这个进程的虚拟地址(用字节来衡量)
  • RLIMIT_CPU就代表CPU占用时间的限制

而我们看到还有一个对应的struct rlimit结构体,第一个是软件限制,第二个是硬件限制,硬件一般设成无穷的,不加约束 (无限,INFINITY)

在这里插入图片描述

#include "../comm/log.hpp"
#include "../comm/util.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner() {}~Runner() {}public://提供设置进程占用资源大小的接口static void SetProcLimit(int _cpu_limit, int _mem_limit){//设置cpu时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_max = RLIM_INFINITY;cpu_rlimit.rlim_cur = _cpu_limit;setrlimit(RLIMIT_CPU,&cpu_rlimit);//设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_cur = _mem_limit * 1024; //转化成KBmem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS,&mem_rlimit);}// 指明⽂件名即可,不需要代理路径,不需要带后缀/******************************************** 返回值 > 0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号* 返回值 == 0: 正常运⾏完毕的,结果保存到了对应的临时⽂件中* 返回值 < 0: 内部错误** cpu_limit: 该程序运⾏的时候,可以使⽤的最⼤cpu资源上限* mem_limit: 改程序运⾏的时候,可以使⽤的最⼤的内存⼤⼩(KB)* *****************************************/static int Run(const std::string &file_name,int cpu_limit,int mem_limit){/********************************************** 程序运⾏:* 1. 代码跑完,结果正确* 2. 代码跑完,结果不正确* 3. 代码没跑完,异常了* Run需要考虑代码跑完,结果正确与否吗??不考虑!* 结果正确与否:是由我们的测试⽤例决定的!* 我们只考虑:是否正确运⾏完毕** 我们必须知道可执⾏程序是谁?* ⼀个程序在默认启动的时候* 标准输⼊: 不处理* 标准输出: 程序运⾏完成,输出结果是什么* 标准错误: 运⾏时错误信息* *******************************************/std::string _execute = PathUtil::Exe(file_name);std::string _stdin = PathUtil::Stdin(file_name);std::string _stdout = PathUtil::Stdout(file_name);std::string _stderr = PathUtil::Stderr(file_name);umask(0);int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 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 || _stderr_fd < 0 || _stdout_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);SetProcLimit(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 = 0;waitpid(pid, &status, 0);// 程序运行异常,一定是因为收到了信号!LOG(INFO) << "运行完毕, info :" << (status & 0x7F) << "\n";return status & 0x7F;}}};
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 大模型 - 分布式训练方法汇总
  • SQL-锁
  • 【算法】最短路径算法思路小结
  • C#中Override与New关键字的运用及实例解析
  • c# 什么是扩展方法
  • Oracle-OracleConnector
  • Linux应用层开发(7):网络编程
  • html+css+js网页设计 找法网2个页面(带js)ui还原度百分之90
  • C语言实现UDP广播
  • 力扣227题基本计算器II(Python实现)
  • Kali Linux——网络安全的瑞士军刀
  • 登录页滑块验证图
  • Windows下编译安装PETSc
  • 简单介绍BTC的Layer2项目RGB
  • Java面试篇(JVM相关专题)
  • [数据结构]链表的实现在PHP中
  • C++类中的特殊成员函数
  • Docker: 容器互访的三种方式
  • iOS帅气加载动画、通知视图、红包助手、引导页、导航栏、朋友圈、小游戏等效果源码...
  • Java Agent 学习笔记
  • markdown编辑器简评
  • Mybatis初体验
  • mysql外键的使用
  • nginx 配置多 域名 + 多 https
  • Python实现BT种子转化为磁力链接【实战】
  • quasar-framework cnodejs社区
  • redis学习笔记(三):列表、集合、有序集合
  • Spring-boot 启动时碰到的错误
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 闭包,sync使用细节
  • 工程优化暨babel升级小记
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 最近的计划
  • 交换综合实验一
  • ​​​【收录 Hello 算法】9.4 小结
  • ![CDATA[ ]] 是什么东东
  • # 深度解析 Socket 与 WebSocket:原理、区别与应用
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #FPGA(基础知识)
  • (1)STL算法之遍历容器
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (二)Linux——Linux常用指令
  • (二十一)devops持续集成开发——使用jenkins的Docker Pipeline插件完成docker项目的pipeline流水线发布
  • (一)spring cloud微服务分布式云架构 - Spring Cloud简介
  • (转)C#开发微信门户及应用(1)--开始使用微信接口
  • (转)用.Net的File控件上传文件的解决方案
  • .htaccess配置常用技巧
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .NET 8.0 中有哪些新的变化?
  • .net core控制台应用程序初识
  • .NET 快速重构概要1
  • .NET/C# 使用 SpanT 为字符串处理提升性能
  • .Net调用Java编写的WebServices返回值为Null的解决方法(SoapUI工具测试有返回值)
  • .Net通用分页类(存储过程分页版,可以选择页码的显示样式,且有中英选择)
  • .set 数据导入matlab,设置变量导入选项 - MATLAB setvaropts - MathWorks 中国