项目介绍

负载均衡的在线OJ平台,和传统的LeetCode,牛客等平台一样,项目的特点,用户可以在上面进行刷题练习,当用户提交代码后,后端会根据最优的策略来进行分配机器为用户提供服务

项目演示

前端部分

首页

在这里插入图片描述

所有题目列表

在这里插入图片描述

特定的一道题目(用户编写区域)

在这里插入图片描述

后端部分

三台编译机器和一台服务器

在这里插入图片描述

所用的技术与开发环境

技术栈:

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

开发环境:

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

项目结构

核心三个模块:

1.comm:公共模块

2.compile_server:编译与运行模块

3.oj_server:获取题目列表,查看题目编写题目界面,负载均衡,其他功能

项目结构

1.comm: 负责提供时间戳,增加文件路径(.cpp,.exe,.stderr),文件操作(判断是否存在,读,写,唯一文件名),字符串切割,日志

2.compile_server:包含runner和compile,负责编译运行远端提供的代码

3.oj_server:MVC架构实现,前端的html

项目配件

1.安装jsoncpp

yum install -y jsoncpp-devel

2.安装cpp-httplib

gitee中下载cpp-httplib,点击标签可以找到历史版本0.7.15
要升级gcc到 7/8/9   
gcc -v 查看gcc版本
1.sudo yum install centos-release-scl scl-utils-build
2.sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
3.scl enable devtoolset-7 bash //只在本会话有效
4.cat ~/.bash_profile 中加入 scl enable devtoolset-7 bash

3.安装boost

yum install -y boost-devel

4.安装ctemplate

1. git clone https://gitee.com/mirrors_OlafvdSpek/ctemplate.git
2.进入ctemplate目录下面
    输入./autogen.sh  
    输入./configure
    输入 make
    输入sudo make install

后端代码实现部分

comm模块

在这里插入图片描述

log.hpp

//日志等级
enum
{
   INFO,//常规  0
   DEBUG,//调试 1
   WARNING,//告警-不影响后序 2
   ERROR,//错误 3 
   FATAL //系统错误 4
};

std::ostream &Log(const std::string& level,const std::string &file_name,int line)
   {
      //添加日志等级
      std::string message ="[";
      message+=level;
      message+="]";

      //添加报错文件名称
      message +="[";
      message +=file_name;
      message +="]";

      //添加报错行
      message+="[";
      message +=std::to_string(line);
      message +="]";

      //日志时间戳
      message +="[";
      message +=TimeUtil::GetTimeStamp();
      message +="]";

      //cout 本质是包含缓冲区的
      std::cout<<message; //放到缓冲区中

      return std::cout;
   }
   

util.hpp

static std::string GetTimeStamp()
{
   struct timeval _time;
   gettimeofday(&_time,nullptr);
    return std::to_string(_time.tv_sec);   
}

//毫秒级别时间戳
static std::string GetTimeMs()
{
    struct timeval _time;
    gettimeofday(&_time,nullptr);
    return std::to_string(_time.tv_sec*1000+_time.tv_usec/1000);
      }
};

//增加路径后缀
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;
}


