C++项目:【负载均衡式在线OJ】

文章目录

一、项目介绍

二、技术栈与开发环境

1.所用技术:

2.开发环境:

三、项目演示

1.运行代码

2.进入项目首页

3.题目列表

4.点击具体一道题

5.编辑代码并提交

四、项目思维导图

五、项目宏观结构

六、Comm公共模块

1.日志工具log.hpp

2.其他工具util.hpp

七、CompilerServer模块

1.整体层次如图

2.编译模块compiler.hpp

3.运行模块runner.hpp

4.编译+运行compile_run.hpp

5.Compiler模块compiler_server.cc

八、基于MVC结构的OJServer模块

1. 什么是MVC结构

2.整体层次如图

3.Model模块oj_model.hpp​编辑

3.1文件版本

3.2数据库版本

4.题库的结构

5.view模块oj_view.hpp

6.oj_control模块oj_control.hpp

6.1.构建题目列表和单个题目网页

6.2.负载均衡模块

6.2.1机器类的设计

6.2.2负载均衡模块设计

6.2.3负载均衡的实现:

6.2.4判题模块

7.oj_server模块oj_server.cc

九、前端页面的设计(了解)

1. indx.html

2. all_questions.html

3. one_question.html

十、项目扩展

十一、项目所需工具

1.升级 gcc

2.安装 jsoncpp

3.安装 cpp-httplib

4.安装boost库

5.安装与测试 ctemplate

6.使用Ace在线编辑器

7.MySQL 建表

十二、项目源码


一、项目介绍

        这个项目是基于负载均衡的在线OJ平台,用户可以在浏览器访问各个题目,在编辑区编写代码提交,后端能够自动分配服务器资源,保持平衡的情况下为用户提供良好的编程运行环境,让代码快速运行和提交。


二、技术栈与开发环境

1.所用技术:

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

2.开发环境:

  • Centos 7 云服务器
  • vscode
  • Mysql Workbench

三、项目演示

1.运行代码


2.进入项目首页


3.题目列表

可以根据需求增加更多的题目 


4.点击具体一道题


 5.编辑代码并提交


四、项目思维导图

超清图片我放在了Gitee上面,需要可以自行下载(文章末尾有链接)


五、项目宏观结构

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

  1. comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。
  2. compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。
  3. oj_server : 请求题目列表;请求一个具体题目,且有编辑区 ;提交判题请求。采用MVC的设计模式,使用负载均衡,访问文件或数据库,调用编译模块,以及把题目列表和编辑界面展示给用户。

注意:我们只实现类似 leetcode 的题目列表+在线编程功能。

        用户直接访问的是OJServer模块,OJServer收到请求后会进行功能路由,根据不同的请求给用户返回不同的结果,如果用户是编写题目提交代码,那么OJServer模块会根据后端的CompilerServer服务器的负载情况,负载均衡地选择主机提供的编译运行服务,然后拿到编译运行结果返回给用户。Compiler服务器和OJ服务器,两个模块之间采用http网络通信,把编译运行模块部署在多台服务器上,OJ服务器只需要一台,能够把用户的请求发送给后端CompilerServer。


六、Comm公共模块

comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。


1.日志工具log.hpp

namespace ns_log
{// 引入公共功能using namespace ns_util;// 日志等级enum{INFO, // 常规,正常的DEBUG, // 调试WARNING, // 告警ERROR, // 错误FATAL // 致命错误};// 打印日志// 参数:日志等级,在哪一个文件,当前在哪一行// 使用方法:Log() << "message"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 += "]";std::cout << message; // 不要endl进行刷新return std::cout;}// 再用宏封装一下// 使用方法:例如:LOG(INFO) << "message" << "\n"#define LOG(level) Log(#level, __FILE__, __LINE__)
}

2.其他工具util.hpp

