IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 在线OJ系统 -> 正文阅读

[C++知识库]在线OJ系统

项目开始之前

需要准备的第三方库

httplib
g++版本必须得是4.9以上

ctemplate

boost: yum install boost-devel.x86_64

jsoncpp: yum install jsoncpp-devel.x86_64

项目分析

在这里插入图片描述
在这里插入图片描述
我们可以看到一个在线OJ至少有
题目ID
名字
难易度
描述
测试用例
代码框架
在这里插入图片描述
然后我们试做一道题,可以大概看出一个在线OJ的流程,
1 在浏览器中展示题目
2 用户可以选择题目进行作答
3 后台获取用户代码
4 针对用户代码进行编译
5 后台运行代码
6 将运行结果进行打包
7 把结果返回给客户端。
因此我将这个流程分为两大模块题目管理模块在线编译模块

两大模块

在线编译模块

整体思路

用户发来请求
将用户请求进行切分
改成json格式
分为用户代码“code”和用户输入“stdin”
然后调用编译模块进行编译
调用运行模块进行运行
把最终结果进行返回,构造json对象

按照&和=进行切分,Split是将boost库中的split进行封装

    static void ParseBody(const std::string& body,
        std::unordered_map<std::string, std::string>* params){
        //将body字符串切分成键值对
        //先按照&切分
        std::vector<std::string> kvs;
        Split(body, "&", &kvs);
        for(size_t i = 0; i < kvs.size(); ++i){
            std::vector<std::string> kv;
            //按照 = 切分
            Split(kvs[i], "=", &kv);
            if(kv.size() != 2){
                continue;
            }
            //对键值对进行urldecode
            (*params)[kv[0]] = UrlDecode(kv[1]);
        }
    }
//解析body,获取到用户提交的代码
std::unordered_map<std::string, std::string> body_kv;
          UrlUtil::ParseBody(req.body, &body_kv);
          const std::string& user_code = body_kv["code"];
//构造json结构的参数
        Json::Value req_json;
        req_json["code"] = user_code + question.tail_cpp;
        req_json["stdin"] = user_code;
        Json::Value resp_json;
//调用编译模块进行编译
       Compiler::CompileAndRun(req_json, &resp_json);

编译模块

整体思路
根据所传来的请求,生成对应的源代码文件,然后调用g++来编译文件,如果编译出错,重定向到文件中,然后调用可执行程序,同时将标准输出和标准错误也重定向到文件,最后把程序的最终结果进行返回。
根据请求对象生成源码文件

      //根据请求对象生成源代码文件
    if (req["code"].empty()){
        (*resp)["error"] = 3;
        (*resp)["reason"] = "code empty";
        LOG(ERROR) << "code empty" <<std::endl; 
        return false;
    }
    const std::string& code = req["code"].asString();
    const std::string&file_name=WriteTmpFile(code,req["stdin"].asString());

我们需要生成多种文件,有源代码文件编译错误文件可执行程序文件标准输入文件标准输出文件标准错误文件
因此将它们用不同的后缀区分开,并放到同一路径下,传入名字,就可以生成对应的临时文件,例如源代码文件

  //源代码文件, name表示当前请求的名字
  static std::string SrcPath(const std::string& name){
      return "./temp_files/" + name + ".cpp";
  }
把代码写到文件中,并且分配一个唯一的名字
   static std::string WriteTmpFile(const std::string& code,
        const std::string& str_stdin){
        static std::atomic_int id(0);
        ++id;
        std::string file_name = "tmp_" + std::to_string(TimeUtil::TimeStamp())
        + "." + std::to_string(id);
        FileUtil::Write(SrcPath(file_name), code);

        FileUtil::Write(StdinPath(file_name), str_stdin);
        return file_name;
    }

注意这里的id需要转成原子操作,因为很有可能有多个用户同时发来请求