//文件操作
 static bool IsFileExists(const std::string& path)
      {
         //int stat(const char *path, struct stat *buf);
         struct stat st;
         if(stat(path.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;

      }

      //把content写入target中
      static bool WriteFile(const std::string &target,const std::string &content)
      {
         std::ofstream out(target);
         if(!out.is_open()){
            return false;
         }
         out.write(content.c_str(),content.size());
         out.close();
         return true;
      }

      //想保留\n的话要传keep为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;
         while(std::getline(in,line))
         {
            (*content) +=line;
            (*content)+=(keep?"\n":"");
         }
         in.close();
         return true;
      }

//字符串切割
 static void SplitString(const std::string &str,std::vector<std::string>*target,std::string sep)
{
     boost::split((*target),str,boost::is_any_of(sep),boost::algorithm::token_compress_on);
}

compiler_server

在这里插入图片描述

runner.hpp

static void SetProcLimit(int _cpu_limit,int _mem_limit)
{
      //设置CPU
      struct rlimit cpu_rlimit;
      cpu_rlimit.rlim_max=RLIM_INFINITY; //无穷
      cpu_rlimit.rlim_cur=_cpu_limit;
      setrlimit(RLIMIT_CPU,&cpu_rlimit);
      //设置内存大小
      struct rlimit mem_rlimit;
      mem_rlimit.rlim_max=RLIM_INFINITY;
      mem_rlimit.rlim_cur=_mem_limit*1024; //转化为kb
      setrlimit(RLIMIT_AS,&mem_rlimit);
}

 /*
      指明文件名就行,不需要路径和后缀
      run跑完的结果对不对不关心,结果正确是由测试用例决定
      run只考虑是否正确运行   
      返回值 >0 :程序运行异常,退出的时候收到了信号,返回值就是信号编号
      返回值 ==0 :正常运行,结果保存在临时文件中
      返回值 <0  :内部错误 
*/

static int Run(const std::string& file_name,int cpu_limit,int men_limit)
    {
      //运行的结果会写入到这些文件中
      std::string execute =PathUtil::Exe(file_name);
      std::string _stdin  =PathUtil::Stdin(file_name);
      std::string _stdout =PathUtil::Stdout(file_name);
      std::string _stderr =PathUtil::Stderr(file_name);

      umask(0);
      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);//标准输入重定向到_stdin_fd
         dup2(_stdout_fd,1);
         dup2(_stderr_fd,2);

         SetProcLimit(cpu_limit,men_limit);
         execl(execute.c_str()/*执行谁*/,execute.c_str()/*在命令行上如何执行*/);
         exit(1);
      }
      else
      {
         close(_stdin_fd);
         close(_stdout_fd);
         close(_stderr_fd);
         int status=0;
         waitpid(pid,&status,0);
         //程序运行异常一定是收到了信号
         LOG(INFO) <<"运行完毕,info:" << (status & 0x7F)<<"\n";
         return status & 0x7F;
      }
    }

