在线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. 提供一个数据解码接口
前面已经实现用户可以获取单个题目的详细信息,并且可以进行作答,现在我们应该在oj_server模块当中提供一个接收用户提交的代码。
但是用户在写完代码后,通过浏览器向服务器提交代码,浏览器会对用户提交的内容进行‘转译’
(原因是提交的数据当中可能有一些特殊符号),所以服务器收到的数据是经过’转译‘后的,所以我们应该将其解码
在工具模块提供数据解码的接口:
tool.hpp
:
class UrlUtil
{
public:
//const std::string& body:待解码的数据
//std::unordered_map<std::string, std::string>* pram:代表保存
//解码出来的数据,是K-V型
static void PraseBody(const std::string& body, std::unordered_map<std::string, std::string>* pram)
{
//这里进行切割是为了后续项目的拓展,比如力扣是支持stdin的
//code=xxx&stdin=xxxx
//进行切割
std::vector<std::string> tokens;
StringTools::Split(body, "&", &tokens);
for(const auto& token:tokens)
{
//code=xxxx(xxx是真正用户提交的代码)
std::vector<std::string> vec;
StringTools::Split(token, "=", &vec);
//代表切割后的数据不完整
if(vec.size() != 2)
{
continue;
}
//k:vec[0] ---》name
//v:UrlDecode(vec[1])---》xxx
(*pram)[vec[0]] = UrlDecode(vec[1]);
}
}
private:
static unsigned char ToHex(unsigned char x)
{
return x > 9 ? x + 55 : x + 48;
}
static unsigned char FromHex(unsigned char x)
{
unsigned char y;
if (x >= 'A' && x <= 'Z') y = x - 'A' + 10;
else if (x >= 'a' && x <= 'z') y = x - 'a' + 10;
else if (x >= '0' && x <= '9') y = x - '0';
else assert(0);
return y;
}
static std::string UrlEncode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
if (isalnum((unsigned char)str[i]) ||
(str[i] == '-') ||
(str[i] == '_') ||
(str[i] == '.') ||
(str[i] == '~'))
strTemp += str[i];
else if (str[i] == ' ')
strTemp += "+";
else
{
strTemp += '%';
strTemp += ToHex((unsigned char)str[i] >> 4);
strTemp += ToHex((unsigned char)str[i] % 16);
}
}
return strTemp;
}
static std::string UrlDecode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
if (str[i] == '+') strTemp += ' ';
else if (str[i] == '%')
{
assert(i + 2 < length);
unsigned char high = FromHex((unsigned char)str[++i]);
unsigned char low = FromHex((unsigned char)str[++i]);
strTemp += high*16 + low;
}
else strTemp += str[i];
}
return strTemp;
}
};
2. 对用户提交的代码和题目的main函数进行拼接
因为用户提交上来的代码可能只是一个函数接口,我们需要在服务器后台oj_model模块提供一个接口,把这道题的相关头文件及main函数信息进行拼接成一份完整的代码
oj_model.hpp
:
//std::string user_code:用户提交并经过解码后的代码
//const std::string& ques_id:这道题的id,根据这个id找对应的tail.cpp
//std::string* code:将拼接完成后的完整代码返回
bool SplicingCode(std::string user_code, const std::string& ques_id, std::string* code)
{
//1.查找下对应id的题目是否存在
auto iter = model_map_.find(ques_id);
if(iter == model_map_.end())
{
LOG(ERROR, "can not find question id is ") << ques_id << std::endl;
return false;
}
//获取tail.cpp内容
std::string tail_code;
int ret = FileOper::ReadDataFromFile(TailPath(iter->second.path_), &tail_code);
if(ret < 0)
{
LOG(ERROR, "Open tail.cpp failed");
return false;
}
//拼接
*code = user_code + tail_code;
return true;
}
3. 将code写入到文件并对文件进行命名的接口
服务器可能会在同一时刻收到多个用户提交的代码,都需要进行编译,但是我们是用的g++编译器,所以用时间戳来对编译文件进行命名。
compile.hpp
:
private:
//const std::string& code:代表完整的代码
static std::string WriteTmpFile(const std::string& code)
{
//1.组织文件名称,组织文件的前缀名称,用来区分源码文件,可执行文件是同一组数据
std::string tmp_filename = "tmp_" + std::to_string(LogTime::GetTimeStamp());
//写文件
int ret = FileOper::WriteDataToFile(SrcPath(tmp_filename), code);
if(ret < 0)
{
LOG(ERROR, "Write code to source failed");
return "";
}
//返回这个命名文件的前缀,后面对文件进行命名的时候会用到,用来区分是否是同一个用户请求的数据
return tmp_filename;
}
4. 对源文件进行编译
需要创建子进程完成来执行编译命令,并对子进程进行进程程序替换
compile.hpp
:
//const std::string& filename:代表源码文件的前缀
static bool Compile(const std::string& filename)
{
//1.构造编译命令--》g++ src -o [exec] -std=c++11
const int commandcount = 20;
char buf[commandcount][50] = {{0}};
char* Command[commandcount] = {0};
for(int i = 0; i < commandcount; i++)
{
Command[i] = buf[i];
}
snprintf(Command[0], 49, "%s", "g++");
snprintf(Command[1], 49, "%s", SrcPath(filename).c_str());
snprintf(Command[2], 49, "%s", "-o");
snprintf(Command[3], 49, "%s", ExePath(filename).c_str());
snprintf(Command[4], 49, "%s", "-std=c++11");
snprintf(Command[5], 49, "%s", "-lpthread");
Command[5] = NULL;
//2.创建子进程
// 2.1 父进程 --》 等待子进程退出
// 2.2 子进程 --》 进程程序替换--》 g++
int pid = fork();
if(pid < 0)
{
LOG(ERROR, "Create child process failed");
return false;
}
else if(pid == 0)
{
//child
int fd = open(ErrorPath(filename).c_str(), O_CREAT | O_RDWR, 0664);
//重定向,把标准错误重定向到文件
dup2(fd, 2);
//程序替换
execvp(Command[0], Command);
//注意这个exit是非常重要的,如果替换失败,就直接exit掉
exit(0);
}
else
{
//father
waitpid(pid, NULL, 0);
}
//3.验证是否生产可执行程序
}
5. 对编译出来的结果运行
static bool Run()
{
}
6. 构造运行结果的响应给浏览器
7. 完整拼接代码并进行编译运行的接口
完整拼接代码并进行编译运行,并构造响应返回给浏览器的接口
compile.hpp
:
#pragma once
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string>
#include <json/json.h>
#include "oj_log.hpp"
#include "tools.hpp"
class Compiler
{
public:
//有可能浏览器对不同的题目提交的数据是不同的
//code="xxx"
//code="xxx"&stdin="xxx" code + tail.cpp
static void CompileAndRun(Json::Value Req, Json::Value* Resp)
{
//1.判空
//{"code":"xxx", "stdin":"xxx"}
if(Req["code"].empty())
{
LOG(ERROR, "Request code is Empty") << std::endl;
return;
}
//2.将代码写到文件当中去
std::string code = Req["code"].asString();
//文件名称进行约定 tmp_时间戳.cpp
std::string tmp_filename = WriteTmpFile(code);
if(tmp_filename == "")
{
LOG(ERROR, "Write Source failed");
return;
}
//3.编译
if(!Compile())
{
}
//4.运行
if(!Run())
{
}
//5.构造响应
}
//生成对应文件后缀的文件名
static std::string SrcPath(const std::string& filename)
{
return "./tmp_files" + filename + ".cpp";
}
static std::string ErrorPath(const std::string& filename)
{
return "./tmp_files" + filename + ".err";
}
static std::string ExePath(const std::string& filename)
{
return "./tmp_files" + filename + ".executable";
}
};
8. 接收用户提交代码的完整代码
接收用户提交代码的完整代码:
oj_server.cpp
:
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);
//LOG(INFO, "code ") << code << std::endl;
Json::Value req_json;
req_json["code"] = code;
//req_json["stdin"] = ""
Json::value Resp_json;
Compiler::CompileAndRun(req_json, &Resp_json);
//3.构造响应,json
std::string html = "1";
resp.set_content(html,"text/html; charset=UTF-8");
});