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

文章目录

  • 🌈 一、项目介绍
  • 🌈 二、项目源码
  • 🌈 三、项目演示
    • ⭐ 1. 前端界面展示
    • ⭐ 2. 后端界面展示
  • 🌈 四、项目准备
    • ⭐ 1. 项目所用技术
    • ⭐ 2. 项目开发环境
    • ⭐ 3. 项目宏观结构
  • 🌈 五、comm 公共模块
    • ⭐ 1. util.hpp 工具
    • ⭐ 2. log.hpp 日志
  • 🌈 六、compile_server 编译与运行模块
    • ⭐ 1. compiler.hpp 编译服务设计
    • ⭐ 2. runner.hpp 运行服务设计
    • ⭐ 3. compile_run.hpp 整合编译和运行服务
    • ⭐ 4. compile_server.cpp 对外提供编译和运行服务
  • 🌈 七、oj_server 用户交互模块
    • ⭐ 1. oj_server.cpp 网络路由功能
    • ⭐ 2. MySQL 题库设计
    • ⭐ 3. oj_model.hpp 数据交互模块
    • ⭐ 4. oj_view 网页构建模块
    • ⭐ 5. oj_control.hpp 控制器模块
  • 🌈 八、顶层 makefile 实现
  • 🌈 九、项目补充
    • ⭐ 1. 安装并测试 jsoncpp
    • ⭐ 2. 安装并测试 cpp-httplib
    • ⭐ 3. 安装并测试 boost
    • ⭐ 4. 安装并测试 ctemplate

🌈 一、项目介绍

  • 本项目主要实现的是类似于 leetcode 的题目列表 + 在线编程功能。
  • 该项目采用负载均衡算法 (轮询检测) 使得多个服务器协同处理大量的提交请求和编译请求。
  • 本项目分为文件版MySQL 版,本文只演示 MySQL 版本的代码。
    • 文件版:使用文件存储题目信息。
    • MySQL 版:使用数据库存储题目信息。

🌈 二、项目源码

  • 本文只展示 MySQL 版本的代码,文件版 和 MySQL 版代码均可在项目源码中查看。
  • 源码链接:https://gitee.com/shangguan-show/cpp_projects/tree/master/1.%E8%B4%9F%E8%BD%BD%E5%9D%87%E8%A1%A1%E5%9C%A8%E7%BA%BFOJ

🌈 三、项目演示

⭐ 1. 前端界面展示

1. 首页展示

  • 由于个人不擅长前端,因此首页的制作比较挫,但该展示的还是展示出来了。

在这里插入图片描述

2. 题目列表展示

  • 由于录题是个重复性很高的大工程,因此没有花费较多精力在这上面,只录了 2 题作为基本功能展示。

在这里插入图片描述
在这里插入图片描述

3. 指定题目展示

  1. 用户提交的代码的结果正确展示。

在这里插入图片描述

  1. 用户提交的代码的结果错误展示。

在这里插入图片描述

  1. 用户提交的代码发生编译时报错展示。

在这里插入图片描述

  1. 用户提交的代码运行超时。

在这里插入图片描述

  1. 用户的代码超出内存限制。

在这里插入图片描述

⭐ 2. 后端界面展示

  • 用户在提交代码后,oj_server 会负载均衡的向多台主机请求提供编译与运行服务,并将结果返回给用户。

在这里插入图片描述

  • 如果有哪个服务器挂掉了,也会自动将对应的主机离线。

在这里插入图片描述

  • 如果所有的服务器都挂掉了的话,再将服务器重新启动之后,也能够将这批服务器重新上线。

在这里插入图片描述

🌈 四、项目准备

⭐ 1. 项目所用技术

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

⭐ 2. 项目开发环境

  • Ubuntu 22.04 云服务器
  • vscode

⭐ 3. 项目宏观结构

1. 项目的核心模块

  1. comm:公共模块。
  2. compile_server:编译与运行模块。
  3. oj_server:获取题目列表,查看题目编写题目界面,负载均衡的选择后台的编译服务,其他功能。
  • compile_server 和 oj_server 会采用网络套接字的方式进行通信,这样就能将编译模块部署在服务器后端的多台主机上。
  • 而 oj_server 只有一台,这样子就会负载均衡的选择后台的编译服务。

2. 项目的宏观结构

在这里插入图片描述

🌈 五、comm 公共模块

  • 该模块主要为所有模块提供文件操作、字符串处理、网络请求、打印日志等公共功能。

模块结构

  • 其中的 httplib.h 文件是第三方开源网络库 cpp-httplib 所提供的,因此之后不展示其代码。

在这里插入图片描述

文件名功能
httplib.h提供网络服务
util.hpp提供各种工具类
log.hpp提供日志打印功能

⭐ 1. util.hpp 工具

  • 该模块主要提供的工具类及其说明如下:
类名说明提供的功能
time_util时间工具获取 秒 级时间戳
获取 毫秒 级时间戳
path_util路径工具根据文件名和路径构建 .cpp 后缀的文件完整名
根据文件名和路径构建 .exe 后缀的完整文件名
根据文件名和路径构建 .compile_error 后缀的完整文件名
根据文件名和路径构建 .stdin 后缀的完整文件名
根据文件名和路径构建 .stdout 后缀的完整文件名
根据文件名和路径构建 .stderr 后缀的完整文件名
file_util文件工具判断指定文件是否存在
用 毫秒级时间戳 + 原子性递增的唯一值 形成一个具有唯一性的文件名
将用户代码写到唯一的目标文件中, 形成临时的 .cpp 源文件
读取目标文件中的所有内容
string_util字符串工具根据指定的分隔符切割字符串,并将切分出的子串用数组存储返回
#pragma once#include <atomic>
#include <vector>
#include <string>
#include <fstream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <boost/algorithm/string.hpp>using std::atomic_uint;
using std::getline;
using std::ifstream;
using std::ofstream;
using std::string;
using std::to_string;
using std::vector;namespace ns_util
{const string temp_path = "./temp/"; // 临时目录的路径// 时间工具class time_util{public:// 获取 秒 级时间戳static string get_time_stamp(){struct timeval _time;gettimeofday(&_time, nullptr);return to_string(_time.tv_sec);}// 获取 毫秒 级时间戳static string get_time_ms(){struct timeval _time;gettimeofday(&_time, nullptr);return to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);}};// 路径工具class path_util{public:// 添加后缀static string add_suffix(const string &file_name, const string &suffix){string path_name = temp_path + file_name + suffix;return path_name;}/* ---------- 编译时需要有的临时文件 ---------- */// 构建源文件路径 + 后缀的完整文件名static string Src(const string &file_name){// xxx -> ./temp/xxx.cppreturn add_suffix(file_name, ".cpp");}// 构建可执行程序的完整路径 + 后缀名static string Exe(const string &file_name){// xxx -> ./temp/xxx.exereturn add_suffix(file_name, ".exe");}// 创建用来存储编译时报错的文件名static string compiler_error(const string &file_name){// xxx -> ./temp/xxx.compiler_errorreturn add_suffix(file_name, ".compile_error");}/* ---------- 运行时需要有的临时文件 ----------*/// 形成一个标准输入文件的文件名static string Stdin(const string &file_name){// xxx -> ./temp/xxx.stdinreturn add_suffix(file_name, ".stdin");}// 形成一个标准输出文件的文件名static string Stdout(const string &file_name){// xxx -> ./temp/xxx.stdoutreturn add_suffix(file_name, ".stdout");}// 形成一个标准错误文件的文件名static string Stderr(const string &file_name){// xxx -> ./temp/xxx.stderrreturn add_suffix(file_name, ".stderr");}};// 文件工具class file_util{public:// 判断指定文件是否存在static bool is_file_exists(const string &path_name){// 可使用 stat 函数获取对应文件的属性,如果获取成功则说明该文件存在struct stat st;if (0 == stat(path_name.c_str(), &st))return true;return false;}// 形成一个具有唯一性的文件名//  用 毫秒级时间戳 + 原子性递增的唯一值 来保证唯一性static string unique_file_name(){static atomic_uint id(0); 				// 原子性递增唯一值string ms = time_util::get_time_ms();	// 获取毫秒级时间戳string unique_id = to_string(++id);		// 拼接形成唯一的文件名return ms + "_" + unique_id;}// 将用户代码 content 写到唯一的 target 文件中, 形成临时 src 源文件static bool write_file(const string &target, const string &content){ofstream out(target); // 打开输出流if (!out.is_open())return false;out.write(content.c_str(), content.size());out.close();return true;}// 读取目标文件中的所有内容static bool read_file(const string &target, string *content, bool keep = false){(*content).clear();ifstream in(target); // 打开输入流if (!in.is_open())return false;string line;while (getline(in, line)){// getline 内部重载了强制类型转化(*content) += line;// getline 不保存行分割符, 有些时候需要保留 \n(*content) += (true == keep ? "\n" : "");}in.close();return true;}};// 字符串工具class string_util{public:// 切割字符串static void split_string(const string &str, vector<string> *target, const string &sep){// 根据指定的 sep 分隔符切割字符串 str// 然后将切割好的所有子串放进输出参数 target 中boost::split(*target, str, boost::is_any_of(sep),boost::algorithm::token_compress_on);}};
}

