在线OJ系统

article/2025/10/8 8:44:37

在线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);}//进行数据的替换,生成最终的htmlctemplate::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的数据转换为htmlstd::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的数据转换为htmlstd::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. 将运行结果构造成HTMLstd::string html;OjView::RenderResult(resp_json["stdout"].asString(),resp_json["reason"].asString(),&html);//6. 将后端处理完的请求返回给客户端resp.set_content(html,"text/html");});

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


http://chatgpt.dhexx.cn/article/tjrnC1qa.shtml

相关文章

分享一个OJ平台——浙江工商大学的OJ平台

1.引言 最近是有总喜欢讨论算法题&#xff0c;因为他们在准备考研复试&#xff0c;为什么我不准备呢&#xff1f;这是一个悲伤的故事&#xff0c;刚好自己也有面试遇到只能使用C和C的代码题&#xff0c;他们说这OJ平台相对简单一些&#xff0c;那些刷不来LeetCode可以试试这个&…

算法OJ题(1)

1.删除有序数组中的重复项 原题链接&#xff1a;https://leetcode.cn/problems/remove-duplicates-from-sorted-array/ 思路&#xff1a;使用双指针算法可以使时间复杂度达到O(1). 具体过程&#xff1a;&#xff08;以画图解释&#xff09; 代码实现&#xff1a; int remove…

你想知道刷题的秘密吗?一篇博客让你明白什么是OJ题,如何刷OJ题,并深入浅出的带你刷遍顺序表的OJ题(小白必看)

目录 0.前言 1.什么是OJ 1.1 OJ简述 1.2 OJ的分类&#xff08;尤其接口型OJ&#xff0c;带你剖析得明明白白&#xff09; 1.3 接口型OJ的输入型参数 2. 剑指 Offer 56 - I. 数组中数字出现的次数 3. 88. 合并两个有序数组 4. 27.移除元素 5. 26. 删除有序数组中的重复…

Online Judge系统(简称OJ)

什么是OJ Online Judge系统&#xff08;简称OJ&#xff09;是一个在线的判题系统。用户可以在线提交程序源代码&#xff0c;系统对源代码进行编译和执行&#xff0c;并通过预先设计的测试数据来检验程序源代码的正确性。 一个用户提交的程序在Online Judge系统下执行时将受到比…

OJ的介绍以及使用方法

OJ系统简介 Online Judge系统&#xff08;简称OJ&#xff09;是一个在线的判题系统。用户可以在线提交程序多种程序&#xff08;如C、C&#xff09; 源代码&#xff0c;系统对源代码进行编译和执行&#xff0c;并通过预先设计的测试数据来检验程序源代码的正确性。 一个用户提…

C++-OJ

题目源自某高校C的OJ 本文字数过多&#xff0c;所有代码&#xff0c;都在这里 将做过的C OJ题目整理合并了一下&#xff0c;总共有一百多道题目&#xff0c;手动合并自然是不可能的 这个时候我们就可以使用shell脚本来取代这种机械化的重复劳动了&#xff0c;由于写成博客需要…

杭电OJ刷题指南(ACM)

除了杭电OJ&#xff0c;还有北大OJ&#xff0c;华科OJ等。 1.首先&#xff0c;打开百度&#xff0c;输入“杭电OJ”进行搜索 2.然后进入杭电OJ官网&#xff0c;进行注册 3.输入注册信息 4.接下来进入详细注册界面&#xff0c;将信息填写完整后点击Submit 5.接下来即可登录…

OJ系统

什么是OJ Online Judge系统&#xff08;简称OJ&#xff09;是一个在线的判题系统。用户可以在线提交程序源代码&#xff0c;系统对源代码进行编译和执行&#xff0c;并通过预先设计的测试数据来检验程序源代码的正确性。 一个用户提交的程序在Online Judge系统下执行时将受到比…

最适合编程训练的三大OJ(从易到难)

按照学习的需求来说依次介绍&#xff0c;方便初学者使用&#xff1a; 1.Dotcpp网www.dotcpp.com 优点&#xff1a;全中文OJ&#xff0c;题目分类&#xff0c;有题解 这可能是初学者最好用的OJ甚至没有之一了 全中文的友好操作&#xff0c;充足的语法基础题、二级C语言题、…

