基于负载均衡的在线OJ实战项目

前言:

该篇讲述了实现基于负载均衡式的在线oj,即类似在线编程做题网站一样,文章尽可能详细讲述细节即实现,便于大家了解学习。

文章将采用单篇不分段形式(ps:切着麻烦),附图文,附代码,代码部署在云服务器上

技术栈

  • C++ STL标准库

  • Boost 标准库

  • cpp-httpib 开源库

  • ctemplate 第三方开源前端网页渲染库

  • jsoncpp 第三方开源序列化、反序列化库

  • 负载均衡的设计

  • 多进程、多线程

  • MYSQL C connect

  • Ace前端在线编辑器

  • html/cdd/js/jquery/ajax

开发环境

  • vscode
  • mysql workbench
  • Centos 7云服务器

宏观结构

  • comm:公共模块
  • compile_sever:编译运行模块
  • oj_server:获取题目,负载均衡等

基于负载均衡的在线OJ实战项目_第1张图片

 

 


项目演示: 


https://blog.csdn.net/Obto_/article/details/132558642?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132558642%22%2C%22source%22%3A%22Obto_%22%7Dhttps://blog.csdn.net/Obto_/article/details/132558642?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132558642%22%2C%22source%22%3A%22Obto_%22%7D


 



项目设计 -- 编译服务

工具类的准备:

供程序中各个部分调用的方法类:

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
namespace ns_util
{
    class TimeUtil
    {
    public:
        static std::string GetTimeStamp()
        {
            // 获取时间戳 gettimeofday
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec);
        }
        // 获得毫秒时间戳
        static std::string GetTimeMs()
        {
            struct timeval _time;
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
        }
    };

    const std::string temp_path = "./temp/";
    class PathUtil
    {
    public:
        static std::string AddSuffix(const std::string &file_name, const std::string &suffix)
        {
            std::string path_name = temp_path;
            path_name += file_name;
            path_name += suffix;
            return path_name;
        }
        // 构建源文件路径+后缀的完整文件名
        // 1234 -> ./temp/1234.cpp
        static std::string Src(const std::string &file_name)
        {
            return AddSuffix(file_name, ".cpp");
        }
        // 构建可执行程序的完整路径 + 后缀名
        static std::string Exe(const std::string &file_name)
        {
            return AddSuffix(file_name, ".exe");
        }
        static std::string CompilerError(const std::string &file_name)
        {
            return AddSuffix(file_name, ".compile_stderr");
        }
        //-------------------------------------------------------------------
        // 构建该程序对应的标准错误完整的路径+后缀名
        static std::string Stderr(const std::string &file_name)
        {
            return AddSuffix(file_name, ".stderr");
        }
        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");
        }
    };

    class FileUtil
    {
    public:
        static bool IsFileExists(const std::string &path_name)
        {
            struct stat st;
            if (stat(path_name.c_str(), &st) == 0)
            {
                // 获取属性成功,文件已经存在
                return true;
            }
            return false;
        }
        static std::string UniqFileName()
        {
            static std::atomic_uint id(0);
            id++;
            // 毫秒时间戳
            std::string ms = TimeUtil::GetTimeMs();
            std::string uniq_id = std::to_string(id);
            return ms + "_" + uniq_id;
        }
        static bool WriteFile(const std::string &target, const std::string &content)
        {
            // waiting
            std::ofstream out(target);
            if (!out.is_open())
            {
                return false;
            }
            out.write(content.c_str(), content.size());
            out.close();
            return true;
        }
        static bool ReadFile(const std::string &target, std::string *content, bool keep = false)
        {
            (*content).clear();
            std::ifstream in(target);
            if (!in.is_open())
            {
                return false;
            }
            std::string line;
            // getline是按行保存的,不保存行分隔符,自动去掉\n
            // 但是有些时候,需要保留行分隔符
            // getline内部重载了强制类型转化
            while (std::getline(in, line))
            {
                (*content) += line;
                (*content) += (keep ? "\n" : "");
            }
            in.close();
            return true;
        }
    };

    class StringUtil
    {
        public:
        /*
        str:输入型,目标要切分的字符串
        target:输出型,保存切分完毕的结果
        sep:指定的分隔符
        */
        static void SplitString(const std::string &str,std::vector *target,std::string sep)
        {
            boost::split(*target,str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
            //boost split
        }
    };

}
  • PathUtil:路径工具
    • 形成exe完整路径
    • 形成cpp完整路径
    • 形成compile_stderr完整路径
    • 形成stderr完整路径
    • 形成stdin完整路径
    • 完整路径指的是当前代码在本地上的保存路径:即输入 1234 要形成 ./temp/1234.cpp等这里的相对路径形成依靠PathUtil工具
  • TimeUtil:时间工具
    • 获取时间戳 get time of day
    • 获得好面时间戳,用于形成文件唯一标识(名字)
  • FileUtil:文件工具
    • IsFileExits:判断某文件是否存在
    • UniqFileName:形成文件唯一名字
    • WriteFile:向指定文件写入指定字符串
    • ReadFile:读取某文件的内容
  • StringUtil:字符串工具
    • 使用boost库中的切分字符串Split()

 

