【C++项目】负载均衡在线OJ系统-2
文章目录
- oj_server模块编写
- oj_server框架的搭建
- -oj_server/oj_server.cpp 路由框架
- oj_model模块编写
- 题目信息设置
- v1.文件版本
- -common/util.hpp boost库spilt函数的使用
- -oj_server/oj_model_file.hpp 文件版本model编写
- v2.mysql数据库版本
- 1.mysql创建授权用户、建库建表录入操作:
- 2.mysql connect cpp操作
- -oj_server/oj_model_mysql.hpp mysql数据库版本model编写
- oj_control模块编写
- 1.获取题库
- -oj_server/oj_control.hpp 获取题库渲染网页返回
- 2.获取单个题目
- -oj_server/oj_control.hpp 获取单个题目渲染网页返回
- 3.判题
- -主机对象
- -oj_server/oj_control.hpp 主机对象设计
- -负载均衡选择
- -oj_server/oj_control 负载均衡选择主机
- oj_view模块编写
- ctemplate库的配置和使用
- -oj_server/oj_view.hpp 网页渲染
- -oj_server/makefile 顶层makefile的编写
- 3.扩展内容
- 增加测试运行
- 增加登录模块
- -oj_server/oj_user.hpp 登录管理
oj_server模块编写
oj_server实际上就是搭建一个小型网站。向后连接题库文件、编译运行服务,向前提供路由服务,渲染题库页面、单个题页面…
那么此网站具体提供的能力如下:
1.获取题库首页,能够全部题目。
2.获取单个题目信息,能够编辑代码。
3.能够上传判题得到结果。
现在,为了将数据逻辑和界面进行分离,我们利用MVC设计模式(百度百科:MVC框架)对我们的oj_server进行设计。
MVC的设计模式
M:model 业务模型,通常是逻辑和数据交互的模块。本项目中则是对题库的加载和访问(全部/单个),版本存在两个:文件版本和mysql数据库版本。oj_model
V:view 用户界面,通常是拿到数据,构建、渲染网页展示给用户的(浏览器)。本项目中需要构建的网页有:index首页、all_questions题库、one_question单个题目。oj_view
C:control 控制器,我们的业务核心逻辑,通过路由根据控制器执行相应的功能。oj_control
综上,我们的MVC图可以如下展示出来:
oj_server框架的搭建
-oj_server/oj_server.cpp 路由框架
那么,现在我们可以利用cpp-httplib将整个服务的路由写好,之后替换为控制器为我们实现逻辑控制。
创建好Server后,提供如下的路由:
int main()
{Server svr; // 服务器对象
}
1.首先,服务器需要提供获取题目列表的路由。用户获取数据,自然是Get方法,规定/all_questions即访问题目列表。
// 1.用户获取题目列表数据svr.Get("/all_questions", [&ctl](const Request& req, Response& resp){// 扩至模块返回后构建渲染网页返回resp});
2.其次,服务器需要提供获取单个题目的路由。因为是单个题目,那么需要获取到题目编号,需要引入正则表达式获取:(\d+),利用请求对象的matches[1]可以获取。因为字符串中存在了反斜杠,可以利用R"()"-C++11的特性转换为原始字符串不至于解析为\d。/one_question/(\d+)
// 2.用户获取单题题目内容svr.Get(R"(/question/(\d+))", [&ctl](const Request& req, Response& resp){ // (\\d+是正则表达式)std::string number = req.matches[1]; // 1对应的就是获取正则表达式中的内容// 控制模块返回结果构建渲染单个题目的网页返回resp});
1. svr.Get 方法
Get 是 Server 类的一个成员函数,用于注册一个处理GET请求的路由。
第一个参数是字符串 R"(/question/(\d+))",表示这个处理器将处理所有发送到路径 /question/<数字> 的GET请求。
R"…" 是C++11中的原始字符串字面量,用于定义字符串时避免转义字符的处理。
(\d+) 是一个正则表达式,表示匹配一个或多个数字。这部分数字将被提取出来,用于后续处理。
第二个参数是一个回调函数,用于定义当请求到达时服务器应该如何响应。
[&ctl] 是lambda表达式的捕获列表,表示将外部变量 ctl 按引用捕获到lambda表达式中,以便在回调函数中使用。
2. 回调函数
[&ctl](const Request& req, Response& resp) { ... }
这是一个lambda表达式,用于处理请求。
它捕获了外部变量 ctl(可能是一个控制模块对象),并接收两个参数:
const Request& req:表示客户端发送的HTTP请求对象。
Response& resp:表示服务器将要发送给客户端的HTTP响应对象。
3. 提取正则表达式匹配的内容
std::string number = req.matches[1];
req.matches 是一个字符串数组,用于存储正则表达式匹配的结果。
req.matches[0] 包含整个匹配的路径(例如 /question/123)。
req.matches[1] 包含第一个括号中匹配的内容(例如 123)。
这行代码从 req.matches[1] 中提取出匹配的数字(题目编号),并将其存储到变量 number 中。
4. 正则表达式的使用
(\d+) 是一个正则表达式,用于匹配一个或多个数字。
\d 表示数字字符(0-9)。
+表示匹配前面的字符一次或多次。
在路径 /question/(\d+) 中,(\d+) 用于捕获路径中的数字部分,例如:
如果请求路径是 /question/123,则 req.matches[1] 的值为 “123”。
如果请求路径是 /question/456,则 req.matches[1] 的值为 “456”。
5. 总结
这行代码的作用是:
- 注册了一个处理GET请求的路由 /question/<数字>。
- 使用正则表达式 (\d+) 匹配路径中的数字部分,并将其提取到变量 number 中。
- 根据提取的题目编号,通过控制模块获取题目内容,并构建响应返回给客户端。
这种动态路由的处理方式非常适合用于处理带有参数的请求,例如获取特定编号的题目内容。
3.最后,用户需要判题功能,就要利用到judge,也需要题目编号,因为是提交代码获取结果,应该是有Post方法:/judge/(\d+)
// 3.用户获取单体解析结果svr.Post(R"(/judge/(\d+))", [&ctl](const Request& req, Response& resp){std::string number = req.matches[1]; // 1对应的就是获取正则表达式中的内容// 控制模块返回结果的json串,返回给resp});
4.设置根目录,其中index.html为首页网页,并且添加服务器的ip和端口,默认定死为0.0.0.0和8080。(端口可以自行设置哦,不冲突即可)
// 设置根目录svr.set_base_dir("./wwwroot");// 监听全体网卡,端口号固定8080svr.listen("0.0.0.0", 8080);
oj_model模块编写
用户需要的自然是题目数据,那么我们首先应该将数据存储的地方先弄好。
数据存储我们设计两个版本,一个文件版本,一个mysql数据库版本。对应不同的版本model数据交互细节不一样(文件操作/CPP-mysql connect),但是接口一致。
题目信息设置
在谈版本之前,我们先谈谈题目需要什么信息,并且如何进行判题。
在常见的oj中,我们经常见到题目提供给我们一部分代码,我们只需要在这里补全即可。比如如下的示例:
class UnusualAdd {
public:int addAB(int A, int B) {}
};
比如另类加法(即叫你不使用+完成A和B的相加)oj中,用户只需要在这里面编写代码即可。那么如何判题呢?
我们知道,一个C/C++程序运行,首先会找到main函数入口,那么我们的判题就可以在此处编写测试用例运行不就好了嘛:
#ifndef COMPILE_ONLINE
#include "header.cpp" // 条件编译,引入头文件只是为了不报错和语法提示,之后会进行拼接
#endifvoid test1(UnusualAdd& use1)
{// 测试用例1int sum = use1.addAB(1, 1);if (sum == 2){cout << "测试用例1通过!" << endl; }else cout << "测试用例1没有通过"<< "1 + 1" << endl;
}void test2(UnusualAdd& use2)
{// 测试用例2int sum = use2.addAB(1, -1);if (sum == 0){cout << "测试用例2通过!" << endl; }else cout << "测试用例2没有通过" << "1 + (-1)" << endl;
}int main()
{UnusualAdd ues;test1(ues);test2(ues);return 0;
}
加入条件编译就是为了编写的时候不会出现报错(两个文件,注意编译运行模块编译的时候加上-D选项将include去掉)。上面只是粗略的展示,真实的测试用例编写过程复杂的很多。
所以,在后端逻辑控制的时候,只需要将用户编写的code数据和上面的测试用例数据拼接到一起就形成了一份完整的代码发送给编译运行服务器进行判题功能了。
题目还存在空间、时间限制,以及题目编号、题目标题、难度、描述等设置。
题目信息:
1.题目编号 number
2.题目标题 title
3.题目难度 star
4.题目描述 desc
5.时间、空间要求 cpu_limit mem_limit
6.用户提交代码 header
7.测试用例代码 tail
v1.文件版本
首先再oj_server目录下建好questions目录。
根据题目细节信息,我们每个题目是需要多份文件的:一份题目描述、两份cpp文件的(header和tail),那么我们需要一份question.list 每一行保存一个题目的编号、标题、难度、时间空间要求,以及存放对应题目三个文件的目录路径。
比如:
#编号 名称 难度 cpu mem 路径(相对路径为oj_server,不在此目录下请带全路径) question.list
1 另类加法 简单 3 40000 ./questions/1
2 把字符串转换为整数 简单 1 70000 ./questions/2
注意上面的时间限制单位为s,空间限制为kb(转换为mb可以在渲染网页view模块去做,也可以直接在描述文件中进行描述)。
而一个./question/1的结构如下:
这样一个题目的信息我们就录好了。等待model的数据交互。
model我们需要向外提供题目的信息,那么我们可以设计一个question的结构体,里面存放的就是题目的细节信息,总共8个。然后再创建此对象的时候,我们根据question.list的路径读取上来,通过ReadFile-先前提供的读取文件的接口读取另外三份文件。只不过需要注意读取配置文件的时候按行读取需要进行分割,直接写的话也能写,较为麻烦,我们可以使用boost开发库(C++准标准库)进行字符串切割。
读取完题目信息后我们利用哈希map根据题目编号进行存储(方便后续单个题目的取出)。存储完后向外提供all_question接口,外部传入vector数组进行保存(外部可以利用此进行排序);one_question接口。获取的单个对象均为question对象,方便取出其中的题目信息。
-common/util.hpp boost库spilt函数的使用
因为是字符串切割,我们放在StringUtil字符串工具类下:
关于boost库的安装:sudo yum install -y boost-devel; 头文件路径为<boost/algorithm/string.hpp>
// 字符串相关工具类class StringUtil{public:// 切割字符串,保存入vector中// src:输入 target:输出 op:分隔符static void SpiltString(const std::string& src, std::vector<std::string>& target, std::string op){boost::split(target, src, boost::is_any_of(op), boost::algorithm::token_compress_on); // 数组、遇到对应分隔符全部切割 压缩(切割出来的空串删除)}};
这一行是函数的核心逻辑,调用了 Boost 库的 split 函数来实现字符串的切割。
boost::split:这是 Boost 库提供的一个函数,用于将一个字符串按照指定的分隔符切割成多个子字符串,并将结果存储到一个容器中。
target:这是函数的输出参数,表示切割后的子字符串将被存储到这个向量中。
src:这是函数的输入参数,表示需要被切割的原始字符串。
boost::is_any_of(op):这是一个谓词(predicate),用于指定分隔符。boost::is_any_of 表示分隔符可以是 op 中的任意字符。例如,如果 op 是 " ,.",那么字符串中的空格、逗号和句号都会被当作分隔符。
boost::algorithm::token_compress_on:这是一个标志,表示在切割时会压缩空白字符。具体来说,如果连续出现多个分隔符,它们会被当作一个分隔符处理,并且不会在结果中生成空字符串。
-oj_server/oj_model_file.hpp 文件版本model编写
整体代码: 含详细注释。
#ifndef __OJ_MODEL_HPP__
#define __OJ_MODEL_HPP__
// OJ题库与数据交互
#include <iostream>
#include <unordered_map>
#include <string>
#include <fstream>
#include <cstring>#include "../common/util.hpp"
#include "../common/log.hpp"// 文件版本
namespace ns_model
{using namespace ns_util;using namespace ns_log;// 每一个题目独享的内存细节struct Question{std::string number; // 题目编号std::string title; // 题目标题std::string star; // 题目难度int cpu_commit; // 运行时间限制Sint mem_commit; // 内存占用限制KBstd::string desc; // 文件描述std::string header; // 用户预设代码std::string tail; // 测试代码};//定义了一个类 Model,用于管理题库。class Model{//questions_path 是一个常量字符串,表示题库文件的路径。const std::string questions_path = "./questions/question.list";private:std::unordered_map<std::string, Question> _map;//定义了一个私有成员变量 _map,类型为 std::unordered_map。它用于存储题库信息,键是题目的编号(std::string),值是 Question 结构体。public:Model(){// 题库信息加载入内存assert(LoadQuestionlist(questions_path));}//构造函数的作用是初始化 Model 对象时,加载题库信息到内存中。它调用了 LoadQuestionlist 方法,并使用 assert 确保加载成功。如果加载失败,程序会终止。~Model(){}// 开始加载题库入内存bool LoadQuestionlist(const std::string& path) // 题库文件questions的路径{std::ifstream in(path);if (!in.is_open()){// 此时致命错误-影响全部,检查文件路径LOG(FATAL) << "题库配置文件未能打开,请检查相关路径或者文件是否存在" << "\n";return false;}std::string line;while (std::getline(in, line)){if (line[0] == '#') continue; // 注释行跳过// 对于配置的一行,我们需要进行字符串分割//使用 StringUtil::SpiltString 将每行内容按空格分割成多个字符串,并存储到 target 中。如果分割后的字符串数量不等于6,则记录警告日志并跳过当前行。std::vector<std::string> target;StringUtil::SpiltString(line, target, " ");if (target.size() != 6){// 此时当前配置行存在误输,警告LOG(WARING) << line[0] << "编号配置文件出错" << "\n"; continue;}//根据分割后的字符串,初始化一个 Question 对象 q。atoi 函数用于将字符串转换为整数。Question q;q.number = target[0];q.title = target[1];q.star = target[2];q.cpu_commit = atoi(target[3].c_str());q.mem_commit = atoi(target[4].c_str());//从指定路径读取题目描述、预设代码和测试代码文件。如果任意一个文件读取失败,则记录警告日志并跳过当前题目。q.desc = FileUtil::ReadFile(target[5] + "/desc.txt", true);q.header = FileUtil::ReadFile(target[5] + "/header.cpp", true);q.tail = FileUtil::ReadFile(target[5] + "/tail.cpp", true);if (q.desc == "" || q.header == "" || q.tail == ""){// 当前记录文件读取失败!或者无记录,那么就无效,不纳入统计LOG(WARING) << "检查编号" << line[0] << "文件目录下文件是否录入成功或者填写信息" << "\n"; LOG(DEBUG) << target[5] << "\n";continue;}//将初始化好的 Question 对象存储到 _map 中,键为题目的编号。_map[target[0]] = q; // 保存记录}in.close();LOG(INFO) << "题库录入内存成功!" << "\n";return true;}// 定义了一个方法 GetAllQuestions,用于获取所有题目的信息。参数 v 是一个引用,用于存储所有题目的信息。//如果 _map 为空,则记录错误日志并返回 false。否则,遍历 _map,将所有题目的信息添加到 v 中。bool GetAllQuestions(std::vector<Question>& v){if (_map.empty()){// 当前内存题目信息为空,获取失败LOG(ERROR) << "当前用户获取题目信息失败,内存中不存在题目信息记录" << "\n";return false;}for (auto& s : _map){v.push_back(s.second);}return true;}// 获取单个题目信息,给我number// 已经提供日志差错处理//定义了一个方法 GetOneQuestion,用于根据题目编号获取单个题目的信息。参数 number 是题目的编号,q 是一个引用,用于存储题目的信息。bool GetOneQuestion(const std::string& number, Question& q){//使用 _map.find 查找指定编号的题目。如果未找到,则记录错误日志并返回 false。否则,将找到的题目信息赋值给 qauto it = _map.find(number);if (it == _map.end()){// 提供的number不存在当前题库中LOG(ERROR) << "当前用户获取题目信息失败,内存中不存在此题目信息记录" << "\n";return false;}q = it->second;return true;}};
}#endif
v2.mysql数据库版本
数据库版本就非常简单了。我们确定需要存储的题目信息一共八个。
首先,我们需要给oj_server进行数据库连接创建一个用户(用户管理),然后利用root创建数据库oj,给创建的用户进行赋权创建表结构,录入数据,程序便就可以进行连接操作编写model模块了。
1.mysql创建授权用户、建库建表录入操作:
create user 'oj_client'@'%' identified by '123456'; --创建用户,任意ip登录,密码--root建库
create database oj;
--root赋权
grant all on oj.* to 'oj_client'@'%'; -- 赋予oj_clinet@%用户oj数据库的所有权限--创建question表,表结构设计出来
use oj;
create table oj_questions(number int primary key auto_increment comment '题目的编号',title varchar(128) not null comment '题目的标题',star char(2) not null, `desc` text not null comment '题目的描述',header text not null comment '预设给用户看的代码',tail text comment '对应题目的测试用例代码',cpu_limit int default 1 comment '对应题目的时间限制',mem_limit int default 50000 comment '对应题目的最大空间'
)engine=innoDB default charset=utf8;
录入我们可以利用insert,但是大文本录入太麻烦了,我们可以借助可以连接mysql的第三方工具,我这里以Navicat Premium 16为例:
2.mysql connect cpp操作
当我们数据录入成功后,model模块想要获取数据就必须让C++/C程序能够连接上mysql数据库。
我们需要访问网站MySQL :: Download MySQL Connector/C (Archived Versions)下载mysql的数据开发包:
下载到本地后上传至云服务器解压,会存在include文件和lib文件分别存放的是头文件和库文件。头文件可放入系统路径下:/user/include,库文件放入:/lib64/下即可。放入系统路径的话记得编译带上选项 -lmysqlclient。当然,也可以不用cp到系统路径下,可以利用软连接ln -s链接到oj_server目录下即可,之后编译指明-I(头文件路径)-L(库文件路径)即可。
mysql连接编码很简单,首先创建mysql句柄,然后根据数据库用户、信息、数据库进行数据库连接,连接成功后mysql_query执行sql命令返回结果,对结果进行解析提取数据即可。需要注意差错处理:
-oj_server/oj_model_mysql.hpp mysql数据库版本model编写
这段代码实现了一个简单的OJ系统模型,通过MySQL数据库存储和管理题目信息。主要功能包括:
- 连接到MySQL数据库。
- 执行SQL查询。
- 解析查询结果并存储到Question对象中。
- 提供获取所有题目和单个题目信息的功能。
#ifndef __OJ_MODEL_HPP__
#define __OJ_MODEL_HPP__
// OJ题库与数据交互
#include <iostream>
#include <unordered_map>
#include <string>
#include <fstream>
#include <cstring>
#include "./mysql-connect/include/mysql.h"// 引用c-mysql-connect#include "../common/util.hpp"
#include "../common/log.hpp"// mysql版本
namespace ns_model
{using namespace ns_util;using namespace ns_log;// 每一个题目独享的内存细节struct Question{std::string number; // 题目编号std::string title; // 题目标题std::string star; // 题目难度int cpu_commit; // 运行时间限制Sint mem_commit; // 内存占用限制KBstd::string desc; // 文件描述std::string header; // 用户预设代码std::string tail; // 测试代码};const std::string oj_table_name = "oj_questions";const std::string oj_host = "自己的公网ip";const std::string oj_user = "oj_client";const std::string oj_password = "123456";const std::string oj_db = "oj";const unsigned int oj_port = 3306;class Model{public:Model(){}~Model(){}// 执行sql语句处bool QueryMySql(const std::string& sql, std::vector<Question>& v){// c连接mysql库,创建mysql对象-初始化mysql句柄MYSQL* oj_client = mysql_init(nullptr);// 连接数据库,连接失败返回nullptr//使用mysql_init和mysql_real_connect连接到MySQL数据库。if (nullptr == mysql_real_connect(oj_client, oj_host.c_str(), oj_user.c_str(), oj_password.c_str(), oj_db.c_str(), oj_port, nullptr, 0)){// 表示连接数据库失败LOG(FATAL) << "数据库连接失败!请尽快联系数据库管理员....." << "\n";return false;}// 连接成功后先执行编码格式// 设置字符集为UTF-8,避免编码问题。mysql_set_character_set(oj_client, "utf8");// 连接成功后执行语句//执行SQL语句,获取查询结果,并逐行解析数据。int result = mysql_query(oj_client, sql.c_str());if (result != 0){// 执行失败LOG(WARING) << "当前sql执行失败: " << sql << " \n";return false;}// 执行成功,提取数据MYSQL_RES* res = mysql_store_result(oj_client);// 解析数据int rows = mysql_num_rows(res); // 获取行数int fields = mysql_num_fields(res); // 获取列数// 读取每一个元组,提取每一个元组的九个属性for (int i = 0; i < rows; ++i){MYSQL_ROW line = mysql_fetch_row(res);Question q;q.number = line[0];q.title = line[1];q.star = line[2];q.cpu_commit = atoi(line[3]);q.mem_commit = atoi(line[4]);q.desc = line[5];q.header = line[6];q.tail = line[7];v.push_back(q);}// LOG(DEBUG) << "正常访问数据库成功..." << "\n";return true;}// 获取当前题目列表信息//GetAllQuestions方法用于获取数据库中所有题目的信息。//构造SQL语句SELECT * FROM oj_questions,并调用QueryMySql方法执行查询。bool GetAllQuestions(std::vector<Question>& v){std::string sql = "select * from ";sql += oj_table_name;return QueryMySql(sql, v);}// 获取单个题目信息,给我number// 已经提供日志差错处理bool GetOneQuestion(const std::string& number, Question& q){//构造SQL语句SELECT * FROM oj_questions WHERE number=number,并调用QueryMySql方法执行查询。std::string sql = "select * from ";sql += oj_table_name;sql += " where number=";sql += number;std::vector<Question> v;if (QueryMySql(sql, v)){//如果查询结果中只有一条记录,则将该记录赋值给Question& q。if (v.size() == 1){// 只能存在一个记录,因为现在是查某个具体的题目q = v[0];return true;}}return false;}};
}#endif
oj_control模块编写
oj_control逻辑控制是oj_server中的核心。http请求中的路由通过control模块完成对应的功能。
根据路由信息,我们首先是需要能够获得题库并且构建渲染成网页的功能,由control-allquestions提供;其次我们也需要获得单个题目并且构建渲染成网页的功能;最后能够通过用户上传的json串完成判题的功能返回结果json串。
model功能我们已经完成了,view功能基本属于前端功能,后续简单介绍即可。control更多的是将这两种联合控制起来,完成逻辑控制。
1.获取题库
model中已经为我们提供了获取整个题目的方法,对象为Question,只需要传入一个vector即可。我们利用其传入view中的题库渲染方法(AllExpandHtml),获取渲染后的网页返回,由http发送给客户端(浏览器渲染)。
需要注意的点就是原本获取的题目可能没有排序,我们利用sort函数根据question中的number转换为数字进行排序即可。
-oj_server/oj_control.hpp 获取题库渲染网页返回
这段代码实现了一个简单的控制类 Control,其核心功能是从题库中获取题目信息,对题目进行排序,并将排序后的题目信息渲染成网页内容。代码结构清晰,通过 Model 和 View 对象分别处理数据和渲染逻辑,符合面向对象的设计原则。
namespace ns_contol
{class Control{private:Model _model; // 数据对象//_model 是一个 Model 类型的对象,用于处理与数据相关的操作(例如从题库中获取题目信息)。View _view; // 渲染对象//_view 是一个 View 类型的对象,用于处理与渲染相关的操作(例如将题目信息渲染成网页)。public:Control(){}~Control(){}// 从题库中获取题目信息(model),渲染成网页返回(view)//返回值是 bool 类型,表示操作是否成功(true 表示成功,false 表示失败)。//参数 std::string& html 是一个引用参数,表示函数将通过这个参数返回渲染后的网页内容。bool AllQuestions(std::string& html){std::vector<Question> v;//用于存储一组 Question 类型的对象。这里用于存储从题库中获取的题目信息。//如果获取成功,则进入 if 代码块。if (_model.GetAllQuestions(v)){// 对数组进行排序std::sort(v.begin(), v.end(), [](Question& q1, Question& q2){return atoi(q1.number.c_str()) < atoi(q2.number.c_str());});// 成功,根据获取的信息渲染网页//调用了 _view 对象的 AllExpandHtml 方法,将排序后的题目信息 v 渲染成网页内容,并存储到 html 参数中。_view.AllExpandHtml(v, html); // 后面完成return true;}else{// 获取题目列表失败return false;}}}
}
排序的规则是通过一个匿名函数(lambda 表达式)定义的:
q1 和 q2 是 Question 类型的引用参数,分别表示两个需要比较的题目对象。
q1.number.c_str() 和 q2.number.c_str() 获取题目编号的 C 风格字符串。
atoi 函数将字符串转换为整数。
比较两个整数的大小,返回 true 表示 q1 的编号小于 q2 的编号。
在对应oj_server.hpp 获取题库路由就可以这样调用控制模块:(注意返回网页格式的类型)
这段代码定义了一个 HTTP 路由处理器,当用户通过 HTTP GET 请求访问 /all_questions 路径时,会触发一个回调函数。回调函数通过 Control 类的 AllQuestions 方法获取题目列表,并将结果以 HTML 格式返回给客户端。
// 1.用户获取题目列表数据svr.Get("/all_questions", [&ctl](const Request& req, Response& resp){std::string html;//定义了一个 std::string 类型的变量 html,用于存储渲染后的 HTML 内容。ctl.AllQuestions(html);//调用了 ctl 对象的 AllQuestions 方法。//ctl 是一个 Control 类的对象,AllQuestions 是 Control 类的一个成员函数。//html 是 AllQuestions 方法的参数,通过引用传递。AllQuestions 方法会将题目列表渲染成 HTML 格式,并存储到 html 变量中。resp.set_content(html, "text/html;charset=utf8");//调用了 resp 对象的 set_content 方法,设置响应的内容。//html 是要返回的 HTML 内容。//"text/html;charset=utf8" 是响应的内容类型(MIME 类型),表示返回的内容是 HTML 格式,并且字符编码是 UTF-8。});
2.获取单个题目
model也为我们提供了获取单个题目的方法,只需要接收Question对象即可,并且传入view模块渲染出单个题目网页(OneExpandHtml),返回网页信息即可。
-oj_server/oj_control.hpp 获取单个题目渲染网页返回
// 根据题目编号获取单个题目细节信息,渲染成网页进行返回bool OneQuestion(const std::string& number, std::string& html){Question q;if (_model.GetOneQuestion(number, q)){// 成功,根据题目文件渲染为网页_view.OneExpandHtml(q, html);return true;}else{// 查无此物return false;}}
在对应oj_server.hpp 获取单个题目路由就可以这样调用控制模块:(注意返回网页格式的类型)
// 2.用户获取单题题目内容svr.Get(R"(/question/(\d+))", [&ctl](const Request& req, Response& resp){ // (\\d+是正则表达式)std::string number = req.matches[1]; // 1对应的就是获取正则表达式中的内容std::string html;ctl.OneQuestion(number, html);resp.set_content(html, "text/html;charset=utf8");});
3.判题
判题是整个项目中较为核心的一块,也是前后端进行联动的地方。首先,用户通过json格式提交上来的代码数据需要获取上来,然后需要通过编译运行服务执行结果,得到结果后构建json串返回结果。
- 那么首先需要定义用户上传的json数据。因为判题路由存在题目编号,那么用户上传的也只有用户编辑后的代码code以及input数据。由于用户测试运行模块属于扩展内容,这里就先将input代码忽视。我们将code和对应题目的tail文件进行拼接组成一份完整代码,在加上题目的空间以及时间限制(编译服务需要的四份属性)组成一份json数据准备发送给编译运行服务。
// 用户通过浏览器上传的json数据
{"code": code,"input": ,
}// 凭借好准备发送给compile_server的json串数据
{"code": code+q.tail,"input": ,"cpu_limit": q.cpu_limit,"mem_limit": q.mem_limit,
}
// 注意上面的q为Question对象,根据路由传上来的题目编号决定,通过model模块获取
- 我们要确定好发送给哪台编译服务主机。 因为业务众多,不可能存在一台编译运行服务主机(负载压力太大),我们设计为网络服务的原因也就是能在不同的主机上部署此服务,方便于oj_server进行选择。
为了减轻压力,我们使用负载均衡的模式进行主机选择。那么我们首先得定义主机对象,并且根据主机的配置文件加载当前的所有主机信息,方便我们进行调用。
-主机对象
Machine的设计。一个网络服务器,我们想向其访问就需要知道其ip和port。除此之外,为了便于后续的负载均衡选择,我们需要设置当前的负载均衡个数。需要注意的是,因为同一时刻存在不同的执行流执行判题功能(http网络服务),为了保证线程安全,我们需要一把互斥锁,保证负载数的访问和修改安全。可以利用C++中的mutex进行定义,需要注意的是mutex在C++中无法进行拷贝,所以需要定义为指针类型。
为了设计为线程安全,我们需要对外提供线程安全的访问负载均衡数,++负载均衡、–负载均衡、以及清空负载。
-oj_server/oj_control.hpp 主机对象设计
Machine 类封装了一个编译服务主机的相关信息和操作:
公有成员变量:存储主机的 IP 地址和端口号。
私有成员变量:存储主机的负载和互斥锁。
构造函数:初始化主机信息,并根据需要分配互斥锁。
析构函数:需要补充释放动态分配的互斥锁。
成员函数:
1.IncLoad:线程安全地增加负载。
2.DecLoad:线程安全地减少负载。
3.Load:线程安全地获取当前负载。
4.ClearLoad:线程安全地清空负载。
这个类的设计考虑了多线程环境下的线程安全性,通过互斥锁保护共享资源 _load 的访问。
class Machine{public:std::string ip; // 编译服务主机ipuint16_t port; // 编译服务主机端口private:uint64_t _load; // 当前主机负载 - 使用个数std::mutex* _mtx; // 当前主机锁,保护对负载数的安全public:// 构造一个主机对象,给定ip和port。为之后的连接编译服务做准备Machine(const std::string& ip_ = "", uint16_t port_ = 0):ip(ip_), port(port_), _load(0), _mtx(nullptr){//如果 ip_ 和 port_ 都不为空(即主机信息有效),则分配一个新的 std::mutex 对象给 _mtx。if (ip_ != "" && port_ != 0) _mtx = new std::mutex();}~Machine(){}// 对负载进行递增 - 可能存在多个对象对此主机对象进行操作,必须保证原子性void IncLoad(){//这样可以确保 _load 的增加操作是线程安全的。if (_mtx){_mtx->lock();++_load;_mtx->unlock();}}// 对负载进行递减void DecLoad(){if (_mtx){_mtx->lock();--_load;_mtx->unlock();}}// 获取当前主机负载uint64_t Load(){uint64_t load = 0;//这样可以确保获取 _load 的值时是线程安全的。if (_mtx){_mtx->lock();load = _load;_mtx->unlock();}return load;}// 清空当前负载void ClearLoad(){if (_mtx){_mtx->lock();_load = 0;_mtx->unlock();}}};
-负载均衡选择
当有了主机对象后,我们需要通过主机配置获取当前的所有主机。配置文件放在oj_server/conf/文件夹下,名字设为server_machine.conf。配置样式如下:
127.0.0.1:8081
127.0.0.1:8082
…
那么编写负载均衡模块的时候,首先根据conf文件的路径进行读入,然后按行读取-利用util工具中的字符串切割工具,切割符为":"进行切分读取即可。
所有主机对象都放在一个vector中(注意,是所有conf配置中的主机对象)。但是并不是意味着只要配置的有这台主机,这台主机就一定在线。所以,我们需要额外的设置两个数组,分别保存在线主机对象和离线主机对象。负载均衡模块提供离线主机接口(传入需要离线的主机对象),因为上层负载均衡调用此主机如果得不到结果说明可能离线了,加入离线数组即可。上线主机我们策划手动全部上线,利用信号3执行-ctrl \。(oj_server捕捉信号3,利用signal接口设置函数即可)一开始默认全部上线,后续负载均衡选择中再逐步淘汰不在线上的主机。
另外,在线主机和离线主机数组只需要存放保存所有主机数组的下标即可,每个下标就唯一确定一台主机,这样可以节省内存空间。
负载均衡的策略也很简单,从当前的在线的主机进行轮询选择,遍历保存当前数组中最小的负载数的主机,最后返回即可。
-oj_server/oj_control 负载均衡选择主机
LoadBlance 类实现了一个简单的负载均衡模块,主要功能包括:
- 加载主机配置:从配置文件中读取主机信息并初始化主机列表。
- 负载均衡选择:选择当前负载最小的在线主机。
- 主机状态管理:支持将主机设置为离线或重新上线。
- 线程安全:通过互斥锁保护对主机状态和负载数据的访问。
// 负载均衡模块class LoadBlance{const std::string confPath = "./conf/server_machine.conf";//const std::string confPath 是一个类内初始化的常量成员变量,表示主机配置文件的路径,默认值为 "./conf/server_machine.conf"。private:std::vector<Machine> _machines; // 存放全体主机对象的地方, 包括离线和在线的主机,下标对应主机编号std::vector<int> _online; // 存放在线主机的地方std::vector<int> _offline; // 存放离线主机的地方 std::mutex _mtx;// 保证在智能选择或者对数据查询时出现不可重入,保证负载均衡数据安全public:LoadBlance(){//assert(LoadConf(confPath)):调用 LoadConf 方法加载主机配置文件。如果加载失败,程序会通过 assert 断言失败并终止。assert(LoadConf(confPath));LOG(INFO) << "编译服务主机配置设置成功..." << "\n";}~LoadBlance(){}// 加载编译服务主机入内存bool LoadConf(const std::string& path_conf){std::ifstream in(path_conf);//加载主机配置文件if (!in.is_open()){LOG(FATAL) << "加载主机配置文件失败, 请检查server_machine.conf文件的相关配置路径..." << "\n";return false;}std::string line; // 一行一行的读取while(std::getline(in, line)){std::vector<std::string> machines;//使用 StringUtil::SpiltString 方法将每一行按 : 分割成 IP 和端口。//如果分割后的结果不是两个部分(即格式错误),记录一条警告日志并跳过当前行。StringUtil::SpiltString(line, machines, ":");if (machines.size() != 2){LOG(WARING) << "当前行配置文件出错:" << line << "...\n";continue;}Machine m(machines[0], atoi(machines[1].c_str()));//使用 machines[0](IP 地址)和 machines[1](端口号)创建一个 Machine 对象。// 先加载入在线主机数组_online.push_back(_machines.size());//将主机编号(即 _machines 的当前大小)加入 _online 列表。_machines.push_back(m);//将主机对象加入 _machines 列表。}in.close();return true;}// 负载均衡选择,轮询查询,找到当前负载最小的编译服务主机// 参数id为主机编号,m为主机对象,均为输出型参数bool SmartChoice(int& id, Machine** m){//加锁以确保线程安全。_mtx.lock();// 首先检测当前主机是否存在在线if (_online.size() == 0){// 此时没有一台主机在线_mtx.unlock();LOG(FATAL) << "请检查后端编译服务主机,当前没有一台在线!..." << "\n";return false;}// 轮询查询//初始化 id 和 m 为第一个在线主机。id = _online[0];*m = &_machines[_online[0]];uint64_t min_load = _machines[_online[0]].Load();//遍历所有在线主机,找到负载最小的主机。//更新 id 和 m 为负载最小的主机。for (int i = 1; i < _online.size(); ++i){uint64_t load = _machines[_online[i]].Load();if (min_load > load){id = _online[i];*m = &_machines[_online[i]];min_load = load; }}_mtx.unlock();return true;}// 离线指定id主机 参数 which 是主机编号。void OfflineMachine(int which){// 操作数据的时候,避免线程安全问题,加锁_mtx.lock();// 从在线列表中退出,加入离线列表for (auto it = _online.begin(); it != _online.end(); ++it){if (*it == which){// 找到了,退出online,加入offline_online.erase(it); // 注意删除后存在迭代器失效问题_offline.push_back(which); // 添加即可break;}}_machines[which].ClearLoad(); // 离线清空负载情况_mtx.unlock();}// 上线主机void OnlineMachine(){// 上线主机默认全部上线_online.insert(_online.end(), _offline.begin(), _offline.end());_offline.clear(); // 清空LOG(INFO) << "当前主机全部上线!" << "\n";}// debug 展示当前在线主机和离线主机 - compile_run server服务void ShowMachines(){// 访问数据上锁访问哦_mtx.lock();std::cout << "当前在线主机id: ";for (auto i : _online){std::cout << i << " ";}std::cout << std::endl;std::cout << "当前离线主机id: ";for (auto i : _offline){std::cout << i << " ";}std::cout << std::endl;_mtx.unlock();}};
现在,上面几个小组件写完后我们就可以进行我们的判题模块。
根据用户的输入,获取题目编号、构建好向编译服务发送的json串,利用负载均衡模块,选择当前负载均衡最小的主机(由于会出现请求失败的情况,我们将选择设置为死循环,直到主机全部下线或者发送成功再退出)。利用cpp-httplib中的client对象发送Post方法,注意格式,并且判断返回的响应对象是否是200成功。别忘了还需要向上层提供全部上线主机功能(上层对LoadBlance透明)。
下面这段代码实现了一个名为 Judge 的函数,用于处理在线判题(OJ)功能。它的主要逻辑包括:
- 获取题目信息:从数据库中获取题目信息。
- 构建请求数据:将用户提交的代码和题目信息拼接成 JSON 格式。
- 负载均衡选择主机:通过负载均衡模块选择当前负载最小的在线主机。
- 发送请求并处理响应:
1.向选中的主机发送 HTTP POST 请求。
2.如果请求成功,返回响应结果并结束。
3.如果请求失败,将失败的主机标记为离线,并尝试其他主机。
- 循环处理:重复选择主机并发送请求,直到成功或所有主机都挂掉。
这个函数实现了在线判题功能的核心逻辑,同时通过负载均衡和主机状态管理确保了系统的高可用性。
// 重新上线主机void AllOnLineMachine(){_load_blance.OnlineMachine();}/******************************* 判题功能judge* 1.number 题目编号* 2.in_json 用户提交的json串,包含写过的code代码、以及input输入* 3.out_json 最后返回给调用的串,结果为compile_run服务的结果 - 输出型参数* 本模块根据配置文件负载均衡选择后端的compile_run服务,实现oj功能*******************************/void Judge(const std::string& number, const std::string& in_json, std::string& out_json){// LOG(DEBUG) << in_json << "\n";Question q;//定义一个 Question 类型的对象 q,用于存储题目信息。//调用 _model.GetOneQuestion 方法,根据题目编号 number 获取题目信息并存储到 q 中。//如果获取失败(返回 false),直接返回,结束函数执行。if (!_model.GetOneQuestion(number, q)) return;// 待提交给后端编译服务的json串std::string result_json;// 1.反序列化Json::Value in;//定义一个 Json::Value 类型的对象 in,用于存储解析后的用户提交的 JSON 数据。Json::Reader read;read.parse(in_json, in);//使用 Json::Reader 的 parse 方法将用户提交的 JSON 字符串 in_json 解析为 Json::Value 对象。// 2.现在开始序列化 根据题目细节和用户提交的code拼接给后端编译服务提交的json串Json::Value out;//定义一个 Json::Value 类型的对象 out,用于构建最终发送给后端编译服务的 JSON 数据。Json::FastWriter write;//定义一个 Json::FastWriter 类型的对象 write,用于将 Json::Value 对象序列化为 JSON 字符串。// 后续测试运行会有扩展//构建发送给后端编译服务的 JSON 数据:out["code"] = in["code"].asString() + "\n" + q.tail; // 拼接代码//out["code"]:将用户提交的代码(in["code"].asString())与题目尾部代码(q.tail)拼接。out["input"] = in["input"].asString();//out["input"]:设置输入数据为用户提交的输入(in["input"].asString())out["cpu_limit"] = q.cpu_commit;out["mem_limit"] = q.mem_commit;//out["cpu_limit"] 和 out["mem_limit"]:设置 CPU 和内存限制,从题目信息 q 中获取。result_json = write.write(out);//使用 Json::FastWriter 的 write 方法将 Json::Value 对象 out 序列化为 JSON 字符串,并存储到 result_json 中。// 3. 负载均衡模块,选择一台服务主机进行发送,接收消息// 因为存在选择一台主机发送消息可能得不到消息,所以决定循环式的发送消息,直到主机全部挂掉或者得到消息为止while(true){int machine_id;Machine* m = nullptr;if (!_load_blance.SmartChoice(machine_id, &m)) break; // 此时全体挂掉了//使用 Json::FastWriter 的 write 方法将 Json::Value 对象 out 序列化为 JSON 字符串,并存储到 result_json 中。//machine_id:输出参数,返回被选中的主机编号。//m:输出参数,返回被选中的主机对象的指针。// 发起http请求//使用 httplib::Client 创建一个 HTTP 客户端对象 cli,连接到选中的主机(IP 地址为 m->ip,端口号为 m->port)。httplib::Client cli(m->ip, m->port);// 注意,请求是需要时间的并且是阻塞式的,同时间内可能会来多个请求,那么需要怎加负载进行访问//调用 Machine 对象的 IncLoad 方法,增加主机的负载。//记录一条日志信息,显示主机编号、IP、端口以及当前负载。m->IncLoad();LOG(INFO) << "主机编号为:" << machine_id << " 详细信息ip:" << m->ip << " port:" << m->port << " 当前负载:" << m->Load() << "\n";if (auto res = cli.Post("/compile_and_run", result_json, "application/json;charset=utf-8")) // 注意res重载了bool,如果没有返回一个响应对象会显示false{// 得到响应了// 先判断是不是正确的响应if(res->status == 200){out_json = res->body; // post json串在body内m->DecLoad(); // 别忘了减少负载LOG(INFO) << "请求编译运行服务成功..." << "\n";break;}}else{// 没有得到响应,说明此服务器可能挂掉了,加载入离线主机m->DecLoad();LOG(ERROR) << "主机编号:" << machine_id << "详细信息ip:" << m->ip << " port:" << m->port << "可能已经离线....." << "\n";_load_blance.OfflineMachine(machine_id);_load_blance.ShowMachines();}}}
- 使用 httplib::Client 的 Post 方法向主机发送 HTTP POST 请求:
请求路径为 /compile_and_run。
请求体为 result_json,内容类型为 application/json;charset=utf-8。 - 如果收到响应(res 不为空):
检查响应状态码是否为 200(成功)。
如果成功,将响应体(res->body)存储到 out_json 中。
调用 m->DecLoad 减少主机负载。
记录成功日志并退出循环。 - 如果没有收到响应(res 为空),说明主机可能已经离线:
调用 m->DecLoad 减少主机负载。
记录错误日志。
调用 _load_blance.OfflineMachine 将主机标记为离线。
调用 _load_blance.ShowMachines 打印当前主机状态。
oj_view模块编写
根据后端oj_control中,view我们需要构建以及渲染的网页有all_questions题库网页,one_question单个题目网页,外加补充的index网页。(补充的只是编写网页,和view逻辑控制无关)
因为需要将后端数据和网页进行一个渲染(即将数据显示到网页中去),我们需要利用到ctemplate库。这是谷歌开源的网页渲染器,能够帮助我们将数据渲染到网页上去。
ctemplate库的配置和使用
ctemplate库的下载:
访问网页:https://gitee.com/mirrors_OlafvdSpek/ctemplate?_from=gitee_search
可选择克隆或者选择压缩包下载:
克隆方法:git clone https 复制内容
下载完成后通过如下方法进行配置:
./autogen.sh
./configure
make 编译即可
sudo make install 安装到系统中
注意,make编译可能出错,当初博主实在网上搜索解决的,遇到问题请自行搜索。
ctemplate的简单使用:
- 头文件:#include <ctemplate/template.h>、
- 编译选项:-lctemplate
- 首先需要明确渲染实际上就是网网页上填充数据。我们为什么需要渲染?向题库界面填充题目信息,单个题库界面填充题目描述,用户代码等。
- 在网页中< p >{{key}}< /p >,key就是待渲染的对象。
- 在编码中,TemplateDict root(“test”); // 创建数据字典 此数据字典名字为test,名字自己随便起,字典是key-value类型,key就是在html中的待渲染对象名,root.SetValue(“key”, value); // 插入字典,即将key替换为value完成渲染。
- 通过ctemplate::Template *tql = ctemplate::Template::GetTemplate(网页路径, 保持原貌:ctemplate::DO_NOT_STRIP); // 保持原貌,打开html,string out_html; tql->Expand(&out_html, &root); // 完成了渲染。添加入html中,完成渲染,得到渲染网页数据out_html,view返回结果即可。
利用ctemplate,view模块依次对题库网页、单个题目网页根据传入的数据渲染即可。针对于网页模板,因为涉及前端的知识,这里不在过多赘述,在仓库中自行参考。
-oj_server/oj_view.hpp 网页渲染
这段代码定义了一个 View 类,用于将题目数据渲染为 HTML 网页。它使用了 ctemplate 模板引擎来实现模板渲染功能,支持两种类型的渲染:
- 题目列表网页:通过 AllExpandHtml 方法渲染题目列表。
- 单题目网页:通过 OneExpandHtml 方法渲染单个题目的详细信息。
这种设计将数据与模板分离,便于维护和扩展。
#ifndef __OJ_VIEW_HPP__
#define __OJ_VIEW_HPP__
// OJ题目渲染网页
#include <iostream>
#include <vector>
#include <ctemplate/template.h>
//这是 ctemplate 模板引擎的头文件,用于模板渲染功能。// #include "oj_model_file.hpp"
#include "oj_model_mysql.hpp"//定义了一个名为 ns_view 的命名空间,用于封装与视图相关的功能。
namespace ns_view
{using namespace ns_model;//使用了 ns_model 命名空间中的内容,可能是为了方便访问 Question 类型和其他模型相关的定义。class View{const std::string template_path = "./template_html/";//template_path 是一个常量成员变量,表示模板文件的路径,默认值为 "./template_html/"。public:View(){}~View(){}// 渲染题目列表网页// 保存题目列表的数组 + 待返回的渲染网页//const std::vector<Question>& questions:题目列表。//std::string& html:输出参数,用于存储渲染后的 HTML 内容。void AllExpandHtml(const std::vector<Question>& questions, std::string& html){// 1.模板网页路径 std::string path = template_path + "all_questions.html";// 2.形成数据字典//创建一个 ctemplate::TemplateDictionary 对象 root,用于存储模板渲染所需的数据。//"all_questions" 是模板的名称,对应模板文件中的 {{#all_questions}} 标签。ctemplate::TemplateDictionary root("all_questions");// 循环渲染一部分网页表格,设定子字典//遍历题目列表 questions。for (auto& q : questions){//对于每个题目 q,创建一个子字典 td,对应模板中的 {{#question_list}} 标签。ctemplate::TemplateDictionary* td = root.AddSectionDictionary("question_list"); // 在模板网页中需要{{#名字}}//使用 SetValue 方法将题目编号、标题和难度等级分别设置到模板变量 {{number}}、{{title}} 和 {{star}} 中。td->SetValue("number", q.number);td->SetValue("title", q.title);td->SetValue("star", q.star);}// 3.获取待渲染网页ctemplate::Template* tql = ctemplate::Template::GetTemplate(path, ctemplate::DO_NOT_STRIP);//ctemplate::DO_NOT_STRIP 表示不删除模板中的空白字符。// 4.渲染网页返回tql->Expand(&html, &root);//调用模板对象的 Expand 方法,将模板渲染为 HTML 内容。//渲染结果存储到 html 参数中。}// 渲染单题目网页// 单题目数据结构 + 待返回的渲染网页void OneExpandHtml(const Question& q, std::string& html){std::string path = template_path + "one_question.html";ctemplate::TemplateDictionary root("one_question");//使用 SetValue 方法将题目编号、标题、难度等级、CPU 限制、内存限制、题目描述和预设代码分别设置到模板变量中。//对于 CPU 和内存限制,将整数值转换为浮点数并格式化为字符串,保留小数点后两位。//q.desc 是题目描述,q.header 是预设代码。root.SetValue("number", q.number);root.SetValue("title", q.title);root.SetValue("star", q.star);std::string str = std::to_string((q.cpu_commit * 1.0));root.SetValue("cpu", str.substr(0, str.find(".") + 3));str = std::to_string(q.mem_commit * 1.0 / 1024);root.SetValue("mem", str.substr(0, str.find(".") + 3));root.SetValue("desc", q.desc);root.SetValue("pre_code", q.header);//加载模板文件并渲染为 HTML 内容,结果存储到 html 参数中。ctemplate::Template* tql = ctemplate::Template::GetTemplate(path, ctemplate::DO_NOT_STRIP);tql->Expand(&html, &root);}};
}#endif
temlate_html网页模板:temlate_html-题库页面+单个题目页面
(注意其中链接的css文件以及js文件在wwwroot下:www.root)
补充index首页网页:index.html
-oj_server/makefile 顶层makefile的编写
当前项目可以告别一段时间了,但是我们还可以设计一个顶层makefie,方便发布时的使用。
其作用就是可以分别编译oj_server、compile_server,并且生成一个output将必要的文件拷贝出来,删除的时候将所有新增的文件删除掉。
.PHONY:all
all:@cd compile_server;\make;\cd -;\cd oj_server;\make;\cd -;.PHONY:output
output:@mkdir -p output/oj_server;\mkdir -p output/compile_server;\cp -rf ./oj_server/oj_server output/oj_server;\cp -rf ./oj_server/conf output/oj_server;\cp -rf ./oj_server/mysql-connect/lib output/oj_server;\cp -rf ./oj_server/questions output/oj_server;\cp -rf ./oj_server/template_html output/oj_server;\cp -rf ./oj_server/wwwroot output/oj_server;\cp -rf ./compile_server/compile_server output/compile_server;\cp -rf ./compile_server/temp output/compile_server;.PHONY:clean
clean:@cd compile_server;\make clean;\cd -;\cd oj_server;\make clean;\cd -;\rm -rf output;
3.扩展内容
增加测试运行
在之前的项目中并没有增加测试运行功能,但是接口已经留好了。前端编写对应的界面后,control模块还是走正常的判题功能,只不过此时返回的json串中新增一种属性test_input类型为bool,如果是那么说明是自测功能。
自测说白了就是输入用户自己的测试数据,在运行环境中就是cin的内容。在运行模块中我们已经将input重定向到了标准输入模块了,也就是说到运行模块,cin中以及存放用户的输入数据了,我们只需要新增一种类型的拼接代码,就是while循环调用cin去输入数据,然后传入用户设置的方法中返回结果输出到stdout中一样的返回结果即可。
所以新增的拼接代码可以如下所示:test_input.cpp
#ifndef COMPILE_ONLINE
#include "header.cpp" // 条件编译,引入头文件只是为了不报错和语法提示,之后会进行拼接
#endif
#include <iostream>int main()
{UnusualAdd ues;int a, b;while (cin >> a >> b){cout << ues.addAB(a, b) << "\n";}return 0;
}
文件版本题库中添加对应文件到对应文件下,并且model中读取即可,数据库同理,修改表结构,添加即可。
下面展示数据库的修改部分:oj_server/ oj_model_mysql.hpp
对于逻辑控制control上,只需要判断是否自测还是正常提交,拼接不同的代码即可,其余的都不用改。oj_server/oj_control.hpp
增加登录模块
登录模块前端设置为弹窗(详情可查看index、one_question、all_questions任意一份html)。
因为是新的模块,我将oj_server设置了新的路由/user。control模块对应增加oj_user.hpp。实际上,用户管理,实际上就是保存一堆的用户在数据库或者文件中(这里我使用mysql数据库完成),登录的时候使用sql语句查询表是否存在或者密码是否正确,注册的时候insert语句插入查看是否id冲突或者密码设置有误。
我在mysql的oj数据库中新增了一种表oj_user。其中属性为id、password。注意其中password存储的是经过password函数形成的数据摘要,所以密码设置格式完全可以由数据库决定,后端我们不需要自己在进行设定了。而oj_user用户管理和oj_model_mysql类似,都是链接数据库,要么获取用户,要么注册用户,获取用户就执行select语句,插入就执行insert语句,失败返回对应的json数据-定义状态和描述。前端提交的也是json数据(利用表格获取数据,提交json结果)。
当前实现的登录模块没有完全,只是提供了登录检测,注册用户,登录成功后只是网络提醒,没有过多的进行处理。
-oj_server/oj_user.hpp 登录管理
这段代码实现了一个用户管理模块,主要功能包括:
- 用户登录验证:通过 FindUser 函数验证用户名和密码是否正确。
- 用户注册:通过 RegisterUser 函数将新用户信息插入数据库。
- 数据库操作:通过 QueryMySqlResult 函数执行 SQL 查询并获取结果。
代码中使用了 MySQL C API 来与数据库进行交互,并通过日志模块记录操作日志。这种设计将用户管理逻辑封装到一个类中,便于维护和扩展。
#ifndef __OJ_USER_HPP__
#define __OJ_USER_HPP__
// 用户管理模块
#include <string>
#include <unordered_map>
#include "./mysql-connect/include/mysql.h"// 引用c-mysql-connect
#include "../common/log.hpp"namespace ns_user
{using namespace ns_log;// 用户模块 先将数据库中存在的所有用户缓存到内存中来,这样每次就不需要访问数据库了struct UserObject{std::string id; // 用户账号std::string password; // 用户密码bool root = false; // 用户管理权限 默认不是};//定义了一系列常量,用于存储数据库的连接信息:const std::string oj_table_name = "oj_questions";//数据库表名const std::string oj_host = "43.143.4.250";//数据库服务器的IP地址const std::string oj_user = "oj_client";//数据库用户名const std::string oj_password = "123456";//数据库密码const std::string oj_db = "oj";//数据库名称const unsigned int oj_port = 3306;//数据库端口号class User{public:User(){}~User(){}// 1 数据库连接失败// 2 执行结果报错 -注意select 查找不到不会报错int QueryMySqlResult(const std::string& sql, MYSQL_RES*& res){// c连接mysql库,创建mysql对象-初始化mysql句柄MYSQL* oj_client = mysql_init(nullptr);// 连接数据库,连接失败返回nullptrif (nullptr == mysql_real_connect(oj_client, oj_host.c_str(), oj_user.c_str(), oj_password.c_str(), oj_db.c_str(), oj_port, nullptr, 0)){// 表示连接数据库失败LOG(FATAL) << "数据库连接失败!请尽快联系数据库管理员....." << "\n";return 1;}// 连接成功后先执行编码格式mysql_set_character_set(oj_client, "utf8");// 连接成功后执行语句int result = mysql_query(oj_client, sql.c_str());if (result != 0){// 执行失败 sql语句的错误// LOG(WARING) << "sql语句存在错误:" << sql << "\n";return 2;}// 执行成功,提取数据res = mysql_store_result(oj_client);// 上层解析数据return 0;}// 0-查找成功,存在此用户// 1-内部错误// 2-用户名错误或不存在// 3-密码错误// 4-权限错误 测试int FindUser(const std::string& user_name, const std::string& password){MYSQL_RES* res1 = nullptr;if (user_name == "") return 2; // 为空std::string sql = "select * from oj_user where id='" + user_name + "';";//执行 SQL 查询,检查用户名是否存在://如果查询失败,返回 1。if(0 != QueryMySqlResult(sql, res1)) return 1;MYSQL_ROW line1 = mysql_fetch_row(res1);if (line1 == 0){// 用户不存在或者没找到return 2;}MYSQL_RES* res2 = nullptr;// 第二次执行sql,检查用户名和密码是否匹配sql = "select * from oj_user where id='" + user_name + \"' and `password`=password('" + password + "');";//如果查询失败,返回 3if(0 != QueryMySqlResult(sql, res2)) return 3;MYSQL_ROW line2 = mysql_fetch_row(res2);if (line2 == 0){// 密码错误return 3;} // UserObject user;// user.id = line2[0];// user.password = line2[1];// user.root = line2[2];//如果用户名和密码都匹配,返回 0。return 0;}// 注册用户// 0 注册成功// 1 内部错误// 2 用户名为空或者已经存在// 3 密码格式错误int RegisterUser(const std::string& user_name, const std::string& password){MYSQL_RES* res = nullptr;if (user_name == "") return 2; // 为空std::string sql = "select * from oj_user where id='" + user_name + "';";if(0 != QueryMySqlResult(sql, res)) return 1; // 内部出错MYSQL_ROW line = mysql_fetch_row(res);if (line == 0){// 用户找不到,可以插入sql = "insert into oj_user(id, password) values('" + user_name + "', password('" + password + "'));";int op = QueryMySqlResult(sql, res);if (op == 1) return 1; // 连接出错else if (op == 2) return 3; // 密码格式问题}else return 2; // 用户名冲突return 0;}};
}
#endif
定义了一个函数 RegisterUser,用于注册新用户。
参数:
1.const std::string& user_name:用户名。
2.const std::string& password:密码。
首先检查用户名是否为空,如果为空返回 2。
执行 SQL 查询,检查用户名是否已存在:
1.如果查询失败,返回 1。
2.如果用户已存在,返回 2。
如果用户不存在,执行 SQL 插入语句,将新用户信息插入数据库:
1.如果插入失败,返回 1 或 3。
返回 0 表示注册成功。
当前用户权限还没使用,之后可扩展root用户可以自行添加题目修改题目,普通用户记录保存网页状态,记录刷题个数等等…