compile.hpp

 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); //把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
          //g++编译报错会向显示器打印信息
            dup2(_stderr,2);

            //子进程--调用编译器完成对代码的编译工作
            //程序替换功能-不影响进程的文件描述符表
            // int execlp(const char *file, const char *arg, ...);
           execlp("g++","g++","-o",PathUtil::Exe(file_name).c_str(),
           PathUtil::Src(file_name).c_str(),"-std=c++11","-D","COMPILER_ONLINE",nullptr); //参数传完之后要以nullptr结尾
            LOG(ERROR)<<"启动编译器失败"<<"\n";
            exit(2);
         }
      else
          {
            //父进程
            waitpid(pid,nullptr,0);
            //编译是否成功 -- >形成可执行程序
            if(FileUtil::IsFileExists(PathUtil::Exe(file_name)))
            {
               LOG(INFO)<<PathUtil::Src(file_name)<<"编译成功"<<"\n";
               return true;
            }

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

compile_run.hpp

/*****************************************************
  *输入:
    1.用户提交的代码
    2.用户提交代码对应的输入
    3.cpu_limit  men_limit
  *输出:
     1.status状态码
     2.请求结果
     3.选填:
     stdout:程序运行完的结果 stderr:程序运行完错误的结果
******************************************************/
static void Start(const std::string &in_json, std::string *out_json)
      {
         Json::Value in_value;
         Json::Reader reader;
         reader.parse(in_json, in_value) ;// 反序列化 (解析谁,解析到哪)
             // in_json{"code","#include--","input","cpu_limit","men_limit"};
             // out_json{"status","reason","stdout","stderr",""};

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

         int status_code = 0;
         int ret_run = 0;
        
         std::string file_name = FileUtil::UniqFileName();
         Json::Value out_value;

         if (code.size() == 0) // 空代码
         {
            status_code = -1; // 代码为空
            goto END;
         }

         // 形成临时的src文件
         if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
         {
            status_code = -2; // 未知错误
            goto END;
         }

         // 编译文件
         if (!Compiler::Compile(file_name))
         {
            status_code = -3; // 编译失败
            goto END;
         }

         // 运行文件
         ret_run = Runner::Run(file_name, cpu_limit, mem_limit);

         if (ret_run < 0) // 内部错误
         {
            status_code = -2; // 未知错误
         }

         else if (ret_run > 0)
         {
            status_code = ret_run; // 程序崩溃了
         }
         else
         {
            // 运行成功
            status_code = 0;
         }
      END:
         out_value["status"] = status_code;
         out_value["reason"] = CodeToDesc(status_code,file_name);
         if (status_code == 0)
         {
            // 整个过程成功
            std::string _stdout;
            // 运行完后结果会写入Stdout中然后用ReadFile读取
            FileUtil::ReadFile(PathUtil::Stdout(file_name),&_stdout,true);
            out_value["stdout"] =_stdout;

            std::string _stderr;
            FileUtil::ReadFile(PathUtil::Stderr(file_name),&_stderr,true);
            out_value["stderr"] =_stderr;
         }

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

         RemoveTempFile(file_name);//清理临时文件
}

oj_server

在这里插入图片描述


oj_server是基于MVC结构设计的

M即model模型是指模型表示业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性

V即View视图是指用户看到并与之交互的界面。比如由html元素组成的网页界面,或者软件的客户端界面。MVC的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操作的方式。

C即controller控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据

在这里插入图片描述

oj_control

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <cassert>
#include <fstream>
#include<algorithm>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include"../comm/httplib.h"
#include "oj_model.hpp"
// #include "./oj_model2.hpp"

#include "oj_view.hpp"
#include<jsoncpp/json/json.h>


namespace ns_control
{
   using namespace ns_util;
   using namespace ns_log;
   using namespace ns_model;
   using namespace ns_view;
   using namespace httplib;

   // 一个编译对应一台机器 
   //提供服务的主机
   class Machine
   {
   public:
      std::string _ip;  // 编译服务的ip
      int _prot;        // 编译服务的端口
      uint64_t _load;   // 编译服务的负载
      std::mutex *_mtx; // 禁止拷贝,使用指针

   public:
      Machine()
          : _ip(""), _prot(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();
      }

      //获取主机负载
      uint64_t Load()
      {
         uint64_t load=0;
         if(_mtx) _mtx->lock();
         load=_load;
         if(_mtx) _mtx->unlock();
         return load;
      }

      //负载清零
      void RestLoad()
      {
          if(_mtx) _mtx->lock();
         _load=0;
         if(_mtx) _mtx->unlock();
      }

   };

   // 负载均衡模块
   const std::string service_machine = "./conf/service_machine.conf";
   class LoadBlance
   {
   private:
      /*
      每一台主机都有自己的下标,用下标充当当前主机的id
      */
      std::vector<Machine> machines; // 提供编译服务的所有主机
      std::vector<int> online;       // 在线主机id
      std::vector<int> offline;      // 离线主机id
      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))
         {
            // 127.0.0.1:8081
            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._prot = 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;
      }

      /*
        负载均衡选择:轮训+hash的方法;
        id:主机id m:机器的地址
      */
      bool SmartChoice(int *id,Machine **m)
      {
         mtx.lock();
         //1.使用选择好的主机(更新对应主机的负载)
         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].RestLoad();
               online.erase(iter);
               offline.push_back(which);
               break;
            }
         }
         mtx.unlock();
      }

      void OnlineMachine()
      {
        
         mtx.lock();
         //当所有的主机离线的时候,统一上线
         online.insert(online.end(),offline.begin(),offline.end());  //把offline加入到online中
         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<<endl;
         std::cout<<"当前离线主机列表";
         for(auto &id:offline)
         {
            std::cout<<id<<" ";
         }
         std::cout<<endl;
         mtx.unlock();
      }
   };

   // 核心业务模块
   class Control
   {
   private:
      Model _model; // 后台数据
      View _view;   // html渲染功能
      LoadBlance _load_balance; //负载均衡
   public:
      Control() {}
      ~Control() {}

   public:
   
      void RecoverMachine()
      {
         _load_balance.OnlineMachine();
      }

      // 根据题目数据构建网页
      bool AllQuestions(string *html)
      {
         bool ret = true;
         vector<struct Question> all;
         if (_model.GetAllQuestions(&all))
         {
            sort(all.begin(),all.end(),[](const struct Question &q1,const struct Question &q2)
            {
               return atoi(q1.number.c_str()) <atoi(q2.number.c_str());
            });
            // 获取题目信息成功,将所有题目构建网页
            _view.AllExpandHtml(all, html);
         }
         else
         {
            *html = "获取题目失败,形成题目列表失败";
            ret = false;
         }
         return ret;
      }

      bool OneQuestion(const std::string &number, string *html)
      {
         Question q;
         bool ret = true;
         if (_model.GetOneQuestion(number, &q))
         {
            // 获取指定题目信息成功,将所有题目构建网页
            _view.OneExpandHtml(q, html);
            LOG(INFO) << "获取指定题目信息成功 " << std::endl;
         }
         else
         {
            *html = "指定题目" + number + "不存在";
            ret = false;
         }
         return ret;
      }

      // 用户提交的信息都在in_Json中
      void Judge(const std::string &number,const std::string in_json, std::string *out_json)
      {     
         // 0.根据题号,拿到题目细节
         struct Question q;
         _model.GetOneQuestion(number,&q);

         // 1. in_json进行反序列化得到题目id,code,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; // Json::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 compline_string=writer.write(compile_value);

         // 3. 选择负载最低的主机
         //    规则:一直选择,直到主机可用,否则,就是全部挂掉
         while(true)
         {
            int id=0;
            Machine *m=nullptr;
            if(!_load_balance.SmartChoice(&id,&m))
            {
               break;
            }
            

         //4.发起http请求,得到结果
         //cpp-http使用
         //Response  version,reason,body
         Client cli(m->_ip,m->_prot);
         m->IncLoad();//请求成功增加负载
          LOG(INFO)<<"选择主机成功,主机id"<<id<<" "<<m->_ip<<":"<<m->_prot<<"当前主机的负载是:"<<m->_load<<"\n";
         if(auto res=cli.Post("/compile_and_run",compline_string,"application/json;charset=utf-8"))
         {
            if(res->status ==200)
            {
               *out_json=res->body;//将结果给回out_json
               m->DecLoad();//减少主机负载
               LOG(INFO)<<"请求编译和运行服务成功..."<<"\n";
               break;
            } 
             m->DecLoad(); // 减少主机负载
         }

         else
         {
           LOG(ERROR)<<"选择当前请求主机失败"<<id<<" "<<m->_ip<<":"<<m->_prot<<"\n";
           _load_balance.OfflineMachine(id);//把该主机离线
           _load_balance.ShowMachines();//test
         }

         }
         
      }
   };
}

