文章目录

0 项目展示代码链接

1. 所用技术与开发环境1.1 所用技术:1.2 开发环境

2 项目基本结构3 CompilerServer模块设计3.1 整体结构设计3.2 util.hpp(后面有不认识的函数调用可以来这里看看有没有它的实现方法)3.1.1 添加日志功能(comm模块)3.1.2 获取日期时间格式的时间戳(comm模块)

3.2 compiler编译模块3.3 runner运行模块3.4 compliler_run模块3.4.1 功能实现概述

3.5 compiler_server模块

4 OJServer模块设计4.1 各个模块功能介绍4.2 oj_server模块4.3 oj_model模块4.4 oj_view模块4.5 oj_control模块

5. 前端页面设计6 顶层项目部署Makefile7 项目组件的安装与使用7.1 jsoncpp7.2 httplib7.3 boost库7.4 ctemplate

0 项目展示

利用文件的方式进行录题

文件版Oj项目演示视频

利用MYSQL数据库的方式录题

负载均衡式在线OJ项目

代码链接

负载均衡项目

1. 所用技术与开发环境

1.1 所用技术:

C++ STL 标准库Boost 准标准库(字符串切割)cpp-httplib 第三方开源网络库ctemplate 第三方开源前端网页渲染库jsoncpp 第三方开源序列化、反序列化库负载均衡设计多进程、多线程MySQL C connectAce前端在线编辑器(了解)html/css/js/jquery/ajax (了解)

1.2 开发环境

Centos 7 云服务器vscodeMysql Workbench

2 项目基本结构

我们的项目核心是三个模块

模块功能comm公共模块,其它两个共同用到的hpp代码。例如:日志信息LOGcompile_server编译与运行模块oj_server获取题目列表,查看题目编写题目界面,负载均衡。

(来自项目资料)

3 CompilerServer模块设计

3.1 整体结构设计

CompilerServer模块: 编译并运行客户端通过网络提交的代码,得到格式化的相关的结果

compiler模块:只负责代码的编译。拿到待编译代码的文件名,进行编译,并形成对应的临时文件。runner模块:只负责运行代码。通过程序替换(execl)—>进行程序的运行—>把运行形成的信息以文件的形式存到temp目录下。compiler_run模块:整合编译模块和运行模块。解析用户发来的json串 -->把用户传过来的代码与后台测试用例的代码整合 ----> 编辑一个名字不重复的源文件—>调用编译和运行两个模块完成功能 —> json串构建的结果返回给编译服务模块。compiler_server模块:负责搭建http服务,接收客户端发来的请求,后调用compiler_run模块编译运行,并将结果返回给客户端。 它们之间的关系

3.2 util.hpp(后面有不认识的函数调用可以来这里看看有没有它的实现方法)

代码里有注释

#pragma once

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

namespace ns_util

{

class TimeUtil

{

public:

static std::string GetTimeStamp()

{

struct timeval _time;

gettimeofday(&_time, nullptr);

return std::to_string(_time.tv_sec);

}

//获得毫秒时间戳

static std::string GetTimeMs()

{

struct timeval _time;

gettimeofday(&_time, nullptr);

return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);

}

};

const std::string temp_path = "./temp/";

class PathUtil

{

public:

static std::string AddSuffix(const std::string &file_name, const std::string &suffix)

{

std::string path_name = temp_path;

path_name += file_name;

path_name += suffix;

return path_name;

}

// 编译时需要有的临时文件

// 构建源文件路径+后缀的完整文件名

// 1234 -> ./temp/1234.cpp

static std::string Src(const std::string &file_name)

{

return AddSuffix(file_name, ".cpp");

}

// 构建可执行程序的完整路径+后缀名

static std::string Exe(const std::string &file_name)

{

return AddSuffix(file_name, ".exe");

}

static std::string CompilerError(const std::string &file_name)

{

return AddSuffix(file_name, ".compile_error");

}

// 运行时需要的临时文件

static std::string Stdin(const std::string &file_name)

{

return AddSuffix(file_name, ".stdin");

}

static std::string Stdout(const std::string &file_name)

{

return AddSuffix(file_name, ".stdout");

}

// 构建该程序对应的标准错误完整的路径+后缀名

static std::string Stderr(const std::string &file_name)

{

return AddSuffix(file_name, ".stderr");

}

};

class FileUtil

{

public:

static bool IsFileExists(const std::string &path_name)

{

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;

}

static bool WriteFile(const std::string &target, const std::string &content)

{

std::ofstream out(target);

if (!out.is_open())

{

return false;

}

out.write(content.c_str(), content.size());

out.close();

return true;

}

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:不保存行分割符,有些时候需要保留\n,

// getline内部重载了强制类型转化

while (std::getline(in, line))

{

(*content) += line;

(*content) += (keep ? "\n" : "");

}

in.close();

return true;

}

};