然后调用g++进行编译,创建子进程,然后重定向标准错误到编译错误文件中,最后用程序替换的方式执行编译命令
    //调用g++进行编译
    bool ret = Compile(file_name);
        static bool Compile(const std::string& file_name){
        // 构造出编译指令
        char* command[20] = {0};
        char buf[20][50] = {{0}};
        for(int i = 0; i < 20; ++i){
            command[i] = buf[i];
        }
        sprintf(command[0], "%s", "g++");
        sprintf(command[1], "%s", SrcPath(file_name).c_str());
        sprintf(command[2], "%s", "-o");
        sprintf(command[3], "%s", ExePath(file_name).c_str());
        sprintf(command[4], "%s", "-std=c++11");
        command[5] = NULL;
        //创建子进程
        int ret = fork();
        if(ret > 0){
            //父进程进行进程等待
            waitpid(ret, NULL, 0);;
        }
        else{
            //子进程进行程序替换
            int fd = open(CompileErrorPath(file_name).c_str(),
                O_WRONLY | O_CREAT, 0666);
            if (fd < 0){
                LOG(ERROR) << "open Compile file error" << std::endl;
                exit(1);
            }
            dup2(fd, 2);
            execvp(command[0], command);
            //如果子进程执行失败,就直接退出
            exit(0);
        }
        //判定可执行文件是否存在来确定编译是否成功
        struct stat st;
        ret = stat(ExePath(file_name).c_str(), &st);
        if(ret < 0){
            //说明文件不存在
            LOG(INFO) << "Compile failed!" << file_name <<  std::endl;
            return false;
        }
        LOG(INFO) << "Compile " << file_name << " OK!" << std::endl;
        return true;
    }

调用可执行程序,思路和上面几乎一样
    //调用可执行程序
    int sig = Run(file_name);
    
        static int Run(const std::string& file_name){
        //创建子进程
        int ret = fork();
        if(ret > 0){
            //父进程进行等待
            int status = 0;
            waitpid(ret, &status, 0);
            return status & 0x7f;
        }
        else{
            //进行标准输入,输出,错误的重定向
            int fd_stdout = open(StdoutPath(file_name).c_str(),
                O_WRONLY | O_CREAT, 0666);
            dup2(fd_stdout, 1);
            int fd_stderr = open(StderrPath(file_name).c_str(),
                O_WRONLY | O_CREAT, 0666);
            dup2(fd_stderr, 2);
            
            //子进程进行程序替换
            execl(ExePath(file_name).c_str(),
                ExePath(file_name).c_str(), NULL);
            exit(0);
        }
    }
把最终结果进行返回,构造json对象,执行到这步,那说明没有什么问题,上面编译和运行出错时都会做错误处理,
这部分我没有贴出来
    (*resp)["error"] = 0;
    (*resp)["reason"] = "";
    std::string str_stdout;
    FileUtil::Read(StdoutPath(file_name), &str_stdout);
    (*resp)["stdout"] = str_stdout;
    std::string str_stderr;
    FileUtil::Read(StderrPath(file_name), &str_stderr);
    (*resp)["stderr"] = str_stderr;
    LOG(INFO) << "Program " << file_name << " Done" << std::endl;
    return true;

至此在线编译模块就告一段落了

题目管理模块

总体思路
获取所有的题目列表
指定题目的详细内容
调用在线编译模块完成代码的编译和运行
根据上述思路,那么我们就需要将题目描述组织起来。
首先基于文件的方式来完成题目的组织,创建一个行文本文件作为总的目录与入口文件,每一个题目对应一个目录,目录的名字就是题目的id
目录里面包含以下几个文件:
1 题目的详细描述
2. 代码框架
3. 代码测试用例
用一个结构体来表示单个的题目

struct Question{
    std::string id;
    std::string name;
    std::string dir; //题目对应的目录,目录包含了题目描述
                     //题目的代码框架/题目的测试用例
    std::string diff; //难度
    std::string desc; //题目的描述
    std::string header_cpp; //题目的代码框架中的代码
    std::string tail_cpp; //题目的测试用例代码
};

数据存储

整体思路
先打开oj_config.cfg文件(这个文件就是上述的总目录与入口文件,每一行都是一个题目,我选择用\t来作为分割符)
然后按行读取oj_config.cfg文件,并且根据\t进行切分与解析
根据解析出来的结果拼装到上述的结构体中,header.cpp文件与tail.cpp文件分别存放着代码框架中的代码与题目的测试用例代码,在编译运行的时候需要将这两个拼接起来。
最后将结构体存入哈希表中

将文件中的数据加载到结构体中
  bool Load(){
      //打开oj_config.cfg文件,按行读取,解析
      std::ifstream file("./oj_data/oj_config.cfg");
      if(!file.is_open()){
          return false;
      }
      std::string line;
      while(std::getline(file, line)){
          //根据解析结果填入结构体
          std::vector<std::string> tokens;
          UrlUtil::Split(line, "\t", &tokens);
          if(tokens.size() != 4){
              LOG(ERROR) <<  "config file format error!\n";
              continue;
          }
          Question q;
          q.id = tokens[0];
          q.name = tokens[1];
          q.diff = tokens[2];
          q.dir = tokens[3];
          FileUtil::Read(q.dir + "/desc.txt", &q.desc);
          FileUtil::Read(q.dir + "/header.cpp", &q.header_cpp);
          FileUtil::Read(q.dir + "/tail.cpp", &q.tail_cpp);
          //插入到hash表
          _model[q.id] = q;
      }
      file.close();
      LOG(INFO) << "Load" << _model.size() << " questions\n";
      return true;
  }