oj_model文件版

#pragma once


// 根据题目list文件,加载所有的题目信息到内存中
// model:和数据交互对外提供访问数据的接口
#include <iostream>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <vector>
#include <string>
#include <unordered_map>
#include <fstream>
#include <cassert>

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

oj_model数据库版本

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <fstream>
#include <cassert>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include "./include/mysql.h"
#include <mysql/mysql.h>

// 数据库版
namespace ns_model
{
    using namespace std;    
    using namespace ns_log;
    using namespace ns_util;
 
    struct Question
    {
        string number; // 题目编号(唯一)
        string title;  // 题目的标题
        string star;   // 题目的难度:简单 中等 困难
        string desc;   // 题目的描述
        string header; // 题目预设给用户在线编辑器的代码
        string tail;   // 题目的测试用例,需要和header拼接,形成完整代码
        int cpu_limit;      // 题目的时间要求(S)
        int mem_limit;      // 题目的空间要求(KB)
    };
    
    std::string oj_questions="oj_questions";
    const std::string host="127.0.0.1";
    const std::string user="oj_client";
    const std::string passwd="12345678";
    const std::string db="oj";
    const int port =3306;
    
    class Model
    {
   
    public:
        Model()
        {
        }

        bool QueryMySql(const std::string &sql,vector<Question> *out)
        {
            MYSQL *my=mysql_init(nullptr);
            if (my == nullptr) {
              LOG(FATAL) << "MySQL 句柄初始化失败!" << std::endl;
              return false;
            }

            //链接数据库
            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) << std::endl;
                LOG(FATAL)<< host.c_str()<<std::endl;
                LOG(FATAL)<< user.c_str()<<std::endl;
                LOG(FATAL)<< passwd.c_str()<<std::endl;
                LOG(FATAL)<< db.c_str()<<std::endl;
                LOG(FATAL)<< port <<std::endl;                
                LOG(FATAL)<<"连接数据库失败!"<<std::endl;
                return false;
            }
            LOG(FATAL)<<"连接数据库成功"<<"\n";