// 传入一个文件名,自动形成路径与后缀
namespace ns_util
{// 引入存储临时文件的路径const std::string temp_path = "./temp/";// 时间功能class TimeUtil{public:// 获取秒级别时间戳static std::string GetTimeStamp(){struct timeval _time;gettimeofday(&_time, nullptr); // 时区不关心,所以设置成nullptrreturn 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); // 秒->毫秒=秒*1000,微秒->毫秒=微秒/1000}};// 对路径操作的方法class PathUtil{public:// 构建文件路径+后缀的完整文件名static std::string AddSuffix(const std::string &file_name, const std::string &suffix){std::string path_name = temp_path; // "./temp/"path_name += file_name; // "./temp/12345"path_name += suffix; // "./temp/12345.后缀"return path_name;}/**************************编译时需要有的临时文件**************************/// 构建源文件路径+后缀的完整文件名// 例如:12345 -> ./temp/12345.cppstatic std::string Src(const std::string &file_name){return AddSuffix(file_name, ".cpp");}// 构建可执行程序的完整路径+后缀名// 例如:12345 -> ./temp/12345.exestatic std::string Exe(const std::string &file_name){return AddSuffix(file_name, ".exe");}// 构建该程序对应的编译时错误文件的完整路径+后缀名// 例如:12345 -> ./temp/12345.compile_errorstatic 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); // 获得唯一idreturn ms + "_" + uniq_id;}// 将代码写入该文件// 参数:target为要写入的文件名,content为要写入的内容static bool WriteFile(const std::string &target, std::string &content){std::ofstream out(target);if (!out.is_open()){// 如果没有被打开成功return false;}out.write(content.c_str(), content.size()); // 写入内容out.close(); // 关闭文件return true;}// 把所有的文件内容读出来// 参数:target为要读取的文件名,content用于存储读到的数据, keep用于判断是否保留\nstatic bool ReadFile(const std::string &target, std::string *content, bool keep = false){(*content).clear(); // 先清空content,为了不影响第一次读取std::ifstream in(target, std::ios::binary);if (!in.is_open()){return false;}std::string line;// 注意:// getline:不能保存行分隔符// getline:内部重载了强制类型转化while (std::getline(in, line)){(*content) += line;(*content) += (keep ? "\n" : ""); // 判断是否保留\n}in.close(); // 关闭文件return true;}};// 字符串工具class StringUtil{public:// 切分字符串/******************************************************* 参数:* str:输入型,目标要切分的字符串* target:输出型,保存切分完毕的结果* sep:指定的分隔符******************************************************/static void SplitString(const std::string &str, std::vector<std::string> *target, const std::string sep){// 用boost库里面的字符串切分功能boost::split((*target), str, boost::is_any_of(sep), boost::algorithm::token_compress_on);}};}

七、CompilerServer模块

compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。

1.整体层次如图


2.编译模块compiler.hpp

// 只负责进行代码的编译
namespace ns_compiler
{using namespace ns_util; // 引入路径拼接功能using namespace ns_log; // 引入日志打印功能// 编译功能class Compiler{public:Compiler(){}~Compiler(){}// 编译代码:// 返回值:编译成功:true,编译错误:false// 输入参数:编译的文件名:file_name// 例如:file_name: 12345(只传进来一个文件名,不带后缀,后缀我们自己添加)// 我们会生成三个文件// 12345 -> ./temp/12345.cpp 源文件// 12345 -> ./temp/12345.exe 可执行文件// 12345 -> ./temp/12345.stderr 标准错误文件static bool Compile(std::string &file_name){pid_t pid = fork(); // 创建一个子进程进行编译代码if (pid < 0){// 如果创建子进程失败就直接退出LOG(ERROR) << "内部错误,创建子进程失败" << "\n";return false;}else if (pid == 0){umask(0); // 先将umask清零,让其不受平台的影响// 先将编译时错误文件打开int _compile_error = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_compile_error < 0){// 如果打开文件失败LOG(WARNING) << "没有成功形成compile_error文件" << "\n";exit(1);}// 重定向标准错误到_compile_errordup2(_compile_error, 2);// 注意:程序替换,并不影响进程的文件描述符表// 子进程:调用编译器,完成对代码的编译工作// g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE", "-std=c++11", nullptr/*不要忘记写nullptr*/); // 用g++替换当前的子进程// 如果程序替换失败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.运行模块runner.hpp

namespace ns_runner
{using namespace ns_util; // 引入路径拼接功能using namespace ns_log; // 引入日志打印功能class Runner{public:Runner() {}~Runner() {}public:// 提供设置进程占用资源大小的接口static void SetProcLimit(int _cpu_limit, int _mem_limit){// 设置cpu占用时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_max = RLIM_INFINITY;cpu_rlimit.rlim_cur = _cpu_limit;setrlimit(RLIMIT_CPU, &cpu_rlimit);// 设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_max = RLIM_INFINITY;mem_rlimit.rlim_cur = _mem_limit * 1024; // 乘以1024,让单位KB变成字节setrlimit(RLIMIT_AS, &mem_rlimit);}// 运行并判断运行是否成功// 注意:我们这里实现的功能,也只需要指明文件名即可,不需要带路径和后缀/**************************************************************** 将这里的返回值设置成int而不设置成bool,是因为:* 返回值 > 0 : 程序异常了,退出时收到了信号,返回值就是对应的信号编号* 返回值 == 0 : 程序正常运行完毕的,结果保存到了对应的临时文件中* 返回值 < 0 : 内部错误:打开文件失败、创建子进程失败等* * 参数介绍:* cpu_limit : 该程序运行的时候,可以使用的最大cpu资源上限* mem_limit : 该程序运行的时候,可以使用的最大的内存大小(KB)***************************************************************/static int Run(const std::string &file_name, int cpu_limit, int mem_limit ){// 获取运行需要的文件std::string _execute = PathUtil::Exe(file_name);    // 获得可执行程序的完整文件名std::string _stdin   = PathUtil::Stdin(file_name);  // 获取标准输入文件std::string _stdout  = PathUtil::Stdout(file_name); // 获取标准输出文件std::string _stderr  = PathUtil::Stderr(file_name); // 获取标准错误文件umask(0); // 先将umask清零,让其不受平台的影响// 打开运行需要的文件int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);// 如果有任何一个文件打开失败,就退出程序(因为如果打开失败就无法获取相应的数据和输出结果,再继续运行就没有意义了)if (_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0) {LOG(ERROR) << "运行时打开标准文件失败" << "\n";return -1; // 代表文件打开失败}// 创建一个子进程运行程序pid_t pid = fork();if (pid < 0){LOG(ERROR) << "运行时创建子进程失败" << "\n";// 如果创建失败,关闭需要的文件close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);return -2; // 代表创建子进程失败}else if (pid == 0){// 子进程创建成功// 将标准输入,标准错误,标准错误都进行重定向到刚才打开的文件dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2);// 设置资源限制SetProcLimit(cpu_limit, mem_limit);// 然后替换程序,将子进程替换成需要的可执行文件// 参数:带路径的可执行文件,要怎么执行......execl(_execute.c_str()/*我要执行谁*/, _execute.c_str()/*我想在命令行上如何执行该程序*/, nullptr);// 如果替换失败exit(1);}else{// 父进程用不到这些文件,所以先将其关闭close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);// 让父进程等待子进程int status = 0; // 拿到子进程的退出结果waitpid(pid, &status, 0);// 程序如果运行异常,一定是因为收到了信号!LOG(INFO) << "运行完毕,info:" << (status & 0x7f) << "\n";return status & 0x7f;}}};
}

4.编译+运行compile_run.hpp

namespace ns_compile_and_run
{using namespace ns_log;      // 引入日志打印功能using namespace ns_util;     // 引入路径拼接功能using namespace ns_compiler; // 引入编译功能using namespace ns_runner;   // 引入运行功能class CompileAndRun{public:// 清空所有的临时文件static void RemoveTempFile(const std::string &file_name){// 特点:清理文件的个数是不确定的,但是有哪些我们是知道的// 删除源文件std::string _src = PathUtil::Src(file_name); // 获取源文件的完整路径if (FileUtil::IsFileExists(_src)) unlink(_src.c_str()); // 如果文件存在,就将其移除// 删除编译错误文件std::string _compile_error = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(_compile_error)) unlink(_compile_error.c_str());// 删除可执行程序std::string _execute = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());// 删除标准输入std::string _stdin = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());// 删除标准输出std::string _stdout = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());// 删除标准错误std::string _stderr = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());}// 将信号转化成为报错的原因// code > 0 : 进程收到了信号导致异常错误// code < 0 : 整个过程非运行报错(代码为空,编译报错等)// cod == 0 : 整个过程全部完成// 待完善...static std::string CodeToDesc(int code, const std::string file_name){std::string desc;switch (code){case 0:desc = "编译运行成功";break;case -1:desc = "用户提交的代码是空";break;case -2:desc = "未知错误";break;case -3: // 代码编译的时候发生了错误FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true); // 读取编译报错的文件break;case SIGABRT: // 信号6desc = "内存超过范围";break;case SIGXCPU: // 信号24desc = "CPU使用超时";break;case SIGFPE: // 信号8desc = "浮点数溢出";break;default:desc = "未知" + std::to_string(code);break;}return desc;}/****************************************************************************** 输入:* 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":"",}*****************************************************************************/static void Start(const std::string &in_json, std::string *out_json){// 将拿到的数据做反序列化Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);std::string code = in_value["code"].asString();   // 拿到用户提交的代码std::string input = in_value["input"].asString(); // 拿到用户输入的数据int cpu_limit = in_value["cpu_limit"].asInt();    // 拿到上层给的cpu占用时长int mem_limit = in_value["mem_limit"].asInt();    // 拿到上层给的内存大小// 创建输出返回的数据int status_code = 0; // 状态码Json::Value out_value;int run_result = 0;    // 运行结果std::string file_name; // 需要内部形成的唯一文件名(因为goto语句之间不能出现定义,所以就在这里定义)if (code.size() == 0){status_code = -1; // 代码为空goto END;}// 形成的文件名只具有唯一性,没有目录没有后缀file_name = FileUtil::UniqFileName();// 形成临时的src文件if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){// 如果写入文件失败status_code = -2; // 未知错误goto END;}// 编译if (!Compiler::Compile(file_name)){// 如果编译失败status_code = -3; // 代码编译的时候发生了错误goto END;}run_result = Runner::Run(file_name, cpu_limit, mem_limit); // 运行if (run_result < 0){// 内部错误status_code = -2;; // 未知错误goto END;}else if (run_result > 0){// 程序运行崩溃了status_code = run_result;}else{// 运行成功status_code = 0;}END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name); // 将错误码转化成错误描述if (status_code == 0){// 整个过程全部成功:// 读取标准输出文件std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] =  _stdout;// 读取标准输出文件std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}// 序列化过程Json::StyledWriter writer;*out_json = writer.write(out_value);// 清空所有的临时文件RemoveTempFile(file_name);}};
}

5.Compiler模块compiler_server.cc

// 使用手册
void Usage(std::string proc)
{std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}// 调用方式:./compile_server port
int main(int argc, char *argv[])
{if (argc != 2){// 如果使用方法不对Usage(argv[0]);return 1;}Server svr; // 创建一个服务器// 参数:req:用户的需求,resp:服务器的相应svr.Post("/compile_and_run", [](const Request &req, Response &resp){// 用户请求的服务正文就是我们想要的json string// in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}// out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}std::string in_json = req.body;std::string out_json;if (!in_json.empty()){// 编译并运行用户传过来的代码CompileAndRun::Start(in_json, &out_json);// 要响应的内容resp.set_content(out_json, "application/json;charset=utf_8");}});// 让服务器在所有ip,指定端口服务svr.listen("0.0.0.0", atoi(argv[1])); // 启动http服务return 0;
}

八、基于MVC结构的OJServer模块

        OJServer模块是直接和用户交互的,用户访问OJ系统,我需要有一个首页,其次需要有一个题目列表网页供用户选择题目,再者还需要一个可以给用户写代码做题的网页,并且可以提交代码,判断用户提交的代码是否正确。


总结用户的请求分为三种:

  1. 请求题目列表
  2. 请求一个具体的题目,并且需要有编译区域
  3. 提交,判题请求 OJServer模块主要要根据这三种请求提供对应的功能。

整个模块采用的是MVC的设计模式进行设计
通过这个设计模式,把数据,业务逻辑和网页界面进行了分离。

1. 什么是MVC结构

        经典MVC模式中,M是指业务模型,V是指用户界面(视图),C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面。 

  • M:model表示的是模型,代表业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据时中立的,模型与数据格式无关,这样一个模型就能够为多个视图提供数据,由于应用于模型的代码只需要写一次就可以被多个视图重用,所以减少了代码的重复性,
  • V:view表示的视图,代表用户看到并与之交互的界面。在视图中没有真正的处理发生,它只是作为一种输出数据并允许用户操作的方式。
  • C:controller表示的是控制器,控制器接收用户的输入并调用模型(M)和视图(V)去完成用户需求。控制器本身不输出任何东西和任何处理。它只接收请求并决定调用那个模型构建去处理请求,然后再确定用那个视图来显示返回的数据。

整个模块就包含四个部分:

  1. oj_model模块:负责模块前两个功能的数据部分,通过与题库交互,得到所有题目的信息或者某一个题目的信息
  2. oj_view模块:负责渲染用户得到网页,根据用户提交的不同请求,渲染不同的题目信息
  3. oj_control模块:负责整个OJServer模块的业务逻辑控制。对下负责负载均衡式的选择主机请求编译服务,对上根据用户的三种请求,配合上面两个模块,完成对应的功能。
  4. oj_server模块:搭建http服务,根据用户的请求,完成功能路由,调用control模块的对应方法完成功能


2.整体层次如图


3.Model模块oj_model.hpp

        oj_model模块主要是和数据交互的,这里的数据就是我们后端文件或者数据库当中的题目信息,题目应该包含如下的信息:

  1. 题目的编号(1)
  2. 题目的标题(求最大值)
  3. 题目的难度(简单、中等、困难)
  4. 题目的时间要求(1s)
  5. 题目的空间要求(30000KB)
  6. 题目的描述(给定一个数组,求最大值)
  7. 题目预设给用户在线编辑的代码(#include<iostream>...)
  8. 题目的测试用例

        到这里我们就需要有自己对应的题库,我们这个模块当中新增一个目录questions,用来存放我们的题库,这个questions目录下包含题目列表(文件形式)和每个题目的文件夹(其中又包含题目的描述、题目预设给用户在线编辑的代码header和题目的测试用例tail)


3.1文件版本

// 文件版
namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 题目编号(唯一)string title;  // 题目的标题string star;   // 题目的难度:简单 中等 困难int cpu_limit;      // 题目的时间要求(S)int mem_limit;      // 题目的空间要求(KB)string desc;   // 题目的描述string header; // 题目预设给用户在线编辑器的代码string tail;   // 题目的测试用例,需要和header拼接,形成完整代码};const string questions_list = "./questions/questions.list"; // 配置文件的路径const string question_path = "./questions/"; // 题库路径class Model{private:// 题号:题目细节unordered_map<string, Question> questions;public:Model(){// 将题目加载进来assert(LoadQuestionList(questions_list));}bool LoadQuestionList(const string &question_list){// 加载配置文件:questions/questions.list + 题目编号文件ifstream in(question_list);if (!in.is_open()){// 如果配置文件打开失败LOG(FATAL) << "加载题库失败,请检查是否存在题库文件" << "\n";return false;}// 进行行读取题目string line;while (getline(in, line)){// 将一行的字符串(题目描述)进行切分vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");// 例如:1 判断回文数 简单 1 30000if (tokens.size() != 5){// 如果配置的内容有问题LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n";continue;}// 进行填充questionQuestion q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.mem_limit = atoi(tokens[4].c_str());// 指定题库路径string path = question_path;path += q.number; // 加上题号path += "/";// 读取所有文件里面的内容,并填充到q中FileUtil::ReadFile(path+"desc.txt", &(q.desc), true);FileUtil::ReadFile(path+"header.cpp", &(q.header), true);FileUtil::ReadFile(path+"tail.cpp", &(q.tail), true);// 最后将单个题目q提交到题库中(哈希表)questions.insert({q.number, q});}LOG(INFO) << "加载题库...成功!" << "\n";in.close(); // 关闭配置文件return true;}// 获取所有的题目bool GetAllQuestions(vector<Question> *out){if (questions.size() == 0){// 如果没有题目LOG(ERROR) << "用户获取题库失败" << "\n";return false;}for (const auto &q : questions){out->push_back(q.second); // first: key, second value}return true;}// 获取单个题目bool GetOneQuestion(const string &number, Question *q){const auto& iter = questions.find(number);if (iter == questions.end()){// 如果没有找到该题目LOG(ERROR) << "用户获取题目失败,题目编号:" << number << "\n";return false;}(*q) = iter->second; // 获取题目成功return true;}~Model(){}};
}

3.2数据库版本

// MySQL版本
// 根据题目list文件,加载所有的题目信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 题目编号(唯一)string title;  // 题目的标题string star;   // 题目的难度:简单 中等 困难string desc;   // 题目的描述string header; // 题目预设给用户在线编辑器的代码string tail;   // 题目的测试用例,需要和header拼接,形成完整代码int cpu_limit; // 题目的时间要求(S)int mem_limit; // 题目的空间要求(KB)};const std::string oj_questions = "oj_questions"; // 要访问的表名const std::string host = "127.0.0.1";            // ip为本地服务器const std::string user = "oj_client";            // MySQL用户名const std::string passwd = "xujiaming+520";      // MySQL密码const std::string db = "oj";                     // 要连接的数据库名const int port = 3306;                           // MySQL的端口号class Model{public:Model(){}// 查询MySQL// 参数:sql:sql查询语句,out:输出查询结果bool QueryMySql(const std::string &sql, vector<Question> *out){// 创建MySQL句柄MYSQL *my = mysql_init(nullptr);// 连接数据库if (nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0)){// 如果连接失败LOG(FATAL) << "连接数据库失败!" << "\n";return false;}// 一定要设置该链接的编码格式,要不然会出现乱码问题mysql_set_character_set(my, "utf8");LOG(INFO) << "连接数据库成功!" << "\n";// 执行sql语句if (0 != mysql_query(my, sql.c_str())){// 如果执行失败LOG(WARNING) << sql << " execute error!" << "\n";return false;}// 提取结果MYSQL_RES * res = mysql_store_result(my);// 分析结果int rows = mysql_num_rows(res);   // 获得行数int cols = mysql_num_fields(res); // 获得列数// 获得每行每列的数据for (int i = 0; i < rows; ++i){MYSQL_ROW row = mysql_fetch_row(res); // 拿到当前这一行的所有数据(这里的row是一个二级指针)Question q; // 用于保存结果// 拿到当前行,每列的所有数据q.number = row[0];q.title = row[1];q.star = row[2];q.desc = row[3];q.header = row[4];q.tail = row[5];q.cpu_limit = atoi(row[6]);q.mem_limit = atoi(row[7]);// 将当前题的所有信息放到返回数组里面out ->push_back(q);}// 释放结果空间free(res);// 关闭MySQL连接mysql_close(my);return true;}// 获取所有的题目bool GetAllQuestions(vector<Question> *out){std::string sql = "select * from ";sql += oj_questions;return QueryMySql(sql, out);}// 获取单个题目bool GetOneQuestion(const string &number, Question *q){bool res = false;std::string sql = "select * from ";sql += oj_questions;sql += " where number=";sql += number;vector<Question> result;if (QueryMySql(sql, &result)) // 判断是否获取题目成功{// 判断获取的题目个数是否只有1个if (result.size() == 1) {*q = result[0];res = true;}}return res;}~Model(){}};
}

4.题库的结构

题目的属性大致可以分为2类:

