在线OJ项目铺垫
文章目录
一.boost库的字符串切割函数(split)
- split头文件
#include <boost/algorithm/string.hpp>
- 函数原型:
boost::split(type,select_list,boost::is_any_of(""),
boost::token_compress_on/off);
- 参数解释:
参数 | 参数解释 |
---|---|
type | 类型是std::vector< std::string > ,用来保存切割后的字符串 |
select_list | 待切割的字符串,可直接传入string 类型变量 |
boost::is_any_of | 告诉split函数按照什么字符进行切割,可以是单个字符,可以是字符串 |
boost::token_compress_on/off | 当是boost::token_compress_on时,对于在boost::is_any_of中传入的多个字符当成一个处理。当是boost::token_compress_off时,boost::is_any_of中有多少个字符就按多少个字符来处理 |
- 测试
#include "Header.hpp"
#include <boost/algorithm/string.hpp>
void TestSplit()
{
vector<string> strout;
string strstr = "https://www.baidu.com";
boost::split(strout,strstr,boost::is_any_of("./"),boost::token_compress_on);
for(auto e : strout)
{
cout<<e<<endl;
}
cout<<endl;
}
int main()
{
TestSplit();
return 0;
}
二.谷歌的html模版库ctemplate
1. ctemplate概述
在进行web开发时,使用传统的CGI方式,在C/C++程序里面既要处理逻辑,也要处理页面显示内容,会比较混乱。可以通过模板引擎,使得逻辑与显示的分离。
Google CTemplate就是其中一个开源的C++模板引擎。使用ctemplate不仅可以产生html,还可以生成xml,json等格式的内容。CTemplate是Google开源的一个使用简单但功能很强大的C++模板库。
2. 入门示例
模板库一般用来隔离数据和展现,这样可以减少数据和展现之间的耦合,使其可以相互独立变化,减少耦合,增加代码的复用,下面我们先看一个简单的例子,模板如下:
Hello {{NAME}},
You have just won ${{VALUE}}!
{{#IN_CA}}Well, ${{TAXED_VALUE}}, after taxes.{{/IN_CA}}
对应的C++代码是:
#include <cstdlib>
#include <iostream>
#include <string>
#include <ctemplate/template.h>
int main() {
ctemplate::TemplateDictionary dict("example");
int winnings = rand() % 100000;
dict["NAME"] = "John Smith";
dict["VALUE"] = winnings;
dict.SetFormattedValue("TAXED_VALUE", "%.2f", winnings * 0.83);
// For now, assume everyone lives in CA.
// (Try running the program with a 0 here instead!)
if (1) {
dict.ShowSection("IN_CA");
}
std::string output;
ctemplate::ExpandTemplate("example.tpl", ctemplate::DO_NOT_STRIP, &dict, &output);
std::cout << output;
return 0;
}
模板中{{NAME}}
和{{VALUE}}
对应的是变量
,在dict中通过dict[“NAME”]或dict的setValue的相关方法进行赋值;{{#IN_CA}}
和{{/IN_CA}}
之间的是一个section
,可以通过dict的ShowSection来控制是否要显示这个section
。
3. 循环
有的数据我们需要循环展示,比如一个table的多个tr,ctemplate本身不支持循环,需要C++来控制循环,核心思想是多次Insert Section
,示例模板如下:
{{#TEST_TABLE}}
<table>
{{#TEST_TABLE_ITEM}}
<tr>
<td>{{INDEX}}</td>
<td>{{NAME}}</td>
<td>{{AGE}}</td>
</tr>
{{/TEST_TABLE_ITEM}}
</table>
{{/TEST_TABLE}}
对于这个模板,我们先显示外面的这个Section,然后再循环多次Insert里面的子Section
就可以了,代码如下:
#include <cstdlib>
#include <iostream>
#include <string>
#include <ctemplate/template.h>
int main() {
ctemplate::TemplateDictionary dict("loop");
dict.ShowSection("TEST_TABLE");
for (int i = 0; i != 3; ++i) {
ctemplate::TemplateDictionary *item = dict.AddSectionDictionary("TEST_TABLE_ITEM");
item->SetFormattedValue("INDEX", "%d", i);
item->SetValue("NAME", "阿牛牛牛牛");
item->SetValue("AGE", "保密");
}
std::string output;
ctemplate::ExpandTemplate("loop.tpl", ctemplate::DO_NOT_STRIP, &dict, &output);
std::cout << output;
return 0;
}
4. 文件包含
对于一个通常的系统来说,一般都有多个模板文件,这多个模板文件之间,有很多相同的部分,比如有同样的header、footer,为了减少重复的代码,并且以后修改的话,只需修改一个地方,我们可以把公共的部分提取出来,然后其它的模板文件包含这个文件。ctemplate很好的支持了文件包含,对于ctemplate来说,包含的文件也是一个Section
,模板代码如下:
{{>HEADER}}
{{#TEST_TABLE}}
<table>
{{#TEST_TABLE_ITEM}}
<tr>
<td>{{INDEX}}</td>
<td>{{NAME}}</td>
<td>{{AGE}}</td>
</tr>
{{/TEST_TABLE_ITEM}}
</table>
{{/TEST_TABLE}}
注意第一行的{{>HEADER}}
,这是一个特殊格式的Section,用来表示文件包含。对应的C++代码如下:
#include <cstdlib>
#include <iostream>
#include <string>
#include <ctemplate/template.h>
int main() {
ctemplate::TemplateDictionary dict("loop");
ctemplate::TemplateDictionary *example = dict.AddIncludeDictionary("HEADER");
example->SetFilename("./header.tpl");
example->SetValue("TITLE", "测试");
dict.ShowSection("TEST_TABLE");
for (int i = 0; i != 3; ++i) {
ctemplate::TemplateDictionary *item = dict.AddSectionDictionary("TEST_TABLE_ITEM");
item->SetFormattedValue("INDEX", "%d", i);
item->SetValue("NAME", "阿牛牛牛牛");
item->SetValue("AGE", "保密");
}
std::string output;
ctemplate::ExpandTemplate("loop.tpl", ctemplate::DO_NOT_STRIP, &dict, &output);
std::cout << output;
return 0;
}
三.序列化反序列化之jsoncpp
1. jsoncpp概述
JSON全称为JavaScript ObjectNotation,它是一种轻量级的数据交换格式,易于阅读、编写、解析。jsoncpp是c++解析JSON串常用的解析库之一。
2. JsonCpp基础
jsonCpp主要包含三种类型的class:value、reader、write
。 jsonCpp总所有对象、类名都在namespace json中,使用时只要包含json.h即可。
3. Json::Value
Json::Value时jsonCpp中最基本、最重要的类,用于表示各种类型的对象
,jsoncpp 支持的对象类型可见 Json::ValueType 枚举值。
例:
Json::Value root;
root["status"] = 1; //新建一个key为status,赋予数值1
root["message"] = "OK"; //新建一个key为message,赋予字符串OK
root["array"].append("arr"); //新建一个key为array,类型为数组,对第一个元素赋值为字符串“arr”
root["array"].append(1234); // 为数组 key_array 赋值,对第二个元素赋值为:1234。
Json::ValueType type = root.type(); //获得root的类型
注:
- 跟C++ 不同,JavaScript 数组可以为任意类型的值,所以 jsoncpp 也可以。
- jsoncpp 还有一些其他同能,比如说设置注释、比较 json 大小、交换 json 对象等
- 在把value插入值后再输出来,输出的值是按字母表的顺序排列。
- Json::ValueType:value的类型,是一个枚举型
enum ValueType {
nullValue = 0, ///< 'null' value
intValue, ///< signed integer value
uintValue, ///< unsigned integer value
realValue, ///< double value
stringValue, ///< UTF-8 string value
booleanValue, ///< bool value
arrayValue, ///< array value (ordered list)
objectValue ///< object value (collection of name/value pairs).
};
4. Json::Writer
- Json::Writer负责将内存中的Value对象转换成JSON文档,输出到文件或者是字符串中。
- Json::Writer是一个纯虚类,不能直接使用,一般使用Json::Writer的子类:
Json::FasterWriter, Json::StyledWriter、Json::StyledStreamWriter
注:在新版中Json::FasterWriter, Json::StyledWriter、Json::Reader都被弃用,替代的是Json::StreamWriterBuilder、Json::CharReaderBuilder
- Json::FasterWriter:速度最快
- Json::StyledWriter:格式化后的json
5. Josn::Reader
用于读取,准确说是用于将字符串或者文件输入流转换为Json::Value对象的。
parse()函数
:
- 使用Json::Reader对json文件进行解析
bool parse(const std::string& document, Value& root, bool
collectComments = true);
参数
:
- document: 包含要读取的文档的UTF-8编码字符串
- root:(输出)Json::Value的对象
- 使用Json::Reader对json输入流(文件)进行解析
bool parse(std:stream& is, Value& root, bool collectComment = true); - 使用Json::Reader对字符串进行解析
bool parse(const char* beginDoc, const char* ednDoc, Value& root, bool
collectComment = true);
6. JsonCpp其他操作
- 判断key是否存在
bool Json::Value::isMember ( const char * key) const;
存在返回1,否则返回0
- 判断是否为null成员函数
注:Json::Value和C++中的map有一个共同的特点,就是当你尝试访问一个不存在的 key 时,会自动生成这样一个key-value默认为null的值对
。
- 得到所有成员
typedef std::vectorstd::string Json::Value::Members;
Value::Members Json::Value::getMemberNames ( ) const;
该函数的类型为一个string的vector
。
- 删除成员
Value Json::Value::removeMember( const char* key)
返回删除的值,或者null
7. 测试:
#include <iostream>
#include <fstream>
#include <string>
#include "json/json.h"
using namespace std;
int main()
{
const char* age;
Json::Value root;
Json::FastWriter fast_writer;
Json::StyledWriter style_writer;
Json::StyledStreamWriter stream_writer;
Json::Reader reader;
Json::Value json_object;
root["null"] = NULL; //注意此处在输出是显示为0,而非null
root["message"] = "OK";
root["age"] = 52;
root["array"].append("arr"); // 新建一个key为array,类型为数组,对第一个元素赋值为字符串“arr”
root["array"].append(123); // 为数组 key_array 赋值,对第二个元素赋值为:1234
Json::ValueType type = root.type(); //root的类型
cout << root.toStyledString() << endl; //格式化输出
cout << "root_type:" <<type << endl; //类型为7,即为类型为对象
// 写到本地文件
cout << "快速输出:" << endl;
string str = fast_writer.write(root);
cout << str << endl; //快速输出,紧凑型
ofstream ofs("fast_writer.json");
ofs << str;
ofs.close();
cout << "格式化输出:" << endl;
str = style_writer.write(root);
cout << str << endl; //格式化输出,排版型
ofs.open("style_writer.json");
ofs << str;
ofs.close();
// 对字符串解析
//const char* json_document = "{\"age\" : 12, \"name\" : \"weng\"}";
string json_document = "{\"age\" : 123, \"name\" : \"weng\"}";
if (!reader.parse(json_document, json_object)){
cout << "error" << endl;
return 0;
}
else{
cout <<"age:" <<json_object["age"] << " name" << json_object["name"] << endl;
}
// 对文件解析
ifstream ifs("E:\\demo\\JsonTest\\JsonTest\\example.json", ios::binary);
if (!reader.parse(ifs, json_object)){
cout << "open error" << endl;
ifs.close();
}else{
cout << "encoding:" << json_object["encoding"].asString() << endl;
cout << "age:" <<json_object["age"].asInt() << endl;
int num = json_object["plug"].size();
for (int i = 0; i < num; i++){
cout << json_object["plug"][i].asString() << " ";
}
ifs.close();
cout << endl;
}
// 判断key是否存在
bool is_age = root.isMember("age");
cout << "isMember:" <<is_age << endl;
// 判断是否为null
//bool is_null = root["null"].isNull(); //注意此处被赋值为0,而非null
bool is_null = json_object["null"].isNull();
cout << "is_null:" << is_null << endl;
// 得到所有的key
for (auto elem : root.getMemberNames()){
cout << elem << " ";
}
cout << endl;
// 删除成员
cout << "remove_age:" << root.removeMember("age") << endl;
system("pause");
return 0;
}
四.服务器搭建cpp-httplib
1. cpp-httplib:一个header-only的跨平台HTTP/HTTPS服务器和客户端C++库
2. Server Example
#include <httplib.h>
int main(void)
{
using namespace httplib;
Server svr;
svr.Get("/hi", [](const Request& req, Response& res) {
res.set_content("Hello World!", "text/plain");
});
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
auto numbers = req.matches[1];
res.set_content(numbers, "text/plain");
});
svr.Get("/stop", [&](const Request& req, Response& res) {
svr.stop();
});
svr.listen("localhost", 1234);
}
Post, Put, Delete and Options
methods are also supported.
3. 将套接字绑定到多个接口和任何可用端口
int port = svr.bind_to_any_port("0.0.0.0");
svr.listen_after_bind();
4. 静态文件服务器Static File Server
svr.set_base_dir("./www");
5. Logging
svr.set_logger([](const auto& req, const auto& res) {
your_logger(req, res);
});
6. 差错处理器Error Handler
svr.set_error_handler([](const auto& req, auto& res) {
const char* fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>";
char buf[BUFSIZ];
snprintf(buf, sizeof(buf), fmt, res.status);
res.set_content(buf, "text/html");
});
7. ‘multipart/form-data’ POST data
svr.Post("/multipart", [&](const auto& req, auto& res) {
auto size = req.files.size();
auto ret = req.has_file("name1"));
const auto& file = req.get_file_value("name1");
// file.filename;
// file.content_type;
auto body = req.body.substr(file.offset, file.length));
})
Client Example
8. GET
#include <httplib.h>
#include <iostream>
int main(void)
{
httplib::Client cli("localhost", 1234);
auto res = cli.Get("/hi");
if (res && res->status == 200) {
std::cout << res->body << std::endl;
}
}
9. 使用HTTP头信息获取GET with HTTP headers
httplib::Headers headers = {
{ "Accept-Encoding", "gzip, deflate" }
};
auto res = cli.Get("/hi", headers);
10. 使用内容接收器获取GET with Content Receiver
std::string body;
auto res = cli.Get("/large-data", [&](const char *data, size_t len) {
body.append(data, len);
});
assert(res->body.empty());
11. POST
res = cli.Post("/post", "text", "text/plain");
res = cli.Post("/person", "name=john1¬e=coder", "application/x-www-form-urlencoded");
12. POST with parameters
httplib::Params params;
params.emplace("name", "john");
params.emplace("note", "coder");
auto res = cli.Post("/post", params);
or
httplib::Params params{
{ "name", "john" },
{ "note", "coder" }
};
auto res = cli.Post("/post", params);
13. POST with Multipart Form Data
httplib::MultipartFormDataItems items = {
{ "text1", "text default", "", "" },
{ "text2", "aωb", "", "" },
{ "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" },
{ "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" },
{ "file3", "", "", "application/octet-stream" },
};
auto res = cli.Post("/multipart", items);
14. PUT
res = cli.Put("/resource/foo", "text", "text/plain");
15. DELETE
res = cli.Delete("/resource/foo");
16. OPTIONS
res = cli.Options("*");
res = cli.Options("/resource/foo");
注意g ++ 4.8无法构建此库,因为<regex>g ++ 4.8中的损坏
!!!
五.在线OJ项目整体框架
1. 在线OJ项目需求
:实现一个在线判题系统,用户通过浏览器编写并提交代码,通过网络传输,将代码上传到服务器,服务器对用户提交对代码进行编译,并把编译结果返回给用户。
2. 客户端和服务器端的工作流程
:
3. 服务器端的工作模块
:
oj_server模块
:专门处理http请求。比如收到客户端请求所有题目列表的请求,服务器就会调用试题模块完成题目列表的加载并返回给客户端。如果收到客户端提交的代码,就需要调用编译运行模块,对提交的代码进行编译并返回运行结果试题模块
:首先服务器需要处理客户端发送过来的获取题目列表的请求,所以服务器端需要对试题列表进行加载编译运行模块
:对用户通过网络提交的代码进行编译运行处理日志模块
:对程序的状态进行跟踪,方便调试工具模块
:避免多次编写重复的代码,提高代码的复用率。如打开关闭文件等
4. 需求细分
:
- 客户端在浏览器当中展示试题列表(oj_server模块,试题模块)
- 客户端可以选择题目进行作答(oj_server模块,试题模块)
- 客户端提交代码到服务器(oj_server模块)
- 服务器端针对客户端提交的代码进行编译(编译运行模块)
- 服务器端运行客户端的程序(编译运行模块)
- 服务气端对运行结果进行打包(编译运行模块)
- 服务器端将打包数据发送给客户端(oj_server模块)
5. 每个模块的功能简介
:
oj_server模块
:提供http服务,串联试题模块和编译运行模块。
- 获取题目列表
- 提交选中的题目
- 根据用户选中的题目,把该题的题目描述和代码模版
试题模块
:
- 从文件(配置文件)当中加载题目
1. 约束配置文件中对题目描述的格式(如果不约束,从文件当中加载文件就没有规则):题目编号(id) 题目名称 题目详情信息的路径 题目的难度信息
。如:1 回文数 ./oj_data/1 简单。
2. 加载每个题目的配置文件,获取完整题目信息并保存在数据结构当中(这个路径一定要对
)
3. 针对每一个题目,还需要根据路径加载相关信息:desc.txt
:题目的描述信息。header.cpp
:存放的是该题目的头文件及实现类。tail.cpp
:存放测试的用例及main函数的入口 - 提供获取所有题目列表的接口
给oj_server模块提供一个获取所有题目列表的接口,展示给用户 - 提供获取单个题目的接口
给oj_server模块提供一个获取单个题目描述和作答的接口,展示给用户
编译运行模块
:将用户提交的代码写入到文件当中编译运行源文件。
-
编译
1. 将用户提交的代码写入到文件
2. 再fork出子进程,进行进程替换为g++程序,编译原码文件。
3. 获取编译结果,写入到文件当中(标准输出/标准错误)
-
运行
- 代码走到运行阶段,说明已经编译出来可执行程序,
- fork出子进程,让子进程进行进程程序替换,替换成编译出来的可执行程序
- 将可执行程序的运行结果输入到标准输出或标准错误文件当中
工具模块
:
-
提供时间戳服务:为了区分不同用户提交的代码。当用户将代码写入到文件当中的时候,使用时间戳来进行区分。
-
提供文件写操作
-
提供读文件操作:
我们可以提供一个一次读取一行的函数,但是前面我们对题目进行约束,题目编号(id) 题目名称 题目详情信息的路径 题目的难度信息
,我们需要对每个信息保存到数据结构当中。所以需要提供一个数据切割对接口 -
提供URL解码操作