在线OJ项目
1.在线OJ项目的目标
- 做出一个在线OJ的系统,支持查看题目列表,支持点击单个题目,支持代码块书写代码,支持提交书写的代码到后端,支持后端编译+运行,支持返回结果
- 最终所达成的效果
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317204026683.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317204048985.png)
2.在线OJ项目的环境搭建
2.1 cpp-httplib开源库
- cpp-httplib,开源库—使用的方法:在代码中直接包含httplib.h就可以了
2.2 升级gcc
- 需要升级gcc,关于升级gcc,首先需要切换到root用户,然后执行命令下面的两个命令
- yum -y install centos-release-scl
- yum -y install devtoolset-7-gcc devtoolset-7-gcc-c++ devtoolset-7-binutils
- 当我们执行完了这两个指令的时候,我们需要去打开bash_profile这个文件,我们使用vim ~/.bash_profile打开对应的文件,然后将scl enable devtoolset-7 bash添加到文件的末尾,然后进行保存并退出的操作,然后在使用命令source ~/.bash_profile(这句话是使得我们刚刚修改的文件进行生效的作用),就可以完成gcc的升级了
2.3 安装jsoncpp
- 执行两个命令,首先需要切换到root权限,然后直接去执行这两个命令就可以了
- yum install jsoncpp
- yum install jsoncpp-devel
2.4 安装boost环境,以及ctemplate环境
- 执行下面的几个命令来安装环境
- sudo yum install -y snappy-devel boost-devel zlib-devel.x86_64 python-pip
- sudo pip install BeautifulSoup4
- git clone https://gitee.com/HGtz2222/ThirdPartLibForCpp.git
- cd ./ThirdPartLibForCpp/el7.x86_64/sh install.sh
- 到底我们的安装全部完成,我们现在来测试一下
3.测试与下httplib-demo
3.1 下载cpp-httplib.h
- 首先我们打开cpp-httplib这个开源库链接,然后下载httplib.h这个头文件下载地址
3.2 创建三个文件
- bin文件夹是用来放可执行程序的文件夹+配置文件
- code是我们的源码目录,是用来写我们正式的代码的
- test是测试目录,是用来进行测试的代码
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304205041431.png)
3.3
- httplib----可以帮助我们创建一个http服务,不需要我们关心tcp和http的解析过程,解析完毕http请求之后,会按照请求的方法,找到我们程序中对应的接口进行处理,我们只需要去关心和url相关的接口其实就是可以的了
- 如果找到请求的对应处理方法,则回调接口
- 如果没有找到对应的处理方法,则不处理
- 我们要依赖class Server类来创建http服务
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304205703484.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
- 下面Server类的回调函数
- 函数的名字表示的是回调函数当前处理的是http的哪个方法,比如说get方法,post方法,delete方法等,也就意味着,如果我一个http请求他是get方法来的,那么我就可能会回调这个函数,到底会不会回调还是取决于pattern
- pattern当中如果说我们请求当中资源的路径和我pattern中的资源的路径是相符合的,那我就回调当前这样的一个函数,如果说现在是Get方法,但是你的资源路径和我pattern中的资源路径不相符合的话,那么我就不会去回调这个函数
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304205945862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
- 主要还是取决于pattern是否相同
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304210406570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
- httplib在使用的时候也需要指定侦听的ip和端口,这个时候我们所需要调用的函数就是listen函数(这个函数也是Server类中的)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304211031956.png)
- 然后我们现在去看一下回调函数他到底是一个什么样子的东西
- 方法里面的Handler其实是一个函数指针
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317211108937.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317211046614.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317211207834.png)
#include <stdio.h>
#include "httplib.h"
void func(const httplib::Request& req, httplib::Response& resp)
{
resp.set_content("<html>57</html>",15,"test/html");
printf("recv abc\n");
}
int main()
{
httplib::Server svr;
svr.Get("/abc", func);
svr.listen("0.0.0.0", 18989);
return 0;
}
- 上面虽然写了一个简单的http服务器,但是现在这个服务器并没有给浏览器返回任何值,如果我们想给浏览器返回值的话,那么就像下面这样去写就可以了
- 如果我们想要去给浏览器返回值的话,那么我们就需要去给response去设置值,那么现在先来看一下response是什么样子的
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304222630470.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
#include <stdio.h>
#include "httplib.h"
void func(const httplib::Request& req, httplib::Response& resp)
{
resp.set_content("<html>57</html>",15,"test/html");
printf("recv abc\n");
}
int main()
{
httplib::Server svr;
svr.Get("/abc", func);
svr.listen("0.0.0.0", 18989);
return 0;
}
4.项目模块划分
- 我们要知道这个项目中都有哪些模块,同时需要知道数据是如何进行流转的
4.1 大概的模型
- 假设我们现在有一个浏览器,然后我们自己写了一个服务端,数据是从服务端发出请求,然后服务端给浏览器返回一个应答,请求数据从浏览器来,服务端处理完毕之后,返回给浏览器
- 服务端需要能够处理浏览器发送过来的请求,并且能给服务器发送过来的请求一个响应,然后我们接下来我们需要对服务端进行拆分,我们需要知道每一个模块和每一个模块都是干嘛的,模块和模块之间会进行通信
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304222642237.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210311001201309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
4.2 划分服务端模块(其实就是拆分需求的一个过程)
- 原始需求:能够处理浏览器发过来的请求,并且能处理浏览器发过来的请求,然后回复回去一个响应
- 当浏览器给我们服务端发送数据的时候,服务端这个时候应该是能接受这个请求的,那是通过什么去接受这个请求的呢?实际上是通过网络来接收这个请求的
- http模块所干的事情就是接收浏览器的请求然后发送响应的,http模块同时会使用到开源库cpp-httplib,但是业务请求并不是http去处理掉的,其实最终的业务处理是由回调函数来处理的,回调函数告诉了我们要给浏览器发送什么响应回去
- 那么,我们现在还需要去了解到当前的浏览器会给我们的服务端发送什么样的请求呢?我们知道了浏览器会给我发送什么请求之后,我们才可以去继续划分接下来的模块,每个模块都要去做什么事情
- 在线OJ项目的业务功能主要有3个大的问题,那么现在先来看一下第一个业务功能,浏览器可以访问当前OJ的所有题目,那么既然你都可以访问当前OJ中的所有题目了,那么也就说明我们的后台必须是要有这些题目的,那么题目从哪来呢?所以我们现在多了一个模块,成为试题模块,是用来管理所有试题的,那么试题又是怎么来的,我们所能想到的就是我们可以在后台把这些试题全部写好,写好之后放在文件中,第二种就是给出一个数据库,然后每次去访问数据库就可以了
- 那么如果说现在浏览器想要去看我们所有的试题,那么他就需要去调用试题模块,然后试题模块就会把他保存的所有的试题返回给http,然后通过响应在返回给浏览器
- 那么当我们看到了所有的试题之后,用户就需要对试题去进行编写,当我们把我们希望写的内容都写完成的时候,那么这个时候就就需要我所写的代码进行编译,所以另一个模块也就出来了,编译运行模块就呼之欲出了,是用来处理浏览器提交的代码的,是编译+运行的
- 还有一个工具模块,工具模块贯穿于始终
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210304223327488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
http模块具体要去完成什么样子的事情
获取请求
- 获取整个试题列表的请求,也就是说浏览器会给我们发送一个http请求,方法是get方法,也就是浏览器会发送一个get方法给服务端,意思就是希望获取到服务端中的所有的试题列表
- 获取单个试题的请求,http请求,方法是get方法,因为只是获取请求并没有进行数据的提交
- 向服务端提交代码的请求:方法:post,向服务端提交我们所写的代码
- 那么,由此而知http模块他至少要有三个接口,三个接口对应不同的功能
#include <iostream>
#include <cstdio>
#include <json/json.h>
#include "httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "compile.hpp"
#include "tools.hpp"
int main()
{
using namespace httplib;
OjModel model;
Server svr;
svr.Get("/all_questions", [&model](const Request& req, Response& resp){
std::vector<Question> questions;
model.GetAllQuestion(&questions);
std::string html;
OjView::DrawAllQuestions(questions, &html);
resp.set_content(html, "text/html");
});
svr.Get(R"(/question/(\d+))", [&model](const Request& req, Response& resp)
{
std::cout << req.version << " " << req.method << std::endl;
std::cout << req.path << std::endl;
Question ques;
model.GetOneQuestion(req.matches[1].str(), &ques);
std::string html;
OjView::DrawOneQuestion(ques, &html);
resp.set_content(html, "text/html");
});
svr.Post(R"(/compile/(\d+))", [&model](const Request& req, Response& resp){
Question ques;
model.GetOneQuestion(req.matches[1].str(), &ques);
std::unordered_map<std::string, std::string> body_kv;
UrlUtil::PraseBody(req.body, &body_kv);
std::string user_code = body_kv["code"];
Json::Value req_json;
req_json["code"] = user_code + ques.tail_cpp_;
req_json["stdin"] = "";
std::cout << req_json["code"].asString() << std::endl;
Json::Value resp_json;
Compiler::CompileAndRun(req_json, &resp_json);
std::string err_no = resp_json["errorno"].asString();
std::string case_result = resp_json["stdout"].asString();
std::string reason = resp_json["reason"].asString();
std::string html;
OjView::DrawCaseResult(err_no, case_result, reason, &html);
resp.set_content(html, "text/html");
});
LOG(INFO, "listn_port") << ": 17878" << std::endl;
svr.set_base_dir("./www");
svr.listen("0.0.0.0", 17878);
return 0;
}
回复响应
试题模块
- 首先我们需要考虑的就是把所有的试题保存到文件当中,那么我们按照什么格式来保存呢?只要我们把文件保存好了,那么我们之后按照正常的方式去读取就可以了
- 针对每一道题目而言,需要按照给出的路径进行加载。(desc.txt题目描述信息(详细介绍这个题目,以及给出的例子)。header.cpp存放的是该题目所包含的头文件以及实现类(也就是我们一般在牛客做题的时候,她有时候会给出一些题目的部分代码,header里面存放的就是这部分代码)。tail.cpp存放的是我们的测试用例,以及main函数的入口 (也就是测试用户代码的例子))
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210311082305261.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
- 我们所要给浏览器所展示的部分包括题目描述(题目的描述从desc.txt中来,当我们把题目描述读出来之后我,我们返回给浏览器就可以了)以及预定义的头,这么看来的话,我们每有一个题目,就需要去遍历oj_data这个文件夹,但是这样使用起来的话,就不是那么的方便了,那么我们知道的是其实每一个文件夹都是一个具体信息,所以我们可以给出一个配置文件,有了这个配置文件之,我们只需要去遍历这个配置文件我们就可以知道我们一共有多少个题目了,以及不同题目之间的难度关系等等,我们就可以得到了
- 配置文件的格式(这个文件中包含有题目的描述,题目的编号(id)题目的名称),首先映入用户眼帘的不是对每个题目的详细描述,而是题目的名字,id,以及难度,通过率,出现次数等这些简述性的信息。当然我们在这里会存入题目详细描述,也就是题目点开之后的,详细内容,在服务器的存储路径,我们的服务器是吧题目名称和题目描述分开存储的。这也就是我们所约定的格式。(配置文件中所包含的信息有题目,题目名称,题目难易程度,以及题目的路径
- 加载所有的题目的配置文件,使用数据结构加载出来的题目的介绍信息,最重要的是:题目所在的路径一定要对。
- 当我们打开了配置文件之后,配置文件中是含有当前文件的路径的,那么我们只需要将这个路径打开,我们就相当于打开了这个题目了, 这比我们之间去遍历每一个题目的文件夹要方便一些
- 对于试题模块来说,首先我们需要梳理出来试题模块都要包含有试题的哪些信息,那么我们的代码就是肯定需要保存这些信息了,那么我们如何来保存这些信息呢?很容易想到我们是需要用到结构体去描述这些信息的
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210311090105163.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)
- 有了对这些对题目的描述信息之后,那么我们就需要对题目去进行从磁盘中读取的过程了,那么我们现在定义一个类出来,那么我们用一个什么样的结构体去保存这个数据结构呢?我们决定选择一个无符号的map去保存多个question
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210311091541699.png)
- 然后我们现在详细的去定义这个类
#pragma once
#include <iostream>
#include <string>
#include<fstream>
#include<boost/alalgorithm/string.hpp>
typedef struct Question
{
std::string id_;
std::string title_;
std::string star_;
std::string path_;
std::string desc_;
std::string header_cpp_;
std::string tail_cpp_;
};
class OjModel
{
public:
OjModel()
{
Load("./oj_data/oj_config.cfg");
}
~OjModel()
{
}
bool Load(std::string filename)
{
std::ifstream file(filename.c_str());
if(!file.is_open())
{
std::cout<<"config file open failed"<<std::endl;
return false;
}
std::stirng line;
while(std::getline(file,line))
{
std::vector<string> vec;
boost::split(vec,line,boost::is_any_of(" "),boost::token_compress_off);
Question ques;
ques.id_ = vec[0];
ques.titie_ = vec[1];
ques.star_ = vec[2];
ques.path_ = vec[3];
}
}
bool GetAllQuestion()
{
}
bool GetOneQuestion()
{
}
private:
std::unordered_map<std::string, Question> ques_map_;
};
- 用来切割字符串的话,需要用到boost::split,这个函数一共有4个参数,第一个参数是他的返回结果,可以给出一个顺序表,第二个参数是需要切割的内容,第三个参数是用什么字符来进行切割的操作,第四个参数是我们是否对当前的字符进行压缩的操作
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210311092819535.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgzMTcyOA==,size_16,color_FFFFFF,t_70)