14.onlineoj项目总结(C++)
流程和架构设计
整个项⽬划分为3个⼤模块:
- comm:公共模块
- compile_server:编译运⾏模块,作为⼦服务器被调⽤
- oj_server:作为⼊⼝服务器,与客⼾端进⾏直接交互,实现获取题⽬列表、查看题⽬、请求特定题⽬的编写、提交代码等功能,将编译运⾏等耗费cpu的⾏为转发到其他⼦服务器中
oj_server是使⽤httplib搭建的⼀个http服务器,只需要编写业务代码处理相关的业务逻辑即
可,包含的业务有:
- 获取题⽬列表
- 查看题⽬
- 请求特定题⽬的编写
- 提交运⾏代码
compile_server作为编译运⾏代码的服务器流程如下:
- 先将客⼾端提交的代码写⼊到⼀个临时的源⽂件中
- fork⼦进程帮助我们编译代码,因为进程之间相互独⽴,⼦进程如果崩溃,不会影响到其他进⾏的运⾏
- 编译通过继续执⾏
- 编译出错,形成⼀个临时⽂件来保存编译出错的结果
- 返回结果给客⼾端
项⽬最终的成果
最终的成果:
项⽬实现了两个服务器程序,
oj_server
主要负责响应客⼾端的通⽤http请求以及将编译运⾏代码任务转发到compile_server上处理,
compile_server
主要是⽤来编译运⾏代码的,能够⽀持客⼾端浏览器发起的http请求(请求题⽬列表、请求特定题⽬的编写、提交运⾏代码)进⾏响应处理。
学到了什么
- C++⼯程能⼒得到了⼀些实际的提升
- 学到了⼀些新技术,⽐如怎么通过httplib构建http服务器、线程池的实现和使⽤、怎么使⽤jsoncpp进⾏序列化/反序列化等等
- 学到了以项⽬为主导的开发应该如何完成,⽐如从项⽬确定到功能拆解,再到开发测试部署。
- 学到了当项⽬中遇到问题,迫使我思考和寻找解决⽅案,这增强了我的问题解决能⼒。
- 学到了如何进⾏有效的测试,以确保项⽬不出现bug。例如功能测试,性能测试等。
使⽤了哪些技术
- C++11标准库:C++⽐较常⽤的标准之⼀
- cpp-httplib :对⽐了httpd、mongoose等其他的第三⽅库,发现httplib是only header模式,⽤法接⼝设计的⽐较简单易⽤
- ctemplate :⼀个⽤于C++的模板引擎,它提供了⼀种将数据插⼊到模板⽂件中的⽅式,⽣成格式化的⽂本输出
- jsoncpp :对⽐了nlohmann/json和RapidJSON库,发现JsonCpp提供了易于使⽤的API来解析和⽣成JSON数据
- MySQL C connect:C++没有⽐较好的ORM框架,所以使⽤原⽣的C语⾔接⼝来操作Mysql数据库,⾃⼰可以加⼀层封装,⽐较灵活
- html/css/js/jquery/ajax 前端通⽤技术,我们只是写⼀些⽐较简单的⻚⾯,暂时还没有⽤到vue、react等框架
遇到了哪些挑战
http服务器的选型,刚开始不知道选择哪⼀个库⽐较合适,经过了⼀轮调研,把开源的⼏个库都做了⼀些调研,才选定了httplib。httplib的优点如下:
a. 轻量级:httplib库⾮常轻量级,只包含⼀个头⽂件,使得使⽤和集成变得⽅便快捷。
b. 灵活性:⽀持HTTP客⼾端和服务器的功能,可以轻松进⾏HTTP请求和响应处理。
c. 跨平台性:可以在多种操作系统上运⾏,包括Windows、Linux和macOS等。
d. ⾼性能:采⽤异步IO模型,能够提⾼HTTP服务器的性能。
e. 开源:httplib库是开源的,⽤⼾可以⾃由地使⽤和修改它。
f. ⽀持HTTPS:⽀持SSL/TLS加密,可以通过CPPHTTPLIB_OPENSSL_SUPPORT宏启⽤。
g. 简单易⽤:API设计简洁明了,易于集成到现有C++项⽬中。
h. ⽀持多种HTTP⽅法:⽀持GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS等HTTP⽅法。
如何测试和部署
⽬前项⽬中只是通过编写浏览器对Http服务器进⾏功能测试和性能测试,没有进⾏部署。
功能测试主要包含:
- 请求题⽬列表功能
- 请求特定题⽬进⾏编写功能
- 请求代码编译运⾏功能
- 负载均衡功能
性能测试主要包含:
- 客⼾端并发量测试
测试:
- 单元测试:编写测试代码来验证每个独⽴模块或函数的正确性。
- 集成与功能测试:在单元测试之后,测试各个模块如何⼀起⼯作,以及功能接⼝是否正确返回。
- ⽇志打印:使⽤⽇志打印来记录、分类和修复发现的问题。
部署: - 构建⾃动化:通过编写脚本及makefile来⾃动编译代码和程序部署。
- 环境准备:确保部署环境(开发、⽣产)都已正确设置。
- 部署:将包发布在阿⾥云服务器并后台启动。
- ⽇志:部署后收集⽇志以便于问题诊断。
如何设计项⽬以⽀持未来的扩展
扩展:
- 基于注册和登陆的录题功能
- 业务扩展,⾃⼰写⼀个论坛,接⼊到在线OJ中
- 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
- ⽬前后端compiler的服务我们使⽤的是http⽅式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调⽤,推荐:rest_rpc,替换我们的httplib(建议,可以不做)
- 功能上更完善⼀下,判断⼀道题⽬正确之后,⾃动下⼀道题⽬
保证可维护性:
回答思路:
- 遵循编码规范:制定并遵守⼀致的编码标准和命名约定,使代码易于理解和维护。
- 模块化设计:将系统分解为独⽴的模块,每个模块负责⼀个特定的功能,这样可以降低复杂性,提⾼可维护性。
- ⽂档化:编写清晰的⽂档,包括代码注释、API⽂档和⽤⼾⼿册,以帮助其他开发者理解代码的⼯作原理。
临时⽂件的命名唯⼀性如何解决的
确定临时⽂件的唯⼀性,通过下⾯两个变量的组合来确定唯⼀性
- ⽤时间戳
- 原⼦⾃增的变量值
如何判定编译成功?如何判定运⾏成功?
如果满⾜以下条件,我们就认为代码编译是成功的
- ⽤来编译的⼦进程正常退出,状态码和退出码都符合预期
- 成功⽣成可执⾏⽂件
oj_server和compile_server能否部署在不同主机上?
是可以的,只需要将配置好的 compile_server 的 ip 和端⼝号配置到 oj_server 的编译主机
配置⽂件中即可
在 oj_server 启动的时候就会⾃动加载这些编译服务。
oj_server和compile_server之间的数据交互格式是怎样的?
oj_server收到⽤⼾提交的数据,使⽤json将该数据反序列化,拼接相应题⽬的测试⽤例代码,将新的代码序列化,利⽤httplib构建⼀个客⼾端,通过RR轮转选择负载最低的主机发送给某个
compile_server主机,compile_server拿到数据进⾏反序列化,最后把编译,运⾏的结果添加进⾏序列化发送给oj_server,oj_server最终把结果发送给⽤⼾。
HTTP报⽂格式?为什么使⽤http作为服务器之间的通信协议?
HTTP请求报⽂格式分为请求⾏,请求头部,空⾏,请求正⽂。HTTP响应报⽂分为状态⾏,响应头部,空⾏,响应正⽂。
HTTP协议被⼴泛⽤于服务器之间的通信,主要因为HTTP是⼀个简单、易于理解和实现的协议并且HTTP是⼀个⼴泛⽀持的协议,⼏乎所有的操作系统和编程语⾔都有对HTTP的⽀持,使得跨平台和跨语⾔的通信变得容易。并且HTTP允许⾃定义头部字段和扩展,能够满⾜各种需求和应⽤场景。
Fork的⼯作原理?为什么使⽤多进程编程?
当进程调⽤fork时,内核会执⾏以下操作:
- 创建⼦进程:内核为新的进程分配⼀个新的进程标识符(PID)。为⼦进程分配内存空间(虚拟地址空间)。通常,⼦进程会复制⽗进程的地址空间,但它们之间的地址空间是独⽴的。
- 复制⽗进程的资源,地址空间:⼦进程继承⽗进程的虚拟地址空间,包括代码、数据和堆栈。⽂件描述符:⼦进程继承⽗进程的打开⽂件描述符。进程状态:⼦进程继承⽗进程的部分状态,如进程优先级和环境变量。
- 执⾏⼦进程代码:⼦进程开始从fork调⽤之后的代码处执⾏。它可以与⽗进程⼀起并⾏执⾏。
在项⽬中,我们编译,运⾏都⽤了⼦进程去处理,因为进程程序替换是进程的替换,每个进程在独⽴的内存空间中运⾏,进程之间的崩溃不会影响到其他进程。这种隔离可以提⾼系统的稳定性和可靠性。不会像线程那样线程崩溃导致整个进程崩溃。
http服务器是如何搭建的
在往上查找⼀些开源的HTTP库,其中找到了httplib,mongoose,httpd,…等⼀些轻量的http库,经过对⼏个库的使⽤以及源码的了解,决定使⽤httplib库。httplib库有以下优点:
a. 是⼀个单头⽂件实现的库,使⽤简单,只需要clone仓库后包含⼀个头⽂件即可,⽆需其他安装以及库的链接。
b. 实现上基于select(win)和epoll(linux)实现描述符I/O事件监控,采⽤线程池对就绪的描述符进⾏后续处理。
c. 代码编写层次逻辑清晰,其内部构造了URI正则表达式和请求处理回调函数的映射,只需要使⽤时实现了业务处理函数,并提前设定好指定请求的处理回调函数就可以通过简单的⼗⼏⾏代码完成服务器的搭建。
d. 基于C++实现,嵌⼊在当前项⽬中更加合适(mongoose基于C++实现,但是使⽤上没有httplib简单,⽽httpd基于C语⾔实现,不是说不好,只是httplib⽤起来更轻松)