负载均衡式的在线OJ项目编写一
负载均衡式的在线OJ项目编写(一)
一.项目演示
本项目的功能为一个在线的 ,实现类似leetcode的题目列表、在线提交、编译、运行等功能。
代码持续开发中,下面是代码链接
二.所用到的技术与开发环境
所⽤技术:
• C++ STL 标准库
• Boost 准标准库(字符串切割)
• cpp-httplib 第三⽅开源⽹络库
• ctemplate 第三⽅开源前端⽹⻚渲染库
• jsoncpp 第三⽅开源序列化、反序列化库
• 负载均衡设计
• 多进程、多线程
• MySQL C connect
• Ace前端在线编辑器(了解)
• html/css/js/jquery/ajax (了解)
开发环境
• Centos 7 云服务器
• vscode
• Mysql Workbench
三.项⽬宏观结构
我们的项⽬核⼼是三个模块
comm :公共模块
compile_server :编译与运⾏模块
oj_server :获取题⽬列表,查看题⽬编写题⽬界⾯,负载均衡,其他功能
I.leetcode 结构
• 只实现类似 leetcode 的题⽬列表+在线编程功能
II.我们的项⽬宏观结构
III.编写思路
先编写 compile_server
oj_server
version1 基于⽂件版的在线OJ
前端的⻚⾯设计
version2 基于 MySQL 版的在线OJ
四.compiler 服务设计
提供的服务:编译并运⾏代码,得到格式化的相关的结果
还需要设计一个makefile自动编译模块
compile_server:compile_server.cc
g++ -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++11
execlp("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.cpp
static 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++11
execlp("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;
}
编译完之后,就直接运行程序
未完待续