各大OJ刷题平台汇总

前言 想成为一名高级工程师&#xff0c;算法还是要有的&#xff0c;至少在入门编程和进入公司之前都是必备的。刷题也可以很好的锻炼动手能力和逻辑思维&#xff0c;今天就给大家介绍一些知名的在线刷题平台,都是干货哦&#xff01; 力扣(leetcode) 江湖人尽皆知的站点&#xf…

OJ的基本原理

OJ的简介&#xff1a;OJ(Online Judge)&#xff0c;就是一个线上判断的接口&#xff0c;通常用于一些比赛中的程序判断&#xff0c;也就是将符合程序的结果放在OJ上&#xff0c;来进行程序验证&#xff0c;后来的大量的互联网公司也开始使用OJ来判断题目的正确性。 OJ的分类 …

【OJ】OJ的介绍和常用OJ推荐

目录 OJ的介绍编程刷题OJ集合计蒜客(主刷编程题 &#xff09;牛客网(编程题和选择题都有)力扣(主要编程大题)领扣(主刷大题)Comet OJACMOREXidian Programming Contest Online Judge晴问浙江中医药OJZOJHDUOJ华东师范OJ洛谷codeforces 常见的测评结果 OJ的介绍 Online Judge 简…

OJ基础题库(1)

收录内容 P1005 最大公约数 p1008 质数还是合数 p1019 修改字符串 p1021 日历问题 #include<iostream> using namespace std;int max_yueshu(int x,int y){int temp;while(y!0){tempx%y;xy;ytemp;}return x; }int main(){int n,i;cin>>n; //输入对数int x,y,res;…

OJ算法分析题库(2)

P1476 加工生产调度 【实验】贪心算法之流水作业调度问题&#xff08;分析&#xff09; P1748 abcd0 P1750 求逆序对 P1746 求解查找最后一个数小于等于指定数的元素问题 P1477 部分背包问题&#xff08;分析&#xff09; 算法分析&#xff1a; 算法大致的思路就是通过四…

小程序生成网址链接,网址链接跳转小程序(附详细流程)

1.点击右上角工具下拉选择生成URL Scheme 2.填写好信息点击生成 3. 可以得出一个 weixin://dl/business/?tbAXXXXX 这样的链接&#xff0c;点击就可以调整到小程序拉&#xff0c;但是这种只能在微信打开哦。

小程序接入流量主、banner广告、激励广告

小程序接入广告 1.什么是微信小程序广告2.流量主开通流程3.微信小程序流量主开通以及添加广告步骤(1) 接入banner广告(2) 接入激励广告 1.什么是微信小程序广告 2.流量主开通流程 申请成为流量主后才能接入广告首先进入小程序后台&#xff0c;点击流量主&#xff0c;点击开通…

微信小程序实现登录注册页面

一、在小程序中引入iconfont 1.在官网搜索需要的图标 2.将图标添加至项目 3.打开我的项目 4.选择font class&#xff0c;点击下方的链接&#xff0c;打开生成的源码 5.在styles文件夹下新建iconfont.wxss&#xff0c;拷贝生成的源码到该文件中 二、编写登录注册页面 …

小程序获取用户的openid(详解)

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识&#xff0c;快速建立小程序内的用户体系然而因为小程序中的openid不可以直接使用需要用code&#xff08;登录凭证&#xff09;去换取openid 获取openid的思路 获取openid首先需要调用小程序的login方法…

微信小程序使用腾讯地图完整流程

前言&#xff1a;开发小程序需要得到定位和位置的省市区文字信息&#xff0c;看了所有的文章都没有一个完整和像样的&#xff0c;全是copy来&#xff0c;copy去的文章 - _ -&#xff01;&#xff0c;一怒之下&#xff0c;开始了自己的踩坑之路 第一步&#xff1a;申请腾讯地图…

小程序生命周期

小程序的生命周期说白了就是指程序从创建、到开始、暂停、唤起、停止、卸载的过程。 我们大概从三个角度看一下小程序的生命周期。 &#xff08;1&#xff09;&#xff1a;应用生命周期 &#xff08;2&#xff09;&#xff1a;页面生命周期 &#xff08;3&#xff09;&…