C++项目 -- 负载均衡OJ(二)compile_server
文章目录
- [C++项目 -- 负载均衡OJ(二)compile_server](#C++项目 -- 负载均衡OJ(二)compile_server)
- 一、compile_server设计
- 二、compiler.hpp
- 三、runner.hpp
- 四、compile_run.hpp
- 五、compile_server.cc
一、compile_server设计
1.总体服务流程
- 远端代码提交上来后,形成一份临时文件用于编译;
- fork出一个子进程,让子进程执行编译(使用程序替换的功能调用g++进行编译),如果编译通过,标准输出没有结果;如果编译出错,需要行程临时文件来保存出错的结果,方便返回给客户端;
- 主进程继续接收代码;
二、compiler.hpp
- 该hpp文件主要提供编译文件的方法;
- Compiler中的
Compile函数
用于将源cpp文件进行编译,源文件保存在现在目录下的temp子目录,如1234.cpp
,我们需要根据源文件生成可执行文件.exe,当编译错误时需要生成标准错误文件.stderr;
Compile函数
使用util.hpp
中PathUtil
类中的方法来获取源文件对应的可执行文件名和标准错误文件名;Compile函数
创建子进程,在子进程中使用程序替换接口execlp
,调用g++对文件进行编译,并将错误信息重定向到临时的错误信息文件.stderr
中;execlp
第一个参数是替换调用哪个程序,后面的参数才是如何调用;- 程序替换并不会影响进程的文件描述符,因此重定向stderr针对子进程替换运行的程序依然生效;
- 在编译之前,需要将标准错误文件重定向到我们定义的临时错误文件中保存下来;
- 父进程用来判断最后是否编译成功,使用comm模块中的
IsFileExists
函数来进行判断;
cpp
#pragma once
#include "../Comm/util.hpp"
namespace ns_compiler
{
using namespace ns_util;
class Compiler
{
// 该类只提供方法
public:
Compiler()
{
}
~Compiler()
{
}
static bool Compile(const string &file_name)
{
pid_t pid = fork();
if (pid < 0)
{
return false;
}
else if (pid == 0)
{
// 子进程,负责调用g++编译器进行编译
// 在编译之前,需要打开保存错误信息的临时文件,并将stderr重定向到该文件
int _stderr = open(PathUtil::Stderr(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
if(_stderr < 0)
{
exit(0);
}
//重定向标准错误到_stderr
dup2(_stderr, 2);
//调用g++完成编译工作
//g++ -o target src -std=c++11
//最后一个参数后面一定要跟一个nullptr
execlp("g++", "-o", PathUtil::Exe(file_name).c_str(),\
PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr);
//程序替换失败
exit(2);
}
else
{
// 父进程
waitpid(pid, nullptr, 0);
//编译是否成功,看对应的文件夹下有没有生成对应的可执行程序
if(FileUtil::IsFileExists(PathUtil::Exe(file_name)))
{
return true;
}
return false;
}
}
};
}
三、runner.hpp
runner.hpp
文件主要用于编译成功的用户代码的运行功能的编写;Run
函数用于运行编译成功的用户代码生成的可执行程序;- 输入参数指明文件名即可,不需要带后缀和路径;输入参数还需要指定子进程的资源占用限制,方便出题者来指定该题的限制;
- 返回值用于标定该程序的运行情况:
返回值>0:程序异常,退出时收到了信号,返回值就是对应的信号编号;
返回值==0:正常运行完毕的,结果保存到了对应的临时文件中;
返回值<0,内部错误; - 该函数无需判断用户的可执行程序的运行结果正确与否,只需考虑是否正确运行完毕;
- 在程序运行期间,我们需要将标准输入、标准输出、标准错误三个文件全部打开并重定向,以便读取输入数据、保存运行结果和运行时的错误信息;
- 文件打开后,文件描述符是可以被子进程继承下去的;
- 由于我们替换的程序是在tmp文件夹里面的,因此在选择程序替换接口的时候,我们不能选择不带路径的接口(直接去环境变量列表里面查询) ,需要选择指定路径的接口
- 我们需要限制子进程运行用户程序所占用的系统资源,避免系统资源的过度占用,可以通过
SetProcLimit
接口设置; - 最后通过信号判断程序运行是否异常,如果没有异常,子进程的状态信息status返回0,如果有异常,一定返回大于0的值;
SetProcLimit
函数用于设置进程占用的资源大小- 输入参数用来指定cpu运行时间和内存空间的占用限制,内存空间使用KB为单位;
- 使用
sertlimit
接口来对进程的资源进行限制:
- resource是限制资源的类型:
- rlim是一个结构体,软限制为一般进程设置的限制,硬限制为我们所能设置的最大资源的上限值,一般设为无限制;
cpp
四、compile_run.hpp
- 该文件主要实现的功能是:
- 适配用户需求,定制通信协议;
- 正确调用compile and run;
- 形成唯一文件名;
CompileAndRun
类中的Start
函数主要完成的功能有:- 接受客户端传来的序列化的字符串,并完成反序列化;
- 序列化工具使用
jsoncpp
库中的接口,
编译时需要链接库:
形成的是序列化的kv值的字符串:
json的作用:将结构化的数据转化成为一个字符串; - 在完成反序列化后,调用FileUtil类中的
UniqueFileName
方法生成唯一的代码源文件,然后调用WriteFile
将代码写入该文件中; reader.parse
接口用于完成反序列化;asString
是将json中的v作为字符串;- 形成用户代码源文件后,对其进行编译和运行操作,返回运行的信息;
- 使用
goto
语句进行统一的差错处理,一旦上面的创建源文件、编译、运行出错,先赋值错误代码给status_code
,然后goto到END处进行错误代码解析,并通过jsoncpp工具完成序列化; goto
语句之间不允许有任何的变量定义;- 最后删除临时文件;
- 接受客户端传来的序列化的字符串,并完成反序列化;
CodeToDesc
函数主要完成返回错误代码对应的错误原因功能:
RemoveTempFile
函数主要完成清理临时文件的功能:
使用unlink接口删除文件
五、compile_server.cc
5.1.编译功能调试:
cpp
#include "compiler.hpp"
using namespace ns_compiler;
int main()
{
std::string file = "code";
Compiler::Compile(file);
return 0;
}
在现路径temp文件夹下创建code.cpp文件,测试编译功能:
编译运行compile_server.cc:
生成.exe和.stderr文件,此时.stderr文件是没有内容的;
- 如果code.cpp中有错误:
日志报错:
只会生成.stderr文件,里面保存报错信息:
5.2.CompileAndRun功能调试
R"()"
是c++11的raw string原生字符串,如果出现特殊字符,保持原貌
cpp
#include "compile_run.hpp"
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_compile_and_run;
int main()
{
//充当客户端请求的json串
string in_json;
Json::Value in_value;
//c++11原生字符串:R"()"
in_value["code"] = R"(#include<iostream>
int main()
{
std::cout << "hello world" << std::endl;
return 0;
})";
in_value["input"] = "";
in_value["cpu_limit"] = 1;
in_value["mem_limit"] = 10240 * 3;
Json::FastWriter writer;
in_json = writer.write(in_value);
cout << in_json << endl;
string out_json;
CompileAndRun::Start(in_json, out_json);
cout << out_json << endl;
return 0;
}
- 编译并运行成功;
成功在路径下生成文件名;
- 编译失败,得到失败原因
- CPU运行超时:
- 超出内存限制:
- 除0错误:
5.3.熟悉cpp-httplib库
- 接入cpp-httplib只需将
httplib.h
拷贝至项目中并包含,就可以使用; - cpp-httplib需使用高版本的gcc进行编译,最好在gcc 7以上;
- cpp-httplib是阻塞式多线程的一个网络http库;
- erver类中Get函数是接受请求并响应,第一个参数是根目录下的服务名称,第二个参数是响应的回调函数,用户访问更目录下的响应服务,服务器就会调用回调函数并返回响应;
- Request是请求类,Responce是响应类;
- content-type中text/explain是文本
- 当用户请求hello服务时,就响应相应的文本
cpp
#include "compile_run.hpp"
#include "../Comm/httplib.h"
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_compile_and_run;
using namespace httplib;
int main()
{
Server svr;
//用户如果请求根目录下的hello服务,服务器就会调用回调函数并返回响应
svr.Get("/hello", [](const Request &req, Response &resp){
//content-type中text/plain表示文本格式
//编码格式设置为为utf-8
resp.set_content("hello httplib!", "text/plain;charset=utf-8");
});
svr.listen("0.0.0.0", 8080);
return 0;
}
- 设置根目录与首页:
5.4.将CompileAndRun服务打包成一个网络服务
- 用户使用Post进行请求CompileAndRun服务;
- 用户的请求存在Request中的body;
- 服务器给用户的响应是json格式的,因此content-type要设置成applocation/json;
- 从命令行参数获取端口号;
cpp
#include "compile_run.hpp"
#include "../Comm/httplib.h"
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_compile_and_run;
using namespace httplib;
void Usage(const string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << "port" << endl;
}
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
string in_json = req.body;
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]));
return 0;
}
return 0;
}
5.5.使用Postman测试网络请求服务
- 设置Postman的请求为POST,请求的Body中内容为Row的Json格式,内容如下:
- 然后运行CompileServer服务,并建立向云服务器的8080号端口的请求:
- Postman端收到了服务端返回的Json串,表明服务运行成功;