实现简易负载式均衡在线判题系统
Posted 4nc414g0n
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现简易负载式均衡在线判题系统相关的知识,希望对你有一定的参考价值。
构建HTTP Server实现简易负载均衡在线判题系统
前言
这个项目的大概:
分为两部分:
- 主server(负载均衡的选择多个从server来处理业务逻辑)
- 多个从server
说明:
- 这个项目主要是想把之前学的知识贯通起来,主业务是oj判题
- 本人水平有限,本来是想自己实现个httpserver,用在上面说的两部分客户端上,但httpserver的Reactor模型构建部分一直出毛病(我只有将构建部分的写事件就绪改为手动调用event的写回调(半个reactor,只监听读就绪)),并且在高并发情况少数情况会出现段错误,再加上这里实现的httpserver只有cgi来处理非网页请求, 使用cgi来进行负载均衡不方便(restful+rpc等方式来实现较为复杂,也还暂时不熟悉这些技术),所以第一部分的主server使用了cpphttplib库(restful回调机制方便,所有请求共享一个Controller),选择的多个编译运行server使用的是自己写的httpserver
- 这个项目把前端到后端打通了,相对以前有了一个比较清晰的概览,但也发现没有学的东西又变多了很多
还没有学习muduo库,这个项目就先这样,有点拉,后面学完后再重新构建一遍httpserver,这篇博客作为记录,方便之后重新构建
1)HTTP Server
单例TcpServer
Tcpserver.hpp
- 固定化写法,只是改为单例
- 端口,监听套接字
- 初始化->创建套接字,绑定监听
…- 复习一遍
提供的函数接口:
#include <cstring> //htos htol... #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> //socket #include <arpa/inet.h> #include <netinet/in.h> //addr family #include <pthread.h> #include <fcntl.h>//non-block void Socket();//获取listen sock void Bind();//绑定sockaddr_in void Listen();//监听 int Sock();//返回sock void SetNonBlock(ssize_t sock);//设置sock为非阻塞
注意:
- 获取listen sock时要
setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// socket地址复用(server挂了立即重启)- 单例模式的懒汉模式,在上一个tcmalloc learn项目中也用的比较多,这里不再赘述
Log简易日志
未实现异步日志 (后续重新构建再实现),这里先简单实现打印日志
日志等级:enum LOGLEVEL LOG_LEVEL_ERROR=1, // error LOG_LEVEL_WARNING, // warning LOG_LEVEL_FATAL, // fatal LOG_LEVEL_INFO, // info ;
有两种方式
LOG(LEVEL, 字符串message);
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__); void Log(std::string level, std::string message, std::string file_name, int line) std::cout <<GetColor(level)<< "[" << level << "] " <<LIGHT_CYAN<< " ["<<TimeStampToTime()<<"] " <<GetColor(level)<< " [" << message << "] " <<LIGHT_CYAN<< " [In file: " << file_name << "] " << " [Line: " << line << "]" <<NONE<< std::endl;
LOG(LEVEL)<<字符串message;
(返回ostream类型的cout,像cout一样使用)#define LOG(level) Log(#level, __FILE__, __LINE__) inline std::ostream &Log(const std::string &level, const std::string &file_name, int line) //cout内部包含缓冲区 std::cerr<<GetColor(level)<<"["<<level<<"]" <<LIGHT_CYAN<<" ["<<TimeStampToTime()<<"]" \\ <<" [In file: "<<file_name<<"]" <<" [Line: "<<line<<"]" <<GetColor(level)<<" Message: " <<NONE;//注意不要endl刷新 return std::cerr;//这里不是cout原因:上层httpserver的cgi部分dup了标准输出
支持不同LEVEL等级不同颜色高亮 和 时间戳转北京时间:
协议定制(Protocol概览)
只实现了HTTP1.0,GET/POST请求,可拓展其他不同的请求方式,未实现Cookie Session,基于短连接
Request Response
协议定制部分
HttpRequest请求类的成员设计//拆分body std::string requestLine;//请求行 std::vector<std::string> requestHeader;//请求头 std::string blank;//空行 std::string requestBody;//请求正文 //解析后结果 //请求行 //解析完毕之后的结果 std::string method;//请求方法 std::string uri; //请求资源(可能包括路径,参数) std::string version;//协议即版本 //请求头 std::unordered_map<std::string, std::string> headerMap; //请求正文 std::string body; //继续细化拆分 int ContentLength; std::string path;//uri中的路径 std::string suffix;//获得的后缀 std::string query_string;//uri中的参数(?后面的就是参数语句) bool cgi;//是否进行cgi处理 int size;
HttpResponed响应类成员设计
//构建需要的部分 std::string status_line;//状态行 std::vector<std::string> response_header;//响应头 std::string blank;//空行 std::string response_body;//响应正文 //细化部分 int statusCode; int fd;//fd用于读取文件,EndPoint的_sock用于接收文件 int size;//request目标文件的大小,也是返回文件大小
通过文件后缀判定Content-Type(可拓展):
static std::string Suffix2Desc(const std::string &suffix) static std::unordered_map<std::string, std::string> suffix2desc = ".html", "text/html", ".css", "text/css", ".js", "application/javascript", ".jpg", "application/x-jpg", ".xml", "application/xml", ".json", "application/json" //...可拓展 ; auto iter = suffix2desc.find(suffix); if(iter != suffix2desc.end()) return iter->second; return "text/html";
通过返回码返回描述(可拓展):
static std::string Code2Desc(int code) std::string desc; switch(code) case 200: desc = "OK"; break; case 404: desc = "Not Found"; break; //case 500:... default: break; return desc;
主部分对端EndPoint
对端EndPoint(提供接口构建请求,解析请求 构建响应 返回响应)
成员设计:private: int sock; HttpRequest http_request; HttpResponse http_response; bool stop;//标记读取(写入)错误 ns_reactor::Event *event;//对应的事件,见后面reactor部分
接口设计(暴露给外部四个接口:Recv Parse Build Send):
private: bool RecvHttpRequestLine();//接收请求行 bool RecvHttpRequestHeader();//接收头部 void ParseHttpRequestLine();//解析请求行 void ParseHttpRequestHeader();//解析头部 bool HasBody();//判断是否有body(GET无,POST有) bool RecvHttpRequestBody()//接收body int ProcessCgi();//进行CGI处理 int ProcessNonCgi();//进行非CGI处理 void BuildHttpResponseHelper();//构建response的帮助函数 void ErrorHandler(std::string page);//根据page构建response自定义不同的返回页面 void OkHandler();//进行正常返回构建 public: void RecvHttpRequest();//调用RecvHttpRequestLine(),RecvHttpRequestHeader() void ParseHttpRequest();//调用ParseHttpRequestLine(),ParseHttpRequestHeader() RecvHttpRequestBody() void BuildHttpResponse();//各种处理逻辑的汇接点,构建出响应HttpResponse void SendHttpResponse();//从私有的http_response读取构建好的数据进行开始send给browser
Entrance入口函数
调用对端逻辑
EndPoint *ep = new EndPoint(sock, ev); ep->RecvHttpRequest(); if(!ep->IsStop()) //如果stop为true了后面的逻辑就没必要执行了 LOG(LOG_LEVEL_INFO, "Start Building&&Sending"); ep->BuildHttpResponse(); ep->SendHttpResponse();
协议定制(Protocol细节)
Util工具类
工具函数:按行读取,字符串切割
static int ReadLine(ns_reactor::Event* event, std::string &out)//按行读取 static bool CutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)//按sep将target字符串切割为两部分
Recv,Parse
HTTP报文结构:
- 请求(每一行都以一个换行符结束:(\\r\\n或\\r或\\n)):
- 响应
Recv:
- RecvHttpRequestLine(),按行读取,Readline返回小于0 读取出错设置标志位stop=true
- RecvHttpRequestHeader(),按行读取,Readline返回小于0 读取出错设置标志位stop=true,同时将空行读取
- RecvHttpRequestBody(),HasBody()判断是否有body(POST), 使用recv系统函数接收,出错设置标志位stop=true
Parse:
- ParseHttpRequestLine(),使用stringstream按空格读取解析前字符串,写入目标,注意:method统一使用transform转大写
- ParseHttpRequestHeader(),使用Util类CutString,按": "分割,将所有请求头以key-value存储到unordered_map中
先了解CGI
见CGI实现部分:CGI机制及实现
Build Send
Build:
- BuildHttpResponse(), 主build逻辑:用程序框图表示:
Send:
- 发送响应行
- 发送响应头
- 对于非CGI:获得请求的文件对应的fd,使用sendfile零拷贝发送:关于0拷贝,参考:sendfile—Linux中的"零拷贝"
- 对于CGI,从http_response读取出body,再send
线程池(阻塞队列)
基于阻塞队列的线程池
- 阻塞队列,生产者消费者模型:Linux----多线程(上)
- 线程池:Linux----多线程(下)
设计回调:
- 任务处理(将Entrance类改为Callback):
class Task private: int sock; ns_reactor::Event* event;//见Reactor部分 CallBack handler; //设置回调 public: Task() Task(int _sock,ns_reactor::Event* ev):sock(_sock),event(ev) //处理任务 void ProcessOn() handler(sock,event); ~Task() ;
在线程池逻辑中,ThreadRoutine在PopTask后会调用ProcessOn(),进行任务处理,Callback类重载了operator(), 调用handler回调,进行处理
主函数Init,Loop
HttpServer
- InitHttpserver
注意
:忽略写入时错误(管道单向写 对端关闭)signal(SIGPIPE, SIG_IGN);
参考:signal(SIGPIPE, SIG_IGN)- 进入Loop
std::shared_ptr<HttpServer> http_server(new HttpServer(port)); http_server->InitServer(); http_server->Loop();
Reactor多线程
之前写过简单Reactor单线程tcp通信server,逻辑参考:Linux----Reactor,
这里基本上是复用了上面说的Reactor,加了个基于阻塞队列的线程池想实现Reactor多线程(阻塞队列:容量确定,便于定制策略)
将Loop主函数改为Reactor逻辑:
- 创建Reactor
- 创建监听Sock
- 创建监听事件
- 将Acceptor回调注册进监听事件
- 监听事件注册到Reactor对象中
- 创建一个struct epoll_event类型的数组
- 进入事件派发逻辑循环, 服务器启动(可以设置epollwait的timeout)
Dispatcher派发逻辑:
- epoll_wait(R->epfd_, epevs, NUM, -1);阻塞等待就绪事件
循环就绪事件个数次,进行事件的读/写就绪回调(是监听事件读就绪就进行Accepor逻辑)Httpserver总体流程图
bug
- 猜测这里有多线程问题(打印日志出现乱序)
- 也可能有读写缓冲区问题打印乱码?
- 段错误:SigmentFault(core dump)
读完muduo库再改
其他拓展方向
- 有限自动机http协议解析
- 异步日志
- 主从reactor
- 内存池
- 其他线程池
- 定时器
- …
2)Load Balance OJ
使用到的第三方库:
- 使用到jsoncpp第三方库:open-source-parsers/jsoncpp
- jsoncpp的使用:JsonCpp Documentation
- cpphttplib:yhirose/cpp-httplib
- boost:yum 直接安装
- ctemplate:OlafvdSpek/ctemplate
准备:工具类Util
解释:CommonUtil:都可以使用,CompileUtil:为编译服务,RunUtil:为运行服务,CompileRunUtil:编译运行
都可以从名字知道函数用途class CommonUtil static void SuffixAdd(std::string& fileName, const std::string& suffix); static void StrCut(std::string &line, std::vector<std::string> *target, const std::string sep); static bool FileExists(const std::string &file); static bool Write2File(const std::string &in, const std::string &Src); static bool ReadFromFile(std::string *out, const std::string &file, bool lineBreak = false); class CompileUtil static std::string File2Src(const std::string& fileName); static std::string File2Exe(const std::string & fileName); static std::string File2Error(const std::string & fileName); class RunUtil static std::string File2Stdin(const std::string & fileName); static std::string File2Stdout(const std::string & fileName) static std::string File2Stderr(const std::string & fileName); static void SetProcLimit(int cpuLimit, int memLimit); class CompileRunUtil static std::string UniqueFileName(); static void RemoveTmpFile(const std::string &fileName); static std::string Status2Desc(int status, const std::string &fileName);
编译服务
总体流程图:
编译
编译部分主要在tmp目录下形成三个文件
- 源文件source: ./tmp/fileName.cc
- g++编译出错error : ./tmp/fileName.stderr
- 可执行文件exe : ./tmp/fileName.exe
编译部分只有一个函数:
static bool Compile(std::string &fileName)
- fileName:由毫秒级时间戳+atomic原子自增结合形成的唯一文件名(在Util头文件中统一实现)
主逻辑:
- 创建子进程:打开./tmp/fileName.stderr,将stderr也就是2 重定向到open的fd(errorFd),execlp程序替换调用g++,进行对源文件的编译生成./tmp/fileName.exe,编译失败形成./tmp/fileName.stderr文件,里面存储编译失败信息
- fork出错:return false
- 父进程:FileExists判定 ./tmp/fileName.exe文件是否存在,返回false/true
运行
运行结果是否正确是由测试用例决定的,这里不考虑,这里仅考虑
- 运行成功
- 程序崩溃
运行部分主要在tmp目录下形成三个文件 :
- 用于存储标准输入: ./tmp/fileName.stdin
- 用于存储标准输出: ./tmp/fileName.stdout
- 用于存储标准错误: ./tmp/fileName.stderr
Runner内部也只有一个函数:
static int Run(std::string& fileName, int cpuLimit, int memLimit)
- fileName:由毫秒级时间戳+atomic原子自增结合形成的唯一文件名(在Util头文件中统一实现)
- cpuLimit:控制程序运行的时间复杂度(系统函数setrlimit在Util头文件的SetProcLimit工具函数中调用)
- memLimit:控制程序运行的空间复杂度
返回值:
- 返回值>0: 对应错误码(程序崩溃)
- 返回值=0: 正常退出
- 返回值<0: 内部错误(open出错,fork出错…)
主逻辑:
- 打开.stdin .stdout .stderr文件,得到对应文件描述符
- fork创建子进程:dup2分别替换对应的文件描述符(.stdin->0 .stdout->1 .stderr->2),
设置程序运行限制,开始程序替换,运行 ./tmp/fileName.exe,
程序运行得到的结果会放到.stdout文件中,得到的error信息会存放到.stderr文件中- 失败:
- 父进程:返回status & 0x7F
编译&&运行
也只有一个函数:
static void Start(const std::string& inJson, std::string *outJson)
- inJson:获取的Json串,反序列化获取内容
- outJson:输入输出型参数,返回序列化好的内容
设计逻辑:
- int status 统一获取所有逻辑的返回值,通过Util头文件中的Status2Desc获得描述message
注意:所有非信号终止错误设为负值,方便Run返回值的信号值判定
- 错误:用户代码为空,写入文件错误,读取文件错误,编译错误,运行错误(注意返回的三种情况)
这里的写入读取使用了C++更方便的ofstream,ifstream- 统一进行序列化形成outJson,
编译&&运行出错:outJson中只需要两个键值:”status“ ”message“
编译&&运行成功:outJson中需要四个键值:”status“ ”message“ ”stdout“ “stderr”- 文件统一删除(./tmp文件夹下的文件全部删除)
测试:
inJson:"code": "#include<iostream>\\nint main()std::cout<<\\"test\\"<<std::endl;", "input": "", "cpuLimit": 1, "memLimit": 102400
outJson:
"message" : "Compile And Run Success...", "status" : 0, "stderr" : "", "stdout" : "test\\n"
结合httpserver形成服务,postman json测试
注意踩过的坑
:
- 设置setrlimit虚拟内存过小,进行程序替换会导致替换的程序被11号信号终止,段错误
- HTTPserver的cgi部分已经重定向了cout cin,进程替换Compile_Run的Log信息只能用cerr打印
httpserver的cgi中新增一个compile_run,形成可执行文件compile_run.json
使用postman进行post请求测试:
负载均衡OJ服务
采用MVC(Model, View, Controller)
Model
文件题库
设计题库:
- 编号文件->question.list(问题描述) header.cpp(基础代码) test.cpp(测试用例代码) ;设计条件编译g++ -D HEADER(ifndef仅仅为了让编译器不要报错)
questions.list存储题目标题等
每个题目文件夹中有描述,precode,测试用例
Model获取数据
题目结构:
struct Question //5个变量描述题目 std::string num;//题号 std::string title;//题目标题 std::string difficulty;//题目难度 std::string desc;//题目描述 int time;//事件复杂度要求(s) int space;//空间复杂度要求(kb) //拼接下面两部分 std::string header;//题目自带代码部分 std::string test;//题目测试用例部分 ;
接口:
利用nginx和docker实现一个简易的负载均衡