1技术栈和项目环境
技术栈:
C++ STL 标准库,Boost 准标准库(字符串切割),cpp-httplib (第三方开源网络库),ctemplate (第三方开源前端网页渲染库),jsoncpp (第三方开源序列化、反序列化库),负载均衡设计,多进程、多线程,MySQL C connect()
前端:
vscode,html/css/js/jquery/ajax
项目环境:
Centos 7 云服务器,vscode,Mysql Workbench(图形化MySQL)
2项目宏观结构
结构:
编写思路:
1.compile_server
2.oj_server
3.版本一:基于文件版本的OJ
4.前端页面设计
5.版本二:基于mysql版本的OJ
关于leetcode:
题目列表+在线编程功能
3编译与运行服务compile_server
**need:**编译代码,运行代码,得到格式化的相关的结果
3.1编译功能–compiler.hpp
compiler.hpp
#pragma once
#include <iostream>
#include <unistd.h> //fork
#include "../comm/util.hpp"
#include<sys/wait.h>//waitpid
#include<sys/types.h>
#include <sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include"../comm/log.hpp"//日志
namespace ns_compiler
{
//引入ns_util工具类(路径拼接)
using namespace ns_util;
using namespace ns_log;
class Compiler{
public:
Compiler(){}
~Compiler(){}
//file_name:编译文件名称(无后缀),返回值:编译是否成功!
//目标:文件名称->./temp/文件名称.cpp
//目标:文件名称->./temp/文件名称.exe
//目标:文件名称->./temp/文件名称.stderr
static bool Compile(const std::string &file_name)
{
pid_t child=fork();
if(child<0) {
LOG(ERROR)<<"内部错误,创建子进程失败!"<<std::endl;
return false; //创建子进程失败
}
else if (child==0)//子进程:调用编译器,完成对代码的编译工作
{
umask(0);//将权限清零,这样设置的权限比较安全
int _stderr = open(PathUtil::CompilerError(file_name).c_str(),O_CREAT|O_WRONLY,0644);
if(_stderr<0)
{
LOG(WARNING)<<"没有成功形成.stderr文件"<<std::endl;
//打开文件失败
exit(1);
}
//重定向标准错误到_stderr
dup2(_stderr,2);
//程序替换,并不影响进程的文件描述符表
// g++ -o target src -std=c++11
execlp("g++","g++","-o",PathUtil::Exe(file_name).c_str(),\
PathUtil::Src(file_name).c_str(),nullptr);//要以nullptr结束
LOG(ERROR)<<"启动编译器g++失败"<<std::endl;
exit(2);
}
else//父进程
{
waitpid(child,nullptr,0);
//编译是否成功,判断可执行程序是否存在
if(FileUtil::IsFileExists(PathUtil::Exe(file_name)))
{
LOG(INFO)<<PathUtil::Src(file_name)<<"编译成功!"<<std::endl;
return true;
}
}
LOG(ERROR)<<"编译失败,没有形成可执行程序"<<std::endl;
return false;
}
};
}
日志
#pragma once
//日志
#include <iostream>
#include<string>
#include"util.hpp"
namespace ns_log
{
using namespace ns_util;
//日志等级,枚举从零开始
enum{
INFO,//常规的,没有任何错误信息,只是一些提示信息
DEBUG,//调试时的调试日志
WARNING,//告警,不影响后续使用
ERROR,//错误,这个用户的请求不能继续了
FATAL,//不光这个用户,整个系统都无法使用,引起系统整个出错
//补充:如果正常工作中出现ERROR,FATAL那么就需要运维来解决
};
//level:错误等级;file_name:错误文件;line:错误行数
//日志非常常用,所以建议设置为inline
inline std::ostream &Log(const std::string &level,const std::string &file_name,int line)
{
std::string message ="[" + level+ "]["+ file_name+"][" + std::to_string(line)+"]";
//日志时间戳
message=message+ "[" + TimeUtil::GetTimeStamp()+"]";
//cout本质内部是包含缓冲区的
std::cout<<message;//不用endl进行刷新,此时message就会暂存在cout中
return std::cout;
}
#define LOG(level) Log(#level,__FILE__,__LINE__)
}
工具util.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h> //stat
#include <sys/time.h>//获取时间
namespace ns_util
{
//全局路径
const std::string temp_path = "./temp/";
//对路径的操作
class PathUtil
{
public:
//构架源文件路径+后缀的完整文件名
//目标:文件名称->./temp/文件名称.cpp
static std::string Src(const std::string &file_name)
{
return temp_path + file_name + ".cpp";
}
//构建可执行程序的完整路径+后缀名成
//目标:文件名称->./temp/文件名称.exe
static std::string Exe(const std::string &file_name)
{
return temp_path + file_name + ".exe";
}
//构建该程序对应的标准错误完整的路径+后缀名
//目标:文件名称->./temp/文件名称.compile_error
static std::string CompilerError(const std::string &file_name)
{
return temp_path + file_name + ".compile_error";
}
};
//对文件(path)的操作方法
class FileUtil
{
public:
static bool IsFileExists(std::string path_name)
{
//方法一:查看文件是否能够正常打开
//方法二:stat(文件路径,文件属性(可以自己选择自己需要的属性));
struct stat st;
if (stat(path_name.c_str(), &st) == 0)
return true; //获取文件成功->文件存在
return false;
}
};
//时间相关工具
class TimeUtil{
public:
//获取系统时间
static std::string GetTimeStamp()
{
struct timeval _time;
gettimeofday(&_time,nullptr);
return std::to_string(_time.tv_sec);
}
};
}
Makefile
Compile_server:compile_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf Compile_server
rm -rf ./temp/code.exe
rm -rf ./temp/code.compiler_error
编译功能测试
1.在temp文件下创建一个code.cpp
2.在code.cpp内写下一段代码(正确的)
3.在compile_server.cc内调用
#include "compiler.hpp"
using namespace ns_compiler;
int main(int argc, char const *argv[])
{
std::string code="code";
Compiler::Compile(code);
return 0;
}
4.make
5.执行Compile_server
6.在temp内执行code.exe
7.在code.cpp内写下一段代码(错误的)
8.执行3456,观察结果
9.查看code.compile_error(错误时,里面会包含错误信息,正确时,里面没有任何信息)
3.2运行功能–runner.hpp
Run
程序运行
1.代码跑完,结果正确
2.代码跑完,结果不正确
3.代码没跑完,异常了
Run不需要考虑代码跑完,结果是否正确,测试用例决定的;我们只考虑:是否正确运行完毕
问题:可执行程序是谁?
一个程序在默认启动的时候
标准输入: 不考虑用户自测
标准输出:程序运行完成,输出结果是什么
标准错误:运行时错误信息
代码:
runner.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h> //fork
#include "../comm/log.hpp"
#include "../comm/util.hpp"
//open
#include<sys/types.h>
#include <sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>//waitpid
namespace ns_runner
{
using namespace ns_util;
using namespace ns_log;
class Runner
{
public:
Runner() {}
~Runner() {}
//指名文件名即可,不需要带路径,带后缀
//返回值>0:程序异常了,退出时收到了信号,返回值就是对应的信号编号
//返回值==0:正常运行完毕,结果保存至对应的临时文件中
//返回值<0:内部错误(打开文件失败,创建子进程失败)
static int Run(const std::string &file_name)
{
std::string _execute = PathUtil::Exe(file_name);
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0);
int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);
int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
if(_stdin_fd<0 || _stdout_fd<0 || _stderr_fd<0)
{
LOG(ERROR)<<"运行时打开标准文件失败!"<<std::endl;
return -1;//打开文件失败
}
pid_t pid = fork();
if (pid < 0) //创建失败
{
LOG(ERROR)<<"运行时创建子进程失败!"<<std::endl;
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
return -2;//创建子进程失败
}
else if (pid == 0) //子进程
{
dup2(_stdin_fd,0);
dup2(_stdout_fd,1);
dup2(_stderr_fd,2);
execl(_execute.c_str(),_execute.c_str(),nullptr);
exit(1);
}
else //父进程
{
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
int status=0;
waitpid(pid,&status,0);
LOG(INFO)<<"运行完毕,info:"<<(status&0x7F)<<std::endl;
return status&0x7F;
}
}
};
}
ns_util::PathUtil新增
//运行时需要拼接的文件
static std::string Stdin(const std::string &file_name)
{
return temp_path + file_name + ".stdin";
}
static std::string Stdout(const std::string &file_name)
{
return temp_path + file_name + ".stdout";
}
//构建该程序对应的标准错误完整的路径+后缀名
//目标:文件名称->./temp/文件名称.stderr
static std::string Stderr(const std::string &file_name)
{
return temp_path + file_name + ".stderr";
}
测试:
Makefile
Compile_server:compile_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf Compile_server
rm -rf ./temp/code.exe
rm -rf ./temp/code.compiler_error
rm -rf ./temp/code.stdin
rm -rf ./temp/code.stdout
在compiler_server路径下
1.make
2…/Compile_server
3.查看是否包含
并查看这些文件内时候有对应内容
例如:code.stdout内包含了输出内容
运行新问题 和 解决
当用户提交的代码是恶意代码:占用大量空间,时间复杂度极高,对程序不友好
引入:setrlimit();
对某个程序进行约束,一旦程序违反这个约束,程序就会直接返回,并发送信号量
runner.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h> //fork
#include "../comm/log.hpp"
#include "../comm/util.hpp"
//open
#include<sys/types.h>
#include <sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>//waitpid
//setrlimit();
#include<sys/time.h>
#include<sys/resource.h>>
namespace ns_runner
{
using namespace ns_util;
using namespace ns_log;
class Runner
{
public:
Runner() {}
~Runner() {}
//设置进程所占用的资源
static void SetProcLimit(int _cpu,int _mem)
{
//设置cpu时长
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_max=RLIM_INFINITY;
cpu_rlimit.rlim_cur=_cpu;
setrlimit(RLIMIT_CPU,&cpu_rlimit);
//设置内存大小
struct rlimit mem_rlimit;
mem_rlimit.rlim_max=RLIM_INFINITY;
mem_rlimit.rlim_cur=_mem*1024;
setrlimit(RLIMIT_AS,&mem_rlimit);
}
//指名文件名即可,不需要带路径,带后缀
//返回值>0:程序异常了,退出时收到了信号,返回值就是对应的信号编号
//返回值==0:正常运行完毕,结果保存至对应的临时文件中
//返回值<0:内部错误(打开文件失败,创建子进程失败)
//cpu_limit: 该程序运行的时候,可以使用的最大cpu资源上限
//mem_limit: 改程序运行的时候,可以使用的最大的内存大小(KB)
static int Run(const std::string &file_name,int cpu_limit,int mem_limit)
{
std::string _execute = PathUtil::Exe(file_name);
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderr = PathUtil::Stderr(file_name);
umask(0);
int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);
int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);
int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);
if(_stdin_fd<0 || _stdout_fd<0 || _stderr_fd<0)
{
LOG(ERROR)<<"运行时打开标准文件失败!"<<std::endl;
return -1;//打开文件失败
}
pid_t pid = fork();
if (pid < 0) //创建失败
{
LOG(ERROR)<<"运行时创建子进程失败!"<<std::endl;
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
return -2;//创建子进程失败
}
else if (pid == 0) //子进程
{
dup2(_stdin_fd,0);
dup2(_stdout_fd,1);
dup2(_stderr_fd,2);
SetProcLimit(cpu_limit,mem_limit);
execl(_execute.c_str(),_execute.c_str(),nullptr);
exit(1);
}
else //父进程
{
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
int status=0;
waitpid(pid,&status,0);
LOG(INFO)<<"运行完毕,info:"<<(status&0x7F)<<std::endl;
return status&0x7F;
}
}
};
}
3.3编译并运行功能–compile_run.hpp
needs
1.适配用户请求,定制通信协议
2.正确的调用compile and run
3.形成唯一文件名
编译服务随时可能被多人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,要不然多个用户之间会互相影响
compile_run.hpp
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include <jsoncpp/json/json.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <signal.h>
namespace ns_compile_and_run
{
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_log;
using namespace ns_util;
class CompileAndRun
{
public:
// sign>0:进程收到了信号导致异常崩溃
// sign<0:整个过程非运行报错
// sign=0:整个过程全部完成
static std::string CodeToDesc(int sign,std::string &file_name)
{
std::string desc;
switch (sign)
{
case 0:
desc = "编译运行成功";
break;
case -1:
desc = "提交的代码是空";
break;
case -2:
desc = "未知错误";
break;
case -3:
// desc = "代码编译的时候发生错误";
FileUtil::ReadFile(PathUtil::CompilerError(file_name),&desc,true);
break;
case SIGABRT:
desc = "内存超过范围";
break;
case SIGXCPU:
desc = "cpu使用超时!";
break;
case SIGFPE:
desc = "浮点数溢出!";
break;
default:
desc = "未知错误:" + std::to_string(sign);
break;
}
return desc;
}
// in_json:code用户提交的代码和input用户给自己提交的代码对应的输入
// cpu_limit时间要求,mem_limit空间要求
// out_json:status状态码和reason请求结果
// stdout:程序运行结果 stderr:程序运行错误结果
static void Start(const std::string &in_json, std::string *out_json)
{
//解析in_json
Json::Value in_value;
Json::Reader reader;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
// out_json
int status_code = 0;
Json::Value out_value;
int run_result = 0;
std::string file_name;
if (code.size() == 0)
{
//代码为空
status_code = -1;
goto END;
}
//形成一个临时的唯一文件
file_name = FileUtil::UniqFileName();
//将code写到file_name.cpp中,形成临时src文件
if (!FileUtil::writeFile(PathUtil::Src(file_name), code))
{
status_code = -2;
goto END;
//没有成功写入到文件中
}
if (!Compiler::Compile(file_name))
{
status_code = -3;
goto END;
//代码编译失败
}
run_result = Runner::Run(file_name, cpu_limit, mem_limit);
if (run_result < 0)
{
status_code = -2;
//没有成功写入到文件中
}
else if (run_result > 0)
{
status_code = run_result;
}
else
{
//运行成功
status_code = 0;
}
END:
out_value["status"] = status_code;
out_value["reason"] = CodeToDesc(status_code,file_name);
if (status_code == 0)
{
//整个过程全部成功
std::string _stdout;
FileUtil::ReadFile(PathUtil::Stdout(file_name),&_stdout,true);
out_value["stdout"] = _stdout;
std::string _stderr;
FileUtil::ReadFile(PathUtil::Stderr(file_name),&_stderr,true);
out_value["stderr"] = _stderr;
}
Json::StyledWriter writer;
*out_json = writer.write(out_value);
}
};
}
util.hpp—>FileUtil(对文件(path)的操作方法)
//对文件(path)的操作方法
class FileUtil
{
public:
//查看文件是否存在
static bool IsFileExists(const std::string &path_name)
{
//方法一:查看文件是否能够正常打开
//方法二:stat(文件路径,文件属性(可以自己选择自己需要的属性));
struct stat st;
if (stat(path_name.c_str(), &st) == 0)
return true; //获取文件成功->文件存在
return false;
}
//形成一个唯一的文件名(形成的文件名没有目录没有后缀)
//唯一性:毫秒级别的时间戳+原子性递增的唯一值
static std::string UniqFileName()
{
static std::atomic_uint id(0);
id++;
std::string ms = TimeUtil::GetTimeMS();
std::string uniq_id = std::to_string(id);
return ms + uniq_id;
}
//将code写到target中,形成临时src文件
static bool writeFile(const std::string &target, const std::string &code)
{
std::ofstream out(target);
if (!out.is_open())
{
return false;
}
out.write(code.c_str(), code.size());
out.close();
return true;
}
//将文件内容读取
// target文件名,content内容保存地址,keep是否保存\n
static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
{
(*content).clear();
std::ifstream in(target);
if (!in.is_open())
{
return false;
}
std::string line;
// getline不保存行分隔符
// getline内部重载了强制类型转化
while (std::getline(in, line))
{
(*content) += line;
(*content) += (keep ? "\n" : "");
}
in.close();
return true;
}
};
测试:
测试代码1:
#include"compile_run.hpp"
using namespace ns_compile_and_run;
int main(int argc, char const *argv[])
{
//通过http让client给我们上传一个json string
//模拟客户端请求的json串
std::string in_json;
Json::Value in_value;
in_value["code"]=R"(#include<iostream>
int main(){
std::cout<<"ceshi!"<<std::endl;
return 0;
})";
in_value["input"]="";
in_value["cpu_limit"]=1;
in_value["mem_limit"]=1024*30;
Json::FastWriter writer;
in_json=writer.write(in_value);
std::string out_json;
CompileAndRun::Start(in_json,&out_json);;
std::cout<<out_json<<std::endl;
return 0;
}
测试代码二:
测试代码三:
测试代码四
新问题–大量临时文件
以前我们的解决方法:
Makefile
Compile_server:compile_server.cc
g++ -o $@ $^ -std=c++11 -ljsoncpp
.PHONY:clean
clean:
rm -rf Compile_server
rm -rf ./temp/*
在CompileAndRun末尾调用这个即可
static void RemoveTempFile(const std::string &file_name)
{
//清理文件的个数是不确定的,但是有哪些我们是知道的
std::string _src = PathUtil::Src(file_name);
if(FileUtil::IsFileExists(_src)) unlink(_src.c_str());
std::string _compiler_error = PathUtil::CompilerError(file_name);
if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());
std::string _execute = PathUtil::Exe(file_name);
if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());
std::string _stdin = PathUtil::Stdin(file_name);
if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());
std::string _stdout = PathUtil::Stdout(file_name);
if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());
std::string _stderr = PathUtil::Stderr(file_name);
if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());
}
3.4形成网络服务
关于httplib的安装,使用和注意事项,我在我的上一个项目:基于boost的搜索引擎中的第八个模块
这个项目只需要将httplib.h移动到comm中
关于使用:百度即可
遇见问题
问题一:
如果遇见下面错误,在makefile中添加 -lpthread 即可
/tmp/ccMFzHXI.o: In functionstd::thread::thread<httplib::ThreadPool::worker>(httplib::ThreadPool::worker&&)': compile_server.cc:(.text._ZNSt6threadC2IN7httplib10ThreadPool6workerEJEEEOT_DpOT0_[_ZNSt6threadC5IN7httplib10ThreadPool6workerEJEEEOT_DpOT0_]+0x21): undefined reference to
pthread_create’
/opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7/libstdc++_nonshared.a(thread48.o): In functionstd::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)())': (.text._ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE+0x11): undefined reference to
pthread_create’
/opt/rh/devtoolset-7/root/usr/lib/gcc/x86_64-redhat-linux/7/libstdc++_nonshared.a(thread48.o): In functionstd::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)())': (.text._ZNSt6thread15_M_start_threadESt10shared_ptrINS_10_Impl_baseEEPFvvE+0x5f): undefined reference to
pthread_create’
collect2: error: ld returned 1 exit status
make: *** [Compile_server] Error 1
问题二:
因为我们的代码比较复杂(还行)
主要是这个httplib所占用空间太多,从而导致系统运行不成功
解决:重启vscode,即可
重启vscode的时候,如果gcc不是默认设置高版本的,就需要重新恢复到原来的版本的,(如果不在vscode上进行编译服务,那就需要更新gcc版本)
问题三:
网络ip加端口号访问不了网站
解决:
1.在云服务器上重新打开端口号
2.在Linux上查看端口号是否被打开,查看那些被打开,重启防火墙
开放8080端口
firewall-cmd --permanent --zone=public --add-port=8080/tcp
查询8080端口开放情况,若返回success,则为开放成功
firewall-cmd --zone=public --query-port=8080/tcp
重启防火墙
firewall-cmd --reload
代码
#include "compile_run.hpp"
using namespace ns_compile_and_run;
#include "../comm/httplib.h"
using namespace httplib;
void Usage(std::string proc)
{
std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}
// ./compile_server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
Server svr;
svr.Post("/compile_and_run", [](const Request &req, Response &resp)
{
// 用户请求的服务正文:json string
std::string in_json = req.body;
std::string out_json;
if(!in_json.empty()){
CompileAndRun::Start(in_json, &out_json);
resp.set_content(out_json, "application/json;charset=utf-8");
} });
svr.listen("0.0.0.0", atoi(argv[1])); //启动http服务
}
4基于MVC 结构的oj 服务设计–oj_server
即:建立一个小型网站
needs
- 首页:题目列表
- 编辑区域页面
- 提交判题功能
M:Model,通常是和数据交互的模块
例如:对题库进行增删查改(文件版,MySQL)
V:view,通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
C:control,控制器–核心业务逻辑
oj_server.cc–用户请求的服务路由功能
#include <iostream>
#include "../comm/httplib.h"
using namespace httplib;
int main()
{
//用户请求的服务路由功能
Server svr;
// 获取所有的题目列表(首页+题目列表)
svr.Get("/all_questions", [](const Request &req, Response &resp)
{ resp.set_content("首页", "text/plain;charset=utf-8"); });
svr.set_base_dir("./wwwroot");
// 用户要根据题目编号,获取题目内容
svr.Get(R"(/questions/(\d+))", [](const Request &req, Response &resp)
{
std::string number=req.matches[1];
resp.set_content("这是:"+number, "text/plain;charset=utf-8"); });
// 用户提交代码,使用我们的判题功能:(1.每道题的测试用例,2.compile_and_run)
svr.Get(R"(/judge/(\d+))", [](const Request &req, Response &resp)
{
std::string number=req.matches[1];
resp.set_content("这是:"+number+" 的判题", "text/plain;charset=utf-8"); });
svr.listen("0.0.0.0", 8080);
return 0;
}
设计题库版本一:文件版本–questions
needs
1.题目的编号
2.题目的标题
3.题目的难度
4.题目的描述,题面
5.时间要求(内部处理)
6.空间要求(内部处理)
两批文件构成:
第一个:questions.list:题目列表(不需要出现题目的内容)
第二个:题目的描述,预设值的代码(hander.cpp),测试用例代码(tail.cpp)
通过文件的编号,产生关联的
题库代码举例:
desc.txt
求一个数组中最大的值
示例 1:
输入: [1,2,3,4,5,6,7]
输出: 7
示例 2:
输入: [-1,1,2,3,4,5,6,7,9]
输出: 9
header.hpp
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
int FindMax(vector<int>& v)
{
return true;
}
};
tail.hpp
#ifndef CompileOnline
// 这是为了编写用例的时候有语法提示. 实际线上编译的过程中这个操作是不生效的.
#include "header.cpp"
#endif
void Test1()
{
vector<int> v={1,2,3,4,5,6,7};
int ret = Solution().FindMax(v);
if (ret==7)
{
std::cout << "Test1 ok!" << std::endl;
}
else
{
std::cout << "测试用例: {1,2,3,4,5,6,7} 未通过" << std::endl;
}
}
void Test2()
{
vector<int> v={-1,1,2,3,4,5,6,7,9};
int ret = Solution().FindMax(v);
if (ret==9)
{
std::cout << "Test2 ok!" << std::endl;
}
else
{
std::cout << "测试用例: {-1,1,2,3,4,5,6,7,9} 未通过" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
oj_model.hpp-逻辑控制模块
和数据进行交互,对外提供访问数据的接口
根据题目.list文件,加载所有的题目信息到内存中
OJ需要的是 header.hpp+用户写的内容 + tail.cpp
#pragma once
//根据题目.list文件,加载所有的题目信息到内存中
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include "../comm/log.hpp" //日志
#include <cassert> //assert
#include <fstream>
#include "../comm/util.hpp"//字符串切分
#include<cstdlib>//atoi
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
//题目信息
struct Question
{
string number; //题目编号
string title; //题目标题
string star; //难度
int cpu_limit; //时间要求
int mem_limit; //空间要求
string desc; //题目描述
string header; //预设代码
string tail; //测试用例
};
const std::string question_list = "./questions/questions.list";
const std::string question_path="./questions/";
class Model
{
private:
unordered_map<string, Question> questions;
public:
Model()
{
assert(LoadQuestionList(question_list));
}
//加载配置文件:questions/question.list+题目编号
bool LoadQuestionList(const string &question_list)
{
ifstream in(question_list);
if(!in.is_open())
{
LOG(FATAL)<<"加载题库失败!"<<std::endl;
return false;
}
string line;
while(getline(in,line))
{
vector<string> v;
StringUtil::SplitString(line,&v," ");
if(v.size()!=5)
{
//切分失败
LOG(WARNING)<<"加载部分题目失败!"<<std::endl;
continue;
}
Question q;
q.number=v[0];
q.title=v[1];
q.star=v[2];
q.cpu_limit=atoi(v[3].c_str());
q.mem_limit=atoi(v[4].c_str());
string path=question_path;
path+=q.number;
path+="/";
FileUtil::ReadFile(path+"desc.txt",&(q.desc),true);
FileUtil::ReadFile(path+"header.cpp",&(q.header),true);
FileUtil::ReadFile(path+"tail.cpp",&(q.tail),true);
questions.insert({q.number,q});
}
LOG(INFO)<<"加载题库成功!"<<std::endl;
in.close();
return true;
}
//获取所有题目
bool GetALLQuestions(vector<Question> *out)
{
if (questions.size() == 0)
{
LOG(ERROR) << "用户获取题库失败" << std::endl;
return false;
}
for(const auto &q : questions){
out->push_back(q.second);
}
return true;
}
//获取一个题目
bool GetOneQuestion(const string &number, Question *q)
{
const auto &it = questions.find(number);
if (it == questions.end())
{
//没有找到
return false;
}
(*q) = it->second;
return true;
}
~Model() {}
};
}
oj_view.hpp–渲染网页
ctemplate安装引入
ctemplate最初被称为谷歌模板,因为它起源于用于谷歌搜索结果页面的模板系统,
功能简单介绍:
in_html:初始网页,
out_html:最终网页
我们有很多的题库,每个题的网页相差无几,只有里面的内容是不一样的,而整体的网页结构是一模一样的
我们在in_html写下网页大纲,在网页不同部分写下{{key}},经过ctemplate,就可以根据每个题将网页设置为自己所需要的网页
(具体请看测试)
ctemplate安装
温馨提醒:这个是2022-8-19日可以安装的
git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git
./autogen.sh---->ll查看是否生成了./configure
./configure ----->ll查看是否生成了 makefile
sudo make---->sudo建议加上,尽量保证 make时,不要出现报错信息
如果make有报错信息,那么重复的 ./autogen.sh ./configure 然后make
sudo make install—> sudo建议加上,尽量保证 make时,不要出现报错信息
如果make install有报错信息,那么重复的 ./autogen.sh ./configure 然后make
无论你重复多少次./autogen.sh ./configure make sudo make install,
只要你的执行过程中没有报错即可
这个时候进行测试
如果出现
那么就是libctemolate.so.3这个库找不到
解决
跳转至这个地方,查看是否有这些文件(正常情况下是有的)
将这些文件cp到
测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试标题</title>
</head>
<body>
<p>{{key}}</p>
<p>{{key}}</p>
<p>{{key}}</p>
</body>
</html>
#include <iostream>
#include <string>
#include<ctemplate/template.h>
int main()
{
std::string in_html = "./test.html";
std::string value = "sakeww!";
// 形成数据字典
ctemplate::TemplateDictionary root("test"); //unordered_map<> test;
root.SetValue("key", value); //test.insert({});
// 获取被渲染网页对象
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP);
// 添加字典数据到网页中
std::string out_html;
tpl->Expand(&out_html, &root);
//完成了渲染
std::cout << out_html << std::endl;
}
有如下结果则为正确
#pragma once
#include <iostream>
#include <string>
#include <ctemplate/template.h>
#include "oj_model.hpp"
namespace ns_view
{
using namespace ns_model;
const std::string template_path = "./template_html/";
class View
{
public:
View() {}
~View() {}
public:
void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
{
// 题目的编号 题目的标题 题目的难度
// 推荐使用表格显示
// 1. 形成路径
std::string src_html = template_path + "all_questions.html";
// 2. 形成数字典
ctemplate::TemplateDictionary root("all_questions");
//将所有的题渲染进去
for (const auto &q : questions)
{
ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
sub->SetValue("number", q.number);
sub->SetValue("title", q.title);
sub->SetValue("star", q.star);
}
// 3. 获取被渲染的html
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
// 4. 开始完成渲染功能
tpl->Expand(html, &root);
}
void OneExpandHtml(const struct Question &q, std::string *html)
{
// 1. 形成路径
std::string src_html = template_path + "one_question.html";
// 2. 形成数字典
ctemplate::TemplateDictionary root("one_question");
root.SetValue("number", q.number);
root.SetValue("title", q.title);
root.SetValue("star", q.star);
root.SetValue("desc", q.desc);
root.SetValue("pre_code", q.header);
// 3. 获取被渲染的html
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
// 4. 开始完成渲染功能
tpl->Expand(html, &root);
}
};
}
oj_control.hpp–负载均衡+核心业务逻辑控制
核心业务逻辑控制 大纲ns_control::Control
//核心业务逻辑控制器
class Control
{
private:
Model _model;//后台数据
View _view;//html功能
public:
Control(){}
~Control(){}
public:
//根据题目数据构建网页
bool AllQuestions(string *html)
{
//Question是一个结构体
vector<struct Question> all;
if(_model.GetALLQuestions(&all))
{
//将拿到的所有题目数据构建成网页
_view.AllExpandHtml(all,html);
}
else
{
*html="获取题目失败,形成题目列表失败!";
return false;
}
return true;
}
//拼接网页
bool Question(const string &number,string *html)
{
struct Question q;
if(_model.GetOneQuestion(number,&q))
{
// 构建指定题目信息成功,将所有的题目数据构建成网页
_view.OneExpandHtml(q,html);
}
else{
*html="获取题目:"+number+"失败!";
return false;
}
return true;
}
void Judge(const std::string in_json,std::string *out_json)
{
// 对in_json进行反序列化,得到用户提交的代码-》input
// 重新拼接用户代码+测试用例代码=新的代码
// 选择负载最低的主机,要进行差错处理
// 发起http请求,得到结果
// 将结果赋值给out_json
}
};
负载均衡 大纲–ns_control::Machine
//服务的主机
class Machine
{
public:
string ip;//编译服务的ip
int port;//编译服务的端口
uint64_t load;//编译服务的负载
//保护负载
mutex *mtx;
public:
Machine():ip(""),port(0),load(0),mtx(nullptr)
{}
~Machine(){}
};
//主机所在位置
const string service_machine="./conf/service_machine.conf";
//负载均衡
class LoadBlance
{
private:
//每一台主机都有自己的下标-》id
std::vector<Machine> machines;//能够给我们提供编译服务的所有主机
std::vector<int> online;//所有在线的主机id
std::vector<int> offline;//所有离线的主机id
public:
LoadBlance()
{
assert(LoadConf(service_machine));
}
//加载配置文件
bool LoadConf(const std::string &machine_list)
{
}
//智能选择
bool SmartChoice()
{
}
//离线主机
void OfflineMachine()
{
}
//上线主机
void OnlineMachine()
{
}
~LoadBlance()
{}
};
control.hpp–增加负载均衡
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <algorithm>
#include <mutex>
#include <cassert>
#include <jsoncpp/json/json.h>
#include "oj_model.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include "../comm/httplib.h"
#include "oj_view.hpp"
namespace ns_control
{
using namespace std;
using namespace ns_log;
using namespace ns_model;
using namespace ns_util;
using namespace ns_view;
using namespace httplib;
//服务的主机
class Machine
{
public:
string ip; //编译服务的ip
int port; //编译服务的端口
uint64_t load; //编译服务的负载
//保护负载
mutex *mtx;
public:
Machine() : ip(""), port(0), load(0), mtx(nullptr)
{}
~Machine() {}
// 提升主机负载
void IncLoad()
{
if (mtx)
mtx->lock();
++load;
if (mtx)
mtx->unlock();
}
// 减少主机负载
void DecLoad()
{
if (mtx)
mtx->lock();
--load;
if (mtx)
mtx->unlock();
}
void ResetLoad()
{
if (mtx)
mtx->lock();
load = 0;
if (mtx)
mtx->unlock();
}
// 获取主机负载
uint64_t Load()
{
uint64_t _load = 0;
if (mtx)
mtx->lock();
_load = load;
if (mtx)
mtx->unlock();
return _load;
}
};
//主机所在位置
const string service_machine = "./conf/service_machine.conf";
//负载均衡
class LoadBlance
{
private:
//每一台主机都有自己的下标-》id
std::vector<Machine> machines; //能够给我们提供编译服务的所有主机
std::vector<int> online; //所有在线的主机id
std::vector<int> offline; //所有离线的主机id
mutex mtx; //保证LoadBlance 主机数据安全
public:
LoadBlance()
{
assert(LoadConf(service_machine));
LOG(INFO) << "加载:" << service_machine << "成功!" << std::endl;
}
//加载配置文件
bool LoadConf(const std::string &machine_list)
{
std::ifstream in(machine_list);
if (!in.is_open())
{
LOG(FATAL) << "加载配置文件:" << machine_list << "失败!" << std::endl;
return false;
}
std::string line;
while (std::getline(in, line))
{
vector<string> v;
StringUtil::SplitString(line, &v, ":");
if (v.size() != 2)
{
//我们只设置了三个端口,所以切分没有什么问题
//但是,一般情况下,这里可能会写一些注释等别的语句,这些是不需要切分的
LOG(WARNING) << "切分" << line << "失败!" << std::endl;
continue;
}
Machine m;
m.ip = v[0];
m.port = atoi(v[1].c_str());
m.load=0;
m.mtx = new mutex();
online.push_back(machines.size());
machines.push_back(m);
}
in.close();
return true;
}
//智能选择
// id:主机id m:需要获取的详细信息
bool SmartChoice(int *id, Machine **m)
{
mtx.lock();
int online_num = online.size(); //当前主机在线个数
if (online_num == 0)
{
//所有主机均已经离线
mtx.unlock();
LOG(FATAL) << "所有的后端编译主机已经离线" << std::endl;
return false;
}
//遍历找到负载最小的机器
*id = online[0];
*m = &machines[online[0]];
uint64_t min_load = machines[online[0]].Load();
for (int i = 1; i < online_num; i++)
{
if (min_load > machines[online[i]].Load())
{
min_load = machines[online[i]].Load();
*id = online[i];
*m = &machines[online[i]];
}
}
mtx.unlock();
return true;
}
//离线主机
void OfflineMachine(int id)
{
mtx.lock();
for (auto iter = online.begin(); iter != online.end(); iter++)
{
if (*iter == id)
{
machines[id].ResetLoad();
//要离线的主机已经找到啦
online.erase(iter);
offline.push_back(id);
break; //因为break的存在,所有我们暂时不考虑迭代器失效的问题
}
}
mtx.unlock();
}
//在线主机
void OnlineMachine()
{
mtx.lock();
online.insert(online.end(), offline.begin(), offline.end());
offline.erase(offline.begin(), offline.end());
mtx.unlock();
LOG(INFO) << "所有的主机有上线啦!" << "\n";
}
//显示在线和离线主机
void ShowMachines()
{
mtx.lock();
std::cout << "当前在线主机: ";
for (auto &id : online)
{
std::cout << id << " ";
}
std::cout << std::endl;
std::cout << "当前离线主机: ";
for (auto &id : offline)
{
std::cout << id << " ";
}
std::cout << std::endl;
mtx.unlock();
}
};
//核心业务逻辑控制器
class Control
{
private:
Model _model; //后台数据
View _view; // html功能
LoadBlance _load_blance; //负载均衡
public:
Control() {}
~Control() {}
public:
void RecoveryMachine()
{
_load_blance.OnlineMachine();
}
//根据题目数据构建网页
bool AllQuestions(string *html)
{
bool ret = true;
vector<struct Question> all;
if (_model.GetALLQuestions(&all))
{
sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
});
// 获取题目信息成功,将所有的题目数据构建成网页
_view.AllExpandHtml(all, html);
}
else
{
*html = "获取题目失败, 形成题目列表失败";
ret = false;
}
return ret;
}
//拼接网页
bool Question(const string &number, string *html)
{
struct Question q;
if (_model.GetOneQuestion(number, &q))
{
// 构建指定题目信息成功,将所有的题目数据构建成网页
_view.OneExpandHtml(q, html);
}
else
{
*html = "获取题目:" + number + "失败!";
return false;
}
return true;
}
// number:题号 in_json:客户提交的代码 out_json:返回结果
void Judge(const std::string number, const std::string in_json, std::string *out_json)
{
// 根据题号,获取题目细节
struct Question q;
_model.GetOneQuestion(number, &q);
// 对in_json进行反序列化,得到用户提交的代码-》input
Json::Reader reader;
Json::Value in_value;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString(); //用户提交的代码
// 重新拼接用户代码+测试用例代码=新的代码
Json::Value compile_value; //编译代码
compile_value["input"] = in_value["input"].asString();
compile_value["code"] = code + "\n" + q.tail;
compile_value["cpu_limit"] = q.cpu_limit;
compile_value["mem_limit"] = q.mem_limit;
Json::FastWriter writer;
std::string compile_string = writer.write(compile_value);
// 选择负载最低的主机,要进行差错处理
// 规则:一直选择,直到主机可用,否则就是,全部挂掉(不需要客户知道)
while (true)
{
int id = 0;
Machine *m = nullptr;
if (!_load_blance.SmartChoice(&id, &m))
{
break;
}
LOG(INFO) << "选择主机:" << id << "成功,主机详情:" << m->ip << ":" << m->port << std::endl;
// 发起http请求,得到结果
Client cli(m->ip, m->port);
m->IncLoad();
LOG(INFO) << " 选择主机id: " << id << "成功 详情: " << m->ip << ":" << m->port << " 该主机的负载是: " << m->Load() << "\n";
if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
{
// 将结果赋值给out_json
if (res->status == 200)
{
*out_json = res->body;
m->DecLoad();
LOG(INFO) << "请求编译和运行服务成功!" << std::endl;
break;
}
}
else
{
//请求失败
LOG(ERROR) << "当前请求的主机id:" << id << "详情:" << m->ip << ":" << m->port << "可能已经离线" << std::endl;
_load_blance.OfflineMachine(id); //离线之后,会将负载清零
_load_blance.ShowMachines(); //显示所有在线和离线主机列表
}
}
}
};
}
最终测试:oj_server.cc
#include <iostream>
#include <signal.h>
#include "../comm/httplib.h"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_control;
static Control *ctrl_ptr = nullptr;
void Recovery(int signo)
{
ctrl_ptr->RecoveryMachine();
}
int main()
{
//使用信号进行主机上线
signal(SIGQUIT, Recovery);
Server svr;
Control ctrl;
ctrl_ptr = &ctrl;
// 返回一张包含有所有题目的html网页
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){
std::string html;
ctrl.AllQuestions(&html);
resp.set_content(html, "text/html; charset=utf-8");
});
// 用户要根据题目编号,获取题目的内容
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){
std::string number = req.matches[1];
std::string html;
ctrl.Question(number, &html);
resp.set_content(html, "text/html; charset=utf-8");
});
// 用户提交代码,进行判题
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){
std::string number = req.matches[1];
std::string result_json;
ctrl.Judge(number, req.body, &result_json);
resp.set_content(result_json, "application/json;charset=utf-8");
});
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
return 0;
}
5项目延伸:使用数据库
建表相关操作
使用workbench建立表格和插入数据
CREATE TABLE IF NOT EXISTS `questions`(
id int PRIMARY KEY AUTO_INCREMENT COMMENT '题目的ID',
title VARCHAR(64) NOT NULL COMMENT '题目的标题',
star VARCHAR(8) NOT NULL COMMENT '题目的难度',
question_desc TEXT NOT NULL COMMENT '题目描述',
header TEXT NOT NULL COMMENT '题目头部,给用户看的代码',
tail TEXT NOT NULL COMMENT '题目尾部,包含我们的测试用例',
time_limit int DEFAULT 1 COMMENT '题目的时间限制',
mem_limit int DEFAULT 5000000 COMMENT '题目的空间限制'
)ENGINE=INNODB DEFAULT CHARSET=utf8;
其中number 和 desc 不能直接写入创建语句中,可以先这样写
workbench支持更改列名
更改oj_model_db.hpp内容
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include "../comm/log.hpp" //日志
#include <cassert> //assert
#include <fstream>
#include "../comm/util.hpp" //字符串切分
#include <cstdlib> //atoi
#include "include/mysql.h"
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
//题目信息
struct Question
{
string number; //题目编号
string title; //题目标题
string star; //难度
string desc; //题目描述
string header; //预设代码
string tail; //测试用例
int cpu_limit; //时间要求
int mem_limit; //空间要求
};
const std::string host = "127.0.0.1";
const std::string user = "fengyin";
co\nst std::s\tring pas\swd = "XXX";//这里输入你的密码即可,忽略\
//CSDN会进行password检测
const std::string db = "OJ";//数据库名
const int port = 3306;
class Model
{
public:
Model()
{}
//根据sql语句,返回需要的Question
bool QueryMySql(const std::string &sql, vector<Question> *out)
{
// 创建mysql句柄
MYSQL *my = mysql_init(nullptr);
// 连接数据库
if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0))
{
LOG(FATAL) << "连接数据库失败!"<<std::endl;
return false;
}
// 设置该链接的编码格式
mysql_set_character_set(my, "utf8");
LOG(INFO) << "连接数据库成功!"<<std::endl;
// 执行sql语句
if (0 != mysql_query(my, sql.c_str()))
{
LOG(WARNING) << sql << " 执行失败!" <<std::endl;
return false;
}
// 提取结果
MYSQL_RES *res = mysql_store_result(my);
// 分析结果
int rows = mysql_num_rows(res);
int cols = mysql_num_fields(res);
struct Question q;
for (int i = 0; i < rows; i++)
{
MYSQL_ROW row = mysql_fetch_row(res);//返回一个数组
q.number = row[0];
q.title = row[1];
q.star = row[2];
q.desc = row[3];
q.header = row[4];
q.tail = row[5];
q.cpu_limit = atoi(row[6]);
q.mem_limit = atoi(row[7]);
out->push_back(q);
}
// 释放结果空间
free(res);
// 关闭mysql连接
mysql_close(my);
return true;
}
bool GetAllQuestions(vector<Question> *out)
{
std::string sql = "select * from oj_questions";
return QueryMySql(sql, out);
}
bool GetOneQuestion(const std::string &number, Question *q)
{
std::string sql = "select * from oj_questions where number=";
sql += number;
vector<Question> result;
if (QueryMySql(sql, &result))
{
if (result.size() == 1)
{
*q = result[0];
return true;
}
}
return false;
}
~Model() {}
};
}
6.补充:
所有代码文件图解
顶层makefile与创建发布版本output
# 编译
.PHONY: all
all:
@cd compile_server;\
make;\
cd -;\
cd oj_server;\
make;\
cd -;
# 创建output文件,存放发布版本
.PHONY:output
output:
@mkdir -p output/compile_server;\
mkdir -p output/oj_server;\
cp -rf compile_server/Compile_server output/compile_server;\
cp -rf compile_server/temp output/compile_server;\
cp -rf oj_server/conf output/oj_server;\
cp -rf oj_server/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 oj_server/OJ_server output/oj_server;
# 删除所有可执行程序
.PHONY:clean
clean:
@cd compile_server;\
make clean;\
cd -;\
cd oj_server;\
make clean;\
cd -;\
rm -rf output;
gitee
https://gitee.com/sakeww/linux_code/tree/master/item_oj