这是本人写的第二个项目,相比第一个代码量更少一些,但是此项目涉及linux中的内容更多,同样是干货满满,实现了 类似 leetcode 的题⽬列表+在线编程功能,地址仓库:xwy/C++学习项目
1. 所用技术与开发环境
C++11和 STL Boost标准库用于字符串切割 http-lib库 ctemplate前端⽹⻚渲染库 jsoncpp 第三⽅开源序列化、反序列化库
2.项目宏观结构
交互流程:
客户端请求所有题目的列表,服务器返回对应html页面,客户端请求某一个题目的具体内容,服务器返回某一个题目具体内容的页面,客户端提交代码,oj_server将接收代码传给compile_server进行编译运行,compile_server将结果返回oj服务器再返回网页。
3.具体实现
3.1 编写compile_server
compile_server.cc 是httplib服务器的post方法,它调用httplib的Server服务器,调用
svr.Post("/compile_and_run", [](const Request &req, Response &resp){}),它调用编译运行类的start函数,我们传入用户代码req,返回响应resp。
编译运行类的start函数:
我们一般执行项目编译是执行g++命令,但我们需要把它写成网络服务的形式,因此我们可以把用户代码放在文件当中,然后使用exec系列函数去调用g++执行它。
编译运行函数首先生成temp目录下的唯一文件名,然后生成.cpp后缀文件,
调用编译函数
我们知道出现编译错误会在屏幕上输出,此时我们可以先创建打开一个compile.err文件,dup2重定向之后再执行程序替换,此时如果出现编译报错,则错误信息会输出到该文件中,然后我们可以通过是否生成对应的exe文件来判断程序是否成功执行。
调用运行函数
运行函数先fork,之后将标准输入,标准输出,标准错误重定向到三个文件中,然后SetProcLimit(cpu_limit, mem_limit),之后执行execl函数。
编译运行函数
需要对差错情况进行统一处理,如果status_code为-1,则用户提交代码为控,code为-2,程序内部错误,code -3编译失败,返回编译错误的文件,code大于等于0的条件下,有信号捕获信号,返回状态码,状态码描述,stdout和stderr文件。
3.2 编写oj_server
oj_server采用mvc结构,我们需要定义control类,传入server的post和get方法之中。一共客户端有三种请求,所有题目的页面get请求,单个题目的页面get请求,代码的提交post请求,我们分别调用control模块的三个方法进行处理。
文件版Model模块
维护了一个题目编号到题目的一个unordered_map,读取文件中的题目编号,标题,难度,cpu限制和时间限制,同时读取题目的详细描述,header.cpp,tail.cpp到Qusetion类中,该类还提供了两个对外部的接口,返回所有题目vector<Question> ,以及根据一个题目编号返回对应Question
数据库版Model模块
通过sql语句在数据库中执行查询,用for循环将返回的row[0],row[1]..。分别赋值给结果Question
View模块
将获取到的Question进行渲染到页面上,返回对应的html字符串
control模块
整合连接上面两个模块,获取全部题目以及获取一个题目的函数不用多讲,关键看Judge函数
我们得到用户的前端代码之中,需要获取对应题目的具体细节,然后还需要拼接code和tail.cpp,将它们重新组织成新的JSON格式字符串,然后负载均衡式的选择主机,最后作为客户端请求compile_server的编译运行服务。
4.遇到问题
采用何种方式生成唯一文件名?
我们只需要保证文件名在各台机器下是唯一的即可,因此我们采取获取毫秒级时间戳和原子性id自增的方式来生成文件名,如果需要多进程下保证唯一性,则需要C++17的UUID或者boost库的雪花算法保证在多进程下的唯一性。
如何判断编译是否完成?
大多数情况都是通过父进程获取子进程的退出码来判断程序是否成功,但是该程序替换后执行g++命令,我们对退出码是多少不可知,因此可以通过/temp目录下是否生成对应.exe文件来判断是否编译成功。
负载均衡模块的具体实现?
每台机器维护了负载数量,锁(httplib使用多线程处理请求),主机ip和port。负载均衡模块维护了离线主机和在线主机,我们遍历在线主机,我们可以只在里面存储主机下标,找到负载最小的主机,对它发起post请求即可