一.实现的功能
实现类似于leetcode的题目列表+在线编程功能
二.项目的宏观结构
有三个模块:
1.comm:公共模块
2.compile_server:编译与运行模块
3.oj_server:获取题目列表,查看题目编写题目界面,负载均衡,其他功能
三.编写思路
1.先编写compile_server
2.oj_server
3.version1基于文件版的在线OJ
4.前端的页面设计
5.version2基于MySQL版的在线OJ
四.编译运行服务设计
4.1 实现的功能
给用户提供代码编译服务,包括编译并运行代码,得到格式化的相关结果。
4.2 模块框架
4.3 代码编写思路
1.先对用户提交的代码信息进行反序列化,提取具体代码,以及时间空间复杂度
2.利用原子计数器以及时间戳创建唯一代码文件
3.创建子进程然后利用程序替换函数来实现代码文件的编译功能,并将标准输出和标准错误输出重定向到文件中,如果生成编译输出文件,则返回错误状态码
4.有了编译输出文件后,创建子进程然后利用程序替换函数来实现编译输出文件的运行,并将标准输出和标准错误输出重定向到文件中,在程序运行过程中,如果发生异常,还要返回程序运行的异常退出码
5.根据状态码,形成不同的输出Json字符串,最后将Json字符串返回给用户
4.4 核心代码
namespace ns_CompilRun
{
class vacCompilRun
{
public:
static void RemoveFile(std::string& sFileName)
{
//清理临时文件,一个一个按序清理
std::string sExeFilePath = vacTools::AddExeSuffix(sFileName);
unlink(sExeFilePath.c_str());
std::string sSrcFilePath = vacTools::AddSrcSuffix(sFileName);
unlink(sSrcFilePath.c_str());
std::string sCompilerrFilePath = vacTools::AddCompilerrSuffix(sFileName);
unlink(sCompilerrFilePath.c_str());
std::string sStdOutFilePath = vacTools::AddStdoutSuffix(sFileName);
unlink(sStdOutFilePath.c_str());
std::string sStdInFilePath = vacTools::AddStdinSuffix(sFileName);
unlink(sStdInFilePath.c_str());
std::string sStderrFilePath = vacTools::AddStderrSuffix(sFileName);
unlink(sStderrFilePath.c_str());
}
//这里的status是编译和运行模块的状态码
//status == 0 //编译和运行完全正常
//status < 0 //表示信息错误,比如代码为空,编译失败,未知错误
//status > 0 //表示运行期间发生异常
static std::string CodeToDecs(int nStatus, std::string& sFileName)
{
std::string sDecs;
switch(nStatus)
{
case 0:
sDecs = "编译运行成功";
break;
case -1:
sDecs = "代码为空";
break;
case -2:
sDecs = "未知错误";
break;
case -3:
vacTools::ReadFile(vacTools::AddCompilerrSuffix(sFileName), sDecs);
break;
case SIGXCPU:
sDecs = "超出运行时间";
break;
case SIGABRT:
sDecs = "超出内存限制";
break;
default:
sDecs = "其他错误" + std::to_string(nStatus);
break;
}
return sDecs;
}
/*在这一个模块中,要实现:
*1.通信协议的搭建:序列化和反序列化
*2.根据用户需求形成唯一代码文件
*3.完成编译加运行功能,将输出信息通过
*/
/*
*1.第一个参数表示从网络中获取到的Json字符串
*2.第二个参数表示将带有结果信息的Json字符串输出给上一层
* 第一个参数中,可能有以下几个值:
* 1.代码code
* 2.资源限制信息:cpu限制,内存限制
* 第二个参数中,可能有以下几个值:
* 1.状态码:0表示全部运行成功
* 2.状态码解释
* 3.当运行完成时,返回的结果(选填)
*/
static void CompileRun(std::string& sJsonIn, std::string *sJsonOut)
{
Json::Value jsInValue;//序列化的Json
Json::Reader jsRead;
jsRead.parse(sJsonIn.c_str(), jsInValue);
std::string sCode = jsInValue["code"].asString();
int nCpuLimit = jsInValue["cpulimit"].asInt();
int nMemLimit = jsInValue["memlimit"].asInt();
int nStatus = 0;//返回的状态码
int nRunStatus = 0;//用于记录运行阶段的状态码
Json::Value jsOutValue;//反序列化的Json
Json::FastWriter JsWrite;
//根据代码,形成唯一文件名,
//毫秒级时间戳+原子性递增唯一值:来保证唯一性
std::string sFileName = vacTools::CreateUniqFileName(sCode);
if(sCode.size() == 0)
{
nStatus = -1;
goto END;
}
if(!vacTools::WriteFile(vacTools::AddSrcSuffix(sFileName), sCode))
{
nStatus = -2;
goto END;
}
if(!vacCompiler::IsCompiler(sFileName))
{
//在编译服务中,我们只关心是否有编译文件输出,所以只需要一个错误码即可
nStatus = -3;
goto END;
}
else
{
//在运行服务中,因为我们要关心运行过程中发生的具体的运行异常情况,比如超过时间复杂度,空间复杂度等异常,所以就要返回错误码,而且这些错误码还要和信息错误作区分,所以就需要多个错误码
nRunStatus = vacRunner::Run(sFileName, nCpuLimit, nMemLimit);//kb
if(nRunStatus > 0)//运行发生异常
{
nStatus = nRunStatus;
goto END;
}
else if(nRunStatus < 0)//信息错误
{
nStatus = -2;
goto END;
}
else//运行成功
{
nStatus = 0;
goto END;
}
}
END:
jsOutValue["status"] = nStatus;
jsOutValue["reason"] = CodeToDecs(nStatus, sFileName);
if(nStatus == 0)
{
std::string sStdOut;
std::string sStdErr;
vacTools::ReadFile(vacTools::AddStdoutSuffix(sFileName), sStdOut);
vacTools::ReadFile(vacTools::AddStderrSuffix(sFileName), sStdErr);
jsOutValue["stdout"] = sStdOut.c_str();
jsOutValue["stderr"] = sStdErr.c_str();
}
*sJsonOut = JsWrite.write(jsOutValue);
RemoveFile(sFileName);
}
};
}
五.OJServer服务设计
5.1 实现的功能
1.根据用户需求,用题目信息构建出html网页,然后将html网页传至前端显示
2.负载均衡的实现编译功能,根据用户提交的过来的代码,然后将代码给不同的主机编译运行,取得结果,返回给用户
5.2 模块框架
5.3 代码编写思路
5.3.1 获取题目信息的代码编写思路
1.创建一个把全部题目具体信息加载到内存中的Model对象
2.然后从该类中提取具体题目的信息或是提取全部题目的信息
3.渲染网页,回传给前端
5.3.2 实现判题的代码编写思路
1.将用户提交的数据反序列化,并提取具体的代码信息
2.根据用户提供的题号,从包含全部题目具体信息的Model对象中,提取该题目的测试用例代码,以及时间复杂度要求,空间复杂度要求,最后将测试用例代码和具体的代码信息结合形成供服务器编译的代码
3.创建一个实现负载均衡的LoadBalance类,将全部服务器信息加载到类中。
4.根据服务器负载最小的原则,以轮询遍历加哈希映射的方式从LoadBalance类中选出具体的服务器。
5.将具体代码还有时间空间复杂度序列化,然后作为报文数据,向该服务器发起http请求。
6.当收到的应答报文的状态码是200,则表示成功接收到结果,然后将结果返回给前端界面,如果状态码不是200,则重新选择合适的服务器。
5.4 核心代码
namespace ns_Control
{
using namespace httplib;
using namespace ns_Tools;
// 每一台服务器的具体信息
struct vacMachine
{
std::string m_sIp;
std::string m_sPort;
int m_nLoad;
std::mutex* m_mtMachine;
int GetLoad()
{
m_mtMachine->lock();
int nLoad = m_nLoad;
m_mtMachine->unlock();
return nLoad;
}
void IncLoad()
{
m_mtMachine->lock();
m_nLoad++;
m_mtMachine->unlock();
}
void DecLoad()
{
m_mtMachine->lock();
m_nLoad--;
m_mtMachine->unlock();
}
};
// 为了让各个服务器都能执行编译服务,该类实现各个服务器的负载均衡
class vacLoadBalance
{
private:
const std::string sMachinesFilePath = "./Machines/MachinesList.list";
std::vector<vacMachine*> vMaContainer;//每个服务器都有对应的下标
std::vector<int> vOnLineMa;//初始值为全部服务器都在线,把每个服务器的下标都放入进去
std::vector<int> vOffLineMa;//下线的服务器的下标
std::mutex mtLoadBalance;
public:
vacLoadBalance()
{
assert(OpenMachinesFile());
for(int i = 0; i < vMaContainer.size(); i++)
{
vOnLineMa.push_back(i);
}
Log(INFO) << "加载服务器配置信息成功" << std::endl;
}
bool OpenMachinesFile()
{
std::ifstream ifsMachinesFile(sMachinesFilePath.c_str());
if(ifsMachinesFile.is_open())
{
std::string sLine;
while(getline(ifsMachinesFile, sLine))
{
std::vector<std::string> vsContent;
vacTools::SplitString((&vsContent), sLine, " ");
if (vsContent.size() != 3)
{
Log(ERROR) << "分割服务器信息字符失败,请重试" << std::endl;
continue;
}
vacMachine* iMachine = new vacMachine;
iMachine->m_sIp = vsContent[0];
iMachine->m_sPort = vsContent[1];
iMachine->m_nLoad = 0;
iMachine->m_mtMachine = new std::mutex;
vMaContainer.push_back(iMachine);
}
}
else
{
Log(FATAL) << "打开服务器配置文件失败!!" << std::endl;
return false;
}
return true;
}
bool SeletMachine(int* nID, vacMachine* ipMachine)
{
//1.选择好的主机(更新该主机的负载)
//2.我们可能需要离线主机
mtLoadBalance.lock();
if(vOnLineMa.size() > 0)
{
*nID = vOnLineMa[0];
ipMachine = vMaContainer[vOnLineMa[0]];
for(int i = 0; i < vOnLineMa.size(); i++)
{
if(ipMachine->GetLoad() > vMaContainer[vOnLineMa[i]]->GetLoad())
{
*nID = vOnLineMa[i];
ipMachine = vMaContainer[vOnLineMa[i]];
}
}
mtLoadBalance.unlock();
Log(INFO) << "成功选择一台主机,ID: " << *nID << " IP: " << ipMachine->m_sIp << " Port: " << ipMachine->m_sPort << std::endl;
return true;
}
else
{
mtLoadBalance.unlock();
Log(FATAL) << "没有在线的主机,请联系运维" << std::endl;
return false;
}
mtLoadBalance.unlock();
return false;
}
void OffLineMachine(int* nID)
{
mtLoadBalance.lock();
for(auto iter = vOnLineMa.begin(); iter != vOnLineMa.end(); iter++)
{
if(*iter == *nID)
{
vMaContainer[vOnLineMa[*nID]]->m_nLoad = 0;
vOnLineMa.erase(iter);
vOffLineMa.push_back(*nID);
break;
}
}
mtLoadBalance.unlock();
}
void OnLineMachine(int* nID)
{
mtLoadBalance.lock();
for(auto iter = vOffLineMa.begin(); iter != vOffLineMa.end(); iter++)
{
if(*iter == *nID)
{
vOffLineMa.erase(iter);
vOnLineMa.push_back(*nID);
break;
}
}
mtLoadBalance.unlock();
}
void ShowMachines()
{
std::cout << "当前在线的主机id: ";
for(auto& iter : vOnLineMa)
{
std::cout << iter << " ";
}
std::cout << std::endl;
std::cout << "当前离线的主机id: ";
for(auto& iter : vOnLineMa)
{
std::cout << iter << " ";
}
std::cout << std::endl;
}
};
using namespace ns_Model;
using namespace ns_View;
class vacControl
{
private:
vacModel iModel;
vacView iView;
vacLoadBalance iBalance;
public:
// 功能:1.从Model中获取所有题目信息,包括:题目题号,题目标题,题目难度
// 2.然后构建html网页,
// 3.最后将html网页传给用户
// sHtml是输出型参数,将构建的参数返回给上一层
// 返回值判定是否读取到题目信息以及是否成功构建网页
bool DisplayAllSubjects(std::string *sHtml)
{
// 存放每一个题目的简要信息
std::vector<vacSubject> vOut;
// 获取题目的简要信息
if (iModel.GetAllSubject(&vOut))
{
// 渲染网页
iView.GetAllSubHtml(vOut, (*sHtml));
return true;
}
else
{
*sHtml = "获取题目失败,不能形成网页";
return false;
}
return false;
}
// 功能:
// 1.从Model中获取单个题目的具体数据,包括:题目描述,题目的前缀代码,题目题号,题目标题,题目难度
// 2.然后构建html网页,
// 3.最后将html网页传给用户
// sHtml是输出型参数,将构建的参数返回给上一层,sNumber也是输入型参数,表示题号
// 返回值判定是否读取到题目信息以及是否成功构建网页
bool DisplayOneSubject(std::string sNumber, std::string *sHtml)
{
// 题目的具体数据类
vacSubject iSubject;
// 获取题目的具体数据
if (iModel.GetOneSubject(sNumber, &iSubject))
{
// 成功获取数据后,渲染网页
iView.GetOneSubHtml(iSubject, (*sHtml));
return true;
}
else
{
*sHtml = "获取题目失败,不能形成网页";
return false;
}
return false;
}
//sInJson: "code" = "#include...." "input" = ...
bool JudgeSubject(const std::string sNumber, const std::string sInJson, std::string* OutJson)
{
//1. sInJson进行反序列化,得到题目id,得到用户提交的源代码
Json::Reader jsReader;
Json::Value jsReadValue;
jsReader.parse(sInJson, jsReadValue);
const std::string sCode = jsReadValue["code"].asString();
const std::string sInput = jsReadValue["input"].asString();
vacSubject iSubject;
if(iModel.GetOneSubject(sNumber, &iSubject))//获取题目成功
{
//2. 重新拼接用户代码+测试用例代码,形成新的代码
const std::string sAllCode = sCode + iSubject.sCodeTest;
Json::FastWriter jsWriter;
Json::Value jsWriteValue;
jsWriteValue["code"] = sAllCode;
jsWriteValue["cpulimit"] = iSubject.nCpuLimit;
jsWriteValue["memlimit"] = iSubject.nMemLimit;
const std::string sCompileStr = jsWriter.write(jsWriteValue);
//3. 选择负载最低的主机(差错处理)
int nID;
vacMachine iMachine;
while(iBalance.SeletMachine(&nID, &iMachine))
{
//4. 然后发起http请求,得到结果
Client iclient(iMachine.m_sIp, stoi(iMachine.m_sIp));
iMachine.IncLoad();
auto rRespon = iclient.Post("/Judge",sCompileStr, "application/json; charset=utf-8");
if(rRespon == nullptr)//没有响应,直接下线该主机
{
iMachine.DecLoad();
Log(ERROR) << "该主机无响应,它的ID: " << nID << " IP: " << iMachine.m_sIp << " Port: " << iMachine.m_sPort << std::endl;
iBalance.OffLineMachine(&nID);
iBalance.ShowMachines();
}
else if(rRespon->status == 200)//状态码为200,说明成功接收到结果
{
//5. 将结果赋值给OutJson
*OutJson = rRespon->body;
iMachine.DecLoad();
return true;
}
iMachine.DecLoad();//其他状态码,继续选择
}
}
else{
return false;
}
}
};
}
六.附录:核心工具
根据特定分隔符提取字符串:
//第一个参数是输出的容器,第二个参数是需要提取字符的字符串, 第三个参数是分隔符
static void SplitString(std::vector<std::string> *vsOut, std::string sLine, std::string sChar)
{
boost::split(*vsOut, sLine, boost::is_any_of(sChar.c_str()), boost::token_compress_on);
}
获取毫秒时间戳工具:
static std::string GetTimeStampMs()
{
struct timeval tvTime;
gettimeofday(&tvTime, nullptr);
return std::to_string(tvTime.tv_sec * 1000 + tvTime.tv_usec / 1000);
}
获取时间戳工具:
static std::string GetTimeStamp()
{
struct timeval tvTime;
gettimeofday(&tvTime, nullptr);
return std::to_string(tvTime.tv_sec);
}
读文件:
static bool ReadFile(const std::string& sPathName, std::string& sReadFile, bool bMode = true/*按行读取*/)
{
std::ifstream isInFile;
isInFile.open(sPathName);
if(!isInFile.is_open())
{
return false;
}
std::string sLine;
while(getline(isInFile, sLine))
{
sReadFile += sLine;
if(bMode)
{
sReadFile += "\n";
}
}
isInFile.close();
return true;
}
写文件:
static bool WriteFile(const std::string& sPathName, const std::string& sContent)
{
std::ofstream osOutFile;
osOutFile.open(sPathName);
if(!osOutFile.is_open())
{
return false;
}
osOutFile << sContent;
osOutFile.close();
return true;
}
创建唯一文件名:
static std::string CreateUniqFileName(const std::string& sCode)
{
std::atomic_uint atID(0);
atID++;
return GetTimeStampMs() += "_" + std::to_string(atID);
}