负载均衡式的在线OJ项目编写(一)
一.项目演示
本项目的功能为一个在线的OJ,实现类似leetcode的题目列表、在线提交、编译、运行等功能。
二.所用到的技术与开发环境
所⽤技术:
• C++ STL 标准库
• Boost 准标准库(字符串切割)
• cpp-httplib 第三⽅开源⽹络库
• ctemplate 第三⽅开源前端⽹⻚渲染库
• jsoncpp 第三⽅开源序列化、反序列化库
• 负载均衡设计
• 多进程、多线程
• MySQL C connect
• Ace前端在线编辑器(了解)
• html/css/js/jquery/ajax (了解)
开发环境
• Centos 7 云服务器
• vscode
• Mysql Workbench
三.项⽬宏观结构
我们的项⽬核⼼是三个模块
1. comm :公共模块
2. compile_server :编译与运⾏模块
3. oj_server :获取题⽬列表,查看题⽬编写题⽬界⾯,负载均衡,其他功能
I.leetcode 结构
• 只实现类似 leetcode 的题⽬列表+在线编程功能
II.我们的项⽬宏观结构
III.编写思路
1. 先编写 compile_server
2. oj_server
3. version1 基于⽂件版的在线OJ
4. 前端的⻚⾯设计
5. version2 基于 MySQL 版的在线OJ
四.compiler 服务设计
提供的服务:编译并运⾏代码,得到格式化的相关的结果
还需要设计一个makefile自动编译模块
compile_server:compile_server.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -rf compile_server
开始设计compile模块(只负责编译代码)
我们还需要加一个temp文件夹来保存代码文件
compile代码的大致思路(不完整的)
#pragma once//只负责代码的编译#include <iostream>
#include <unistd.h>namespace ns_compiler
{class Compiler{public:Compiler(){}~Compiler(){}//返回值: 编译成功: true, 否则: false//输入参数: 编译文件名//filename -> name//name -> ./temp/name.cpp//name -> ./temp/name.exe//name -> ./temp/name.stderr//文件路径存储在temp路径下static bool Compile(const std::string& file_name){pid_t res = fork();if(res < 0){//创建子进程失败return false;}else if(res == 0){//子进程 执行编译代码//调用编译器,执行编译//g++ -o target src -std=c++11execlp("g++","-o")}else{//父进程}}};
}
可以发现我们不仅要生成.exe文件和.stderr文件,后续运行文件也要用到路径拼接,所以我们可以在comm(公共模块)中设计一个路径拼接util类
//路径拼接大致思路代码#pragma once#include <iostream>
#include <string>namespace ns_util
{class PathUtil{public://构建源文件路径 + 后缀的完整文件名static std::string Src(const std::string & file_name){}//构建可执行程序的完整路径 + 后缀static std::string Exe(const std::string & file_name){}//构建该程序对应的标准错误的完整路径 + 后缀static std::string Stderr(const std::string & file_name){}};
}
util类的具体实现
#pragma once#include <iostream>
#include <string>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;}//构建源文件路径 + 后缀的完整文件名//name -> ./temp/name.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 Stderr(const std::string & file_name){return AddSuffix(file_name,".stderr");}};
}
compile的具体实现
static bool Compile(const std::string& file_name){pid_t pid = fork();if(pid < 0){//创建子进程失败return false;}else if(pid == 0){//子进程 执行编译代码//调用编译器,执行编译//g++ -o target src -std=c++11execlp("g++","-o",PathUtil::Exe(file_name).c_str(),\PathUtil::Src(file_name).c_str(),"-std=c++11",nullptr);exit(1);}else{//父进程//nullptr:表示不关心子进程退出的状态//0:表示阻塞等待waitpid(pid,nullptr,0);//编译是否成功,就看是否形成了可执行的程序if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){return true;}}return false;}
要判断文件是否存在,这个函数使用的频率也较高(也放到comm模块中)
新的判断文件是否存在的接口介绍
class FileUtil{public:static bool IsFileExists(const std::string& path_name){struct stat st;if(stat(path_name.c_str(),&st) == 0){//获取属性成功,文件已经存在return true;}return false;}};
编译成功已经写好了,那编译出错呢?
编译出错的话,我们将错误重定向到标准错误文件中写入错误信息
创建并打开文件
重定向接口
五.log日志服务的编写
上面初步完成了我们编译的过程,必要时还是要打印日志的(所以我们来写一个日志功能)
#pragma once#include <iostream>
#include <string>
#include "util.hpp"namespace ns_log
{using namespace ns_util;//日志等级enum {INFO,DEBUG,WARNING,ERROR,FATAL};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本质 内部是包含缓存区的//没有endl就不会将缓存区内部的内容全部刷新出来std::cout << message;return std::cout;}// LOG(INFO) << "message" << "\n"//开放式日志#define LOG(level) Log(#level,__FILE__,__LINE__)}
六.测试编译模块
还有一个比较重要的一点,在vsocde创建的文件,是root的,所有不能用其他普通用户去执行(会出错)
报错测试
补充
七.运行功能开发
子进程来执行代码,父进程来进行等待
可以思考以下的问题:
/******************************
* 程序运行:
* 1. 代码跑完,结果正确
* 2. 代码跑完,结果不正确
* 3. 代码没跑完,异常了
* Run模块需要考虑代码跑完,结果正确与否吗??? 不考虑
* 结果正确与否: 是由我们的测试用例来决定的!
* 我们只考虑: 是否正确运行完毕
*
*
* 我们必须知道可执行程序是谁? ./temp/code.exe
* 一个程序在默认启动的时候
* 标准输入: 不处理(用户自测)
* 标准输出: 程序运行完成,输出结果是什么
* 标准错误: 运行时错误信息
* 全部写道同名文件中
******************************/
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>#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://指明文件名即可,不需要代理路径,不需要带后缀/*********************************** 返回值 > 0:程序异常了,退出时受到了信号,返回值就是对应的信号编号* 返回值 = 0:正常运行完毕的,结果保存到了对应的临时文件中* 返回值 < 0:内部错误* *********************************/static int Run(const std::string & file_name){/******************************* 程序运行:* 1. 代码跑完,结果正确* 2. 代码跑完,结果不正确* 3. 代码没跑完,异常了* Run模块需要考虑代码跑完,结果正确与否吗??? 不考虑* 结果正确与否: 是由我们的测试用例来决定的!* 我们只考虑: 是否正确运行完毕* * * 我们必须知道可执行程序是谁? ./temp/code.exe* 一个程序在默认启动的时候* 标准输入: 不处理(用户自测)* 标准输出: 程序运行完成,输出结果是什么* 标准错误: 运行时错误信息* 全部写道同名文件中******************************/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 || _stdout_fd < 0 || _stderr_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);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; //将程序是否受到异常(信号 1-30)进行返回}}};
}
八.测试运行模块
#include "compiler.hpp"
#include "runner.hpp"using namespace ns_compiler;
using namespace ns_runner;int main()
{std::string code = "code";Compiler::Compile(code);Runner::Run(code);return 0;
}
编译完之后,就直接运行程序
未完待续