在线OJ项目(二)
文章目录
一、回顾oj_server模块整体框架:
oj_server.cpp
:
#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()
{
using namespace httplib;
Server svr;
//1.获取题目列表接口
//2.获取单个题目接口
//3.服务器接收用户通过浏览器提交code接口
svr.listen("0.0.0.0", 19999);
return 0;
}
在在线OJ项目(一)当中已经完成了第一步:获取题目列表接口并返回给浏览器。
二、封装日志模块
封装日志模块方便记录日志和调试
1. 封装时间模块
gettimeofday
函数:
int gettimeofday(struct timeval *tv, struct timezone *tz);
timeval
结构体:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
time
:函数
time_t time(time_t *t);
localtime
:函数
struct tm *localtime(const time_t *timep);
tm
结构体:
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
oj_log.hpp
:
#pragma once
#include <string.h>
#include <sys/time.h>
#include <iostream>
#include <cstdio>
#include <string>
class LogTime
{
public:
//为后面区分不同用户文件做铺垫
static int64_t GetTimeStamp()
{
struct timeval tv;
gettimeofday(&tv, NULL);
//返回秒
return tv.tv_sec;
}
//返回 年-月-日 时:分:秒
static void GetTimeStamp(std::string* TimeStamp)
{
time_t SysTime;//秒
time(&SysTime);
struct tm* st = localtime(&SysTime);
char buf[30] = {'\0'};
snprintf(buf, sizeof(buf) - 1, "%04d-%02d-%02d %02d:%02d:%02d",
st->tm_year + 1900, st->tm_mon + 1, st->tm_mday,
st->tm_hour, st->tm_min, st->tm_sec);
TimeStamp->assign(buf, strlen(buf));
}
};
2. 封装日志等级
oj_log.hpp
:
//日志等级
//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)
{
std::string level_info = Level[lev];
std::string TimeStamp;
LogTime::GetTimeStamp(&TimeStamp);
//[时间 日志等级 文件:行号] 具体的日志信息
std::cout << "[" << TimeStamp << " " << level_info << " "
<< file << ":"
<< line << "]" << logmsg;
return std::cout;
}
#define LOG(lev, msg) Log(lev, __FILE__, __LINE__, msg)
注意
:
- Log函数必须是内联函数,否则拿到的永远都是oj_log.hpp所在的行号,我们封装的目的是为了更快的查错,每次都拿到oj_log.hpp所在的行号是没有任何意义的,而inline函数是在编译阶段展开,就可以拿到展开处的行号,就可以记录日志
三、获取单个题目
1. 在工具模块定义文件读写接口
既然要获取单个题目信息,就需要从对应题目的路径下获取题目的相关描述、代码头,所以先提供一个读写文件的操作:
tools.hpp
:
//实现文件操作的类
class FileOper
{
public:
//const std::string& filename:文件所在路径及文件名
//std::string* content:用来保存文件的内容
static int ReadDataFromFile(const std::string& filename, std::string* content)
{
std::ifstream file(filename.c_str());
if(!file.is_open())
{
LOG(ERROR, "Open file failed! filename is") << filename << std::endl;
return -1;
}
std::string line;
while(std::getline(file, line))
{
*content += line + "\n";
}
file.close();
return 0;
}
static int WriteDataToFile(const std::string& filename, const std::string& Data)
{
std::ofstream file(filename.c_str());
if(!file.is_open())
{
LOG(ERROR, "Open file failed") << filename << std::endl;
return -1;
}
file.write(Data.data(), Data.size());
file.close();
return 0;
}
};
2. 获取单个文件信息接口
我们指定了<a href="/question/{{id}}">
,即当用户点击某个具体的题目之后,浏览器应该跳转到对应题目的页面,但是还没有实现。
所以我们应该先在oj_model模块
提供一个获取单个题目信息的接口
oj_model.cpp
:
//const std::string& id:代表某个题的id
//std::string* desc:保存读取后文件的题目描述信息
//std::string* header:保存读取后文件的代码头部信息
//Question* ques:题号为id的题目路径
bool GetOneQuestion(const std::string& id, std::string* desc, std::string* header, Question* ques)
{
//1.根据id去查找对应题目的信息,最重要的就是这个题目在哪里加载
auto iter = model_map_.find(id);
//如果没找到直接return false;
if(iter == model_map_.end())
{
LOG(ERROR, "Not Found Question id is") << id << std::endl;
return false;
}
//iter->second.path_; + 文件名称(desc.txt header.cpp)
*ques = iter->second;
//加载具体的单个题目信息,从保存的路径上面去加载
//从具体的题目文件当中去获取两部分信息,描述, header头
int ret = FileOper::ReadDataFromFile(DescPath(iter->second.path_), desc);
if(ret == -1)
{
LOG(ERROR, "Read desc failed") << std::endl;
return false;
}
ret = FileOper::ReadDataFromFile(HeaderPath(iter->second.path_), header);
if(ret == -1)
{
LOG(ERROR, "Read desc failed") << std::endl;
return false;
}
return true;
}
private:
//这几个函数的作用就是把路径和文件名称组合在一起
// ./xxx/desc.txt
std::string DescPath(const std::string& ques_path)
{
return ques_path + "desc.txt";
}
// ./xxx/tail.cpp
std::string HeaderPath(const std::string& ques_path)
{
return ques_path + "tail.cpp";
}
// ./xxx/tail.cpp
std::string TailPath(const std::string& ques_path)
{
return ques_path + "tail.cpp";
}
注意:
- 在加载单个题目的时候,需要在对应题目的路径下增加
题目的描述desc.txt
和题目的代码头header.cpp
以及tail.cpp的main函数入口
3. 组织单个题目的预定义html页面
对于每个题目的html页面当中,我们应该提供两部分信息:题目详细描述 + 用户答题窗口
同样我们应该采用模版技术进行动态更新!
question.html
:
<html>
<head>
<title>在线oj</title>
</head>
<body>
<!--描述-->
<div>{{id}}.{{name}} {{star}}</div>
<div><pre>{{desc}}</pre></div>
<!--代码的编辑框-->
<div>
<!--action:代表向服务器提交-->
<!--method:代表提交方法-->
<!--textarea name:代表编辑框的名称-->
<!--rows=50 cols=150:代码编辑框的行列-->
<!--input type="submit":代表提交-->
<!--formenctype="appliaction/json":代表提交的数据格式-->
<form action="/question/{{id}}" method="POST">
<textarea name="code" rows=50 cols=150>{{header}}</textarea>
<br>
<!--<textarea name="stdin" rows=10 cols=150></textarea>-->
<input type="submit" formenctype="appliaction/json" value="Submit">
</form>
</div>
</body>
</html>
4. 对单个题目的预定义html页面进行填充渲染
oj_view.hpp
:
//id name star desc header ==> string html
//const Question& ques:单个题目的结构
//std::string& desc:单个题目的描述信息
//std::string& header:单个题目的代码头部信息
//std::string* html:渲染完成后的html页面
static void ExpandOneQuestion(const Question& ques, std::string& desc,
std::string& header, std::string* html)
{
ctemplate::TemplateDictionary dict("question");
dict.SetValue("id", ques.id_);
dict.SetValue("name", ques.name_);
dict.SetValue("star", ques.star_);
dict.SetValue("desc", desc);
dict.SetValue("header", header);
//./template/question.html:代表预定义html页面的路径
//ctemplate::DO_NOT_STRIP:代表逐行逐句进行填充
ctemplate::Template* tpl = ctemplate::Template::GetTemplate("./template/question.html", ctemplate::DO_NOT_STRIP);
//渲染
tpl->Expand(html, &dict);
}
5. 将单个题目的页面返回给浏览器
完整的获取单个题目并返回给浏览器的代码:
oj_server.hpp
:
//正则表达式
// \b:单词的分界
// *:匹配任意字符串
// \d:匹配一个个位数字
// \d+:匹配一个数字(一位或者多位)
// ():分组应用 (\d+)-(\d+)-(\d+)
// 12 - 13 - 14
// arr[0] = 12
// arr[1] = 13
// arr[2] = 14
//源码转义:特殊字符就按照特殊字符字面源码来编译
// R"(str)"
// 使用正则表达式匹配任意题目id
svr.Get(R"(/question/(\d+))", [&ojmodel](const Request& req, Response& resp){
// question/1
// 1.去试题模块去查找对应题号的具体的题目信息
// map当中 (序号 名称 题目的地址 难度)
std::string desc;
std::string header;
//从querystring当中获取id
//cout<<req.path.c_str()<<endl;path就是querystring:/question/(\d+)
//cout<<req.matches[0]<<endl;matches就是切割querystring字符串的
//matches[0]就是整个querystring
//matches[1]就是querystring中正则表达式部分(\d+)
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");
});