然后就是读取全部的题目与读取单个题目了

  bool GetAllQuestions(std::vector<Question>* questions) const{
      //遍历hash表
      questions->clear();
      for(const auto& kv : _model){
          questions->push_back(kv.second); 
      }
      return true;
  }

bool GetQuestion(const std::string& id, Question* q) const{
      const auto pos = _model.find(id);
      if(pos == _model.end()){
          return false;
      }
      *q = pos->second;
      return true;
  }

页面显示

这里我们根据数据生成对应的html文件,我们这里使用ctemplate来帮助我们完成。
这里我们需要三个html,一个是所有的题目的html,一个是单个题目的html,还有一个是返回的结果的html。
ctemplate可以使我们所要填充的数据和界面分离,相当于把我们需要计算填入的位置挖出一个空,然后在编写代码的时候,我们将其替换。

将所有题目页面进行渲染
    static void RenderAllQuestions(const std::vector<Question>& all_questions,
        std::string* html) {
      //将所有的题目数据转换成题目列表页html
      //通过网页模板的方式来构造html
      //创建一个总的ctemplate对象
      ctemplate::TemplateDictionary dict("all_question");
      for(const auto& question : all_questions){
          //循环往这个对象中田间一些子对象
          ctemplate::TemplateDictionary* table_dict
            = dict.AddSectionDictionary("question");
          //每个子对象再设置一些键值对和模板中的{{}}对应
          table_dict->SetValue("id", question.id);
          table_dict->SetValue("name", question.name);
          table_dict->SetValue("diff", question.diff);
      }

      //进行数据的替换,生成最终的html
      ctemplate::Template* tpl;
      tpl = ctemplate::Template::GetTemplate(
          "./template/all_questions.html",
          ctemplate::DO_NOT_STRIP);
      tpl->Expand(html, &dict);
    }

另外两个的渲染原理相同。

服务器

这里构建服务器,使用了第三方库httplib,同时用C++11中的正则表达式,来忽略转义字符,并且获取题号。最终完成三种页面的创建。

int main(){
    //加载题库数据
    OjModel model;
    model.Load();

    //使用第三方库httplib 来搭建服务器
    using namespace httplib;
    Server server;
    
    //所有题目页面
    server.Get("/",[&model](const Request& req, Response& resp){
        (void) req;

        //通过model获取所有的题目信息
        std::vector<Question> all_questions;
        model.GetAllQuestions(&all_questions);

        //将all_questions的数据转换为html
        std::string html;
        OjView::RenderAllQuestions(all_questions,&html);

        //将后端处理完的请求返回给客户端
        resp.set_content(html,"text/html");
    });

    //具体题目的页面
    server.Get(R"(/question/(\d+))",[&model](const Request& req, Response& resp){
        //通过model获取指定题目的信息
        Question question;
        model.GetQuestion(req.matches[1].str(),&question);

        //将question的数据转换为html
        std::string html;
        OjView::RenderQuestion(question,&html);

        //将后端处理完的请求返回给客户端
        resp.set_content(html,"text/html");
    });

    //代码运行结果界面
    server.Post(R"(/compile/(\d+))",[&model](const Request& req, Response& resp){
        //1. 通过model获取指定题目的信息
        Question question;
        model.GetQuestion(req.matches[1].str(),&question);

        //2. 解析body, 获取用户提交的代码
        std::unordered_map<std::string,std::string> body_kv;
        UrlUtil::ParseBody(req.body,&body_kv);
        const std::string& user_code = body_kv["code"];
        
        //3. 构造json格式的参数
        Json::Value req_json;
        //   编译的代码 = 用户提交的代码 + 测试用例代码
        req_json["code"] = user_code + question.tail_cpp;
        req_json["stdin"] = user_code;
        
        //4. 调用编译模块进行编译
        Json::Value resp_json;
        Compiler::CompileAndRun(req_json,&resp_json);

        //5. 将运行结果构造成HTML
        std::string html;
        OjView::RenderResult(resp_json["stdout"].asString(),
                             resp_json["reason"].asString(),&html);

        //6. 将后端处理完的请求返回给客户端
        resp.set_content(html,"text/html");
    });

以上就是全部内容啦
GitHub地址
在这里插入图片描述

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-08 11:03:10  更:2021-08-08 11:04:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/8 21:39:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码