compiler编译服务设计 :

目的:能够编译并运行代码,得到格式化的相关结果

基于负载均衡的在线OJ实战项目_第2张图片

 

#pragma once

#include 
#include
#include
#include
#include
#include
#include"../comm/util.hpp"
#include"../comm/log.hpp"
//只负责进行代码的编译
namespace ns_compiler{
    //引入路径拼接功能
    using namespace ns_util;
    using namespace ns_log;
    class Compiler{
        public:
        Compiler()
        {}
        ~Compiler()
        {}
        //返回值是编译成功TRUE;else FALSE
        //输入参数是编译的文件名
        //file_name : 1234
        //1234 -> ./temp/1234.cpp
        //1234 -> ./temp/1234.exe
        //1234 -> ./temp/1234.stderr
        static bool Compile(const std::string &file_name)
        {
            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(ERROR) << "内部错误,创建子进程失败"<<"\n";
                return false;
            }
            else if(pid == 0)//子进程
            {
                umask(0);
                int _stderr = open(PathUtil::CompilerError(file_name).c_str(),O_CREAT | O_WRONLY,0644);
                if(_stderr < 0){
                    LOG(WARNING)<<"没有成功行成stderr文件"<<"\n";
                    exit(1);
                }
                //重定向标准错误到_stderr
                dup2(_stderr,2);
                //程序替婚,并不影响进程的文件描述符表
                //子进程:调用编译器
                execlp("g++","g++","-o",PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(),"-std=c++11","-D","COMPILER_ONLINE",nullptr);
                LOG(ERROR) <<"启动编译器g++失败,可能是参数错误"<<"\n";
                exit(2);
            }
            else//父进程
            {
                waitpid(pid,nullptr,0);
                //编译是否成功,就看有没有形成对应的可执行程序
                if(FileUtil::IsFileExists(PathUtil::Exe(file_name).c_str())){
                    LOG(INFO) <

compiler编译服务只管编译传过来的代码,其他一律不管,它只关心程序是否能够编译过

LOG日志的添加:

#pragma once
#include
#include
#include"util.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 += "]";

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

runner运行功能设计:

#pragma once

#include 
#include
#include
#include
#include 
#include 
#include 
#include 
#include
#include
#include
#include 
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner
{
    using namespace ns_log;
    using namespace ns_util;
    class Runner
    {
    public:
        Runner() {}
        ~Runner() {}

    public:
        //提供设置进程占用资源大小的接口
        static void SerProcLimit(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;//转化成kb
            setrlimit(RLIMIT_AS,&mem_rlimit);
        }
        // 指明文件名即可,不需要带路径和后缀
        /*
        返回值如果是大于 0 :程序异常了,退出时收到了信号,返回值就是对应的信号
        返回值 == 0 就是正常运行完毕,结果是什么保存到了临时文件中,我不清楚
        返回值 < 0 属于内部错误
        cpu_limit:该程序运行的时候,可以使用的最大cpu的资源上限
        mem_limit:该程序运行的时候,可以使用的最大内存大小KB
        */
        static int Run(const std::string &file_name,int cpu_limit,int mem_limit)
        {
            /*程序运行:
            1.代码跑完结果争取
            2.代码跑完结果不正确
            3.代码没跑完,异常了
            run不需要考虑运行完后正确与否,只管跑

            首先我们必须知道可执行程序是谁?
            标准输入:不处理
            标准输入:程序运行完成,输出结果是什么
            标准错误:运行时错误信息
            */
            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);

                SerProcLimit(cpu_limit,mem_limit);
                execl(_execute.c_str()/*我要执行谁*/,_execute.c_str()/*我想在命令航商如何执行*/,nullptr);
                exit(1);
            }
            else
            {
                
                int status = 0;
                waitpid(pid,&status,0);
                //程序运行异常,一定是因为收到了信号
                LOG(INFO)<<"运行完毕,info:"<<(status & 0x7F)<<"\n";
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                return status&0x7F;
            }
        }
    };
}

compile_run:编译并运行功能:

#pragma once
#include "compiler.hpp"
#include
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include 
#include 
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 _compiler_error = PathUtil::CompilerError(file_name);
            if(FileUtil::IsFileExists(_compiler_error))unlink(_compiler_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());
        }
        static std::string CodeToDesc(int code, std::string file_name) // code >0 <0 ==0
        {
            std::string desc;
            switch (code)
            {
            case 0:
                desc = "编译运行成功";
                break;
            case -1:
                desc = "用户提交的代码是空";
                break;
            case -2:
                desc = "未知错误";
                break;
            case -3:
                // desc = "编译发生报错";
                FileUtil::ReadFile(PathUtil::Stderr(file_name), &desc, true);
                break;
            case -4:
                break;
            case SIGABRT:
                desc = "内存超过范围";
                break;
            case SIGXCPU:
                desc = "CPU信号超时";
                break;
            case SIGFPE:
                desc = "除零错误,浮点数溢出";
                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)
        {
            LOG(INFO)<<"启动compile_and_run"<<"\n";
            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();
            int men_limit = in_value["mem_limit"].asInt();

            int status_code = 0;
            Json::Value out_value;
            int run_result = 0;
            std::string file_name; // 需要内部形成的唯一文件名

            if (code.size() == 0)
            {
                // 说明用户一行代码都没提交
                status_code = -1;
                goto END;
            }

            // 形成的文件名只具有唯一性,没有目录没有后缀
            // 毫秒计时间戳+原子性递增的唯一值:来保证唯一性
            file_name = FileUtil::UniqFileName(); // 形成唯一文件名字
            LOG(DEBUG)<<"调用UniqFileName()形成唯一名字"< 0)
            {
                status_code = run_result;
            }
            else
            {
                // 运行成功
                status_code = 0;
            }
        END:
            std::cout<<"到达end语句"<

compile_run:它的功能是接收远端传进来的json包,并反序列化得到其中的代码与输入,并调用compile进行编译

  • 编译成功:调用runner将代码运行起来->将执行结果分别保存到.exe、.stdin、.stdout 、.stderr、.compile_stderr文件中
  • 编译失败:不调用runner

最后按对应构造json 返回给上级调用,即write进out_json中,收尾清除创建的文件

 

compile_server .cc文件编写:

 

#include"compile_run.hpp"
#include
#include"../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;

//编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,不然影响多个用户
void Usage(std::string proc)
{
    std::cerr <<"Usage:"<<"\n\t"<
    // int main(){
    //         std::cout<<"测试成功"<

直接引入的httplib库, 设置好ip和端口就可以直接监听了

  • svr.get() :就是当对该服务器发起/hello 请求的时候,我就会接受到该请求,以Get的方式返回resp

makefile:

由于当前使用的c++11的新特性,引入了json库,和多线程

compile_server:compile_server.cc
	g++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
	rm -f compile_server

项目设计 -- 基于MVC结构的oj服务 

本质:建立一个小型网站

1.获取首页

2.编辑区域页面

3.提交判题功能(编译并运行) 

 

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

V:view,通常是拿到数据之后,要进行构建网页,渲染网页内容

C:control,控制器,也就是我们核心业务逻辑

用户的请求服务路由功能:


#include "../comm/httplib.h"
#include "login.hpp"
#include 
#include 
#include"oj_control.hpp"
using namespace httplib;
using namespace ns_control;
const std::string login_path = "../oj_login/wwwroot/";
static Control *ctrl_ptr = nullptr;
void Recovery(int signo)
{
  ctrl_ptr->RecoveryMachine();
}
int main() {
  signal(SIGQUIT,Recovery);
  // 用户请求的服务路由功能
  Server svr;
  Control ctrl;
  Login login;
  ctrl_ptr = &ctrl;
  /*
  1获取所有的题目列表

  */
  svr.Get(R"(/all_questions)", [&ctrl](const Request &req, Response &resp) {
    std::string html;
    ctrl.AllQuestions(&html);
    resp.set_content(html, "text/html;charset=utf-8");
  });

  //   2用户要根据题目编号来选择题目
  // 这里的\d是正则表达式 + 是匹配数字
  // R"()"保持原始字符串不会被特殊字符影响比如\d \r \n之类的不需要做相关的转义
  svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp) {
    std::string number = req.matches[1];
    std::string html;
    ctrl.OneQuestion(number,&html);
    resp.set_content(html,"text/html;charset=utf-8");
  });

  // 3用户提交代码,使用我们的判题功能(1.没道题目的测试用例 2.compile_and_run)
  svr.Post(R"(/judge/(\d+))",[&ctrl](const Request &req, Response &resp){
    std::string number = req.matches[1];
    // resp.set_content("这是指定的一道题目的判题:" + number,
    //                  "text/plain;charset=utf-8");
    std::string result_json;
    ctrl.Judge(number,req.body,&result_json);
    resp.set_content(result_json,"application/json;charset=utf-8");
  });
  svr.Post(R"(/dealregister)",[&ctrl](const Request &req, Response &resp){
    int status = 1;
    std::string in_json = req.body;
    std::string out_json;
    if(!ctrl.UserRegister(in_json,&out_json)){
      status = 0;
    
    }
    LOG(INFO)<<"用户注册status : "<

这样当用户通过http请求我们的oj_server服务器的时候我们可以正确的路由到合适的功能

model功能:提供对数据的操作(文件版)

#pragma once
//文件版本
/*
编号
标题
难度
描述
时间(内部),空间(内部处理)

两批文件构成
1.question.list:题目列表:不需要出现题目描述
2.需要题目的描述,需要题目的预设置代码(header.cpp),测试用例代码(tail.cpp)

这两个内容是通过题目的编号,产生关联的
*/
#pragma once
#include "../comm/log.hpp"

#include "../comm/util.hpp"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// 根据题目list文件,加载所有信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口

namespace ns_model {
using namespace std;
using namespace ns_log;
using namespace ns_util;
class Question {
public:
  std::string number; // 题目编号
  std::string title;  // 题目的标题
  std::string star;   // 难度:简单中等困难
  int cpu_limit;      // 时间要求 s
  int mem_limit;      // 空间要求 kb
  std::string desc;   // 题目的描述
  std::string header; // 题目预设给用户在线编辑器的代码
  std::string tail; // 题目的测试用例,需要和header拼接形成完整代码
};
const std::string question_list = "./questions/questions.list";
const std::string question_path = "./questions/";
class Model {
private:
  // 题号:题目细节
  unordered_map questions;

public:
  Model() { assert(LoadQuestionList(question_list)); }
  bool LoadQuestionList(const std::string &question_list) {
    // 加载配置文件questions/question.list + 题目编号文件
    ifstream in(question_list);
    if (!in.is_open()) {
      LOG(FATEL) << "加载题库失败,请检查是否存在题库文件"
                 << "\n";
      return false;
    }
    std::string line;
    while (getline(in, line)) {
      vector tokens;
      StringUtil::SplitString(line, &tokens, " ");
      if (tokens.size() != 5) {

        LOG(WARNING) << "加载部分题目失败,请检查文件格式"
                     << "\n";
        continue;
      }
      Question 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());

      std::string path = question_path;
      path += q.number;
      path += "/";
      FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);
      FileUtil::ReadFile(path + "header.cpp", &(q.header), true);
      FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);

      questions.insert({q.number, q});
    }
    LOG(INFO) << "加载题库成功!"
              << "\n";
    in.close();
    return true;
  }
  bool GetAllQuestion(vector *out) {
    if (questions.size() == 0) {
      LOG(ERROR) << "用户获取题库失败"
                 << "\n";
      return false;
    }
    for (const auto &q : questions) {
      out->push_back(q.second); // fir是key' sec是value
    }
    return true;
  }
  bool GetOneQuestion(const std::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() {}
};
} // namespace ns_model

该设计中有一个 question的题目清单,像题库的目录一样,填写每道题目的基本信息:

对应的是:

1.题目编号 2.题目名字 3.题目难度 4.时间限制 5.空间限制

基于负载均衡的在线OJ实战项目_第3张图片

 

 

 model功能:提供对数据的操作(数据库版)

#pragma once
//这个是mysql版本
/*
编号
标题
难度
描述
时间(内部),空间(内部处理)

两批文件构成
1.question.list:题目列表:不需要出现题目描述
2.需要题目的描述,需要题目的预设置代码(header.cpp),测试用例代码(tail.cpp)

这两个内容是通过题目的编号,产生关联的
*/
#pragma once
#include "../comm/log.hpp"

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

#include"include/mysql.h"
// 根据题目list文件,加载所有信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口

namespace ns_model {
using namespace std;
using namespace ns_log;
using namespace ns_util;
class Question {
public:
  std::string number; // 题目编号
  std::string title;  // 题目的标题
  std::string star;   // 难度:简单中等困难
  int cpu_limit;      // 时间要求 s
  int mem_limit;      // 空间要求 kb
  std::string desc;   // 题目的描述
  std::string header; // 题目预设给用户在线编辑器的代码
  std::string tail; // 题目的测试用例,需要和header拼接形成完整代码
};

const std::string oj_questions ="oj_questions"; 
const std::string oj_user = "oj_user";
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 {
private:
  // 题号:题目细节
  unordered_map questions;
public:
  Model() { }
  bool QueryMySql(const std::string &sql,vector *out)
  {
    //创建mysql句柄
    MYSQL *my = mysql_init(nullptr);
    //连接数据库
    if(mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0) == nullptr){
      LOG(FATAL)<<"连接数据库失败!"<<"\n";
      return false;
    }
    //一定要设置该链接的编码格式默认是拉钉的
    mysql_set_character_set(my,"utf8mb4");
    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);//获得列数量

    
    Question q;
    for(int i = 0;ipush_back(q);
    }


    //释放结果空间
    free(res);
    //关闭mysql连接
    mysql_close(my);

    return true;
  }
  bool GetAllQuestion(vector *out) {
    std::string sql ="select *from ";
    sql += oj_questions;
    return QueryMySql(sql,out);
  }
  bool GetOneQuestion(const std::string &number, Question *q) {
    bool res = false;
    std::string sql = "select *from ";
    sql+=oj_questions;
    sql+= " where number=";
    sql+=number;
    vector result;
    if(QueryMySql(sql,&result))
    {
      if(result.size() == 1)
      {
        *q = result[0];
        res = true;
      }
    }
    return res;
  }
  
  bool UserRegister(const std::string& in_json,std::string* out_json)
  {
    //这里先对in_json反序列化
    Json::Reader reader;
    Json::Value in_value;
    reader.parse(in_json,in_value);
    std::string number = in_value["number"].asString();
    std::string name = in_value["name"].asString();
    std::string password = in_value["password"].asString();
    int limit = in_value["limit"].asInt();
    int level = in_value["level"].asInt();
    //判断账号密码是否可行
    std::string sql = " select *from ";
    sql+=oj_user;
    sql+=" where number=";
    sql+=number;

    //创建数据库
    MYSQL *my = mysql_init(nullptr);
    //连接数据库
    if(mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0) == nullptr)
    {
      LOG(WARNING)<<"连接到用户数据库失败"<<"\n";
      return false;
    }
    //一定要记得设置该链接的编码格式
    mysql_set_character_set(my,"utf8");
    LOG(INFO)<<"连接懂啊用户数据库成功"<<"\n";

    if(0 != mysql_query(my,sql.c_str())){
      LOG(WARNING)<< sql <<"execute error!"<<"\n";
      return false;
    }
    MYSQL_RES *res = mysql_store_result(my);
    if(mysql_num_rows(res) == 0)//获得行数量
    { 
      //当前输入的数据可以创建用户
      MYSQL_STMT *stmt = mysql_stmt_init(my);
      const char* query = "insert into oj_user values (?,?,?,?,?)";
      if(mysql_stmt_prepare(stmt,query,strlen(query)) != 0){
        LOG(WARNING)<<"stmt出现错误"<<"\n";
        mysql_stmt_close(stmt);
        mysql_close(my);
        return false;
      }
      //下面开始绑定
      MYSQL_BIND bind_params[5];
      memset(bind_params,0,sizeof bind_params);

      bind_params[0].buffer_type = MYSQL_TYPE_STRING;
      bind_params[0].buffer = (char*)number.c_str();
      bind_params[0].buffer_length = number.size();

      bind_params[1].buffer_type = MYSQL_TYPE_STRING;
      bind_params[1].buffer = (char*)name.c_str();
      bind_params[1].buffer_length = name.size();

      bind_params[2].buffer_type = MYSQL_TYPE_STRING;
      bind_params[2].buffer = (char*)password.c_str();
      bind_params[2].buffer_length = password.size();

      bind_params[3].buffer_type = MYSQL_TYPE_LONG;
      bind_params[3].buffer = &limit;
      bind_params[3].is_unsigned = 1;

      bind_params[4].buffer_type = MYSQL_TYPE_LONG;
      bind_params[4].buffer = &level;
      bind_params[4].is_unsigned = 1;
      
      if(mysql_stmt_bind_param(stmt,bind_params) !=0){
        LOG(WARNING) <<"绑定stmt参数出错"<<"\n";
        mysql_stmt_close(stmt);
        mysql_close(my);
        return false;
      }

      //执行插入语句
      if(mysql_stmt_execute(stmt)!=0){
        LOG(WARNING)<<"执行stmt语句的时候出现错误..."<<"\n";
        mysql_stmt_close(stmt);
        mysql_close(my);
        return false;
      }
      
      mysql_stmt_close(stmt);
      mysql_close(my);
      return true;
    }
    else{
      //服务器有重复的用户num ,不允许再创建了
      return false;
    }
    //保存到服务器

    //这里out_json暂时没有用,没有要返回的值
    return true;
  }
  ~Model() {}
};
} // namespace ns_model