  1. 一种是题目编号,题目标题,题目难度,时间限制和内存限制这些字段,这些字段都比较小,可以把所有题目的这些信息存在一个文件里面。
  2. 另一种是题目描述,预置代码,测试用例等等,这类信息一般都比较大,可以根据题目编号给每道题建立一个与编号对应的文件夹,然后用三个文件保存这三个信息,到时候就可以通过题目编号找到题目对应的路径,然后读取对应的文件,不仅读取方便,还便于我们录题。
     


5.view模块oj_view.hpp

        oj_view模块负责渲染给用户显示的网页。比如说用户请求访问题目列表,题目列表里的题目信息是从我们后端的题库中得到的,而把这些信息显示到网页上,这就是渲染网页。所有说view模块也应该提供两个接口,一个渲染题目列表,一个渲染单个题目的网页。


 我们需要引入一个第三方库ctemplate;功能如下:

namespace ns_view
{using namespace ns_model;const std::string template_path = "./template_html/"; // 要渲染的路径class View{public:View(){}~View(){}public:// 将所有的题目数据构建成网页void AllExpandHtml(const vector<struct Question> &questions, std::string *html){// 题目编号 题目标题 题目难度 // 使用表格显示// 1. 形成路径std::string src_html = template_path + "all_questions.html"; // 要被渲染的网页// 2. 形成数据字典ctemplate::TemplateDictionary root("all_questions");// 形成一个子字典for (const auto &q : questions){ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list"); sub->SetValue("number", q.number);sub->SetValue("title", q.title);sub->SetValue("star", q.star);}// 3. 获取被渲染的网页ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4. 开始完成渲染功能tpl->Expand(html, &root); // 这个root字典里面会包含所有的子字典}// 将单个题目的所有数据构建成网页void OneExpand(const struct Question &q, std::string *html){// 1. 形成路径std::string src_html = template_path + "one_question.html"; // 要被渲染的网页// 2. 形成数据字典ctemplate::TemplateDictionary root("one_questions");// 2.1 插入题目描述root.SetValue("number", q.number);   // 题号root.SetValue("title", q.title);     // 题目标题root.SetValue("star", q.star);       // 题目难度root.SetValue("desc", q.desc);       // 题目描述root.SetValue("pre_code", q.header); // 题目预设代码// 3. 获取被渲染的网页ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4. 开始完成渲染功能tpl->Expand(html, &root); // 这个root字典里面会包含所有的子字典}};
}

6.oj_control模块oj_control.hpp