            //设置编码格式
            mysql_set_character_set(my,"utf8");
            
            //执行sql
            if(0!=mysql_query(my,sql.c_str()))
            {
                LOG(WARNING)<<sql<<"有问题"<<"\n";  
                return false;   
            }

            //获取结果
            MYSQL_RES *res=mysql_store_result(my);

            //分析结果
            int rows=mysql_num_rows(res);
            int cols=mysql_num_fields(res);
            for (int i = 0; i < rows; ++i)
            {
                MYSQL_ROW row = mysql_fetch_row(res); // 拿到当前这一行的所有数据(这里的row是一个二级指针)
                Question q; // 用于保存结果

                // 拿到当前行,每列的所有数据
                q.number = row[0];
                q.title = row[1];
                q.star = row[2];
                q.desc = row[3];
                q.header = row[4];
                q.tail = row[5];
                q.cpu_limit = atoi(row[6]);
                q.mem_limit = atoi(row[7]);
                
                // 将当前题的所有信息放到返回数组里面
                out ->push_back(q);
            }

            free(res);
            
            mysql_close(my);
            return true;
        }
        // 获取所有的题目
        bool GetAllQuestions(vector<Question> *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<Question> result; // 只是转化一下,满足调用接口的参数要求

            if (QueryMySql(sql, &result))
            {
                LOG(FATAL)<<"QueryMySql语句执行成功"<<std::endl;
                if (result.size() == 1)
                {
                    *q = result[0];
                    res = true;
                }
            }
            return res;
        }
 
        ~Model()
        {}
    };
}


oj_view

#pragma once

#include<iostream>
#include<string>
#include <ctemplate/template.h>

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


namespace ns_view
{
   using namespace ns_model;
   const string template_path="./template_html/";
   class View
   {
   public:
      View(){}
      ~View(){}
   public:
      void AllExpandHtml(const std::vector<struct Question>&qs,std::string* html)
      {
        //形成路径
        std::string src_html=template_path+"all_questions.html";
        //形成字典
        ctemplate::TemplateDictionary root("all_questions");
        for(const auto& q:qs)
        {
            ctemplate::TemplateDictionary *sub =root.AddSectionDictionary("question_list");
            sub->SetValue("number",q.number);
            sub->SetValue("title",q.title);
            sub->SetValue("star",q.star);

        }
        //获取被渲染的网页
        ctemplate::Template* tpl=ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
        //开始渲染
        tpl->Expand(html,&root);
      }
   
      void OneExpandHtml(const Question& q,std::string* html)
      {
         std::string src_html =template_path+"one_question.html";
         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("pre_code",q.header);

         ctemplate::Template *tpl =ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
         tpl->Expand(html,&root);
      }

   };
}




前端代码实现部分