⭐ 2. log.hpp 日志

  • 该模块主要是提供打印日志的功能,方便后续代码调试。

1. 日志主要内容

  • 日志等级
  • 打印该日志的文件名
  • 对应日志所在的行号
  • 添加对应日志的时间
  • 日志信息

2. 日志使用方式

LOG(日志等级) << "message" << "\n";	// 如: LOG(INFO) << "这是一条日志" << "\n";

3. 日志代码展示

#pragma once#include <string>
#include <iostream>#include "util.hpp"	// 工具库using std::cout;
using std::endl;
using std::ostream;
using std::string;
using std::to_string;namespace ns_log
{using namespace ns_util;// 日志等级enum{INFO,    // 常规提示信息DEBUG,   // 调试日志WARRING, // 警告信息,不影响后续使用ERROR,   // 只影响用户的请求FATAL    // 导致整个系统不能使用};// 制作日志信息,并将其写入到缓冲区中inline ostream &log(const string &level, const string &file_name, int line){string message = "[" + level + "]";                 // 1.添加日志等级message += "[" + file_name + "]";                   // 2.添加报错文件名message += "[" + to_string(line) + "]";             // 3.添加报错行message += "[" + time_util::get_time_stamp() + "]"; // 4.添加日志时间戳cout << message;                                    // 5.不写 endl 刷新缓冲区return cout;}// 使用方式: LOG(日志等级) << "message" << "\n";//	__FILE__ 会用调用该宏的文件名替换//	__LINE__ 会用调用该宏的行号替换#define LOG(level) log(#level, __FILE__, __LINE__)
}

🌈 六、compile_server 编译与运行模块

  • 该模块的主要功能:把用户提交的代码在服务器上形成临时文件,对临时文件进行编译并运行,最后得到运行结果。

1. 模块所需文件

在这里插入图片描述

2. 各文件功能说明

文件名文件类型说明
compiler.hpp文件为用户提交的代码提供编译服务,并将编译时产生的信息重定向到指定文件
runner.hpp文件为编译生成的可执行程序提供运行服务,并将运行时产生的信息重定向到指定文件
compile_run.hpp文件整合编译与运行服务
compile_server.cpp文件对外提供编译与运行服务
makefile文件一键编译 compile_server.cpp 文件,以及一键删除产生的 .exe 文件
temp目录用来存放编译和运行用户的代码时所产生的临时文件

⭐ 1. compiler.hpp 编译服务设计

  • 当用户提交代码的时候,需要为提交的代码提供编译服务,可以将提交的代码打包,使用进程替换的方式进行 g++ 编译。
  • 为了防止远端代码是程序错误的代码或者恶意代码,需要 fork 出子进程去执行进程替换对用户提交的代码执行编译功能。
  • 编译服务只关心编译有没有出错,如果出错,则需要知道是什么原因导致的错误。
    • 需要形成一个临时文件,保存编译出错的结果。

在这里插入图片描述