class StringUtil

{

public:

/*************************************

* str: 输入型,目标要切分的字符串

* target: 输出型,保存切分完毕的结果

* sep: 指定的分割符

* **********************************/

static void SplitString(const std::string &str, std::vector *target, const std::string &sep)

{

//boost split

boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on);

}

};

}

3.1.1 添加日志功能(comm模块)

#pragma once

#include

#include

#include "util.hpp"

namespace ns_log

{

using namespace ns_util;

/*日志设计为五个等级

NORMAL:正常

DEBUG:dubug

WARNING:警告

ERROR:错误

DEADLY:致命*/

// 日志等级

enum

{

INFO, //就是整数

DEBUG,

WARNING,

ERROR,

FATAL

};

inline std::ostream &Log(const std::string &level, const std::string &file_name, int line)

{

// 添加日志等级

std::string message = "[";

message += level;

message += "]";

// 添加报错文件名称

message += "[";

message += file_name;

message += "]";

// 添加报错行

message += "[";

message += std::to_string(line);

message += "]";

// 日志时间戳

message += "[";

message += TimeUtil::GetTimeStamp();

message += "]";

// cout 本质 内部是包含缓冲区的

std::cout << message; //不要endl进行刷新

return std::cout;

}

// LOG(INFo) << "message" << "\n";

// 开放式日志

#define LOG(level) Log(#level, __FILE__, __LINE__)

}

3.1.2 获取日期时间格式的时间戳(comm模块)

获取当前时间:系统调用gettimeofday接口获取当前的时间戳

namespace ns_util

{

class TimeUtil

{

public:

static std::string GetTimeStamp()

{

struct timeval _time;

gettimeofday(&_time, nullptr);

return std::to_string(_time.tv_sec);

}

//获得毫秒时间戳

static std::string GetTimeMs()

{

struct timeval _time;

gettimeofday(&_time, nullptr);

return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);

}

};

3.2 compiler编译模块

子进程进行编译(需要进行程序替换)父进程等待子进程编译后的结果

具体实现的流程图如下: 当然这里就需要公共模块util.cpp里的代码(把无后缀的filename文件通过Pathutile类中的静态函数形成·所需要的相关后缀文件、例如—.Cpp文件)

#pragma once

#include

#include

#include

#include

#include

#include

#include "../comm/util.hpp"

#include "../comm/log.hpp"

// 只负责进行代码的编译

namespace ns_compiler

{

// 引入路径拼接功能

using namespace ns_util;

using namespace ns_log;

class Compiler

{

public:

Compiler() {}

~Compiler() {}

// 返回值:编译成功:true,否则:false

// 输入参数:编译的文件名

// file_name: 1234

// 1234 -> ./temp/1234.cpp

// 1234 -> ./temp/1234.exe

// 1234 -> ./temp/1234.stderr

static bool Compile(const std::string &file_name)

{

pid_t pid = fork();

if (pid < 0)

{

// 内部错误,创建子进程失败

LOG(ERROR) << "内部错误,创建子进程失败"

<< "\n";

return false;

}

else if (pid == 0)

{

// 子进程

umask(0);

int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);

if (_stderr < 0)

{

// 没有成功形成stderr文件

LOG(WARNING) << "没有成功形成stderr文件"

<< "\n";

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(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr);

LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\n";

exit(2);

}

else

{

// 父进程

waitpid(pid, nullptr, 0); //阻塞等待子进程完成编译

// 编译是否成功,就看有没有形成对应的可执行程序

if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))

{

// 编译成功!

LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";

return true;

}

}

LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";

return false;

}

};

}

3.3 runner运行模块

运行实现的功能可以分三种情况:

代码跑完,结果正确代码跑完,结果不正确代码没跑完,异常了 把结果通过返回值的不同交给compliler_run模块处理。

3.4 compliler_run模块

3.4.1 功能实现概述

这里就涉及到网络服务,用户的代码会以json串的方式传给compliler_run模块。首先每次用户提交的代码都是唯一性的源文件,然后调用编译模块和运行模块编译并运行该源文件,然后通过编译与运行的结果构建相关的json串返回给上层,两个参数,一个输入形的json串,一个输出形的json串。

json串的body内容如下: * 输入:

* code: 用户提交的代码

* input: 用户给自己提交的代码对应的输入,不做处理

* cpu_limit: 时间要求

* mem_limit: 空间要求

*

* 输出:

* 必填

* status: 状态码

* reason: 请求结果

* 选填:

* stdout: 我的程序运行完的结果

* stderr: 我的程序运行完的错误结果

*

* 参数:

* in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}

* out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}

3.5 compiler_server模块