index.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>jiantao的OJ系统</title>
    <style>
         /* 保证我们所设置不受网页默认设置影响*/
        * {
            /* 消除网页的默认外边距 */
            margin:0px;
            /* 消除网页的默认内边距 */
            padding:0px;
        }
        html,
        body{
            width: 100%;
            height: 100%;
        }
        
         /* 导航栏 */
        .container .navbar{
            width: 100%;
            height: 60px;
            background-color: black;
            /* 给父级标签设置overflow */
            overflow:hidden;
        }

        .container .navbar a{
            /* 设置成行内块元素 */
            display: inline-block;
            /* 设置a标签的宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: rgb(246, 5, 174);
            /* 设置大小 */
            font-size: larger;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 60px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件,鼠标放在上面会变成蓝色 */
        .container .navbar a:hover{
            background-color: rgb(7, 214, 225);
        }

        .container .navbar .login{
            float: right;
        }

        .container .content{
            /* 设置标签的宽度,px像素点 */
            width: 2000px;
            /* 背景色 
            background-color:#cd6a07; */
            /*整体居中*/
            margin: 0px auto;
            /* 文字居中 */
            text-align: center;
            /* 设置上外边距 */
            margin-top: 150px;
        }


        /*每个字体的样式*/
        .container .content .font_{
            /* 设置标签为块级元素:独占一行,可以设置高度宽度等属性 */
            display: block;
            /* 设置每个文字的上外边距 */
            margin-top: 52px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置字体大小 */
             font-size:42px; 
            
        }

        .container2 .footer{
            margin-top: 400px;
            width: 100%;
            height: 50px;
            background-color: black;
            text-align: center;
            color: rgb(237, 231, 237);
        }

    </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 href="#">VIP</a>
            <a class="login" href="#">登录</a>
        </div>
        <!-- 网页的内容 -->
        <div class="content">
            <h1 class="font_">欢迎进入健涛的在线OJ平台</h1>
            <p  class="font_">这是个人模拟力扣开发的,目前只有首页和题库按钮能用</p>
            <a class="font_" href="/all_questions">开始编程</a>
        </div>
    </div>
    <div class="container2">
        <div class="footer">
            <h4>@jiantao</h4>
        </div>
    </div> 
</body>
</html>

all_questions.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>在线OJ-题目列表</title>
    <style>
      
          /* 保证我们所设置不受网页默认设置影响*/
          * {
            /* 消除网页的默认外边距 */
            margin:0px;
            /* 消除网页的默认内边距 */
            padding:0px;
        }
        html,
        body{
            width: 100%;
            height: 100%;
        }
        
         /* 导航栏 */
        .container .navbar{
            width: 100%;
            height: 60px;
            background-color: black;
            /* 给父级标签设置overflow */
            overflow:hidden;
        }

        .container .navbar a{
            /* 设置成行内块元素 */
            display: inline-block;
            /* 设置a标签的宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: rgb(246, 5, 222);
            /* 设置大小 */
            font-size: larger;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 60px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件,鼠标放在上面会变成蓝色 */
        .container .navbar a:hover{
            background-color: rgb(7, 214, 225);
        }

        .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: “Arial”,“Microsoft YaHei”,“黑体”,“宋体”,sans-serif;
            margin-top: 50px;
            background-color: rgb(212, 245, 231);
        }

         /*OnlineJuge题目列表*/   
        .container .question_list h1 {
            color: rgb(229, 8, 189);
        }
        
         /*题目表格中字体*/  
        .container .question_list table .item {
            width: 120px;
            height: 50px;
            font-size: larger;
            font-family:“Arial”,“Microsoft YaHei”,“黑体”,“宋体”,sans-serif;
        }

        .container .question_list table .item a {
            text-decoration: none;
            color: rgb(13, 13, 13);
        }
        
        /*点击到题目的颜色*/
        .container .question_list table .item a:hover {
            color: rgb(48, 4, 245);
            text-decoration:underline;
        }

        /*防伪*/    
        .container .footer {
            width: 100%;
            height: 50px;
            text-align: center;
            line-height: 50px;
            color: #6f05a3;
            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 href="#">VIP</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="/questions/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/question_list}}
            </table>
        </div>
        <div class="footer">
            <h4>@jiantao</h4>
        </div>
    </div>

</body>

</html>

one_questions.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{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: 0px;
            /* 消除网页的默认内边距 */
            padding: 0px;
        }

        html,
        body {
            width: 100%;
            height: 100%;
        }

        /* 导航栏 */
        .container .navbar {
            width: 100%;
            height: 60px;
            background-color: black;
            /* 给父级标签设置overflow */
            overflow: hidden;
        }

        .container .navbar a {
            /* 设置成行内块元素 */
            display: inline-block;
            /* 设置a标签的宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: rgb(246, 5, 222);
            /* 设置大小 */
            font-size: larger;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 60px;
            /* 去掉下划线 */
            text-decoration: none;
            /* 设置文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件,鼠标放在上面会变成蓝色 */
        .container .navbar a:hover {
            background-color: rgb(7, 214, 225);
        }

        .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 {
            font-size: 26px;
            padding-top: 10px;
            padding-left: 10px;
        }

        /*描述里面文字设置*/
        .container .part1 .left_desc pre {
            padding-top: 10px;
            padding-left: 10px;
            font-size: 20px;
            background-color: rgb(227, 241, 235);
            font-family: “Arial”,“Microsoft YaHei”,“黑体”,“宋体”,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 href="#">VIP</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" οnclick="submit()">提交代码</button>
    </div>
</div>

    <script>
        //初始化对象
        editor = ace.edit("code");
        editor.setTheme("ace/theme/tomorrow_night_eighties");
        editor.session.setMode("ace/mode/c_cpp");
        // 代码大小
        editor.setFontSize(22);
        // 设置默认制表符的大小:
        editor.getSession().setTabSize(4);
        // 设置只读(true时只读,用于展示代码)
        editor.setReadOnly(false);
        // 启用提示菜单
        ace.require("ace/ext/language_tools");
        editor.setOptions({
            enableBasicAutocompletion: true,
            enableSnippets: true,
            enableLiveAutocompletion: true
        });

        function submit(){
            // 1. 收集当前页面的有关数据, 1. 题号 2.代码
            var code = editor.getSession().getValue();
            // console.log(code);
            var number = $(".container .part1 .left_desc h3 #number").text();

            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)
            {
                // 拿到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>

json的使用


测试

用postman可以进行测试

在这里插入图片描述

补充

项目所遇到的问题

1**./oj_server: error while loading shared libraries: libctemplate.so.3: cannot open shared object file: No such file or directory**

解决办法:export LD\_LIBRARY\_PATH=$LD\_LIBRARY\_PATH:/usr/local/lib

2.g++: internal compiler error: Killed (program cc1plus)

sudo dd if=/dev/zero of=/swapfile bs=64M count=16
#count的大小就是增加的swap空间的大小,64M是块大小,所以空间大小是bs*count=1024MB
sudo mkswap /swapfile
#把刚才空间格式化成swap格式
sudo swapon /swapfile
#使用刚才创建的swap空间

3**.用数据库之后出现乱码的问题**

mysql_set_character_set(my,"utf8");

4.引入数据库之后出现这个问题./oj_server: error while loading shared libraries: libmysqlclient.so.18:cannot open shared object file: No such file or directory

解决办法:
sudo ln -s /home/jiantao/thirdpart/mysql-connector/lib/libmysqlclient.so /usr/lib/
sudo ln -s /home/jiantao/thirdpart/mysql-connector/lib/libmysqlclient.so.18 /usr/lib/
sudo ln -s /home/jiantao/thirdpart/mysql-connector/lib/libmysqlclient.so /usr/lib64/ 
sudo ln -s /home/jiantao/thirdpart/mysql-connector/lib/libmysqlclient.so.18  /usr/lib64/

4**.连接数据库失败!错误信息: SSL connection error: protocol version mismatch**

sudo vim /etc/my.cnf
加上skip_ssl
再重启一下mysql   service mysqld restart

数据库版操作

use mysql;
set global validate_password_policy=0;//降低安全
create user oj_client@'%' identified by '12345678';
create database oj;
grant all on oj.* to oj_client@'%';
mysql -uoj_client -p;

创建表结构

use oj;

CREATE TABLE IF NOT EXISTS `oj_questions` (
    `number` INT PRIMARY KEY AUTO_INCREMENT COMMENT '题目的编号',
    `title` VARCHAR(128) NOT NULL COMMENT '题目的标题',
    `star` VARCHAR(8) NOT NULL COMMENT '题目的难度',
    `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=INNODB , CHARSET=UTF8;
ln -s ~/thirdpart/mysql-connector/include/ include
ln -s ~/thirdpart/mysql-connector/lib/ lib

json的使用

//传入的 string in_json
//反序列化
Json::value in_value;
Json::Reader reader;
//把in_json解析到in_value中
reader.parse(in_json,in_value);
string code =in_value["code"].asString();
int men_limit=invalue["men_limit"].asInt();

//序列化
Json::Value out_value;
out_value["status"]=status_code;
Json::StyledWriter writer;
out_json=writer.write(out_value);
Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