#pragma once#include <string>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>#include "../comm/log.hpp"  // 日志库
#include "../comm/util.hpp" // 工具库using std::cout;
using std::endl;
using std::string;using namespace ns_log;
using namespace ns_util;namespace ns_compiler
{class compiler{public:compiler(){}// 执行编译功能//  a.返回值: 编译成功 true,编译失败 false//  b.输入参数: 编译的文件名//  c.将临时文件全部放到 temp 目录下static bool compile(const string &file_name){// 1.创建子进程去进行编译pid_t pid = fork();if (pid < 0)       // 子进程创建失败{LOG(ERROR) << " 内部错误,创建子进程失败" << "\n";return false;}else if (0 == pid) // 子进程创建成功{umask(0);// 创建特定路径下的 .compiler_error 文件,用来存储编译错误时的错误信息int _compiler_error = open(path_util::compiler_error(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_compiler_error < 0){LOG(WARRING) << " 没有成功形成 .compiler_error 文件" << "\n";exit(1);}// 将本应该输出到标准错误件文件的内容重定向到形成的 .compiler_error 临时文件中dup2(_compiler_error, 2);// 使用进程替换函数让子进程去调用 g++ 编译器完成对代码的编译// 	file_name 只有文件名,没有后缀,需要添加上 .cpp 和 .exe 后缀execlp("g++", "g++", "-o", path_util::Exe(file_name).c_str(),path_util::Src(file_name).c_str(), "-std=c++11", "-D", "COMPILER_ONLINE", nullptr);// 如果走到这里说明进程替换失败LOG(ERROR) << " 启动 g++ 编译器失败, 可能是参数错误" << "\n";exit(2);}else                    // 父进程{// 父进程阻塞等待子进程退出waitpid(pid, nullptr, 0);// 判断编译是否成功 (即判断是否形成了对应的可执行程序)if (file_util::is_file_exists(path_util::Exe(file_name))){LOG(INFO) << path_util::Src(file_name) << " 编译成功!" << "\n";return true;}}LOG(DEBUG) << " " << path_util::Src(file_name) << "\n";LOG(ERROR) << " 编译失败, 没有形成可执行程序" << "\n";return false;}~compiler(){}};
}

⭐ 2. runner.hpp 运行服务设计

  • 编译完成也要能将代码运行起来才能知道代码的结果是否正确,因此还需要体提供运行服务。运行服务也是需要 fork 出子进程执行运行服务
  • 运行服务需要有的临时文件分别有 4 个:
    • .exe 可执行程序,没有这个代码可没法运行,在编译时已经创建好了该文件,直接用就行。
    • .stdin 标准输入文件,用来重定向保存用户的输入。
    • .stdout 标准输出文件,只用来保存程序运行完成后的结果。
    • .stderr 标准错误文件,如果用户代码在运行时发生错误了,需要用该文件保存运行时的错误信息。
  • 运行服务只关心程序是否正常运行完成,有没有收到信号 (使用进程等待的方式查看) 即可。运行结果是否正确由测试用例决定。
  • 同时还需要限制用户代码所占用的资源,不能让用户无限制的占用 CPU 资源以及内存资源。这就是平时刷题时最常见的资源限制。
    • 可以借助 setrlimit() 函数去限制用户代码所占用的时空资源
#pragma once#include <string>
#include <fcntl.h>
#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/resource.h>#include "../comm/log.hpp"
#include "../comm/util.hpp"using std::cout;
using std::endl;
using std::string;using namespace ns_log;
using namespace ns_util;namespace ns_runner
{class runner{public:runner(){}// 设置调用该函数的进程所占用的资源static void set_proc_limit(int _cpu_limit, int _mem_limit){// 限制进程所能占用的时间资源struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = _cpu_limit;	// 设置资源上限cpu_rlimit.rlim_max = RLIM_INFINITY;// 资源上限得在这个范围内,设置成无穷即可setrlimit(RLIMIT_CPU, &cpu_rlimit);	// RLIMIT_CPU 选项限制的是占用 CPU 的时间// 限制进程所能占用的内存资源struct rlimit mem_rlimit;mem_rlimit.rlim_cur = _mem_limit * 1024; // 转化成为 KBmem_rlimit.rlim_max = RLIM_INFINITY;	 // 资源上限得在这个范围内,设置成无穷即可setrlimit(RLIMIT_AS, &mem_rlimit);	     // RLIMIT_AS 选项限制的是占用的内存}// 指明要运行的文件名即可,不需要带路径,也不需要带后缀//  返回值 > 0: 程序运行异常,退出时收到了信号,返回值就是对应的信号编号//  返回值 = 0: 正常运行完毕,但是不关心结果是什么//  返回值 < 0: 内部错误// cpu_limit: 表示该程序运行时,可占用的 CPU 时间的上限// mem_limit: 表示该程序运行时,可占用的内存空间的上限 (KB)static int Run(const string &file_name, int cpu_limit, int mem_limit){umask(0);string _execute = path_util::Exe(file_name);   // 获取要执行的可执行程序的文件名称string _stdin = path_util::Stdin(file_name);   // 获取要创建的标准输入文件的文件名string _stdout = path_util::Stdout(file_name); // 获取要创建的标准输出文件的文件名string _stderr = path_util::Stderr(file_name); // 获取要创建的标准错误文件的文件名int _stdin_fd = open(_stdin.c_str(), O_WRONLY | O_CREAT, 0644);   // 以写方式(打开/创建)标准输入文件int _stdout_fd = open(_stdout.c_str(), O_WRONLY | O_CREAT, 0644); // 以写方式(打开/创建)标准输出文件int _stderr_fd = open(_stderr.c_str(), O_WRONLY | O_CREAT, 0644); // 以写方式(打开/创建)标准错误文件// 这 3 个文件任何一个打开失败都直接退出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 (0 == pid){// 子进程创建成功// 子进程会继承父进程的文件描述符表dup2(_stdin_fd, 0);  // 将标准输入文件重定向到打开的 _stdin_fd  文件dup2(_stdout_fd, 1); // 将标准输出文件重定向到打开的 _stdout_fd 文件dup2(_stderr_fd, 2); // 将标准错误文件重定向到打开的 _stderr_fd 文件// 限制子进程能够使用的资源上限set_proc_limit(cpu_limit, mem_limit);// 让子进程进程替换去执行对应的 .exe 可执行程序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) << " 运行完毕, 退出信号: " << (status & 0x7f) << "\n";return status & 0x7F; // 如果出现了异常,返回值会大于 0}}~runner(){}};
}

⭐ 3. compile_run.hpp 整合编译和运行服务

  • 该模块需要整合编译和运行功能适配用户请求定制通信协议字段正确的调用 compile 和 run 模块

在这里插入图片描述

该模块需要实现的功能

  1. 远端会传来序列化的报文,使用 jsoncpp 第三方开源序列化、反序列化库,把报文进行反序列化,切割出以下 4 部分,调用 compiler 和 runner 模块进行编译和运行,把结果最后再进行序列化,作为 htttp 协议的响应正文,日后使用。

    • code:用户提交的代码;
    • input:用户给自己提交的代码所做的输入,不做处理;
    • cpu_limit:对指定题目的运行时间限制;
    • mem_limit:对指定题目的内存限制,即空间限制;
  2. 能够将编译和运行时产生的状态码转换成对应的状态信息。

  3. 能够删除编译和运行时所产生的临时文件。

#pragma once#include "runner.hpp"
#include "compiler.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <signal.h>
#include <jsoncpp/json/json.h>using std::istream;
using std::ostream;namespace ns_compile_and_run
{using namespace ns_log;using namespace ns_util;using namespace ns_runner;using namespace ns_compiler;// 编译并运行class compile_and_run{public:// 将状态码转成对应的描述信息 (待完善)static string code_to_desc(int status_code, const string &file_name){// status_code = 0: 表示整个过程全部完成// status_code > 0: 表示进程收到了信号,导致异常崩溃// status_code < 0: 表示整个过程非运行报错 (代码为空、编译报错等)string desc;switch (status_code){case 0:desc = "编译运行成功";break;case -1:desc = "提交的代码为空";break;case -2:desc = "未知错误";break;case -3:file_util::read_file(path_util::compiler_error(file_name), &desc, true);break;case SIGABRT:desc = "内存超过范围";break;case SIGXCPU:desc = "CPU 使用超时";break;case SIGFPE:desc = "浮点数溢出";break;default:desc = "未知状态码: " + to_string(status_code);break;}return desc;}// 清理生成的临时文件 (临时文件的个数不确定,但最多会生成 6 个)static void remove_all_temp_file(const string &file_name){// 删除 .cpp 文件string _src = path_util::Src(file_name);if (file_util::is_file_exists(_src))unlink(_src.c_str());// 删除 .compile_error 文件string _compile_error = path_util::compiler_error(file_name);if (file_util::is_file_exists(_compile_error))unlink(_compile_error.c_str());// 删除 .exe 文件string _exe = path_util::Exe(file_name);if (file_util::is_file_exists(_exe))unlink(_exe.c_str());// 删除 .stdin 文件string _stdin = path_util::Stdin(file_name);if (file_util::is_file_exists(_stdin))unlink(_stdin.c_str());// 删除 .stdout 文件string _stdout = path_util::Stdout(file_name);if (file_util::is_file_exists(_stdout))unlink(_stdout.c_str());// 删除 .stderr 文件string _stderr = path_util::Stderr(file_name);if (file_util::is_file_exists(_stderr))unlink(_stderr.c_str());}/** 输入: 一个 json 串,in_json 里面包含如下内容*  1.code: 用户提交的代码*  2.input: 用户给自己提交的代码的对应的输入 (不做处理)*  3.cpu_limit: 时间要求*  4.mem_limit: 空间要求* 输出: 一个 json 串,out_json 应该带出去如下内容*  1.status: 状态码    (必填)*  2.reason: 请求结果  (必填)*  3.stdout: 程序运行完后的结果    (选填)*  4.stderr: 程序运行完的错误结果  (选填)*/static void Start(const string &in_json, string *out_json){/* ---------- 反序列化 ---------- */// 将 in_json 中的内容反序列化读取到 in_value 中Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);// 拿取 in_json 中包含的信息string code = in_value["code"].asString();string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int mem_limit = in_value["mem_limit"].asInt();/* ---------- 序列化 ---------- */int status_code = 0;Json::Value out_value;int run_result = 0;string file_name; // 需要内部形成的唯一文件名// 如果用户一行代码都没写if (0 == code.size()){status_code = -1; // 代码为空goto END;}// 获得一个具有唯一性的文件名,该文件名没有目录也没有后缀file_name = file_util::unique_file_name();// 形成临时唯一的 .cpp 源文件, 将用户代码 code 写到这个唯一的文件中if (false == file_util::write_file(path_util::Src(file_name), code)){status_code = -2; // 未知错误goto END;}// 调用编译功能if (false == 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; 			// 未知错误else if (run_result > 0)status_code = run_result; 	// 运行时崩溃elsestatus_code = 0; 			// 运行成功END:out_value["status"] = status_code;out_value["reason"] = code_to_desc(status_code, file_name);if (0 == status_code){// 整个过程全部成功,读取写到 stdout 和 stderr 两个文件中的内容string _stdout;file_util::read_file(path_util::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;string _stderr;file_util::read_file(path_util::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}// 将 out_value 序列化给 out_jsonJson::StyledWriter writer;*out_json = writer.write(out_value);// 清理所有的临时文件remove_all_temp_file(file_name);}};
}

⭐ 4. compile_server.cpp 对外提供编译和运行服务

  • 本模块需要用到 cpp-httplib 库。
  • 当用户提交代码时,oj_server 会向该模块请求 /compile_and_run 服务用于编译并运行用户的代码。
  • 服务端要暴露出自己的 ip 地址和端口号,让远端的 oj_server 能够访问到服务器。
  • 因此启动服务端进程的方式位:./compile_run.exe 端口号
#include "compile_run.hpp"
#include "../comm/httplib.h"using namespace ns_compile_and_run;
using namespace httplib;using std::cerr;void usage(string proc)
{cerr << "usage: " << "\n\t" << proc << " port" << endl;
}// ./compile_server.exe port
int main(int argc, char *argv[])
{if (2 != argc){usage(argv[0]);return 1;}Server svr;// 服务器对外只提供一个服务,用户请求的是一个 out_json 串svr.Post("/compile_and_run", [](const Request &req, Response &resp){string in_json = req.body;  string out_json;if (!in_json.empty()){compile_and_run::Start(in_json, &out_json);resp.set_content(out_json.c_str(), "application/json; charset=utf-8");} });svr.listen("0.0.0.0", atoi(argv[1])); // 启动 http 服务return 0;
}

🌈 七、oj_server 用户交互模块

  • 该模块会采用 MVC 的设计模式来调用后端的编译模块,以及访问 文件 / 数据库 将题目列表和编辑界面展示给用户。
    • 该模块会统计每台服务器的负载情况,然后智能的选择使用哪台服务器。
  • 在运行时还要进行资源限制,限制用户代码的运行时间以及内存占用,不然用户写个死循环或开辟个很大的空间,就直接让服务器崩掉了。

MVC 设计模式介绍

  • M:model,通常是和数据交互的模块,对外提供访问题目的接口。
  • V:view,通常是拿到数据之后,要进⾏构建网页,渲染网页内容,展示给用户的 (浏览器)。
  • C:control,控制器控制拿什么数据, 什么时候拿数据, 拿多少数据等,通常说的编写核心业务逻辑指的就是这个。

oj_server 目录结构

在这里插入图片描述

文件名文件类型说明
oj_moder.hpp文件数据交互模块,对外提供访问题目的接口
oj_view.hpp文件对获取到的题目进行构建网页、网页渲染
oj_control.hpp文件提供负载均衡、控制核心业务逻辑、判题功能
include软链接负责链接 mysql-connector 下的 include 库
lib软链接负责链接 mysql-connector 下的 lib 库
template_html目录其中的 all_questions.html 负责展示题目列表;one_question.html 负责展示指定题目的编辑页面
wwwroot目录其中的 index.html 展示的是网页的首页

⭐ 1. oj_server.cpp 网络路由功能

  • 实现用户请求的服务路由功能,使用 http 进行路由选择,主要分为了三个路由,获取所有题目、获取指定题目内容、对提交代码进行判题。
#include <string>
#include <signal.h>
#include <iostream>#include "oj_control.hpp"
#include "../comm/httplib.h"using std::cout;
using std::endl;
using std::string;using namespace httplib;
using namespace ns_control;static control *ctrl_ptr = nullptr;// 将服务器重新上线
void recover(int sig)
{ctrl_ptr->recovery_machine();
}int main()
{// 当收到 3 号信号时,重新将所有的主机上线signal(SIGQUIT, recover);Server svr;control ctrl;ctrl_ptr = &ctrl;/* 获取所有的题目列表 */svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){// 返回一张包含所有的题目的 html 网页string html;ctrl.all_questions(&html);resp.set_content(html, "text/html; charset=utf-8"); });/* 用户要根据题目编号获取题目信息 *///  \d+ 能够适应任意题号 -> 正则匹配//  R"()", 原始字符串,保持字符串内容的原貌svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){string question_num = req.matches[1]; string html; ctrl.question(question_num, &html);resp.set_content(html, "text/html; charset=utf-8"); });/* 用户提交代码,使用我们的判题功能 *///  判题的构成: a.每道题的测试用例 b.compile_and_run 服务svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){string question_num = req.matches[1]; // 获取题目编号string result_json;ctrl.judge(question_num, req.body, &result_json);resp.set_content(result_json, "application/json; charset=utf-8"); // 设置响应内容});svr.set_base_dir("./wwwroot"); // 设置网页首页svr.listen("0.0.0.0", 8080);   // 启动 http 服务return 0;
}

⭐ 2. MySQL 题库设计

  • 用户在提交代码后,我们接收到的其实不止有用户的代码,还有对特定题目的测试用例代码,只有根据完整代码才能判断题目是否正确。
    • 完整的代码 = 用户提交的代码 + 数据库中存储的测试用例代码。
  • 刷过 leetcode 的都知道,刷题是会提供给你一份预设代码的,因此一开始我们的数据库得准备两部分代码 (预设代码 + 测试代码)。

1. 创建 MySQL 用户及数据库

  1. 要创建的用户为 oj_client:
create user oj_client@'%' identified by '123456';	// 123456 是密码,你也可以自己更改
  1. 建立数据库 oj:
create database oj;
  1. 赋权,让 oj_client 这个用户只能看见 oj 这个数据库。
grant all on oj.* to oj_client@'%';

2. 创建表结构并录入数据

create table if not exists `oj_questions`
(id            int primary key auto_increment comment '题目编号',title         varchar(128) 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          '测试用例',cpu_limit     int default 1 		comment          '时间限制',mem_limit     int default 50000 	comment          '空间限制'
) engine = innodbdefault charset = utf8;

⭐ 3. oj_model.hpp 数据交互模块

  • 该模块负责执行对应的 sql 语句从数据库中获取全部或指定题目的信息。
#pragma once#include <vector>
#include <string>
#include <cassert>
#include <fstream>
#include <iostream>
#include <unordered_map>#include "include/mysql.h"
#include "../comm/log.hpp"
#include "../comm/util.hpp"using std::cout;
using std::endl;
using std::getline;
using std::ifstream;
using std::ofstream;
using std::stoi;
using std::string;
using std::unordered_map;
using std::vector;using namespace ns_log;
using namespace ns_util;namespace ns_model
{// 题目的细节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";            // ipconst std::string user = "oj_client";            // 访问的用户const std::string passwd = "123456";             // 对应用户名的用户密码const std::string db = "oj";                     // 要连接的数据库const int port = 3306;                           // 数据库对应的端口号class model{public:model(){}// 执行对应的 sql 语句去获取题目bool query_mysql(const string &sql, vector<struct 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) << " 连接数据库失败!" << mysql_error(my) << "\n";return false;}// 连接成功后,需要设置该连接的编码格式,防止出现乱码mysql_set_character_set(my, "utf8");LOG(INFO) << " 数据库连接成功!" << "\n";// 执行 sql 语句进行数据访问if (0 != mysql_query(my, sql.c_str())){LOG(WARRING) << " 访问失败, 请检查 sql 语句: " << sql << "\n";return false;}// 提取结果MYSQL_RES *res = mysql_store_result(my);// 分析结果int rows = mysql_num_rows(res);   // 获得行数int cols = mysql_num_fields(res); // 获得列数for (size_t i = 0; i < rows; i++){struct question q;MYSQL_ROW row = mysql_fetch_row(res); // 拿取一行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_close(my); // 关闭 MySQL 连接return true;}// 从数据库获取所有的题目bool get_all_questions(vector<struct question> *out){const string sql = "select * from " + oj_questions;return query_mysql(sql, out);}// 从数据库获取指定一个题目bool get_one_question(const string &number, struct question *q){bool res = false;const string sql = "select * from " + oj_questions + " where id=" + number;vector<struct question> result;if (query_mysql(sql, &result)){if (1 == result.size()){*q = result[0];res = true;}}return res;}~model(){}};
}

⭐ 4. oj_view 网页构建模块

  • 通常是拿到数据之后,要进⾏构建网页,渲染网页内容,展⽰给⽤⼾的 (浏览器)。
  • 该模块需要用到 ctemplate 库。
#pragma once#include <string>
#include <iostream>
#include <ctemplate/template.h>#include "oj_model.hpp"using std::cout;
using std::endl;
using std::string;using namespace ns_model;namespace ns_view
{// 要渲染的网页的 html 文件的所在路径const string template_path = "./template_html/";class view{public:view(){}// 将所有的题目数据构建成网页void all_expand_html(const vector<struct question> &questions, string *html){// 需要显示的内容: 题目编号、题目标题、题目难度// 推荐使用表格显示// 1.形成要被渲染的网页文件的所在路径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);}// 将指定的题目数据构建成网页void one_expand_html(const struct question &q, string *html){// 1.形成要被渲染的网页文件的所在路径string src_html = template_path + "one_question.html";// 2.构建数据字典ctemplate::TemplateDictionary root("one_question");root.SetValue("number", q.number);root.SetValue("title", q.title);root.SetValue("star", q.star);root.SetValue("desc", q.desc);root.SetValue("header", q.header);// 3.获取被渲染的网页对象ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4.添加数据字典到网页中tpl->Expand(html, &root);}~view(){}};
}

⭐ 5. oj_control.hpp 控制器模块

1. 该模块拥有的类及其说明

类名说明提供的功能
machine主机操作模块初始化构造主机 (主机的 ip、port、负载、互斥锁)
增加主机负载
减少主机负载
清空主机负载
获取主机负载
load_blance负载均衡模块配合 boost 库加载配置文件中的所有主机
智能选择负载最低的主机提供编译和运行服务
展示所有离线和在线的主机 id
上线主机 (统一上线)
离线特定主机
control控制器模块获取所有的题目, 根据题目数据,构建 html 网页
获取指定的题目, 根据题目数据,构建 html 网页
提供对特定题目的判题功能

2. control 模块的判题功能要执行的操作

  1. 根据题目编号,拿到题目
  2. 对 in_json 进行反序列化,得到用户对指定题目的 code、input
  3. 重新拼接用户代码 + 测试用例,得到新的代码
  4. 利用负载均衡模块,选择负载最低的主机调用编译和运行功能,该模块还得能够将对应得主机上线或下线。
  5. 对负载最低的主机发起 http 请求得到结果。
  6. 结果赋值给输出参数out_json。

3. 代码展示

#pragma once#include <mutex>
#include <string>
#include <cassert>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <jsoncpp/json/json.h>
#include <boost/algorithm/string.hpp>#include "oj_view.hpp"
#include "oj_model.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include "../comm/httplib.h"using std::cout;
using std::endl;
using std::getline;
using std::ifstream;
using std::min;
using std::ofstream;
using std::sort;
using std::string;using namespace ns_log;
using namespace ns_util;
using namespace ns_view;
using namespace ns_model;
using namespace httplib;namespace ns_control
{// 提供服务的主机class machine{public:string ip;       // 该主机的 ip 地址int port;        // 该主机中提供编译服务的进程uint64_t load;   // 当前主机编译服务的负载情况,需要被锁保护起来std::mutex *mtx; // mutex 是禁止拷贝的,使用指针来完成public:machine(): ip(""), port(0), load(0), mtx(nullptr){}// 增加主机负载void inc_load(){if (mtx != nullptr) mtx->lock();load++;if (mtx != nullptr) mtx->unlock();}// 减少主机负载void dec_load(){if (mtx != nullptr) mtx->lock();load--;if (mtx != nullptr) mtx->unlock();}// 将主机负载清零void reset_load(){if (mtx != nullptr) mtx->lock();load = 0;if (mtx != nullptr) mtx->unlock();}// 获取主机负载uint64_t get_load(){uint64_t _load = 0;if (mtx != nullptr) mtx->lock();_load = load;if (mtx != nullptr) mtx->unlock();return _load;}~machine(){}};// 记录提供服务的主机列表文件的所在路径const string service_machine = "./conf/service_machine.conf";// 负载均衡模块class load_blance{private:vector<machine> machines; // 存放所有能提供编译服务的主机, 用数组下标作为每台主机的 idvector<int> online;       // 记录所有 在线 的主机的 idvector<int> offline;      // 记录所有 离线 的主机的 idstd::mutex mtx;           // 保证 load_blance 的数据安全public:load_blance(){assert(load_conf(service_machine));LOG(INFO) << " 加载" << service_machine << " 成功" << "\n";}// 加载所有主机bool load_conf(const string &machine_conf){ifstream in(machine_conf);if (!in.is_open()){LOG(FATAL) << " 加载: " << machine_conf << " 失败" << "\n";return false;}string line;// 切分主机信息,获取对应主机的 ip 和 portwhile (getline(in, line)){string sep = ":";vector<string> tokens;string_util::split_string(line, &tokens, ":");if (tokens.size() != 2){LOG(WARRING) << " 切分 " << line << " 失败" << "\n";continue;}machine _m;_m.ip = tokens[0];_m.port = stoi(tokens[1]);_m.load = 0;_m.mtx = new std::mutex();online.push_back(machines.size()); // 一开始所有的主机都应该是在线状态machines.push_back(_m);}in.close();return true;}// 智能选择负载最低的主机bool smart_choice(int *id, machine **m){// 1.使用选择好的主机 (核心: 更新该主机的负载)// 2.可能需要离线该主机mtx.lock();// 负载均衡的算法: 1.随机数 + hash; 2.轮询 + hash;int online_num = online.size(); // 获取在线主机的个数if (0 == online_num)            // 所有主机均已离线{mtx.unlock();LOG(FATAL) << " 所有的后端编译主机已全部离线, 请运维的同事尽快查看" << "\n";return false;}// 通过遍历的方式,找到所有负载最小的机器*id = online[0];*m = &machines[online[0]];uint64_t min_load = machines[online[0]].get_load();for (size_t i = 1; i < online_num; i++){uint64_t curr_load = machines[online[i]].get_load();if (min_load > curr_load){min_load = curr_load;      // 获取最小负载的主机的负载*id = online[i];           // 获取负载最小的主机的 id*m = &machines[online[i]]; // 获取负载最小的主机的地址}}mtx.unlock();return true;}// 上线主机 (统一将主机上线)void online_machine(){mtx.lock();// 将 offline 的内容全部添加到 online 中, 再将 offline 清空online.insert(online.end(), offline.begin(), offline.end());offline.erase(offline.begin(), offline.end());mtx.unlock();LOG(INFO) << " 所有的主机均已上线" << "\n";}// 离线对应主机void offline_machine(int id){mtx.lock();for (auto iter = online.begin(); iter != online.end(); iter++){if (*iter == id){// 将要离线的主机的负载清零machines[id].reset_load();// 已经找到了要离线的主机online.erase(iter);    // 将对应的主机利离线offline.push_back(id); // 在离线主机集中将其添加进去break;}}mtx.unlock();}// for testvoid show_machine(){mtx.lock();// 展示所有的在线主机cout << "当前在线主机列表: ";for (auto &id : online)cout << id << " ";cout << endl;// 展示所有的离线主机cout << "当前离线主机列表: ";for (auto &id : offline)cout << id << " ";cout << endl;mtx.unlock();}~load_blance(){}};// 核心业务逻辑的控制器class control{private:model _model;             // 提供后台数据view _view;               // 提网 html 渲染功能load_blance _load_blance; // 核心负载均衡器public:control(){}// 将所有的主机重新上限void recovery_machine(){_load_blance.online_machine();}// 获取所有的题目, 根据题目数据,构建 html 网页bool all_questions(string *html){bool ret = true;vector<struct question> all;if (_model.get_all_questions(&all)){// 将获取到的题目按照题目编号进行升序排序sort(all.begin(), all.end(),[](const struct question &q1, const struct question &q2){return stoi(q1.number) < stoi(q2.number);});// 获取所有的题目信息成功, 将所有的题目数据构建成网页_view.all_expand_html(all, html);}else{// 获取所有的题目失败*html = "获取题目失败, 形成题目列表失败";ret = false;}return ret;}// 获取指定题目信息bool question(const string &question_num, string *html){bool ret = true;struct question q;if (_model.get_one_question(question_num, &q)){// 获取指定的题目信息成功, 将指定的题目数据构建成网页_view.one_expand_html(q, html);}else{// 获取所有的题目失败*html = "指定题目 " + question_num + " 不存在";ret = false;}return ret;}// 提供判题功能//  in_json 应该有的内容: 题目 id、用户提交的代码void judge(const string &number, const string in_json, string *out_json){// 0.根据题编号,直接拿到对应的题目细节struct question q;_model.get_one_question(number, &q);// 1.对 in_json 反序列化: 得到题目 id,用户提交的源代码,用户的输入Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);string code = in_value["code"].asString();// 2.序列化,拼接 用户提交的源代码 + 测试用例代码,形成新的代码Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = code + q.tail;compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::StyledWriter writer;string compile_string = writer.write(compile_value);// 3.选择负载最低的主机//  规则: 一直选择,直到主机可用,否则就是服务端全部挂掉while (true){int id;machine *m = nullptr;// 如果选择失败,则说明所有主机都挂掉了, 不需要给用户返回if (false == _load_blance.smart_choice(&id, &m))break;// 4.向负载最低的主机发起 http 请求,得到结果Client cli(m->ip, m->port);m->inc_load(); // 被请求的主机要先增加负载LOG(INFO) << " 选择主机成功, 主机 id: " << id << ", ip: " << m->ip<< ", port: " << m->port << ", 负载: " << m->get_load() << "\n";if (auto res = cli.Post("/compile_and_run", compile_string, "application/json; charset=utf-8")){// 5.将判题结果交给 out_jsonif (200 == res->status)    // 此时的 http 请求才算完全成功{*out_json = res->body; // 拿到这次编译并运行的结果m->dec_load();         // 请求的服务已经结束,让该主机的负载减少LOG(INFO) << " 请求编译和运行服务成功" << "\n";break;}else{// 本次请求访问到了目标主机,但结果不对// 对应主机提供的服务结束,让负载减少m->dec_load();}}else{// 请求失败,没得到任何 responseLOG(ERROR) << " 当前请求的主机 id: " << id << " ip: " << m->ip<< " port: " << m->port << " 可能已经离线" << "\n";_load_blance.offline_machine(id); // 将请求的对应主机离线_load_blance.show_machine();      // for test}}}~control(){}};
}

🌈 八、顶层 makefile 实现

  • 项目写好之后,不是直接将代码交给别人,只需要把可执行文件和运行该程序需要的配置文件给用户即可。

  • 顶层 makefile 需要完成的任务有 3 个:

    • 一键编译:一键形成对应的 compile_server 和 oj_server 两个模块对应的可执行程序。
    • 一键发布:将要交付的项目的可执行程序及其相关配置文件一键放到统一的目录底下。
    • 一键清除:清除一键编译和一键发布后生成的文件。
# 编译
.PHONY:all
all:@cd compile_server;\make;\cd -;\cd oj_server;\make;\cd -;# 发布
.PHONY:publish
publish:@mkdir -p publish/compile_server;\mkdir -p publish/oj_server;\cp -rf compile_server/compile_server.exe publish/compile_server;\cp -rf compile_server/temp publish/compile_server;\cp -rf oj_server/conf publish/oj_server;\cp -rf oj_server/lib publish/oj_server;\cp -rf oj_server/template_html publish/oj_server;\cp -rf oj_server/wwwroot publish/oj_server;\cp -rf oj_server/oj_server.exe publish/oj_server;# 清理
.PHONY:clean
clean:@cd compile_server;\make clean;\cd -;\cd oj_server;\make clean;\cd -;\rm -rf publish;

🌈 九、项目补充

⭐ 1. 安装并测试 jsoncpp

1. 安装 jsoncpp

  • 按顺序在命令执行如下顺序即可。
sudo apt-get update						// 更新源sudo apt-get install libjsoncpp-dev		// 安装ls /usr/include/jsoncpp/json/			// 检查是否安装成功

在这里插入图片描述

2. 使用 jsoncpp

  • 编代码时要包含头文件 #include <jsoncpp/json/json.h>
  • 编译时要连接 jsoncpp 的库 g++ -ljsoncpp
    • 使用 jsoncpp 进行序列化和反序列化示例:
#include <string>
#include <fstream>
#include <iostream>
#include <jsoncpp/json/json.h>using namespace std;struct student
{string name;int age;double weight;public:void print(){cout << "name:" << name << endl;cout << "age:" << age << endl;cout << "weight:" << weight << endl;}
};int main()
{/* ---------- 序列化 ---------- */// 结构化数据struct student zs = {"张三", 18, 70};// 将结构化对象转换成 json 的 Value 对象Json::Value root1;root1["name"] = zs.name;root1["age"] = zs.age;root1["weight"] = zs.weight;// 序列化Json::FastWriter writer; 					// 序列化出来的字符串就是一整条串// Json::StyledWriter writer;              	// 序列化出来的字符串看着像结构体string json_string = writer.write(root1);	// 序列化后写入到 json_string 中cout << "---------- 序列化 ----------" << endl;cout << json_string << endl;            	// 打印序列化后的字符串/* ---------- 反序列化 ---------- */Json::Value root2;Json::Reader reader;bool res = reader.parse(json_string, root2);// 将 string 对象中的内容反序列化读取到 root2 中struct student sz;sz.name = root2["name"].asString();         // 要提取出的 name 的类型为 stringsz.age = root2["age"].asInt();              // 要提取出的 age 的类型为 intsz.weight = root2["weight"].asDouble();     // 要提取出的 weight 的类型为 doublecout << "---------- 反序列化 ----------" << endl;sz.print();                                 // 打印反序列化后的结构化数据return 0;
}

在这里插入图片描述

⭐ 2. 安装并测试 cpp-httplib

  • 最新的 cpp-httplib 在使用的时候,如果 gcc 不是特别新的话有可能会有运行时错误的问题,建议使用 cpp-httplib 0.7.15
    • cpp-httplib gitee链接:https://gitee.com/yuanfeng1897/cpp-httplib?_from=gitee_search
    • cpp-httplib 0.7.15 版本链接: https://gitee.com/yuanfeng1897/cpp-httplib/tree/v0.7.15

下载方式

  1. 点击链接之后,下载 zip 安装包到 Windows 中。

在这里插入图片描述
在这里插入图片描述

  1. 将该安装包传到 Linux 机器中,然后将其解压即可。

在这里插入图片描述

  1. 最后再将 httplib.h 拷贝到我们的项目中的 comm 目录下。

在这里插入图片描述
在这里插入图片描述

2. 使用 cpp-httplib

  • 由于 httplib 库的实现用到了原生线程库,因此在编译时需要添加上 -lpthread 选项。
  • 现在由网页向服务端请求一个 /hello 服务,
#include "../comm/httplib.h"using namespace httplib;void usage(string proc)
{cerr << "usage: " << "\n\t" << proc << " port" << endl;
}// ./compile_server.exe port
int main(int argc, char *argv[])
{if (2 != argc){usage(argv[0]);return 1;}Server svr;// 获取指定资源 (用来进行基本测试)//	req 用于接收请求,resp 用于响应请求svr.Get("/hello", [](const Request &req, Response &resp){ resp.set_content("hello httplib, 你好 httplib", "text/plain; charset=utf-8"); });// 启动 http 服务,监听对指定 IP 和 port 的请求svr.listen("0.0.0.0", atoi(argv[1])); return 0;
}

在这里插入图片描述
在这里插入图片描述

⭐ 3. 安装并测试 boost

  • 在命令行中输入以下指令可安装 boost 库。
sudo apt install libboost-dev
  • 使用 boost 库。
#include <vector>
#include <string>
#include <iostream>
#include <boost/algorithm/string.hpp>int main()
{vector<string> tokens;							// 存储分割出来的子串const string str = "1 判断回文数 简单 1 30000";	// 待被切割的串const string sep = " ";							// 分隔符boost::split(tokens, str, boost::is_any_of(sep),boost::algorithm::token_compress_on);for (auto &s : tokens)cout << s << endl;return 0;
}

在这里插入图片描述

⭐ 4. 安装并测试 ctemplate

  • 将我提供的安装包下载下来,再上传到自己的 Linux 服务器上,然后解压。
    • 通过百度网盘分享的文件:ctemplate 链接:https://pan.baidu.com/s/13m5Z6ZbRqFIGohrWYz_8DA?pwd=4nz3 提取码:4nz3
  • 解压完之后再在命令行执行以下步骤即可将 ctemplate 安装到系统中。
./autogen.sh
./configure
make			// 编译
make install	// 安装到系统中

使用 ctemplate 库

  • ctemplate 会采用 key-value 模型,用后端的 value 值替换掉 html 中用双括号包裹起来的 key 值。
    • 在编码时需要包含头文件 #include <boost/algorithm/string.hpp>;
    • 在编译时需要链接 g++ -lctemplate 库。
#include <vector>
#include <string>
#include <iostream>
#include <ctemplate/template.h>using namespace std;int main()
{string in_html = "./test.html";string value = "这是一个 ctemplate 测试用例";// 形成数据字典ctemplate::TemplateDictionary root("test"); // 类似于 unordered_map<> test;root.SetValue("key", value);                // 类似于 test.insert({});// 获取被渲染的网页对象ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP);// 添加字典数据到网页中string out_html;tpl->Expand(&out_html, &root);// 完成了渲染, 输出替换了之后的 html 文本内容cout << out_html << endl;return 0;
}
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用来测试 ctemplate 库</title>
</head><body>用双花括号括起来的就是要被替换的内容<p>{{key}}</p><p>{{key}}</p><p>{{key}}</p><p>{{key}}</p><p>{{key}}</p>
</body></html>

在这里插入图片描述

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

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

相关文章

【Android】安卓四大组件之Service用法

文章目录 使用Handler更新UIService基本特点启动方式非绑定式服务使用步骤 绑定式服务步骤 生命周期非绑定式启动阶段结束阶段 绑定式启动阶段结束阶段 前台Service使用步骤结束结束Service本身降级为普通Service降级为普通Service 使用Handler更新UI 主线程创建Handler对象&a…

房产中介小程序

本文来自&#xff1a;ThinkPHPFastAdmin房产中介小程序 - 源码1688 应用介绍 产中介小程序是一款基于ThinkPHPFastAdmin开发的原生微信小程序&#xff0c;为房地产中介提供房源管理、发布、报备客户、跟踪客户以及营销推广获客等服务的系统。 前端演示&#xff1a; 后台演示&am…

冷数据归档(历史库),成本与性能如何兼得?| OceanBase应用实践

随着数据量的迅猛增长&#xff0c;企业和组织在数据库管理方面遭遇的挑战愈发凸显。数据库性能逐渐下滑、存储成本节节攀升&#xff0c;以及数据运维复杂性的增加&#xff0c;这些挑战使得DBA和开发者在数据管理上面临更大的压力。 为了应对这些挑战&#xff0c;对数据生命周期…

奇异值分解(SVD)

1 奇异值分解(SVD)简介 Beltrami 和 Jordan 被认为是奇异值分解&#xff08;Singular Value Decomposition&#xff0c;SVD&#xff09;的共同开创者&#xff0c;二人于19世纪70年代相继提出了相关理论。奇异值分解主要解决的问题是数据降维。在高维度的数据中&#xff0c;数据…

Tied and Anchored Stereo Attention Network for Cloud Removal in Optical

论文名称 基于固定锚定立体注意力网络的光学遥感图像去云方法代码运行 论文代码 https://github.com/ningjin00/TASANet?tabreadme-ov-file 论文地址 1环境创建 模型环境给了这几个包&#xff0c;如果你自带环境 那就运行代码 提示缺哪个装哪个 python 3.12rasterio 1.3.10…

【AI人工智能】文心智能体 - 你的专属车牌设计师

引言 自AI盛行以来&#xff0c;不断有各种各样的人工智能产品崭露头角。我们逐步跟着不断产生的人工智能来使自己的工作和生活变得更加智能化&#xff01;那么我们是否能够创造一款专属于自己的人工智能产品呢&#xff1f; 文心智能体平台就给我们提供了这样的机会&#xff0c…

数值微分求梯度、计算图求梯度,实现单层线性回归 模型速度差异及损失率比对

文章目录 简述测试结果完整代码 简述 先将前面两篇文章的代码重构一下&#xff0c;抽离共同函数到utils.py。 重构后结构&#xff1a; ComputationGraphLinearNet.py&#xff1a; 使用计算图&#xff08;forward、backward&#xff09;求梯度构建的线性模型&#xff0c;代码…

分库分表的使用场景和中间件

文章目录 一、为什么要分库分表&#xff1f;分库分表的使用场景&#xff1f;二、分库分表常用中间件1、Cobar2、TDDL3、Atlas4、Sharding-jdbc5、Mycat6、总结 一、为什么要分库分表&#xff1f;分库分表的使用场景&#xff1f; 场景1&#xff1a;注册用户就 20 万&#xff0c…

<数据集>集装箱缺陷识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;3793张 标注数量(xml文件个数)&#xff1a;3793 标注数量(txt文件个数)&#xff1a;3793 标注类别数&#xff1a;4 标注类别名称&#xff1a;[DAMAGE - DEFRAME, DENT, DAMAGE - RUST, DAMAGE - HOLE] 序号类别名…

飞睿智能8km无人机WiFi图传模块,高清、稳定、超远距!实时传输新高度

在数字化飞速发展的今天&#xff0c;无人机已经从一个遥不可及的科幻概念&#xff0c;变成了我们日常生活中的得力助手。无论是航拍美景、农业植保&#xff0c;还是紧急救援、物流配送&#xff0c;无人机都展现出了其独特的优势。而在这背后&#xff0c;一个至关重要的技术支撑…

ThinkPHP教程

thinkPHP笔记 01. phpEnv配置安装 主讲老师 - 李炎恢 1. 学习基础 ThinkPHP8.x: 前端基础:HTML5/CSS(必须)、JavaScript(可选、但推荐有);后端基础:PHP基础,版本不限,但不能太老,至少PHP5.4以上语法,TP8是兼容PHP8.x的;数据库基础:MySQL数据库,掌握了常规的SQL…

再谈表的约束

文章目录 自增长唯一键外键 自增长 auto_increment&#xff1a;当对应的字段&#xff0c;不给值&#xff0c;会自动的被系统触发&#xff0c;系统会从当前字段中已经有的最大值1操作&#xff0c;得到一个新的不同的值。通常和主键搭配使用&#xff0c;作为逻辑主键。 自增长的…

面向服务架构(SOA)介绍

在汽车电子电气架构还处于分布式时代时&#xff0c;汽车软件的开发方式主要是采用嵌入式软件进行开发&#xff0c;而随着汽车智能化程度的加深&#xff0c;更加复杂且多样的功能需求让汽车软件在复杂度上再上一层。在整车的自动驾驶方面&#xff0c;由于未来高阶自动驾驶能力的…

《Unity3D网络游戏实战》正确收发数据流

TCP数据流 系统缓冲区 当收到对端数据时&#xff0c;操作系统会将数据存入到Socket的接收缓冲区中 操作系统层面上的缓冲区完全由操作系统操作&#xff0c;程序并不能直接操作它们&#xff0c;只能通过socket.Receive、socket.Send等方法来间接操作。当系统的接收缓冲区为空&…

RCE绕过技巧

目录 EVAL长度限制突破技巧 1.使用反引号 2.file_put_contents写入文件 3.php5.6变长参数usort回调后门 命令长度限制突破技巧 1.拼接文件名 无字母数字的webshell命令执行 1.取反码 2.上传临时文件 EVAL长度限制突破技巧 分析代码&#xff1a;首先传递一个param参数&…

OceanBase V4.3 列存引擎之场景问题汇总

在OceanBase 4.3版本发布后&#xff08;OceanBase社区版 V4.3 免费下载&#xff09;&#xff0c;其新增的列存引擎&#xff0c;及行列混存一体化的能力&#xff0c;可以支持秒级实时分析&#xff0c;引发了用户、开发者及业界人士的广泛讨论。本文选取了这些讨论中较为典型的一…

企业应该如何准备 EcoVadis 审核?

企业准备 EcoVadis 审核可以参考以下步骤&#xff1a; 注册&#xff1a;在网上注册并提供公司的相关信息&#xff0c;包括法律实体名称、国家和地区、企业规模和行业等。如果是受客户邀请参加评估&#xff0c;需按照邀请邮件中的链接进行注册&#xff0c;并确保客户能随时获知评…

安卓默认混淆规则文件的区别

在 Android 项目中&#xff0c;ProGuard 是一个优化和混淆代码的工具。proguard-android-optimize.txt 和 proguard-android.txt 是两个用于配置 ProGuard 的默认规则文件&#xff0c;如图下 它们有以下区别&#xff1a; proguard-android-optimize.txt: 优化&#xff1a;这个配…

Django中事务的基本使用

1. Django事务处理 事务(Transaction): 是一种将多个数据库操作组合成一个单一工作单元的机制. 如果事务中的所有操作都成功完成, 则这些更改将永久保存到数据库中. 如果事务中的某个操作失败, 则整个事务将回滚到事务开始前的状态, 所有的更改都不会被保存到数据库中. 这对于…

系统编程 day10 进程2

进程创建之后&#xff1a; 1.任务-----子进程与父进程干的活差不多 2.父进程创建出子进程之后&#xff0c;子进程做的与父进程完全不同 shell程序-----bash----- 以上为进程运行的过程中&#xff0c;典型的两种应用场景 能够改变子进程的执行效果的函数是exec函数族 l和v&a…