control:逻辑控制模块

#pragma once

#include
#include
#include
#include
#include
#include
#include
#include
#include"oj_view.hpp" 
// #include"oj_model.hpp"
#include"oj_model2.hpp"
#include"../comm/log.hpp"
#include"../comm/util.hpp"
#include"../comm/httplib.h"
namespace ns_control
{
    using namespace std;
    using namespace httplib;
    using namespace ns_log;
    using namespace ns_util;
    using namespace ns_model; 
    using namespace ns_view;
    //提供服务的主机的内容
    class Machine
    {
    public:
        std::string ip; //编译服务器的ip
        int port;       //编译服务器的端口
        uint64_t load;  //编译服务器负载
        std::mutex *mtx;//mutex是禁止拷贝的,使用指针来完成
    public:
        Machine():ip(""),port(0),load(0),mtx(nullptr)
        {}
        ~Machine()
        {}
    public:
    void ResetLoad()
    {
        if(mtx)mtx->lock();
        load = 0;
        LOG(DEBUG)<<"当前ip:"<unlock();
    }
    //提升主机负载
        void IncLoad()
        {
            if(mtx) mtx->lock();
            ++load;
            if(mtx) mtx->unlock();
        }
    //减少主机负载
        void DecLoad()
        {
            if(mtx) mtx->lock();
            --load;
            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:
        //可以给我们提供编译服务的所有的主机
        //每一台主机都有自己的下标,充当当前主机的id
        std::vector machines; 
        //所有在线的主机
        std::vector online;
        //所有离线主机的id
        std::vector offline;
        //保证选择主机上的这个东西要保证数据安全
        std::mutex mtx;
    public:
        LoadBalance(){
            assert(LoadConf(service_machine));
            LOG(INFO)<<"加载"< tokens;
                StringUtil::SplitString(line,&tokens,":");
                if(tokens.size()!=2)
                {
                    LOG(WARNING) <<"切分"< 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();
                    LOG(DEBUG)<<"当前离线主机的负载更改为:"< all;
            if(model_.GetAllQuestion(&all))
            {
                sort(all.begin(),all.end(),[](const Question &q1,const Question &q2){
                    return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
                });

                //获取题目信息 成功,将所有的题目数据构建成网页
                view_.AllExpandHtml(all,html);
            }
            else
            {
                *html="获取题目失败,形成题目列表失败";
                ret = false;
            }
            return ret;
        }
        bool OneQuestion(const string &number,string *html)
        {
            Question q;
            bool ret = true;
            if(model_.GetOneQuestion(number,&q))
            {
                //获取指定信息的题目成功,构建程网页
                view_.OneExpandHtml(q,html);
            }
            else
            {
                *html="获取指定题目题目失败,形成题目列表失败";
                ret = false;
            }
            return ret;
        }
        void Login(const std::string in_json,std::string *out_json)
        {
            //in_json是发送过来的请求数据,用户的账号等待
            //返回渲染的登录界面
            view_.LoginExpandHtml(out_json);

        }
        void Register(const std::string in_json,std::string *out_json)
        {
            if(view_.RegisterExpandHtml(out_json)){
                LOG(INFO)<<"插入成功"<<"\n";
            }
            else{
                LOG(INFO)<<"插入失败,可能是重复的用户"<<"\n";
            }
            
        }
        bool UserRegister(const std::string in_json,std::string *out_json)
        {
            return model_.UserRegister(in_json,out_json);
        }
        //id:: 100 
        //code:include
        //input:
        void Judge(const std::string &number,const std::string in_json,std::string *out_json)
        {
            // LOG(INFO)<<"调用Judge功能"<<"\n";
            // LOG(DEBUG)<ip,m->port);
                m->IncLoad();
                LOG(DEBUG)<<"选择主机成功,主机id:"<ip<<":"<port<<"当前主机负载:"<Load()<<"\n";
                if(auto res = cli.Post("/compile_and_run",compile_string,"application/json;charset=utf-8"))
                {
                    //将我们的结果返回给out_json
                    if(res->status == 200)
                    {
                        *out_json = res->body;
                        m->DecLoad();
                        LOG(INFO)<<"请求编译和运行服务成功..."<<"\n";
                        break;
                    }                        
                    m->DecLoad();

                }
                else
                {
                    //请求失败
                    LOG(ERROR)<<"选择主机失败,主机id:"<ip<<":"<port<<"可能已经离线"<<"\n";
                    load_blance_.OfflineMachine(id);
                    load_blance_.ShowMachines();//仅仅为了调试
                    
                }
                //m->DecLoad();
            }
           

        }
        Control(){}
        ~Control(){}
    };
}

control模块实现了 负载均衡

