文章目录
前面部分请看这里C++ – 负载均衡式在线OJ (一)
三、compile_server模块
2.编译运行模块开发(compile_run模块)
编写compile_run模块去进行组合。compile_run模块就需要去适配用户请求,定制通信协议字段,然后逐次完善功能,正确的调用compile和runner
jsoncpp库
jsoncpp最简单的用法就是创建一个Value类型的万能对象,然后以KV的方式进行序列化,然后给对方,对端接收到之后进行read反序列化
#include <json/jsoncpp/json.h>
#include <string>
int main()
{
Json::Value root;
root["code"] = "mycode";//KV的形式,读的时候就可以通过key读出value
root["user"] = "zhupi";
root["age"] = "19";
//序列化
Json::StyleWriter writer;//不止一种序列化的类,区别就在于形成的json string不同
std::string str = writer.write(root);//str就是序列化之后的结果
std::cout<<str<<std::endl;
}
//假设对端接收到了一个json string
void jsonTest(const std::string & in_json)
{
Json::Value in_value;
Json::Reader reader;//反序列化对象
reader.parse(in_json,in_value);//把in_json反序列化到in_value当中
std::string code = in_value["code"].asString(); // 当成字符串
std::string user = in_value["user"].asString();
std::string age = in_value["age"].asString();
//就得到了对端发给我的结果
}
注意: 因为我们使用了jsoncpp,他是一个第三方库,在编译的时候我们需要给g++一些选项,g++ -o test test.cc -std=c++11 -ljsoncpp
compile_run模块
明确步骤:
- 把被人通过网络传给我们的json string 反序列化,取出里面规定好的内容(这是我们定制的协议,我们 规定里面需要有code代码,input自测输入(目前不支持),cpu_limit占用时间限制,mem_limit占用空间限制)
- 生成独特的文件名,不能和其他的起冲突,这个文件名就用来后面生成本次提供编译运行服务的临时文件。
- 生成一份源文件程序,把code代码放进去
- 正确调用compiler和runner模块的接口进行处理(编译运行)
- 结果发回给对端
独特文件名的形成
我们采用毫秒级时间戳和原子性的唯一值来保证形成的文件名的唯一性,或者我们用mutex互斥锁去进行计数也是一样的。
在comm模块中的util.hpp中
namespace ns_util
{
class TimeUtil
{
public:
static std::string GetTimeStamp()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec);
}
// 获得毫秒时间戳
static std::string GetTimeMs()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
}
};
}
唯一文件名及读写文件接口
在comm模块中的util.hpp中
namespace ns_util
{
class FileUtil
{
public:
static std::string UniqFileName()
{
static std::atomic_uint id(0);
id++;
// 毫秒级时间戳+原子性递增唯一性:来保证唯一性
std::string ms = TimeUtil::GetTimeMs();
std::string uniq_id = std::to_string(id);
return ms + "_" + uniq_id;
}
};
static bool WriteFile(const std::string &target, const std::string &code)
{
std::ofstream out(target);
if (!out.is_open())
return false;
out.write(code.c_str(), code.size());
out.close();
}
static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
{
(*content).clear();
std::ifstream in(target);
if (!in.is_open())
{
return false;
}
std::string line;
// getline 不报错行分隔符,有些时候需要保留\n
// getline内部重载了强制类型转换
while (std::getline(in, line))
{
(*content) += line;
(*content) += (keep ? "\n" : "");
}
in.close();
return true;
}
}
清理所有临时文件
我们会在执行过程中产生多少个临时文件的数目是不确定。但是有哪些类型我们是知道的,上面都说过
一共有六个
- .cpp
- .exe
- .compile_error
- .stdin
- .stdout
- .stderr
我们只需要判断文件存不存在FileUtil::IsFileExists()来判断,再进行删除就可以了unlink()函数
namespace ns_compile_and_run
{
using namespace ns_log;
using namespace ns_util;
using namespace ns_compiler;
using namespace ns_runner;
class CompileAndRun
{
public:
static void RemoveTempFile(const std::string &file_name)
{
// 清理文件的个数是不确定的
// 但是有哪些我们是知道的
std::string src = PathUtil::Src(file_name);
if (FileUtil::IsFileExists(src))
unlink(src.c_str());
std::string compiler_error = PathUtil::CompilerError(file_name);
if(FileUtil::IsFileExists(compiler_error))
unlink(compiler_error.c_str());
std::string execute = PathUtil::Exe(file_name);
if(FileUtil::IsFileExists(execute))
unlink(execute.c_str());
std::string _stdin = PathUtil::Stdin(file_name);
if(FileUtil::IsFileExists(_stdin))
unlink(_stdin.c_str());
std::string _stdout = PathUtil::Stdout(file_name);
if(FileUtil::IsFileExists(_stdout))
unlink(_stdout.c_str());
std::string _stderr = PathUtil::Stderr(file_name);
if(FileUtil::IsFileExists(_stderr))
unlink(_stderr.c_str());
}
};
}
compile_run模块的实现
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>
namespace ns_compile_and_run
{
using namespace ns_log;
using namespace ns_util;
using namespace ns_compiler;
using namespace ns_runner;
class CompileAndRun
{
public:
static void RemoveTempFile(const std::string &file_name)
{
// 清理文件的个数是不确定的
// 但是有哪些我们是知道的
std::string src = PathUtil::Src(file_name);
if (FileUtil::IsFileExists(src))
unlink(src.c_str());
std::string compiler_error = PathUtil::CompilerError(file_name);
if(FileUtil::IsFileExists(compiler_error))
unlink(compiler_error.c_str());
std::string execute = PathUtil::Exe(file_name);
if(FileUtil::IsFileExists(execute))
unlink(execute.c_str());
std::string _stdin = PathUtil::Stdin(file_name);
if(FileUtil::IsFileExists(_stdin))
unlink(_stdin.c_str());
std::string _stdout = PathUtil::Stdout(file_name);
if(FileUtil::IsFileExists(_stdout))
unlink(_stdout.c_str());
std::string _stderr = PathUtil::Stderr(file_name);
if(FileUtil::IsFileExists(_stderr))
unlink(_stderr.c_str());
}
// code >0 : 进程收到了信号导致崩溃
// <0 : 整个过程非运行报错(代码为空,编译报错)
// =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:
// desc = "代码编译的时候发⽣了错误";
FileUtil::ReadFile(PathUtil::CompilerError(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: ⽤⼾提交的代码
* input: ⽤⼾给⾃⼰提交的代码对应的输⼊,不做处理
* cpu_limit: 时间要求
* mem_limit: 空间要求
* 输出:
* 必填
* status: 状态码
* reason: 请求结果
* 选填:
* stdout: 我的程序运⾏完的结果
* stderr: 我的程序运⾏完的错误结果
* 参数:
* in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
* out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
*/
static void Start(const std::string &in_json, std::string *out_json)
{
Json::Value in_value;
Json::Reader reader;
reader.parse(in_json, in_value); // 最后在处理差错问题
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
int status_code = 0;
Json::Value out_value;
int run_result = 0;
std::string file_name; // 需要内部形成的唯⼀⽂件名
if (code.size() == 0)
{
status_code = -1; // 代码为空
goto END;
}
// 形成的文件名只具有唯一性,没有目录没有后缀
// 毫秒级时间戳+原子性递增唯一值:来保证唯一性
file_name = FileUtil::UniqFileName();
if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) // 形成临时src文件
{
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 = -2; // 未知错误
}
else if (run_result > 0)
{
// 程序运行崩溃了
status_code = run_result;
}
else
{
// 运行成功
status_code = 0;
}
END:
out_value["status"] = status_code;
out_value["reason"] = CodeToDesc(status_code, file_name);
if (status_code == 0)
{
// 整个过程全部成功
std::string _stdout;
FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);
out_value["stdout"] = _stdout;
std::string _stderr;
FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);
out_value["stderr"] = _stderr;
}
Json::StyledWriter writer;
*out_json = writer.write(out_value);
RemoveTempFile(file_name);
}
};
}
设计测试用例对compile_run模块进行测试
测试这种复杂程序的时候,一定要单元化的测试,不要等最后代码全写完了才来测试,像这样的话代码根本跑不通。
compile_run需要对端传入一个json串,我们这里本地的构建一个,但是实际上是oj_server服务器负载均衡选择后通过http传过来的。
注意: 这里用到了一个R"()"的语法,这是C++的语法,意思是Row String 原生字符串的意思,他就是说括号里面的东西保持原貌,不要和其他东西进行匹配(主要就是因为里面的双引号和字符串的双引号会匹配,冲突一些东西)
运行结果
形成的临时文件
引入cpp-httplib第三方网络库
compile_run打包成网络服务
#include <../comm/httplib.h>
...
int main()
{
//1.构建服务器对象
Server svr;
//2.功能路由(资源相对路径,lambda表达式);
svr.Get("hello",[](const Request&req,Response&resp){
resp.set_content("hello httplib,你好httplib","text/plain;charset utf-8");
//第二个参数就是这个内容的content-type,我们这是纯文本,字符编码utf8
});
//3.启动服务器
svr.listen("0.0.0.0",8080);//指定IP地址和PORT端口号
return 0;
}
上面的content-type就是内容的形式,比如纯文本text/plain,比如html 的类型text/html,json串的content-type就是application/json
利用httplib将compile_run服务打包成网络服务,我们需要的服务是compile_run
#include "compile_run.hpp"
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;
void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}
// 编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性
// 不然多个用户之间会相互影响
//./compile_server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
Server svr;
svr.Post("/compile_and_run", [](const Request &req, Response &resp)
{
// ⽤⼾请求的服务正⽂是我们想要的json string
std::string in_json = req.body;
std::string out_json;
if(!in_json.empty()){
CompileAndRun::Start(in_json, &out_json);
resp.set_content(out_json, "application/json;charset=utf-8");
} });
svr.listen("0.0.0.0", atoi(argv[1])); //启动http服务
return 0;
}
我们现在完成的就是最右边的compile_server。