把整个模块打包成一个网络服务,用户使用POST方法请求服务器上的compiler_run服务,请求的正文就是我们编译运行模块需要的json串。服务器用过json串调用编译运行模块,得到返回的json串后见响应返回给用户。

4 OJServer模块设计

4.1 各个模块功能介绍

基于MVC 结构的oj 服务设计本质:建立一个小型网站

OJ模块实现如下三个部分

获取首页,用题目列表充当编辑区域页面提交判题功能(编译并运行)

整个模块采用的是MVC的设计模式进行设计 M: Model,通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL) V: view, 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(谷歌浏览器) C: control, 控制器,就是我们的核心业务逻辑

整个模块可分为四个部分: oj_model模块:负责模块前两个功能的数据部分,通过与题库交互,得到题目的信息。 oj_view模块:负责渲染用户得到网页。 oj_control模块:负责整个OJServer模块的业务逻辑控制。对下负责选择不同的主机请求编译服务,对上根据用户的三种请求,配合上面两个模块,完成对应的功能。 oj_server模块:搭建http服务,根据用户的请求,完成功能。

4.2 oj_server模块

用户请求的服务路由功能

#include

#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;

// 获取所有的题目列表

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");

// resp.set_content("这是所有题目列表", "Text/plain; charset=utf-8");

});

// 用户要根据题目编号,获取题目的内容

// /question/100 -> 正则匹配

// R"()", 原始字符串raw string,保持字符串内容的原貌,不用做相关的转义

svr.Get(R"(/qustion/(\d+))", [&ctrl](const Request &req, Response &resp)

{

std::string number = req.matches[1];

std::string html;

ctrl.Questions(number, &html);

resp.set_content(html, "text/html; charset=utf-8"); });

// 用户提交代码,使用我们的判题功能(1. 每道题的测试用例 2. compile_and_run)

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");

// resp.set_content("指定题目判题" + number, "Text/plain; charset=utf-8");

});

svr.set_base_dir("./wwwroot");

svr.listen("0.0.0.0", 8080);

return 0;

}

4.3 oj_model模块

model功能,提供对数据的操作。主要是为了得到题库中对应的题目信息。

因此我们设计的类的成员属性如下(结构化):

文件版:

struct Question

{

std::string number; // 题目编号,唯一

std::string title; // 题目的标题

std::string star; // 难度: 简单 中等 困难

int cpu_limit; // 题目的时间要求(S)

int mem_limit; // 题目的空间要去(KB)

std::string desc; // 题目的描述

std::string header; // 题目预设给用户在线编辑器的代码

std::string tail; // 题目的测试用例,需要和header拼接,形成完整代码

};

注意:文件需要指明路径。

MYSQL版:

struct Question

{

std::string number; // 题目编号,唯一

std::string title; // 题目的标题

std::string star; // 难度: 简单 中等 困难

std::string desc; // 题目的描述

std::string header; // 题目预设给用户在线编辑器的代码

std::string tail; // 题目的测试用例,需要和header拼接,形成完整代码

int cpu_limit; // 题目的时间要求(S)

int mem_limit; // 题目的空间要去(KB)

};

4.4 oj_view模块

渲染的意思就是把网页中的代码相关的关键字替换了,就相当与c语言的宏替代(我说的不准,只是类比一下)具体做法就需要在Linux上下载ctemplate

4.5 oj_control模块

control,逻辑控制模块oj_control模块负责整个OJServer模块的业务逻辑控制。对下负责选择不同的主机请求编译服务,对上根据用户的三种请求,配合model和control两个模块,完成对应的功能。它需要能够提供三个功能,即:一个可以构建好题目列表网页,一个可以根据题目编号构建好单个题目网页,还有一个判题功能。要实现它的功能就需要前面那些模块的配合,网页获取题目列表的两个功能肯定需要model模块和view模块实现。判题功能需要调用compile_server模块,使用它的编译与运行的结果帮我完成判题。(当然服务器的选择需要计数来实现负载均衡;而普通数字肯定不行,我们需要加锁保护。)

5. 前端页面设计

前端的内容大家看一下,感兴趣的话可以去菜鸟教程看看。

6 顶层项目部署Makefile

在顶层新建一个Makefile文件,该文件的功能是make时可以同时编译CompilerServer服务和OJServer服务,当输入make output时会自动形成一个output文件,里面包含了compiler_server和oj_server的应用程序和一些运行程序必须的文件。输入make clean不光会清理掉创建的可执行程序,还会清理掉output。 output的内容就可以发布出去了。

7 项目组件的安装与使用

7.1 jsoncpp

jsoncpp安装及使用

7.2 httplib

httplib库的安装及使用

7.3 boost库

Linux上boost 安装及使用

7.4 ctemplate

ctemplate 安装及使用

相关阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: