实战项目: 负载均衡

0. 前言

这个项目使用了前后端,实现一个丐版的LeetCode刷题网站,并根据每台主机的实际情况,选择对应的主机,负载均衡的调度

0.1 所用技术与开发环境

所用技术:

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

开发环境: Centos 7 云服务器, vscode, Mysql Workbench

0.2 建立目录及文件

0.3 项目宏观结构

  • 具体的功能类似 leetcode 的题目列表+在线编程功能

1. compile 服务设计

  • 由于compiler这个模块管理的是编译与运行,则可以先直接就创建所需要的文件

1.0 书写makefile文件

  • 随着后续代码的跟进,并不断引入第三方库,这里还会新增编译选项

1.1 compiler_server

1.1.0 编译功能(compiler.hpp)

  • 在编译的时候,无非存在**2种情况,**a)要么通过,b)要么不通过
  • 要确定编译通过:
    只需要确定是否生成对应的.exe文件
  • 要当编译出错的时候(stderr):
    需要将出错信息,重定向到一个临时文件中,保存编译出错的结果
    还需要调用fork();子进程完成编译工作
    父进程继续执行

  • 由于需要频繁的文件名转换,所以在comm模块中,新建util.hpp文件并将文件名转换的函数放在一起

  • 还有后面判断编译成功生成的可执行程序,虽然可以直接暴力的打开文件判断是否存在,但这里使用stat函数会好一些
  • stat结构体会记录文件的各种信息

  • 注意:程序替换是不会影响进程的文件符描述符表的

1.1.1 日志模块(log.hpp)

由于一般日志都会带上时间, 这里还需要实现一个得到当前时间的函数,则我又在util.hpp把得到时间函数的类封装成了一个类



由于会频繁的调用日志进行打印信息,也为了更简便的调用,我进行了以下处理

  • 如果在宏定义中使用#,那么这个宏就被称为带有字符串化操作的宏。这种宏可以将其参数转换成字符串常量,并在预处理阶段进行替换。

由于引入了日志,则就可以把之前所有的输出信息,换成日志输出

1.1.2 测试编译模块

  • Compile的参数是文件名,它内部会自动拼接
  • 我们还需要再./temp中创建一个code.cpp文件

  • 上面我的代码有一个错误,在编译成功的时候,并没有return,导致LOG日志打印有问题

  • 要是我们的源文件有问题,错误信息就会重定向到 文件.compile_error中
  • 在测试的时候,还需要把 文件.exe 文件.compile_error文件删除,就是上次生成的文件

1.1.3 运行功能(runner.hpp)

程序运行: 1)代码跑完,结果正确 2)代码跑完,结果不正确, 3)代码没跑完,异常了
程序结果是否正确,是由oj_server中的测试用例决定的,则run模块只考虑是否正确运行完毕

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>// fork接口需要

#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner
{

    using namespace ns_util;
    using namespace ns_log;
    class Runner
    {
    public:
        Runner(){}
        ~Runner(){}
        static int Run(const std::string &file_name){
            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);
            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);
                LOG(INFO) << "123";// 是不是有问题啊
                // 这个程序替换等价于 ./tmp/code.exe ./tmp/code.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) << "运行完毕,infor: " << (status & 0x7f) << "\n";
                return status & 0x7f;
            }
        }
    };
}
  • 返回值 > 0: 程序异常了,退出时收到了信号,返回值就是对应的信号编号
    **返回值 == 0: 正常运行完毕的,**结果保存到了对应的临时文件中
    返回值 < 0: 内部错误

  • run.hpp也是一样的,把自己的各种输出信息,输出到一个临时文件中

  • 要判断一个程序是否异常,只需要看它是否收到了异常信号


解释waitpid第2个输出型参数

  • status并不是按照整数来整体使用的,而是按照比特位的方式,将32个比特位进行划分,只需要学习低16位
  • 这也是上面为什么会写成status & 0x7F的原因

6个程序替换的系统接口,具体使用那个看实际情况

  • 没有p就需要带路径
  • 有l,就是列表式传命令
  • 有v就是数组式传命令
  • 有e就需要传自己设置的环境变量

1.1.4 测试运行模块

  • 虽然运行模块已经能正常运行了,但是万一code.cpp是恶意程序了,比如死循环,不停消耗CPU资源 , 所以还需要进一步的资源约束

1.1.5 添加资源限制(setrlimit)

  • 资源不足,导致OS终止进程,是通过信号终止的

  • 为了方便上层调用,我直接在Run函数中增加了cpu_limit和mem_limit形参

这个项目走到这里就需要编写compile_run.hpp,将编译和运行的逻辑连接在一起,且code.cpp需要被处理的源文件, 不应该是我们自己添加的,而是需要再客户端中导入

1.1.6 编译 && 运行功能 (compile_run.hpp)

这个模块要做的是:
**a)**适配用户请求,引入json定制通信协议字段
**b)**形成唯一文件名
**c)**正确调用compile and run

复制代码
在centos中安装: sudo yum install json-c-devel

头文件 #include <jsoncpp/json/json.h>
  • 注意: 在编译引入了json的文件,需要加上-ljsoncpp

  • 虽然这个code就是文件名了,但client可能会提交大量的代码,所以内部就会需要形成唯一的文件名(待完善)
  • 还有很多个出错问题怎么解决(待完善)

complie_run.hpp

cpp 复制代码
#pragma once

#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <jsoncpp/json/json.h>

namespace ns_compile_and_run
{
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_compiler;
    using namespace ns_runner;
    
    // 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){
        // step1: 反序列化过程
        Json::Value in_value;
        Json::Reader reader;
        // 把in_json中的数据写到in_value中
        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();
        int mem_limit = in_value["mem_limit"].asInt();

        Json::Value out_value;
        int status_code = 0;
        int run_result = 0;
        std::string file_name;// 唯一文件名
        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){
            // runnem模块内部错误
            status_code = -2;// 未知错误
        }
        else if(run_result > 0){
            // 程序运行崩溃
            status_code = run_result;// 这里的run_result是信号
        }
        else{
            // 运行成功
            status_code = 0;
        }
    END:
        out_value["status"] = status_code;
        out_value["reason"] = CodeToDest(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::Stdout(file_name),&_stderr,true);
            out_value["stdout"] = _stdout;
        }

        // step2: 序列化
        Json::StyledWriter writer;
        *out_json = writer.write(out_value);
        

    }
    

}

1.1.7 基于compile_run.hpp对util.hpp的补充


  • 注意引入流时需要引入头文件: #include <fstream>
  • getline:不保存行分割符,有些时候需要保留\n,getline内部重载了强制类型转化

1.1.8 测试编译运行模块

cpp 复制代码
#include "compile_run.hpp"
using namespace ns_compile_and_run;

int main()
{
    std::string in_json;
    Json::Value in_value;
    // R"()", raw string
    in_value["code"] = R"(#include<iostream>
    int main(){
    std::cout << "你可以看见我了" << std::endl;
    return 0;
    })";
    in_value["input"] = "";
    in_value["cpu_limit"] = 1;
    in_value["mem_limit"] = 10240 * 3;
    Json::FastWriter writer;
    in_json = writer.write(in_value);
    // std::cout << in_json << std::endl;
    // 这个是将来给客户端返回的json串
    std::string out_json;
    CompileAndRun::Start(in_json, &out_json);
    std::cout << out_json << std::endl;
    return 0;
}
  • 实际上这里的代码应该是client自动提交给我们的,我们直接使用第三方库就行了
  • 待优化: 可以把临时生成的这些文件都清理掉,

1.1.9 清理临时文件

  • 这个函数直接放在compile_server.cc中的start函数的最后,清理临时文件

1.1.10 引入cpp-httplib 网络库

下载地址: cpp-httplib: C++ http 网络库 - Gitee.com

  • 这个就是别人写好的网络库,我们直接使用就行了

1.1.11 更新gcc

安装scl : sudo yum install centos-release-scl scl-utils-build
安装新版本gcc: sudo yum install - y devtoolset - 9 - gcc devtoolset - 9 - gcc - c ++

  • 把 scl enable devtoolset-9 bash 放在 ~/.bash_profile中
  • 想每次登陆的时候,都是较新的gcc

如果不更新在使用cpp-httplib时**可能会报错,**用老的编译器,要么编译不通过,要么直接运行报错

1.1.12 测试cpp-httplib网络库

  • 可能会出现服务器的公网ip无法访问的问题,可以试试把防火墙关闭,并打开端口

1.1.13 将compiler_server打包成网络服务

compiler_server.cc

cpp 复制代码
#include "compile_run.hpp"
#include "../comm/httplib.h"// 引入

using namespace ns_compile_and_run;
using namespace httplib;// 引入

void Usage(std::string proc){
    std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}

//./copile_server port
int main(int argc,char *argv[])
{
    if(argc != 2){
        Usage(argv[0]);
        return 1;
    }
    Server svr;
    svr.Post("/compile_and_run",[](const Request&req,Response & resp){
        // 用户请求的服务正文是我们想要的json string
        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=uft-8");
        }
    });
    svr.listen("0.0.0.0",atoi(argv[1]));
    return 0;
}
  • 由于我这里没有写客户端代码,则这里暂时不好测试,不过可以借助第三方工具进行测试

2. oj_server服务设计

本质: 建立一个小型网站

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

2.1 书写makefile文件

  • 随着后续代码的跟进,并不断引入第三方库,这里还会新增编译选项

2.2 服务路由功能(oj_server.cc)

  • 为用户实现的路由功能就3个a. 获取所有的题目列表 b.根据题目编号,获取题目内容 c.判断用户提交的代码

2.3 MVC 结构的oj 服务设计(M)

Model , 通常是和数据交互的模块 ,比如,对题库进行增删改查(文件版, MySQL )

2.3.1 安装boost库 && 字符切分功能

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

  • 第一个参数为缓冲区,第二个参数为被分割的字符串
  • 第三个参数为分割符,第四个参数为是否压缩
    • 要压缩: 当sep = "空格"时,sepsepsep -> 空格
    • 不压缩: 当sep = "空格"时,sepsepsep -> 空格空格空格

2.3.2 数据结构

header.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>

using namespace std;

class Solution{
    public:
        bool isPalindrome(int x)
        {
            //将你的代码写在下面
            
            return true;
        }
};

tail.cpp

cpp 复制代码
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif

// 这里先把测试用例 暴露出来
void Test1()
{
    // 通过定义临时对象,来完成方法的调用
    bool ret = Solution().isPalindrome(121);
    if(ret){
        std::cout << "通过用例1, 测试121通过 ... OK!" << std::endl;
    }
    else{
        std::cout << "没有通过用例1, 测试的值是: 121"  << std::endl;
    }
}

void Test2()
{
    // 通过定义临时对象,来完成方法的调用
    bool ret = Solution().isPalindrome(-10);
    if(!ret){
        std::cout << "通过用例2, 测试-10通过 ... OK!" << std::endl;
    }
    else{
        std::cout << "没有通过用例2, 测试的值是: -10"  << std::endl;
    }
}

int main()
{
    Test1();
    Test2();

    return 0;
}
  • des.txt表示题目信息
  • header.cpp表示预设代码
  • tail.cpp表示测试用例

  • 真正代码 = 用户在head.cpp中的代码 + header.cpp + tail.cpp 并去到COMPILER_ONLINE
  • 这个条件编译只是为了编写tail.cpp时不报错

2.3.3 model功能(oj_model.cpp)

数据交互 && 提供接口

cpp 复制代码
#pragma once
// 文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include <unordered_map>
#include <cassert>
#include <vector>

namespace ns_model
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;

    struct Question{
        string number;// 题目编号,唯一
        string tile;// 题目标题
        string star;// 难度: 简单 中等 困难
        
        int cpu_limit;// 题目的时间复杂度(S)
        int mem_limit;// 题目的空间复杂度(KB)
        
        string desc;// 题目描述
        string header; // 题目预设给用户在线编辑器的代码
        string tail;// 题目测试用例,需要和header拼接
    };    

    const string questions_list = "./question/quetions.list";
    const string questions_path = "./question";

    class Model
    {
    public:
        Model(){
            // 加载所有题目:底层是用hash表映射的
            assert(LoadQuestionList(questions_list));
        }
        ~Model(){
            ;
        }

        // 获取所有题目,这里的out是输出型参数
        bool GetAllQuestions(vector<Question>*out){
            if(questions.size() == 0){
                LOG(ERROR) << "用户获取题库失败" << "\n";
                return false;
            }
            for(const auto&q: questions){
                out->push_back(q.second);
            }
            return true;
        }

        // 获取指定题目,这里的q是输出型参数
        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;
        }

        // 加载配置文件: questions/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," ");// 被分割的字符串 缓冲区 分割符
                // eg: 1 判断回文数 简单 1 30000
                if(tokens.size()!=5){
                    LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n";
                    continue;
                }
                Question q;
                q.number = tokens[0];
                q.tile = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = atoi(tokens[3].c_str());
                q.mem_limit = atoi(tokens[4].c_str());

                string path = questions_list;
                path += q.number;
                path += "/";

                // 第三个参数代表 是否加上 \n
                FileUtil::ReadFile(path+"desc.txt",&(q.desc),true);
                FileUtil::ReadFile(path+"header.cpp",&(q.header),true);
                FileUtil::ReadFile(path+"tail.txt",&(q.tail),true);
               
                questions.insert({q.number,q});// 录题成功
            }
            LOG(INFO) << "加载题库...成功" << "\n";
            in.close();
        }
    private:
        // 题号 : 题目细节
        unordered_map<string,Question> questions;
    };
} 

2.4 MVC 结构的oj 服务设计(C)

2.4.1 负载均衡模块

cpp 复制代码
namespace ns_control
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_model;
    using namespace ns_view;
    using namespace httplib;

    // 提供服务的主机
    class Machine
    {
    public:
        std::string ip;  //编译服务的ip
        int port;        //编译服务的port
        uint64_t load;   //编译服务的负载
        std::mutex *mtx; // 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 LoadBlance
    {
    private:
        // 可以给我们提供编译服务的所有的主机
        // 每一台主机都有自己的下标,充当当前主机的id
        std::vector<Machine> machines;
        // 所有在线的主机id
        std::vector<int> online;
        // 所有离线的主机id
        std::vector<int> offline;
        // 保证LoadBlance它的数据安全
        std::mutex mtx;

    public:
        LoadBlance()
        {
            assert(LoadConf(service_machine));
            LOG(INFO) << "加载 " << service_machine << " 成功"
                      << "\n";
        }
        ~LoadBlance()
        {
        }

    public:
        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))
            {
                std::vector<std::string> tokens;
                StringUtil::SplitString(line, &tokens, ":");
                if (tokens.size() != 2)
                {
                    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();
            // 负载均衡的算法
            // 1. 随机数+hash
            // 2. 轮询+hash
            int 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 curr_load = machines[online[i]].Load();
                if (min_load > curr_load)
                {
                    min_load = curr_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);
                    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";
        }
        //for test
        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();
        }
    };
}

2.4.1 control功能(oj_control.hpp)

逻辑控制模块

cpp 复制代码
// 这是我们的核心业务逻辑的控制器
    class Control
    {
    private:
        Model model_; //提供后台数据
        View view_;   //提供html渲染功能
        LoadBlance load_blance_; //核心负载均衡器
    public:
        Control()
        {
        }
        ~Control()
        {
        }

    public:
        void RecoveryMachine()
        {
            load_blance_.OnlineMachine();
        }
        //根据题目数据构建网页
        // 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());
                });
                // 获取题目信息成功,将所有的题目数据构建成网页
                // ...
            }
            else
            {
                *html = "获取题目失败, 形成题目列表失败";
                ret = false;
            }
            return ret;
        }
        bool Question(const string &number, string *html)
        {
            bool ret = true;
            struct Question q;
            if (model_.GetOneQuestion(number, &q))
            {
                // 获取指定题目信息成功,将所有的题目数据构建成网页
                // ....
            }
            else
            {
                *html = "指定题目: " + number + " 不存在!";
                ret = false;
            }
            return ret;
        }
        // code: #include...
        // input: ""
        void Judge(const std::string &number, const std::string in_json, std::string *out_json)
        {
            
        }
    };
  • control模块中的判题功能,我打算最后设计

2.5 MVC 结构的oj 服务设计(V)

2.4 安装与测试ctemplate(网页渲染)

渲染本质就是key-value之间的替换


test.cpp

cpp 复制代码
#include <iostream>
#include <string>
#include <ctemplate/template.h>
int main()
{
    std::string html = "./test.html";
    std::string html_info = "测试ctemplate渲染";

    // 建立ctemplate参数目录结构
    ctemplate::TemplateDictionary root("test"); // unordered_map<string,string> test;
    
    // 向结构中添加你要替换的数据,kv的
    root.SetValue("info", html_info); // test.insert({key, value});
    
    // 获取被渲染对象
    // DO_NOT_STRIP:保持html网页原貌
    ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html,ctemplate::DO_NOT_STRIP); 
    
    // 开始渲染,返回新的网页结果到out_html
    std::string out_html;
    tpl->Expand(&out_html, &root);
    
    std::cout << "渲染的带参html是:" << std::endl;
    std::cout << out_html << std::endl;
    
    return 0;
}

test.html

cpp 复制代码
<!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>Document</title>
</head>

<body>
    <p>{{info}}</p>
    <p>{{info}}</p>
    <p>{{info}}</p>
    <p>{{info}}</p>
</body>

</html>

错误原因: error while loading shared libraries: libmpc.so.3: cannot open shared object file

cpp 复制代码
export LD\_LIBRARY\_PATH=$LD\_LIBRARY\_PATH:/usr/local/lib
  • 在命令行上输入 上面这段命令,注:只在当前会话中有效

cat /etc/ld.so.conf

include ld.so.conf.d/*.conf

echo "/usr/local/lib" >> /etc/ld.so.conf

ldconfig

2.5.2 渲染功能(oj_view.hpp)

cpp 复制代码
#pragma once
#include <iostream>
#include <ctemplate/template.h>
#include "./oj_model.hpp"

namespace ns_view
{
    using namespace ns_model;

    const std::string template_path = "./template_html/";

    class View
    {
    public:
        View(){}
        ~View(){}

        // 渲染所有题目
        void ALLExpandHtml(const vector<struct Question>&question,std::string *html){
            // 题目编号 题目标题 题难度
            // 推荐表格实现
            // 1.形成路径
            string src_html = template_path + "all_quetions.html";

            // 2.形成数字典
            ctemplate::TemplateDictionary root("all_question");
            for(const auto& q: question){
                ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
                sub->SetValue("number",q.number);
                sub->SetValue("title",q.title);
                sub->SetValue("star",q.star);
            }

            // 3. 获取被渲染的html
            ctemplate::Template*tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);

            // 4.开始完成渲染功能
            tpl->Expand(html,&root);
        }

        // 渲染一道题目
        void OneExpandHtml(const struct Question &q,string *html){
            // 1.形成路径
            std::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.获取被渲染的html
            ctemplate::Template*tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
            // 4.开始完成渲染功能
            tpl->Expand(html,&root);
        }
    };
}

2.6 联动MVC模块并测试


oj_server.cc

cpp 复制代码
#include <iostream>
#include "../comm/httplib.h"// 引入
#include "oj_control.hpp"

using namespace httplib;// 引入
using namespace ns_control;

int main()
{
    // 用户请求的服务器路由功能
    Server svr;
    Control ctrl;
    // 获取所有的题目列表
    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");
    });

    // 根据题目编号,获取题目内容
    // \d+ 是正则表达式的特殊符合
    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.每道题c测试用例,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");
    });
    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0",8080);
    return 0;
}
  • 这里的前端都是提前做好了的,我们可以不关心前端;
  • control功能还有个判题功能没有实现

2.7 完善oj_control.hpp中的判题功能

cpp 复制代码
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);
            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;
            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_blance_.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"))
                {
                    // 5. 将结果赋值给out_json
                    if(res->status == 200)
                    {
                        *out_json = res->body;
                        m->DecLoad();
                        LOG(INFO) << "请求编译和运行服务成功..." << "\n";
                        break;
                    }
                    m->DecLoad();
                }
                else
                {
                    //请求失败
                    LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线"<< "\n";
                    load_blance_.OfflineMachine(id);
                    load_blance_.ShowMachines(); //仅仅是为了用来调试
                }
            }

2.8 测试oj_server服务

  • 在编译时需要加上-D COMPILER_ONLINE条件编译,
  • 设计到前端网页,下面会有提及

2.9 一个BUG

  • 把tail.txt改成tail.cpp,不然后面无法进行代码拼接

3. 前端页面设计(了解)

3.1 index.html

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

        html,
        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>

3.2 all_questions.html

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

        html,
        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', 'Lucida Sans 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>@lyc</h4>
        </div>
    </div>

</body>

</html>

3.3 one_questions.html(ACE插件&&JQuery&&ajax)

cpp 复制代码
<!-- 引入ACE插件 -->
    <!-- 引入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>
  • ACE插件是一个 编写代码的编译框
  • 收集当前页面的有关数据 , a. 题号 a. 代码 , 我们采用 JQuery 来进行获取 html 中的内容
  • 构建json,并通过 ajax向后台 发起基于http的json请求

全部代码

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插件 -->
    <!-- 引入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;
        }

        html,
        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.html
        editor.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(){
            // alert("嘿嘿!");
            // 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,我给你的是什么格式
                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
                });
                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
                    })

                    stdout_lable.appendTo(result_div);
                    stderr_lable.appendTo(result_div);
                }
                else{
                    // 编译运行出错,do nothing
                }
            }
        }
    </script>
</body>

</html>

3.4 相关测试


4. MySQL版题目设计

4.1 注册用户 && 赋予权限

  • create user 'oj_client'@'localhost' identified by '123456';
  • create database oj;
  • grant select on oj.* to 'oj_client'@'localhost';
  • select user,Host from user;

4.2 下载第三方工具-workbench

  • 下载下来之后,就不断的下一步,下一步就行了

4.3 录题到mysql中

cpp 复制代码
use oj;
drop table if exists oj_table;

create table if not exists oj_table(
	_number varchar(200)  comment '题目编号',
    _titie varchar(200) comment '题目标题',
    _start varchar(200) comment '题目简单中等困难',
    _desc varchar(2000) comment '题目描述',
    _header varchar(2000) comment '题目预设',
    _tail varchar(2000) comment '题目测试用例',
    _cpu_limit int comment '时间要求',
    _mem_limt int comment '空间要求'
);

insert into oj_table values(
	1,
    '判断回文数',
    '简单',
    '判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

	示例 1:

	输入: 121
	输出: true
	示例 2:

	输入: -121
	输出: false
	解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
	示例 3:

	输入: 10
	输出: false
	解释: 从右向左读, 为 01 。因此它不是一个回文数。
	进阶:

	你能不将整数转为字符串来解决这个问题吗?',
    '#include <iostream>
	#include <string>
	#include <vector>
	#include <map>
	#include <algorithm>

	using namespace std;

	class Solution{
		public:
			bool isPalindrome(int x)
			{
				//将你的代码写在下面
				
				return true;
			}
	};',
    '#ifndef COMPILER_ONLINE
	#include "header.cpp"
	#endif


	void Test1()
	{
		// 通过定义临时对象,来完成方法的调用
		bool ret = Solution().isPalindrome(121);
		if(ret){
			std::cout << "通过用例1, 测试121通过 ... OK!" << std::endl;
		}
		else{
			std::cout << "没有通过用例1, 测试的值是: 121"  << std::endl;
		}
	}

	void Test2()
	{
		// 通过定义临时对象,来完成方法的调用
		bool ret = Solution().isPalindrome(-10);
		if(!ret){
			std::cout << "通过用例2, 测试-10通过 ... OK!" << std::endl;
		}
		else{
			std::cout << "没有通过用例2, 测试的值是: -10"  << std::endl;
		}
	}

	int main()
	{
		Test1();
		Test2();

		return 0;
	}',
    1,
    30000
);


select * from oj_table;
  • 这里我只录入了一道题为了测试

4.4 下载并引入mysql库文件

MySQL :: Download MySQL Community Server

要使用C/C++连接MySQL,需要使用MySQL官网提供的库

下载完毕后需要将其上传到云服务器,这里将下载的库文件存放在下面的目录:

然后使用tar命令将压缩包解压到当前目录下:

xz -d mysql-8.0.37-linux-glibc2.28-i686.tar.xz

tar xvf mysql-8.0.37-linux-glibc2.28-i686.tar

进入解压后的目录当中,可以看到有一个include子目录和一个lib子目录,其中,include目录下存放的一堆头文件。而lib64目录下存放的就是动静态库。


然后在我们的项目中建立软连接

4.5 一个BUG

  • 如果你当时下载myql把mysql-devel也下载了,不需要进行上面步骤

  • 这种引入第三方库的操作,可能会因为版本不兼容,而导致出错
    skipping incompatible ./lib/libmysqlclient.so when searching for -lmysqlclient

  • 建议直接安装: yum -y install mysql-devel

4.5 重新设计oj_model

因为oj_model模块是管理数据,提供接口的模块,所以要把这个项目变成mysql就需要重新设计

cpp 复制代码
#pragma once
// 文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include <unordered_map>
#include <cassert>
#include <vector>

#include "./include/mysql.h"

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 std::string oj_questions = "oj_table";
    const std::string host = "127.0.0.1";
    const std::string user = "oj_client";
    const std::string passwd = "123456";
    const std::string db = "oj";
    const int port = 3306;

    class Model
    {
    public:
        Model(){
        }
        ~Model(){
            ;
        }
        bool QueryMysql(const std::string &sql,vector<Question>*out){
            // 这里的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);// 本质就是一个2级指针

            // 分析结果
            int rows = mysql_num_rows(res);// 获取行的数量
            int cols = mysql_num_fields(res);// 获取列的数量

            Question q;
            for(int i = 0;i < rows;i++){
                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连接
            mysql_close(my);

            return true;
        }
        // 获取所有题目,这里的out是输出型参数
        bool GetAllQuestions(vector<Question>*out){
            std::string sql = "select * from ";
            sql += oj_questions;
            return QueryMysql(sql,out);
        }

        // 获取指定题目,这里的q是输出型参数
        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)){
                if(result.size() == 1){
                    *q = result[0];
                    res = true;
                }
           }

           return res;
        }

        
    private:
        // 题号 : 题目细节
        unordered_map<string,Question> questions;
    };
} 
  • mysql_init: 创建mysql句柄

  • mysql_real_connect: 创建mysql连接

  • mysql_query: 发起mysql请求

  • mysql_close: 关闭mysql连接

4.6 相关测试

  • 编译期间告诉编译器头文件和库文件在哪里 -I指明搜索的头文件,-L指明搜索的lib
  • 并加上**-lmysqlclient**

5. 扩展

  • 功能上更完善一下,判断一道题目正确之后,自动下一道题目
    • 基于注册和登陆的录题功能
      .....

6. 完整项目链接

projects/负载均衡/OnlineJudge at main · 1LYC/projects · GitHub

相关推荐
Fcy6481 小时前
Linux下 进程(一)(冯诺依曼体系、操作系统、进程基本概念与基本操作)
linux·运维·服务器·进程
袁袁袁袁满1 小时前
Linux怎么查看最新下载的文件
linux·运维·服务器
代码游侠2 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
主机哥哥2 小时前
阿里云OpenClaw部署全攻略,五种方案助你快速部署!
服务器·阿里云·负载均衡
Harvey9032 小时前
通过 Helm 部署 Nginx 应用的完整标准化步骤
linux·运维·nginx·k8s
珠海西格电力科技3 小时前
微电网能量平衡理论的实现条件在不同场景下有哪些差异?
运维·服务器·网络·人工智能·云计算·智慧城市
释怀不想释怀3 小时前
Linux环境变量
linux·运维·服务器
zzzsde3 小时前
【Linux】进程(4):进程优先级&&调度队列
linux·运维·服务器
聆风吟º5 小时前
CANN开源项目实战指南:使用oam-tools构建自动化故障诊断与运维可观测性体系
运维·开源·自动化·cann
NPE~5 小时前
自动化工具Drissonpage 保姆级教程(含xpath语法)
运维·后端·爬虫·自动化·网络爬虫·xpath·浏览器自动化