仿照 leetcode 的一个在线 OJ 系统.
项目功能
实现一个在线判题系统,用户通过浏览器获取对应题目进而编写代码并提交代码,将代码上传到后台,后台需要对提交的代码与测试用例整合并进行编译运行,将结果反馈给用户。
核心功能
- 在线编译运行
- 题目管理
模块划分
该项目被划分为四大模块:
oj_server模块
1、提供http服务,串联试题模块和编译运行模块
(1)获取题目列表
(2)获取选中的题目
(3)提交题目代码和题目描述,代码的编辑框
#include <stdio.h>
#include <string>
#include <string.h>
#include "httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "oj_log.hpp"
#include "compile.hpp"
int main()
{
//需要使用httplib提供的命名空间
using namespace httplib;
Server svr;
OjModel ojmodel;
svr.Get("/all_questions", [&ojmodel](const Request& req, Response& resp){
std::vector<Question> ques;
ojmodel.GetAllQuestions(&ques);
//使用模板技术去填充html页面
std::string html;
OjView::ExpandAllQuestionshtml(&html, ques);
//LOG(INFO, html);
resp.set_content(html,"text/html; charset=UTF-8");
});
svr.Get(R"(/question/(\d+))", [&ojmodel](const Request& req, Response& resp){
// question/1
// 1.去试题模块去查找对应题号的具体的题目信息
// map当中 (序号 名称 题目的地址 难度)
//
std::string desc;
std::string header;
//从querystr当中获取id
LOG(INFO, "req.matches") << req.matches[0] << ":" << req.matches[1] << std::endl;
// 2.在题目地址的路径下去加载单个题目的描述信息
struct Question ques;
ojmodel.GetOneQuestion(req.matches[1].str(), &desc, &header, &ques);
// 3.进行组织,返回给浏览器
std::string html;
OjView::ExpandOneQuestion(ques, desc, header, &html);
resp.set_content(html,"text/html; charset=UTF-8"); // 将html网页设置到响应中
});
svr.Post(R"(/question/(\d+))", [&ojmodel](const Request& req, Response& resp){
//key:value
//1.从正文当中提取出来提交的内容。主要是提取code字段所对应的内容
// 提交的内容当中有url编码--》提交内容进行 解码
// 提取完成后的数据放到 unordered_map<std::string, std::string>
std::unordered_map<std::string, std::string> pram;
UrlUtil::PraseBody(req.body, &pram);
//for(const auto& pr:pram)
//{
// LOG(INFO, "code ") << pr.second << std::endl;
//}
//2.编译&运行
// 2.1 需要给提交的代码增加头文件,测试用例,main函数
std::string code;
ojmodel.SplicingCode(pram["code"], req.matches[1].str(), &code); //给提交的代码增加头文件,测试用例,main函数
//LOG(INFO, "code ") << code << std::endl;
// //3、构造 JSON结构的参数
Json::Value req_json;
req_json["code"] = code;
//req_json["stdin"] = ""
Json::Value Resp_json;
Compiler::CompileAndRun(req_json, &Resp_json);
//3.构造响应
const std::string errorno = Resp_json["errorno"].asString();
const std::string reason = Resp_json["reason"].asString();
const std::string stdout_reason = Resp_json["stdout"].asString();
std::string html;
OjView::ExpandReason(errorno, reason, stdout_reason, &html);
resp.set_content(html,"text/html; charset=UTF-8");
});
LOG(INFO, "listen in 0.0.0.0:19999") << std::endl;
LOG(INFO, "Server ready") << std::endl;
//listen 会阻塞
svr.listen("0.0.0.0", 19999);
return 0;
}
试题模块
1、从配置文件(oj_config.cfg)中加载题目
1.1 配置文件的格式(行文本文件,每一行对应一道题目)
(1)约定配置文件当中对题目的描述
(2)题目的编号、题目名称、题目难度、题目路径 (以\t分隔)
1.2 加载题目的配置文件,使用数据结构保存加载出来的题目的介绍信息,保证题目路径的正确
(1)创建Question结构体 , 以字符串形式存储题目的属性(id_\name_\start_\desc_\header.cpp\tail.hpp)
(2)按行读取 oj_config.cfg文件,并且解析(解析就是字符串分割)
1.3 根据解析结果拼装成 Question 结构体对象,针对每一道题而言,根据给出的路径进行加载
desc.txt : 题目的描述信息
header.cpp: 存放的是该题目所包含的头文件以及实现类
tail.cpp:存放测试用例以及main函数的入口
1.4 以 id:question键值对形式 存储到 unordered_map 数据结构中
2、 提供获取整个题目的接口
2.1 接口参数为 输出参数 vector
2.2 通过遍历hashtable 获取每个题目,存储在 Question结构体数组中
3、提供获取单个题目的接口(id , & qustion)
3.1 通过题目编号 , 在hashtable中找到对应的 题目输出
#include "tools.hpp"
#include "oj_log.hpp"
//试题id 试题名称 试题路径 试题难度
typedef struct Question
{
std::string id_;
std::string name_;
std::string path_;
std::string star_;
}QUES;
class OjModel
{
public:
//加载试题
OjModel(){LoagQuestions("./config_oj.cfg");
//获取题目列表
bool GetAllQuestions(std::vector<Question>* ques);
//获取单个题目列表
bool GetOneQuestion(const std::string& id,std::string *desc,std::string *header,Question* ques);
//合并题目信息和用户代码
bool SplicingCode(std::string user_code,const std::string& ques_id,std::string* code);
private:
//加载试题
bool LoadQuestions(const std::string& configfile_path);
//获取题目的描述信息路径
std::string DescPath(const std::string& ques_path);
//获取该题目所包含的头文件以及实现类路径
std::string HeaderPath(const std::string& ques_path);
//测试用例以及main函数的入口路径
std::string TailPath(const std::string& ques_path);
private:
std::unordered_map<std::string ,Question> model_map_;
};
编译运行模块
1、编译
1.1 将用户提交的代码写到文件中去
1.2 fork子进程进行进程程序替换为g++程序,进行编译源码文件
1.3 获取编译结果写道便准输出文件中去或者写入到标准错误文件中去
2、 运行
2.1 如果代码走到这个阶段,说明一定产生可执行程序,fork子进程,让子进程进行进程程序替换,执行可执行程序
2.2 将程序的运行结果,保存到标准输出或者标准错中去
#include "oj_model.hpp"
class OjView
{
public:
//渲染html页面,并且将该页面返回给调用
static void ExpandAllQuestionshtml(std::string* html,std::vector<Question>& ques);
static void ExpandOneQuestion(const Question& ques,std::string& desc,std::string& header,std::string* html);
static void ExpandReason(const std::string& errorno,const std::string& reason,const std::string& stdout_reason,std::string* html);
};
工具模块
1.提供时间戳服务
2.提供写文件操作
3.提供读文件操作
4.提供URL解码操作
5.提供字符串分割
#pragma once
#include <string.h>
#include <sys/time.h>
#include <iostream>
#include <cstdio>
#include <string>
//当前实现的log服务也是在控制台进行输出
//输出的格式
//[时间 日志等级 文件:行号] 具体的日志信息
class LogTime
{
// 获取时间戳
// 通过gettimeofday 函数 把时间包装成为一个结构体返回
public:
static int64_t GetTimeStamp()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec;
}
//返回 年-月-日 时:分:秒
static void GetTimeStamp(std::string* TimeStamp);
};
//日志等级
//INFO WARNING ERROR FATAL DEBUG
const char* Level[] =
{
"INFO",
"WARNING",
"ERROR",
"FATAL",
"DEBUG",
};
enum LogLevel
{
INFO = 0,
WARNING,
ERROR,
FATAL,
DEBUG
};
inline std::ostream& Log(LogLevel lev, const char* file, int line, const std::string& logmsg);
#define LOG(lev, msg) Log(lev, __FILE__, __LINE__, msg)
//实现一个切割字符串的工具函数
class StringTools
{
public:
static void Split(const std::string input,const std::string& split_char,std::vector<std::string>* output);
};
/实现文件操作类
class FileOper
{
public:
static int ReadDataFromFile(std::string& filename,std::string* content);
static int WriteDataToFile(const std::string&filename,const std::string& Data);
};
//url解码
class UrlUtil
{
public:
static void PraseBody(const std::string& body,std::unordered_map<std::string,std::string>* pram);
private:
static unsigned char ToHex(unsigned char x) ;
static unsigned char FromHex(unsigned char x) ;
static std::string UrlEncode(const std::string& str);
static std::string UrlDecode(const std::string& str)
};
技术支持
搭建 HTTP 服务器
认识 cpp-httplib
轻量级的 c++ http_server 框架,安装简单, header only 风格.
啥是 header only 风格?
c++ 的第三方库的管理一直是一个非常僵硬的话题, 安装使用第三方库都非常麻烦.
因此现代 c++ 推崇 header only 风格, 即第三方库只提供一个 .h / .hpp 头文件, 包含头文件即可使用, 不需要
额外编译库, 也不需要增加额外的编译选项(例如 -I -L -l)。
具体使用方法参见官方文档即可。
但是这个库依赖 C++ 11 中的正则表达式。而 Centos7 自带的 gcc4.8 正则表达式有 bug. 需要升级 gcc 版本
使用C++当中的文件流来加载文件,并获取文件当中的内容
在C++中,对文件的操作是通过stream的子类fstream(file stream)来实现的,所以,要用这种方式操作文件,就必须加入头文件fstream.h。
fstream有两个子类:
ifstream(input file stream)和ofstream(outpu file stream),
ifstream默认以输入方式打开文件。
ofstream默认以输出方式打开文件。
试题模块中存储试题的数据结构是哪个?
map or undordered_map?选择unordered_map
map–>红黑数,有序的树形结构,查询的效率不高
undordered_map<ket,value>–>哈希表,无序,查询效率高,基本上查询的时候就是常数完成的
在c++中直接通过字符串拼接的方式构造 html
使用模板技术填充html页面–google ctemplate
可以是逻辑和界面分离,后台负责计算,在使用模板技术将计算后的值填充到预定义的html页面当中
模板类似于填空题 , 实现准备好一个 html把其中一些需要动态计算的数据挖个空留下来,
处理请求过程中,更具计算结果填写空。
两个部分
1.模板:定义界面真是的形式,预定义的html页面
2.数据字典:填充模板的数据
数据字典
片段(子字典) id name star
片段(子字典) id name star
四种标记
1.变量 {{变量名}}
2.片段 {{#片段名}}
3.包含 {{>模板名称}} 一个模板当中可以包含另外一个模板,对应的就是一个数据字典
4.注释 {{!}} 定义html页面模板的时候的注释
先创建 ctemplate 对象,借助对象完成填空
1、先创建一个 template 对象 ,这个是一个总的数据对象
2、循环的往这个对象中添加一些子对象
3、每一个子对象在设置一些键值对和模板中留下的{{}} 是要对应的
4、进行数据的替换,生产最终的html
源码转义:
R"()" C++11 引入的语法,原始字符串(忽略字符串中的转义字符)
Json数据格式–使用开源库JsonCpp
yum -y install epel-release
yum -y install jsoncpp-devel
jsonCpp主要包含三种类型的class:Value、Reader、Write。
Json::Value时jsonCpp中最基本、最重要的类,用于表示各种类型的对象。
Json::Writer 负责将内存中的Value对象转换成JSON文档,输出到文件或者是字符串中。
Josn::Reader用于读取,准确说是用于将字符串或者文件输入流转换为Json::Value对象的
jsonCpp中所有对象、类名都在namespace json中,使用时只要包含json.h即可。