负载均衡OJ

相关技术

  • 多进程、多线程
  • 系统级别文件读写、重定向
  • C++ STL 标准库
  • Boost库(split字符串切割)
  • jsoncpp第三方开源序列化、反序列化库
  • cpp-httplib第三方开源网络库
  • ctemplate第三方开源网页渲染库
  • MySQL C connect链接库

开发环境

  • CentOS7云服务器
  • vscode
  • MySQL Workbench
  • Postman

项目宏观结构及介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqEosusT-1680577501824)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20230403213309376.png)]

用户进行在线oj时,主要有三种行为:

  • 查看题目列表
  • 进入具体题目
  • 编写并提交代码

在服务器上部署了多台主机,执行compile_server服务,即编译运行服务,只负责对代码进行编译运行,

而在其中一台主机上部署oj_server服务,它是前端和后端的桥梁,为用户提供服务路由,负责接收用户请求、

统筹每台主机的任务数,负载均衡式地将用户代码转交给compile_server模块,再将结果返回给用户。

项目共三个核心模块:

  • 公共模块:提供各个模块需要的工具类,如日志、文件操作等
  • 编译运行模块:只负责对客户端上传的代码进行编译运行,并将运行信息返回给上层
  • oj_server: MVC为用户获取题目、提供编写能力,负载均衡、其他功能

项目详细规划和API

在这里插入图片描述

编译模块(compiler)

只负责编译接收到的代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1wN0Q9lk-1680577501826)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20230404084145565.png)]

运行模块(run)

运行结果有三种:

  • 运行成功,结果正确
  • 运行成功,结果错误
  • 运行失败,程序异常

运行模块只关心运行成功与否,结果正确与否无需关心。

结果是否正确,是由上层测试用例运行程序后判断结果决定的,与运行模块无关。

编译运行模块(compile_run):

直接与上层交互的模块,整合compile模块和run模块.

上面的compile和run模块虽然都能各自实现了功能,但是还需要我们自己在对应路径下先创建好源文件/可执行文件。

该模块要能够自动将用户上传数据进行序列化并生成文件,最后反序列化通过网络返回给用户。

在这里插入图片描述

compile_server:

void Usage()
{
    std::cout << "Please Enter 'port'" << std::endl;
    exit(-1);
}

int main(int args, char *argv[])
{

    //test();
    if (args != 2)
    {
        Usage();
    }

    // 服务端
    Server srv;

    // 注册处理Post请求的函数
    srv.Post("/compile_run", [](const Request &rqt, Response &rsp)
             {
                LOG(INFO) << "接收到编译运行请求" << std::endl;
                 // 提取请求正文
                 std::string in_json = rqt.body;
                 std::string out_json;

                 if (!in_json.empty())
                 {
                     // 调用cr模块
                     CompileAndRun::Start(in_json, out_json);
                     

                    std::cout << "运行完毕, json: " << std::endl << out_json << std::endl;
                    
                     //  LOG(INFO) << "cr完成,正在构建响应报文并回复..." << std::endl;
                     // 构建响应正文内容
                     rsp.set_content(out_json, "application/json;charset=utf-8");
                 }
                 else
                 {
                    LOG(ERROR) << "请求正文为空" << std::endl;
                    rsp.set_content("request is empty...", "text/plain;charset=utf8");
                 }
             });

    // 绑定ip and 端口号
    srv.listen("0.0.0.0", atoi(argv[1]));
}

oj_model模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QulmWWZv-1680577501827)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20230404093913417.png)]

获取所有题目列表:

  • 首先要有一个题目列表,在某个路径下,一定需要有一个文件夹(question)存放了每个题目文件
  • ./question/questionlist记录每个文件的属性,包括:
题目编号 题目标题 难度等级 时间限制 空间限制

例如:
1  两数之和  简单 10 300000
2  判断回文数 简单 10  200000