  • 负载均衡
    • 第一种:随机数+hash
    • 第二种:轮询+hash , 本文是在用轮询+hash
  • 为了实现负载均衡所有要把所有主机管理起来,有了Machine类
    • std::string ip :编译服务器的ip
    • int port:编译服务器的端口
    • uint64_t load :编译服务器的负载
    • std::mutex *mtx:每个机器可能会同时被多个用户访问,所以要有锁来保证临界资源,并且mutex是不允许拷贝的,所以这里直接用指针,这样在赋值构造和拷贝构造就没事了

 

 view渲染功能:将后端的代码渲染到html返回给前端

这里就要使用到ctemplate库了:

基于负载均衡的在线OJ实战项目_第4张图片

 

 

#pragma once

#include
#include
#include
// #include"oj_model.hpp"
#include"oj_model2.hpp"



namespace ns_view
{
    using namespace ns_model;

    const std::string template_path ="./template_html/";
    const std::string login_path = "./login_html/";
    class View
    {

    public:
        View(){}
        ~View(){}
        bool RegisterExpandHtml(std::string *html)
        {
            //新城路径
            std::string src_html = login_path + "register.html";
            //形成数据字典
            ctemplate::TemplateDictionary root("register");
            //获取渲染的网页
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
            //开始渲染
            tpl->Expand(html,&root);
            return true;
        }
        void LoginExpandHtml(std::string *html)
        {
            //形成路径
            std::string src_html = login_path + "login.html";
            //形成数据字典
            ctemplate::TemplateDictionary root("my_login");
            //获取渲染网页
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
            //开始渲染
            tpl->Expand(html,&root);
        }
        void AllExpandHtml(const vector &questions,std::string *html)
        {
            // 题目的编号 题目的标题 题目的难度
            // 推荐使用表格显示
            //1。形成路径
            std::string src_html = template_path + "all_questions.html";
            LOG(INFO)<<"形成路径成功:"<< src_html <<"\n";
            //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.获取被渲染的网页html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
            LOG(INFO)<<"获取渲染网页的html成功"<<"\n";

            //4.开始完成渲染功能
            tpl->Expand(html,&root);
            LOG(INFO)<<"渲染成功"<<"\n";

        }
        void OneExpandHtml(const Question &q,std::string *html)
        {
            //形成路径
            std::string src_html = template_path + "one_question.html";
            LOG(DEBUG)<<"one expand html :"<Expand(html,&root);
        }
    };
}

 

 

总结 

1.前端的代码在博客最上端绑定的文件当中 ,篇幅太长不展示出来了

2.该项目的技术栈众多,是c++后端和前端进行交互的一个项目

3.项目的难点有:负载均衡的分配到每一台编译服务器、容错处理,能够处理多种不同的错误原因、并发处理要对临界资源的管理、以及高并发访问的话要对效率有所保证,毕竟在线oj服务是具有时效性的

4.debug困难,要在test.cc下测试成功后再进行编写,便于修改bug

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(实战项目,负载均衡,运维,linux,c++,开发语言,javascript,ajax)