        oj_control模块是整个OJSever模块的逻辑功能部分,在上层做好了功能路由之后,通过调用control模块实现各个功能,所有oj_control模块既要可以返回对应的网页,还要可以负载均衡的判题。

提供三个功能:

  1. 构建好题目列表网页的接口
  2. 根据题目编号构建好单个题目网页的接口
  3. 判题接口。

6.1.构建题目列表和单个题目网页

// 根据题目数据构建网页
// html:输出型参数
bool AllQuestions(string *html)
{bool ret = true;vector<struct Question> all;if (model_.GetAllQuestions(&all)){// 给题目按编号进行排序sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){return atoi(q1.number.c_str()) < atoi(q2.number.c_str());}); // 获取题目信息成功,将所有的题目数据构建成网页view_.AllExpandHtml(all, html);}else{// 获取失败*html = "获取题目失败,形成题目列表失败";ret = false;}return ret;
}// 根据题号构建网页
bool Question(const string &number, string *html)
{bool ret = true;struct Question q;if (model_.GetOneQuestion(number, &q)){// 获取指定题目信息成功,将该题目的所有数据构建成网页view_.OneExpand(q, html);}else{// 获取失败*html = "指定题目:" + number + " 不存在!";ret = false;}return ret;
}

6.2.负载均衡模块

6.2.1机器类的设计

        负载均衡模块,最重要的功能就是可以负载均衡式的选择主机,我如何得知有哪些主机可以供我选择,怎么实现负载均衡。

        所以模块内部要有一个结构包含提供服务的主机信息,用来表述主机的结构命名为Machine,然后用一个vector把所有提供服务的主机组织起来。

        Machine类里有主机的IP,端口,还有负载情况。负载均衡判断的依据就是看主机的负载,所有类里还要提供方法,在有新请求请求该机器时增加负载,服务结束时减少负载,如果中途服务主机突然挂了,还要可以清空负载。
因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证对负载操作的安全性,需要一个mutex互斥锁。


6.2.2负载均衡模块设计

        将来选择主机可以在vector中选,在此之前需要知道有哪些主机可以选,在当前路径下加一个.conf文件里面会存放所有的可以提供服务的主机信息,包括了IP和端口,每一行是一个主机的信息,负载均衡模块在构建时就可以读取该文件,初始化自己的vector结构。
        然后就是选择主机功能,首先在同一时刻可能有很多执行流都在选择主机,所以对主机的选择需要加锁,也就是说负载均衡模块也需要一个互斥锁。
        设计在control模块调用负载均衡模块时,如果说后端的编译服务主机出问题挂了,不应该影响我的OJServer服务,OJ服务正常运行,编译服务如果恢复了,那我正常请求,如果有一部分挂了,那我请求别的主机,全挂我就不请求,提示后端。
        这个功能就由负载均衡模块负责,负载均衡模块除了可以选择主机,还要能够知道主机的情况,并能够根据情况更新。使用数组的下标表示每一个主机的编号,用两个数组,一个表示上线主机,元素的值就是主机编号,另一个表示下线的主机。提供方法,在后端编译服务重启时可以更新状态让主机上线,当请求主机失败时要更新状态让主机下线。


6.2.3负载均衡的实现:

        负载均衡就是尽量让每一台机器负责的请求平均,那就需要从所有在线的主机中选择出对应的主机。一是随机挑选主机,但是这种方法不能排除有时候一直选中某几台,有几台又一直选不上。还有一种比较严格,遍历所有在线的主机,找出负载最小的。


 
// 提供服务的主机
class Machine
{
public:std::string ip;  // 编译服务的ipint port;        // 编译服务的端口号uint64_t load;   // 编译服务的负载(计数器)std::mutex *mtx; // 用于保护计数器的锁(注意:cpp中的mutex是禁止拷贝的,所以我们这里定义指针来进行后面的拷贝)
public:Machine():ip(""), port(0), load(0), mtx(nullptr){}~Machine(){}
public:// 提升主机负载void IncLoad(){if (mtx) mtx->lock(); // 加锁++load;if (mtx) mtx->unlock(); // 解锁}// 减小主机负载void DecLoad(){if (mtx) mtx->lock(); // 加锁--load;if (mtx) mtx->unlock(); // 解锁}// 将主机的负载清零void ResetLoad(){if (mtx) mtx->lock();load = 0;if (mtx) mtx->unlock();}// 获取主机负载(没有太大的意义,只是为了统一接口)uint64_t Load(){uint64_t _load = 0;if (mtx) mtx->lock(); // 加锁_load = load;if (mtx) mtx->unlock(); // 解锁return _load;}};const std::string service_machine = "./conf/service_machine.conf"; // 提供服务的主机列表的路径
// 负载均衡模块
class LoadBalance
{
private:std::vector<Machine> machines; // 可以给我们提供编译服务的主机(每一台主机都有自己的下标,充当当前主机的id)std::vector<int> online;       // 所有在线的主机idstd::vector<int> offline;      // 所有离线的主机idstd::mutex mtx;                // mtx是LoadBalance的锁,是保证LoadBalance它的数据安全(注意:每个Machine也有自己的小锁,不要与这里的mtx搞混)
public:LoadBalance(){assert(LoadConf(service_machine));LOG(INFO) << "加载 " << service_machine << " 成功" << "\n";}~LoadBalance(){}
public:// 将主机加载进来// 参数:machine_list:主机列表bool LoadConf(const std::string &machine_conf){std::ifstream in(machine_conf);if (!in.is_open()){// 如果打开文件失败LOG(FATAL) << "加载:" << machine_conf << " 失败" << "\n";return false;}std::string line;while (std::getline(in, line)){// 按行读取machine_conf文件数据// 进行字符串切割:将字符串分成两部分:ip和portstd::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, ":"); if (tokens.size() != 2){// 如果切分出来的字符串不是ip和port这两部分LOG(WARNING) << " 切分 " << line << " 失败" << "\n";continue;}Machine m;m.ip = tokens[0];m.port = atoi(tokens[1].c_str());m.load = 0;m.mtx = new std::mutex();online.push_back(machines.size()); // 上线当前主机machines.push_back(m);}in.close(); // 关闭文件return true;}// 智能选择合适的主机提供服务// 参数:// id:输出型参数// m:输出型参数bool SmartChoice(int *id, Machine **m){// 1. 使用选择好的主机(更新该主机的负载)// 2. 我们需要可能离线的主机mtx.lock(); // 将选择功能加锁// 使用的负载均衡算法:轮询 + hashint online_num = online.size(); // 主机在线数if (online_num == 0){// 如果所有的主机都离线了mtx.unlock(); // 将选择功能解锁LOG(FATAL) << " 所有的后端编译主机已经离线,请运维的人尽快查看" << "\n";return false;}// 通过遍历的方式,找到所有负载最小的机器*id = online[0]; // 默认最小负载的机器*m = &machines[online[0]]; // 默认最小负载主机的地址uint64_t min_load = machines[online[0]].Load(); // 默认最小负载数for (int i = 1; i < online_num; ++i){    uint64_t cur_load = machines[online[i]].Load();if (min_load > cur_load){min_load = cur_load;*id = online[i];*m = &machines[online[i]];}}mtx.unlock(); // 将选择功能解锁return true;}// 离线指定主机void OfflineMachine(int which){mtx.lock(); // 将离线功能加锁(因为在离线的同时,有可能有人正在进行智能选择)// 遍历在线主机列表,找到要离线的主机for (auto iter = online.begin(); iter != online.end(); ++iter){if (*iter == which){// 先将要离线的主机的负载清零,不然后面再将其上线的时候,负载还是和现在一样machines[which].ResetLoad();// 要离线的主机找到了,将其进行离线online.erase(iter); offline.push_back(which); // 注意:这里不能写成offline.push_back(*iter);因为这样为导致迭代器失效break; // 因为break的存在,所以我们暂时不用考虑迭代器失效的问题}}mtx.unlock(); // 将离线功能解锁}// 上线对应的主机void OnlineMachine(){// 规定:当所有主机都离线的时候,我们统一上线mtx.lock(); // 将上线功能加锁// 将离线列表里面的所有主机插入到上线列表里面,并删除离线列表里面的所有主机online.insert(online.end(), offline.begin(), offline.end());offline.erase(offline.begin(), offline.end()); mtx.unlock(); // 将上线功能解锁LOG(INFO) << "所有的主机又上线啦!" << "\n";}// 显示所有在线和离线的主机(仅仅用于测试)void ShowMachines(){mtx.lock(); // 加锁std::cout << "当前在线主机列表: ";for (auto &id : online){std::cout << id << " ";}std::cout << std::endl;std::cout << "当前离线主机列表: ";for (auto &id : offline){std::cout << id << " ";}std::cout << std::endl;mtx.unlock(); // 解锁}
};

6.2.4判题模块

        得到的参数是需要判的题目编号和用户传进来的json串形式的代码,通过题目编号,调用model模块得到题目相关的信息,然后通过反序列化用户传来的代码,得到代码内容。有了题目的信息和用户的代码,就可以拼接出可以用来编译的源码内容,构建出CompilerServer需要的json串。请求后端编译服务器主机。
        选到主机之后通过主机的IP+端口,使用网络请求方式发起请求,除了通过请求的返回值判断请求是否成功,还需要判断请求的状态码,只有状态呢是200才表示请求成功。且需要更新请求时机器的负载情况。

// 判题功能
// 参数:
// number:题号,in_json:客户上传上来的代码,out_json:要返回的结果
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{// LOG(DEBUG) << in_json << "\nnumber: " << number << "\n";// 0. 根据题目编号,直接拿到对应的题目细节struct Question q;model_.GetOneQuestion(number, &q);// 1. 将in_json进行反序列化,得到题目id,得到用户提交的源代码(input)Json::Reader reader;Json::Value in_value;reader.parse(in_json, in_value); // 参数:你想要反序列化谁,你想要反序列化的json_value是谁std::string code = in_value["code"].asString();// 2. 重新拼接用户代码+测试用例代码,形成新的代码Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = code + "\n" + q.tail; // 注意这里需要加一个换行符,否则可能会将#ifndef拼接到 ‘;’ 后面,导致拼接错误compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::FastWriter writer;std::string compile_string = writer.write(compile_value); // 将拼接好的代码进行序列化// 3. 选择负载最低的主机(差错处理)// 规则:一直选择,直到主机可用,否则,就是全部挂掉while (true){int id = 0;Machine *m = nullptr;// 进行智能选择if (!load_balance_.SmartChoice(&id, &m)){// 如果选择失败,那么所有的主机都已经挂掉了break;}// 4. 然后发起http请求,得到结果Client cli(m->ip, m->port);m->IncLoad(); // 增加主机负载LOG(INFO) << " 选择主机成功, 主机id: " << id << ", 详情: " << m->ip << ":" << m->port << ", 当前主机的负载是:" << m->Load() << "\n";if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf_8")){if (res->status == 200){// 5. 如果http请求成功,将编译运行后的结果赋值给out_json  *out_json = res->body; m->DecLoad(); // 减少主机负载LOG(INFO) << "请求编译和运行服务成功..." << "\n";break;}m->DecLoad(); // 减少主机负载}else{// 请求失败LOG(ERROR) << " 当前请求的主机id: " << id << ", 详情: " << m->ip << ":" << m->port << ", 可能已经离线" << "\n";load_balance_.OfflineMachine(id); // 将当前主机离线(将主机离线后负载会自动清零)load_balance_.ShowMachines(); // 仅仅是为了用来调试}}
}

7.oj_server模块oj_server.cc

        搭建一个http服务,通过用户请求的不同资源,完成功能路由的任务,调用oj_control模块的功能。

static Control *ctrl_ptr = nullptr; // 定义一个控制器指针,让其既可以局部使用,也可以全局使用// 使用手册
void Usage(std::string proc)
{std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}// 写一个恢复主机的回调方法
void Recovery(int signo)
{ctrl_ptr->RecoveryMachine();
}// 调用方式:./compile_server port
int main(int argc, char *argv[])
{// 捕捉3号信号(ctrl + \).用快捷键(ctrl + \)一键上线所有离线的主机signal(SIGQUIT, Recovery);if (argc != 2){// 如果使用方法不对Usage(argv[0]);return 1;}// 用户请求的服务路由功能Server svr; // 创建有个服务器Control ctrl; // 创建一个控制器ctrl_ptr = &ctrl;// 获取所有的题目列表// 参数:req:用户的需求,resp:服务器的相应svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){// 返回一张包含所有题目的html网页std::string html;ctrl.AllQuestions(&html); resp.set_content(html, "text/html; charset=utf-8"); });// 用户要根据题目编号,获取题目的内容// /question/100 -> 正则匹配// R"()"作用:原始字符串raw string,保持字符串内容的原貌,不用做相关的转义svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1]; // 拿到题号std::string html;ctrl.Question(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");// 让服务器在所有ip,指定端口服务svr.listen("0.0.0.0", atoi(argv[1])); // 启动http服务return 0;
}

九、前端页面的设计(了解)

前端简单使用:
html/css/js/jquery/ajax
Ace前端在线编辑器


1. indx.html

当用户访问根目录时显示的网页

<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>这是我的个人OJ系统</title><style>/* 起手式, 100%保证我们的样式设置可以不受默认影响 */* {/* 消除网页的默认外边距 */margin: 0px;/* 消除网页的默认内边距 */padding: 0px;}h tml,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow,取消后续float带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素,允许你设置宽度 */display: inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */width: 80px;/* 设置字体颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签中的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: green;}.container .navbar .login {float: right;}.container .content {/* 设置标签的宽度 */width: 800px;/* 用来调试 *//* background-color: #ccc; *//* 整体居中 */margin: 0px auto;/* 设置文字居中 */text-align: center;/* 设置上外边距 */margin-top: 200px;}.container .content .font_ {/* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */display: block;/* 设置每个文字的上外边距 */margin-top: 20px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置字体大小font-size: larger; */}</style>
</head><body><div class="container"><!-- 导航栏, 功能不实现--><div class="navbar"><a href="#">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><!-- 网页的内容 --><div class="content"><h1 class="font_">欢迎来到我的OnlineJudge平台</h1><p class="font_">这个我个人独立开发的一个在线OJ平台</p><a class="font_" href="/all_questions">点击我开始编程啦!</a></div></div>
</body></html>

2. all_questions.html

当用户获取题目列表的时候显示的网页 

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>在线OJ-题目列表</title><style>/* 起手式, 100%保证我们的样式设置可以不受默认影响 */* {/* 消除网页的默认外边距 */margin: 0px;/* 消除网页的默认内边距 */padding: 0px;}h tml,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow,取消后续float带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素,允许你设置宽度 */display: inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */width: 80px;/* 设置字体颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签中的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: green;}.container .navbar .login {float: right;}.container .question_list {padding-top: 50px;width: 800px;height: 100%;margin: 0px auto;/* background-color: #ccc; */text-align: center;}.container .question_list table {width: 100%;font-size: large;font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'LucidaSans Unicode', Geneva, Verdana, sans-serif;margin-top: 50px;background-color: rgb(243, 248, 246);}.container .question_list h1 {color: green;}.container .question_list table .item {width: 100px;height: 40px;font-size: large;font-family: 'Times New Roman', Times, serif;}.container .question_list table .item a {text-decoration: none;color: black;}.container .question_list table .item a:hover {color: blue;text-decoration: underline;}.container .footer {width: 100%;height: 50px;text-align: center;line-height: 50px;color: #ccc;margin-top: 15px;}</style>
</head><body><div class="container"><!-- 导航栏, 功能不实现--><div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><div class="question_list"><h1>OnlineJuge题目列表</h1><table><tr><th class="item">编号</th><th class="item">标题</th><th class="item">难度</th></tr>{{#question_list}}<tr><td class="item">{{number}}</td><td class="item"><a href="/question/{{number}}">{{title}}</a></td><td class="item">{{star}}</td></tr>{{/question_list}}</table></div><div class="footer"><!-- <hr> --><h4>@不一样的烟火a</h4></div></div>
</body></html>

3. one_question.html

当用户获取单道题目所显示的网页

<!-- 编写代码的页面 -->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>{{number}}.{{title}}</title><!-- 引入ACE插件 --><!-- 官网链接:https://ace.c9.io/ --><!-- CDN链接:https://cdnjs.com/libraries/ace --><!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 --><!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/--><!-- 引入ACE CDN --><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"charset="utf-8"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"charset="utf-8"></script><!-- 引入jquery CDN --><script src="http://code.jquery.com/jquery-2.1.1.min.js"></script><style>* {margin: 0;padding: 0;}h tml,body {width: 100%;height: 100%;}.container .navbar {width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow,取消后续float带来的影响 */overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素,允许你设置宽度 */display: inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */width: 80px;/* 设置字体颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签中的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {background-color: green;}.container .navbar .login {float: right;}.container .part1 {width: 100%;height: 600px;overflow: hidden;}.container .part1 .left_desc {width: 50%;height: 600px;float: left;overflow: scroll;}.container .part1 .left_desc h3 {padding-top: 10px;padding-left: 10px;}.container .part1 .left_desc pre {padding-top: 10px;padding-left: 10px;font-size: medium;font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;}.container .part1 .right_code {width: 50%;float: right;}.container .part1 .right_code .ace_editor {height: 600px;}.container .part2 {width: 100%;overflow: hidden;}.container .part2 .result {width: 300px;float: left;}/* 按钮属性 */.container .part2 .btn-submit {width: 120px;height: 50px;font-size: large;float: right;background-color: #26bb9c;color: #FFF;/* 给按钮带上圆角 */border-radius: 1ch;border: 0px;margin-top: 10px;margin-right: 10px;}.container .part2 button:hover {color: green;}.container .part2 .result {margin-top: 15px;margin-left: 15px;}.container .part2 .result pre {font-size: large;}</style>
</head><body><div class="container"><!-- 导航栏, 功能不实现--><div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><!-- 左右呈现,题目描述和预设代码 --><div class="part1"><div class="left_desc"><h3><span id="number">{{number}}</span>.{{title}}_{{star}}</h3><pre>{{desc}}</pre></div><div class="right_code"><pre id="code" class="ace_editor"><textarea class="ace_text-input">
{{pre_code}}</textarea></pre></div></div><!-- 提交并且得到结果,并显示 --><div class="part2"><div class="result"></div> <!--运行结果(由下面手动填充)--><button class="btn-submit" onclick="submit()">提交代码</button></div></div><script>//初始化对象editor = ace.edit("code");//设置风格和语言(更多风格和语言,请到github上相应目录查看)// 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.htmleditor.setTheme("ace/theme/monokai");editor.session.setMode("ace/mode/c_cpp");// 字体大小editor.setFontSize(16);// 设置默认制表符的大小:editor.getSession().setTabSize(4);// 设置只读(true时只读,用于展示代码)editor.setReadOnly(false);// 启用提示菜单ace.require("ace/ext/language_tools");editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true});function submit() {// 1. 收集当前页面的有关数据, 1. 题号 2.代码var code = editor.getSession().getValue(); // 获得用户提交的代码console.log(code);var number = $(".container .part1 .left_desc h3 #number").text();// console.log(number);var judge_url = "/judge/" + number; // 发起向后端的请求console.log(judge_url);// 2. 构建json,并通过ajax向后端发起基于http的json请求$.ajax({method: 'Post',   // 向后端发起请求的方式url: judge_url,   // 向后端指定的url发起请求dataType: 'json', // 告知server,我需要什么格式contentType: 'application/json;charset=utf-8',  // 告知server,我给你的是什么格式// 构建一个json串(给server的数据)data: JSON.stringify({'code': code, // 代码'input': ''  // 输入}),// 如果执行成功就执行匿名回调函数success: function (data) {// 成功得到结果// console.log(data);show_result(data);}});// 3. 得到结果,解析并显示到 result中function show_result(data){// console.log(data.status);// console.log(data.reason);// 拿到result结果标签var result_div = $(".container .part2 .result");result_div.empty(); // 清空上一次运行的结果// 首先拿到结果的状态码和原因结果var _status = data.status;var _reason = data.reason;var reason_lable = $( "<p>",{text: _reason});// 将手动创建的标签插入到result_div里面reason_lable.appendTo(result_div)if (status == 0){// 请求成功// 编译运行过程没问题,但是结果是否通过需要看测试用例的结果var _stdout = data.stdout;var _stderr = data.stderr;var stdout_lable = $("<pre>", {text: _stdout});var stderr_lable = $("<pre>", {text: _stderr});// 将手动创建的标签插入到result_div里面stdout_lable.appendTo(result_div);stderr_lable.appendTo(result_div);}else{// 请求失败// 编译运行出错// do nothing}}}</script>
</body></html>

十、项目扩展

  1. 基于注册和登陆的录题功能
  2. 业务扩展,自己写一个论坛,接入到在线OJ中
  3. 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
  4. 目前后端compiler的服务我们使用的是http方式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调用,推荐:rest_rpc,替换我们的httplib(建议,可以不做)
  5. 功能上更完善一下,判断一道题目正确之后,自动下一道题目
  6. navbar中的功能可以一个一个的都实现一下
  7. 其他
     

十一、项目所需工具

1.升级 gcc

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --
infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enablebootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-systemzlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --
enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,objc++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --
with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --
with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install -
-enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-
redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)cpp-httplib 用老的编译器,要么编译不通过,要么直接运行报错百度搜索:scl gcc devsettool升级gcc//安装scl
$ sudo yum install centos-release-scl scl-utils-build
//安装新版本gcc,这里也可以把7换成8或者9,我用的是9,也可以都安装
$ sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
$ ls /opt/rh///启动: 细节,命令行启动只能在本会话有效
$ scl enable devtoolset-7 bash
$ gcc -v//可选:如果想每次登陆的时候,都是较新的gcc,需要把上面的命令添加到你的~/.bash_profile中
$ cat ~/.bash_profile
# .bash_profile# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi# User specific environment and startup programsPATH=$PATH:$HOME/.local/bin:$HOME/binexport PATH#添加下面的命令,每次启动的时候,都会执行这个scl命令
scl enable devtoolset-7 bashorscl enable devtoolset-8 bashorscl enable devtoolset-9 bash

2.安装 jsoncpp

$ sudo yum install -y jsoncpp-devel
[sudo] password for whb:
Loaded plugins: aliases, auto-update-debuginfo, fastestmirror, protectbase
Repository epel is listed more than once in the configuration
Loading mirror speeds from cached hostfile
* base: mirrors.aliyun.com
* epel-debuginfo: mirrors.tuna.tsinghua.edu.cn
* extras: mirrors.aliyun.com
* updates: mirrors.aliyun.com
0 packages excluded due to repository protections
Package jsoncpp-devel-0.10.5-2.el7.x86_64 already installed and latest version
//我已经安装了

3.安装 cpp-httplib

最新的cpp-httplib在使用的时候,如果gcc不是特别新的话有可能会有运行时错误的问题建议:cpp-httplib 0.7.15下载zip安装包,上传到服务器即可cpp-httplib gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib?_from=gitee_search
v0.7.15版本链接: https://gitee.com/yuanfeng1897/cpp-httplib/tree/v0.7.15把httplib.h拷贝到我们的项目中即可,就这么简单使用样例:$ cat http_server.cc#include "httplib.h"int main()
{httplib::Server svr;svr.Get("/hi", [](const httplib::Request &req, httplib::Response &rsp){rsp.set_content("你好,世界!", "text/plain; charset=utf-8");});svr.listen("0.0.0.0", 8080);return 0;
} 更多的细节可以看gitee上面的使用手册

4.安装boost库

$ sudo yum install -y boost-devel //是boost 开发库

5.安装与测试 ctemplate
 

# 国内github镜像网站,如果挂掉可以私信老师要
https://hub.fastgit.xyz/OlafvdSpek/ctemplate$ git clone https://hub.fastgit.xyz/OlafvdSpek/ctemplate.git
$ ./autogen.sh
$ ./configure
$ make         //编译
$ make install //安装到系统中# 注意gcc版本
# 如果安装报错,注意使用sudo测试样例:1. 建立文件
$ ll
total 8
-rw-rw-r-- 1 whb whb 529 May 12 11:52 test.cc
-rw-rw-r-- 1 whb whb 230 May 12 11:52 test.html2. 编写ctemplate代码
$ cat test.cc#include <iostream>
#include <string>
#include <ctemplate/template.h>int main()
{std::string html = "./test.html";std::string html_info = "Hello";//建立ctemplate参数目录结构ctemplate::TemplateDictionary root("test"); //unordered_map<string,string> test;//向结构中添加你要替换的数据,kv的root.SetValue("info", html_info); //test.insert({key, value});//获取被渲染对象ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html,ctemplate::DO_NOT_STRIP); //DO_NOT_STRIP:保持html网页原貌//开始渲染,返回新的网页结果到out_htmlstd::string out_html;tpl->Expand(&out_html, &root);std::cout << "渲染的带参html是:" << std::endl;std::cout << out_html << std::endl;return 0;
} 3. 编写简单html
$ cat test.html
<html><header></header><body><!--渲染参数,会被我们C++代码中的数据替换, info就是上面SetValue("info", html_info)代码中的
info,会自动被std::string info_html中的内容替换--><p>{{info}}</p><p>{{info}}</p><p>{{info}}</p><p>{{info}}</p></body>
</html>4. 编译
$ g++ test.cc -o mytest -lctemplate -lpthread
$ ls
mytest test.cc test.html
$ ./mytest5. 对比结果
渲染前:
<html><header></header><body><!--渲染参数,会被我们C++代码中的数据替换--><p>{{info}}</p><p>{{info}}</p><p>{{info}}</p><p>{{info}}</p></body>
</html>渲染后:
渲染的带参html是:
<html><header></header><body><!--渲染参数,会被我们C++代码中的数据替换--><p>Hello</p><p>Hello</p><p>Hello</p><p>Hello</p></body>
</html>

6.使用Ace在线编辑器

直接复制粘贴即可

<!DOCTYPE html><html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Ace测试</title><!-- 引入ACE插件 --><!-- 官网链接:https://ace.c9.io/ --><!-- CDN链接:https://cdnjs.com/libraries/ace --><!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 --><!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-
%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ 
--><script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript"charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js" type="text/javascript"charset="utf-8"></script><style>* {margin: 0;比特就业课padding: 0;}html,body {width: 100%;height: 100%;}div .ace_editor {height: 600px;width: 100%;}</style>
</head>
<body><div><pre id="code" class="ace_editor"><textarea class="ace_textinput">#include<iostream>intmain()
{std::cout << "hello ace editor" << std::endl;return 0;
}</textarea></pre><button class="bt" onclick="submit()">提交代码</button><br/></div><script>//初始化对象editor = ace.edit("code");//设置风格和语言(更多风格和语言,请到github上相应目录查看)// 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.htmleditor.setTheme("ace/theme/monokai");editor.session.setMode("ace/mode/c_cpp");// 字体大小editor.setFontSize(16);// 设置默认制表符的大小:editor.getSession().setTabSize(4);// 设置只读(true时只读,用于展示代码)editor.setReadOnly(false);// 启用提示菜单ace.require("ace/ext/language_tools");editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true});</script>
</body>
</html>

7.MySQL 建表

CREATE TABLE IF NOT EXISTS `questions`(id int PRIMARY KEY AUTO_INCREMENT COMMENT '题目的ID',title VARCHAR(64) NOT NULL COMMENT '题目的标题',star VARCHAR(8) NOT NULL COMMENT '题目的难度',question_desc TEXT NOT NULL COMMENT '题目描述',header TEXT NOT NULL COMMENT '题目头部,给用户看的代码',tail TEXT NOT NULL COMMENT '题目尾部,包含我们的测试用例',time_limit int DEFAULT 1 COMMENT '题目的时间限制',mem_limit int DEFAULT 5000000 COMMENT '题目的空间限制'
)ENGINE=INNODB DEFAULT CHARSET=utf8;

十二、项目源码

        项目到这里就已经圆满结束了,但是由于篇幅已经很长了,项目中有些地方只给了文字说明与框架,具体的完整实现代码和项目的超清思维导图我会放在Gitee上供大家参考。

项目源码icon-default.png?t=N7T8https://gitee.com/what-you-want-a/load-balanced-online-oj

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/158939.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Excel恢复科学技术法显示的数据

Excel中输入位数较大的数据时&#xff0c;软件会自动使用科学计数法显示。很多时候并不需要这样的计数格式&#xff0c;所以需要把它转变为普通的数字格式 操作方法 选中单元格/列/行》右键》设置单元格式 在打开的窗口中&#xff0c;切换到“数字”选项卡&#xff0c;点击“自…

Mybatis用Byte[]存图片,前端显示图片

前端页面 static下 也就是说byte[] 转成JSON字符串后,和用BASE64编码后是一摸一样的,那么SpringBoot会自动将实体类转JSON字符串,也就是说根本不需要Base64编码 注意:两个值并非一摸一样,一个多了个双引号 byte[]的值前后有个双引号 有一点点区别 一个有双引号,一个没有…

【LeetCode刷题(数据结构)】:对称二叉树

给你一个二叉树的根节点 root 检查它是否轴对称 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false 提示&#xff1a; 树中节点数目在范围 [1, 1000] 内 -100 < Node.val < 100 对称二叉…

nginx的location的优先级和匹配方式

nginx的location的优先级和匹配方式 在http模块中有server&#xff0c;server模块中有location&#xff0c;location匹配的是uri 在一个server中&#xff0c;会有多个location&#xff0c;如何来确定匹配哪个location niginx的正则表达式 ^ 字符串的起始位置 $ 字符串的…

Jenkins+Gitlab+Docker(Dockerfile)部署

Docker部署运行 ​ 上一篇内容中使用Jenkins(运行服务器)Gitlab(代码存储库)Webhook(网络钩子)的方式部署运行我们的项目。需要我们在服务器上做好很多相关的环境配置及依赖。 ​ 那么假如有这样一个场景&#xff1a;需要把不同技术栈的项目部署到同一台服务器上运行。比如PH…

7.定时器

定时器资源 CC2530有四个定时器TIM1~TIM4和休眠定时器 TIM1 定时器1 是一个独立的16 位定时器&#xff0c;支持典型的定时/计数功能&#xff0c;比如输入捕获&#xff0c;输出比较和PWM 功能。定时器有五个独立的捕获/比较通道。每个通道定时器使用一个I/O 引脚。定时器用于…

组件协作模式

二、组件协作模式 组件协作模式概念1、模板方法模式&#xff08;Template_Method&#xff09;模式定义动机(Motivation)具体代码举例实现要点总结 2、策略模式&#xff08;Strategy&#xff09;3、观察者模式&#xff08;Observer/Event&#xff09; 组件协作模式概念 现代软件…

智能警用装备管理系统-科技赋能警务

警用物资装备管理系统&#xff08;智装备DW-S304&#xff09;是依托互云计算、大数据、RFID技术、数据库技术、AI、视频分析技术对警用装备进行统一管理、分析的信息化、智能化、规范化的系统。 &#xff08;1&#xff09;感知智能化 装备感知是整个方案的基础&#xff0c;本方…

Python爬虫(二十三)_selenium案例:动态模拟页面点击

本篇主要介绍使用selenium模拟点击下一页&#xff0c;更多内容请参考:Python学习指南 #-*- coding:utf-8 -*-import unittest from selenium import webdriver from selenium.webdriver.common.keys import Keys from bs4 import BeautifulSoup import timeclass douyuSelenium…

Linemod算法研究

转载&#xff0c;这篇博客写的比较详细&#xff0c;分析也到位. https://www.cnblogs.com/aoru45/p/16810996.html

【Windows日志】记录系统事件的日志

文章目录 一、概要二、Windows日志介绍 2.1 应用程序日志2.2 系统日志2.3 安全日志 三、查看与分析日志四、常见事件ID 4.1 登录事件 4.1.1 4624登陆成功4.1.2 4625登陆失败 4.2 特权使用4.3 账户管理事件4.4 账户登录事件5.2 事件ID汇总 一、概要 Windows主要有以下三类日…

【Android知识笔记】图片专题(BitmapDrawable)

如何计算一张图片的占用内存大小? 注意是占用内存,不是文件大小可以运行时获取重要的是能直接掌握计算方法基础知识 Android 屏幕像素密度分类: (其实还有一种 ldpi = 120,不过这个已经绝种了,所以最低的只需关心mdpi即可) 上表中的比例为:m : h : xh : xxh: xxxh = …

自动驾驶学习笔记(四)——变道绕行仿真

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《2023星火培训【感知专项营】》免费课程—>传送门 文章目录 前言 仿真内容 启动Dreamview 开启Sim…

如何降低海康、大华等网络摄像头调用的高延迟问题(一):海康威视网络摄像头的python sdk使用(opencv读取sdk流)

目录 1.python sdk使用 1.海康SDK下载 2.opencv读取sdk流 先说效果&#xff0c;我是用的AI推理的实时流&#xff0c;延迟从高达7秒降到小于1秒 如果觉得这个延迟还不能接受&#xff0c;下一章&#xff0c;给大家介绍点上不得台面的小方法 SDK&#xff08;Software Developme…

《3D 数学基础》几何检测-最近点

目录 1. 直线上的最近点 2. 射线上的最近点 3. 点到平面的距离 4. 圆或球上的最近点 5. AABB上的最近点 1. 直线上的最近点 q是距离q的最近点&#xff0c;也就是q在直线上的投影。 其中p是直线上的点&#xff08;向量表示&#xff09;&#xff0c;n是直线的法向量&#x…

【苍穹外卖 | 项目日记】第四天

前言&#xff1a; 今天状态还可以&#xff0c;既有自己实战独立写接口&#xff0c;又听了课&#xff0c;学习了新的知识 目录 前言&#xff1a; 今日完结任务&#xff1a; 今日收获&#xff1a; 实现店铺状态接口 杂项知识点&#xff1a; 总结&#xff1a; 今日完结任务…

2023.10.14 培训总结

培训内容 数字模型联合仿真及集成测试技术 MBSE(Model-Based-System-Engiaeering&#xff09; 参数化建模参数化仿真 产生的疑问 支持面向对象支持CAE CFD工具优化工具 飞机的业务功能 开发分布式架构 新技术 WSDL协议DDS 发布/订阅SAOPCORBA 明显开发者 Chris Garrett 美…

【基于Kmeans、Kmeans++和二分K均值算法的图像分割】数据挖掘实验三

文章目录 Ⅰ、项目任务要求任务描述&#xff1a;主要任务要求&#xff1a; II、实现过程数据集描述实现描述具体实现过程 III、完整代码代码①代码② Ⅰ、项目任务要求 任务描述&#xff1a; 图像分割是图像处理和计算机视觉中重要的一环&#xff0c;在实际生活中得到了广泛的…

P1433 吃奶酪

#include <iostream> #include <cmath> using namespace std; #define M 15 #define S(n) ((n) * (n)) double indx[M 5], indy[M 5], ans 0, sum 0;//坐标数组&#xff0c;从下标为1开始记录 int n, vis[M 5] { 0 };//vis数组&#xff0c;选过的数字标记为1…

openssl学习——消息认证码原理

消息认证码原理 消息认证码&#xff08;Message Authentication Code, MAC&#xff09;是一种技术&#xff0c;它的原理是通过对消息和密钥进行特定的处理&#xff0c;生成一个固定长度的数据&#xff0c;这个数据就是消息认证码&#xff08;MAC&#xff09;。这个过程可以看作…