在question目录中,每一道题又都有自己的目录,以题目编号命名,例如:/question/1存放了题目的具体信息,包括:

  • desc:题目描述
  • header.cpp:题目预设代码,如:
#include <iostream>

class Solution
{
public:
    static bool isPalindrome(int x)
    {
        // write...
    }
};

显示给用户该代码,让用户实现该接口

  • tail.cpp:添加测试用例,调用head.cpp中用户填写的接口,判断是否通过用例。

最后提交给compile_run模块的代码 = header.cpp + tail.cpp 拼接起来的代码:

#include <iostream>

class Solution
{
public:
    static bool isPalindrome(int x)
    {
        // write...
    }
};



//这三行是为了在编写的时候,包含了header.cpp中的接口,不让编译器报一堆红色波浪号,便于编写。
//在实际拼接时,这几行代码我们要去掉。
#ifndef COMPILE_ONLINE
#include "header.cpp"
#endif


void test1()
{
    //121
    if(Solution::isPalindrome(121))
    {
        std::cout << "通过测试用例, 测试用例: 121" << std::endl;
    }
    else
    {
        std::cout << "未通过测试用例, 测试用例: 121" << std::endl;
    }
}


void test2()
{
    //-121
    if( !Solution::isPalindrome(-121))
    {
        std::cout << "通过测试用例, 测试用例: -121" << std::endl;
    }
    else
    {
        std::cout << "未通过测试用例, 测试用例: -121" << std::endl;
    }
}


int main()
{
    test1();
    test2();
}

oj_view模块

了解ctemplate库的使用

渲染网页

#include <iostream>
using namespace std;
#include <ctemplate/template.h>
#include <string>

int main()
{
    string in_html = "./test.html";

    //创建数据字典
    //dic是对象,test是字典的名字
    ctemplate::TemplateDictionary dic("test");  //类比  unordered<> test;
    
    string value = "你好,世界";

    //设置kv值
    dic.SetValue("key", value);  //类比 test.insert({pair{}})


    /***
     * GetTemplate:
     * 获取被渲染网页对象
     * 参数1:要渲染的网页
     * 参数2:选项,要不要去除空行、空格啥的?不要
    */
    ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP);


    //添加字典数据到网页中
    std::string out_html; //渲染后的网页

    /**
     * 参数1:输出性参数,返回渲染后的网页
     * 参数2:把数据字典交给被渲染的网页,共同构成渲染后的网页
    */
    tpl->Expand(&out_html, &dic);

    cout << out_html;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97ID5R9l-1680579021583)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20230330183541159.png)]

  • 在网页中, {{ }}两个花括号中的内容表示将来要被渲染的内容,填的是key值,最后被渲染时替换成value值

看下列代码:

{{#question_list}}
<tr>
    <td>{{number}}</td>
    <td>{{title}}</td>
    <td>{{star}}</td>
</tr>
{{/question_list}}

每一个题目都需要被渲染,每一个题目就需要三列这样的属性,如果有1万道题,那么就要重复写一万个这样的渲染,这显然是很抵消的,所以有了循环渲染

{{#}} :#表示这是循环渲染的起点,后面紧跟着起点名(锚点名)

{{/}} :/表示这是循环渲染的终点,后面紧跟着锚点名(起点名 = 终点名)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r6YofzjF-1680579021584)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20230404094558608.png)]

//循环渲染
bool AllExpandHtml(std::vector<Question> &all, std::string &html)
    {

      if (all.size() == 0)
      {
        LOG(FATAL) << "准备渲染所有题目列表的网页,但题目列表为空" << std::endl;
        exit(-1);
      }

      // 待渲染网页
      std::string in_html = template_path + "all_question.html";

      // 创建数据字典
      ctemplate::TemplateDictionary dic("all_quetion"); 

      for (auto &elm : all)
      {
        ctemplate::TemplateDictionary* sub = dic.AddSectionDictionary("question_list");

        /**
         * 创建子字典
         * 子字典名称不能随意取,它决定最后循环渲染的是哪部分,
         * 当然,如果你没有子字典,说明你不需要循环渲染,只有一个主字典
         * 那就像上面说的,主字典怎么取都无所谓,因为只要进行一次kv映射就结束了,整个网页一共就一个要渲染的,那就不用区分
         * */
        sub->SetValue("number", elm.number);
        sub->SetValue("title", elm.title);
        sub->SetValue("star", elm.star);
        //LOG(INFO) << "创建子字典成功" << std::endl;
      }

循环渲染时,需要创建子字典:

  • 主字典名字可以随便取,因为有待渲染的网页的路径,所以无所谓

  • 子字典名称不能随意取。

    循环渲染网页时需要指明子字典名称。所以html文件中渲染字典的名称必须和子字典名称相同。

    当然,如果你没有子字典,不需要循环渲染,只有一个主字典

    那就像上面说的,主字典怎么取都无所谓,因为只要进行一次kv映射就结束了,整个网页一共就一个要渲染的,那就不用区分。

oj_control模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qS4v4Por-1680577501828)(C:/Users/Z-zp/AppData/Roaming/Typora/typora-user-images/image-20230404104238349.png)]

oj_server:

#include <iostream>
#include "../comm/httplib.h"
#include "../comm/util.hpp"
#include "oj_control.hpp"
using namespace httplib;
using namespace ns_util;
using namespace ns_control;

int main(int args, char* argv[])
{
    Server svr;
    Controller ctl; //控制器
    
    /**
     * 注册资源路由
     * 获取所有题目列表
     *
     * */
    svr.Get("/all_question", [&ctl](const Request &req, Response &resp)
            {
        std::string _html;
        //LOG(INFO) << "申请所有题目列表:" << std::endl; 
        ctl.AllQuestion(_html);
        resp.set_content(_html, "text/html;charset=utf-8"); 
        });


    /**
     * 注册资源路由
     * 获取指定题目
     * \d 正则表达式,匹配多个数字
     * */

    svr.Get(R"(/question/(\d+))", [&ctl](const Request &req, Response &resp)
            {
        std::cout << "收到请求报文, url: " << req.path << std::endl;
        std::string number = req.matches[1];
        LOG(INFO) << "申请获取题目编号:" << number << std::endl; 
        std::string _html;
        ctl.Question(number, _html);
        resp.set_content(_html, "text/html;charset=utf-8"); });

    /***
     * 注册资源路由
     * 提交代码, 测试用例
     */

    svr.Post(R"(/judge/(\d+))", [&ctl](const Request &req, Response &resp)
             {
                //  LOG(INFO) << "编译运行路由" << std::endl;
                 std::string number = req.matches[1];
                 std::string out_json;
                 ctl.Judge(number, req.body, out_json);
                 resp.set_content(out_json, "application/json;charset=utf-8");
             });

    svr.set_base_dir("./wwwroot");
    svr.listen("0.0.0.0", 8080);
}
svr.Post(R"(/judge/(\d+))", [&ctl](const Request &req, Response &resp)
             {
                //  LOG(INFO) << "编译运行路由" << std::endl;
                 std::string number = req.matches[1];
                 std::string out_json;
                 ctl.Judge(number, req.body, out_json);
                 resp.set_content(out_json, "application/json;charset=utf-8");
             });

(\d+):正则表达式

\d表示匹配数字,+表示一个或多个

合起来就是匹配一个或多个数字。

因为我们的题目编号,比如像 100,101,102…
如果写具体的数字,就把资源路由写死了,比如 /questions/100,那注册的这个Get函数,只能用于处理路径/questions/100中的资源。这样的话,如果有很多题,那么每一道题都需要注册一个Get函数处理,不现实。

std::string number = req.matches[1];

matches中保存了正则表达式的内容

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿波呲der

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值