【项目设计】负载均衡在线OJ

🎇Linux:


  • 博客主页:一起去看日落吗
  • 分享博主的在Linux中学习到的知识和遇到的问题
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。

在这里插入图片描述


🌿1. 项目说明

实现了一个负载均衡式的在线OJ平台,用户可以在浏览器访问各个题目,在编辑区编写代码提交,后端对代码进行编译运行,最终为用户返回结果。

OJ模块基于MVC结构,调用数据库将题目显示给用户,用户编写提交代码,OJ模块通过网络通信,负载均衡式地选择compiler模块服务器,将用户代码和测试用例组合,编译运行后将结果返回给用户。

🌿2. 所用技术与开发环境

所用技术:

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

开发环境:

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

🌿3. 项目宏观结构

代码结构:

在这里插入图片描述
项目核心是三个模块:

  • comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。
  • compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。
  • oj_server : 请求题目列表;请求一个具体题目,且有编辑区 ;提交判题请求。采用MVC的设计模式,使用负载均衡,访问文件或数据库,调用编译模块,以及把题目列表和编辑界面展示给用户。

在这里插入图片描述
编写思路:

  1. compile_server
  2. oj_server
  3. 版本一:基于文件版本的OJ
  4. 前端页面设计
  5. 版本二:基于mysql版本的OJ

🌿4. 编译与运行服务

🍃 4.1 编译功能

在这里插入图片描述


🍁4.1.1 compiler.hpp

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>//waitpid
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

#include "../comm/log.hpp"

//只负责进行代码的编译

namespace ns_compiler
{
    //引入ns_util工具类(路径拼接)
    using namespace ns_util;
    using namespace ns_log;

    class Compiler
    {
    public:
        Compiler()
        {}
        ~Compiler()
        {}
        //返回值:编译成功:true 否则:false
        //输入参数:编译的文件名
        //flie_name:1234
        //1234.cpp -> ./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)
            {
                int _stderr =  open(PathUtil::Stderr(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if(_stderr < 0)
                {
                    LOG(WARNING) << "没有成功形成stderr文件" << "\n";
                    exit(1);
                }
                //重定向标准错误到_stderr
                dup2(_stderr, 2);
                
                //程序替换,并不影响进程的文件描述符表
                //子进程:调用编译器,完成对代码的编译工作
                //g++ -o target src -std=c++11
                execlp("g++","g++","-o",PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(),"-std=c++11",nullptr);
                LOG(ERROR) << "启动编译器g++失败,可能是参数错误" << "\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;
        }
    };
}

🍁4.1.2 log.hpp

#pragma once

#include <iostream>
#include <string>
#include "util.hpp"

namespace ns_log
{
    using namespace ns_util;

    //日志等级
    enum
    {
        INFO,//常规的,没有任何错误信息,只是一些提示信息
        DEBUG,//调试时的调试日志
        WARNING,//告警,不影响后续使用
        ERROR,//错误,这个用户的请求不能继续了
        FATAL,//不光这个用户,整个系统都无法使用,引起系统整个出错
        //补充:如果正常工作中出现ERROR,FATAL那么就需要运维来解决
    };

    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 << message;//不要endl进行刷新

        return std::cout;
    }
    //LOG() << "message" << "\n"
    //开放式日志
    #define LOG(level) Log(#level,__FILE__,__LINE__)
}

🍁4.1.3 util.hpp

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <unistd.h>
#include <sys/time.h>

namespace ns_util
{
    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 Stderr(const std::string &file_name)
        {
            return AddSuffix(file_name,".stderr");
        }
    };

    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;
        }
    };

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

🍁4.1.4 makefile

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

🍁4.1.5 测试

  1. 在temp文件下创建一个code.cpp
  2. 在code.cpp内写下一段代码(正确的)
  3. 在compile_server.cc内调用
#include <iostream>

int main()
{

    std::cout << "hello byh" << std::endl;

    return 0;
}

make生成可执行程序之后即可查看

  • 成功:
    在这里插入图片描述

  • 失败:

在这里插入图片描述


🍃 4.2 运行功能

🍁4.2.1 runner.hpp

  • 设计思路:

程序运行:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,异常了

Run不需要考虑代码跑完,结果是否正确,测试用例决定的;我们只考虑:是否正确运行完毕


  • 问题:可执行程序是谁?

一个程序在默认启动的时候

  1. 标准输入: 不考虑用户自测
  2. 标准输出:程序运行完成,输出结果是什么
  3. 标准错误:运行时错误信息
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <wait.h>

#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:
        //指明文件名即可,不需要代理路径,不需要带后缀
        //指名文件名即可,不需要带路径,带后缀
        //返回值>0:程序异常了,退出时收到了信号,返回值就是对应的信号编号
        //返回值==0:正常运行完毕,结果保存至对应的临时文件中
        //返回值<0:内部错误(打开文件失败,创建子进程失败)
        static int Run(const std::string &file_name)
        {
            /*********************************************
            * 程序运行:
            * 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 || _stderr_fd < 0 || _stdout_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);

                execl(_execute.c_str()/*我要执行谁*/, _execute.c_str()/*我想在命令行上如何执行该程序*/, nullptr);
                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;
            }
        }
    };
}

🍁4.2.2 util.hpp新增

//运行时需要有的临时文件
        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");
        }
        //构建该程序对应的标准错误完整路径+后缀名
        static std::string Stderr(const std::string &file_name)
        {
            return AddSuffix(file_name,".stderr");
        }

🍁4.2.3 测试

  • 成功:

在这里插入图片描述

  • 往标准错误中输入
#include <iostream>

int main()
{

    std::cout << "hello byh" << std::endl;
    //写入标准错误
    std::cerr << "hello error" << std::endl;

    return 0;
}

在这里插入图片描述

  • 失败:

在这里插入图片描述

在这里插入图片描述


🍁4.2.4 资源限制测试

假设当用户提交的代码是恶意代码:占用大量空间,时间复杂度极高,对程序不友好,所以我们必要要对资源进行限制

  • 无限循环

我们用一个while(1)模拟用户提交一个恶意程序,我们可以看到程序一直会在运行无法终止,所以我们需要增加限制使他停下来
在这里插入图片描述

在这里插入图片描述
并且我们可以查看导致退出的原因是什么信号,这里导致无限循环错误的退出的信号是24,我们可以通过kill -l来查看信号大全

在这里插入图片描述

  • 内存问题

我们模拟程序一直申请内存,会导致程序资源严重浪费,所以同样需要对程序进行限制

在这里插入图片描述

我们可以看到,进行限制之后当内存申请到一定限度之后就会退出,返回的是6号信号

在这里插入图片描述

  • 测试代码
#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
#include <signal.h>

void handler(int signo)
{
    std::cout << "signo: " << signo << std::endl;
    exit(1);

}

int main()
{
    //资源不足,导致OS终止进程,是通过信号终止
    //查看终止信号;
    for(int i = 1;i <= 31;i++)
    {
        signal(i,handler);
    }


    //时间问题

    //限制累计运行时常
    struct rlimit r;
    r.rlim_cur = 1;
    r.rlim_max = RLIM_INFINITY;
    setrlimit(RLIMIT_CPU,&r);
    
    while(1);

    //内存问题

    // 限制空间
    struct rlimit r;
    r.rlim_cur = 1024 * 1024 * 20;//20M
    r.rlim_max = RLIM_INFINITY;
    setrlimit(RLIMIT_AS,&r);

    int count = 0;
    while(true)
    {

        int *p = new int[1024*1024];
        count++;
        std::cout << "size: " << count << std::endl;
        sleep(1);
    }
    return 0;
}
  • 查看信号

在这里插入图片描述


🍁4.2.5 运行限制

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <wait.h>
#include <sys/time.h>
#include <sys/resource.h>

#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 SetProcLimit(int _cpu_limit,int _men_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=_men_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 || _stderr_fd < 0 || _stdout_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);


                SetProcLimit(cpu_limit,mem_limit);

                execl(_execute.c_str()/*我要执行谁*/, _execute.c_str()/*我想在命令行上如何执行该程序*/, nullptr);
                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;
            }
        }
    };
}

🍃 4.3 编译并运行功能

🍁4.3.1 为什么需要单独实现

编译服务随时可能被多人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,要不然多个用户之间会互相影响

所以我们需要做:

  1. 适配用户请求,制定通信协议
  2. 正确调用compile 和 run
  3. 形成唯一的文件名

在这里插入图片描述


🍁4.3.2 认识json

json实际上是序列化的工作,作用是将结构化数据转化成为一个字符串,而Value是一个Json的中间类,可以填充KV值

  • 按照json库

不同的json库的用法是不一样的,这里我们运用一个对于云服务器来说最简单的

sudo yum install jsoncpp-devel

安装成功之后我们就可以运用json库了

#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
#include <signal.h>
#include <signal.h>
#include <jsoncpp/json/json.h>


int main()
{
    //序列化工作
    //将结构化数据转化为一个字符串
    //Value是一个Json的中间类,可以填充KV值
    Json::Value root;
    root["code"] = "mycode";
    root["user"] = "byh";
    root["age"] = 20;

    Json::StyledWriter writer;
    std::string str = writer.write(root);
    std::cout << str << std::endl;
}
  • 测试

必须得链接jsoncpp库才不会报错

在这里插入图片描述


🍁4.3.3 compile_run.hpp

#pragma once

#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <signal.h>
#include <unistd.h>

#include <jsoncpp/json/json.h>

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


        // code > 0 : 进程收到了信号导致异常奔溃
        // code < 0 : 整个过程非运行报错(代码为空,编译报错等)
        // code = 0 : 整个过程全部完成
        //待完善
        static std::string CodeToDesc(int code, const std::string &file_name)
        {
            std::string desc;
            switch(code)
            {
                case 0:
                    desc = "编译运行成功";
                    break;
                case -1:
                    desc = "提交的代码是空";
                    break;
                case -2:
                    desc = "未知错误";
                    break;
                case -3:
                    //desc = "代码编译的时候发生了错误";
                    FileUtil::ReadFile(PathUtil::CompilerError(file_name),&desc,true);
                    break;
                case SIGABRT: // 6
                    desc = "内存超过范围";
                    break;
                case SIGXCPU: // 24
                    desc = "CPU使用超时";
                    break;
                case SIGFPE: // 8
                    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)
        {
            //反序列化读取信息
            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 mem_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)
            {
                    // out_value["status"] = -1;//代码为空
                    // out_value["reson"] = "用户提交的代码是空的";
                    // //序列化过程
                    // return;

                    status_code = -1;//代码为空
                    goto END;
            }
            //形成的文件名只具有唯一性,没有目录没有后缀
            //毫秒级时间戳+原子性递增唯一值,来保证唯一性
            file_name = FileUtil::UniqFileName();
            
            //形成临时src文件
            if(!FileUtil::WriteFile(PathUtil::Src(file_name),code))
            {
                // out_value["status"] = -2;//未知错误
                // out_value["reson"] = "发生未知错误";
                // //序列化过程
                // return;

                status_code = -2;//未知错误
                goto END;
            }

            if(!Compiler::Compile(file_name))
            {
                // //编译失败
                // out_value["status"] = -3;//代码编译的时候发生错误
                // out_value["reson"] = FileUtil::ReadFile(PathUtil::CompilerError(file_name));
                // //序列化过程
                // return;

                status_code = -3;//代码编译的时候发生错误
                goto END;
            }

            run_result = Runner::Run(file_name,cpu_limit,mem_limit);
            if(run_result < 0)
            {
                // out_value["status"] = -2;//未知错误
                // out_value["reson"] = "发生未知错误";
                // //序列化过程
                // return;

                status_code = -2;
            }
            else if(run_result > 0)
            {
                // out_value["status"] = code;//运行报错
                // out_value["reson"] = SignoToDesc(code);//将信号转化为报错原因
                // //序列化过程
                // return;

                //程序运行崩溃了
                status_code = run_result;
            }
            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;
                    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);
        }
    };
}

🍁4.3.4 util.hpp部分更新


    //对文件(path)的操作方法
    class FileUtil
    {
    public:
        //查看文件是否存在
        static bool IsFileExists(const std::string &path_name)
        {
            //方法一:查看文件是否能够正常打开
            //方法二:stat(文件路径,文件属性(可以自己选择自己需要的属性));
            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;
        }

        //将code写到target中,形成临时src文件
        static bool writeFile(const std::string &target, const std::string &code)
        {
            std::ofstream out(target);
            if (!out.is_open())
            {
                return false;
            }
            out.write(code.c_str(), code.size());
            out.close();
            return true;
        }

        //将文件内容读取
        // target文件名,content内容保存地址,keep是否保存\n
        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不保存行分隔符
            // getline内部重载了强制类型转化
            while (std::getline(in, line))
            {
                (*content) += line;
                (*content) += (keep ? "\n" : "");
            }
            in.close();
            return true;
        }
    };


🍁4.3.5 测试

  • 测试
#include "compile_run.hpp"

using namespace ns_compile_and_run;

//编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有
//唯一性,要不然多个用户之间会互相影响

int main()
{
    //提供的编译服务,打包形成一个网络服务
    //cpp-httplib

    // in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
    // out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
    // 通过http 让client 给我们 上传一个json string
    // 下面的工作,充当客户端请求的json串
    std::string in_json;
    Json::Value in_value;
    
    //R"()", raw string
    in_value["code"] = R"(#include<iostream>
    int main(){
    std::cout << "你可以看见我了" << std::endl;
    return 0;
    })";
    
    in_value["input"] = "";
    in_value["cpu_limit"] = 1;
    in_value["mem_limit"] = 10240*3;
    Json::FastWriter writer;
    in_json = writer.write(in_value);
    std::cout << in_json << std::endl;
    
    //这个是将来给客户端返回的json串
    std::string out_json;
    CompileAndRun::Start(in_json, &out_json);
    std::cout << out_json << std::endl;
    return 0;
}

在这里插入图片描述

可以看到我们的代码已经运行成功了,同时我们也可以设置无限循环和不断申请内存来测试,最后都可以测试通过,但是后两者会返回所异常的信号量


🍁4.3.6 处理临时文件

我们每次测试代码都会产生很多的临时文件,当达到一定程度的时候就肯定会出问题,所以我们需要对临时文件进行处理,对此我们还需要设计一个函数,在CompileAndRun后面调用来清理临时文件。

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


🍃 4.4 形成网络服务

这里我们需要用到一个网络库,cpp-httplib,这里只需要百度一下就可以安装了,或者有个更加简单的方法,直接把httplib拷贝到项目下,引入头文件直接就可以使用了

🍁4.4.1 可能遇见问题

  • 问题一

这里我们需要用到高版本的gcc,不然就有可能报错,所以我们这里还是需要升级一下GCC,百度有很多方法,自行解决,建议升级最新版

  • 问题二

编译的时候可能会遇到线程库问题,这里我们还需要在makefile后面添加-lpthread

  • 问题三

httplib所占用空间太多,有时候导致系统运行不成功,所以这时我们需要重启一下vscode


🍁4.4.2 实现代码

#include "compile_run.hpp"
using namespace ns_compile_and_run;

#include "../comm/httplib.h"
using namespace httplib;

void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}

//  ./compile_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    Server svr;
    svr.Post("/compile_and_run", [](const Request &req, Response &resp)
             {
        // 用户请求的服务正文:json string
        std::string in_json = req.body;
        std::string out_json;
        if(!in_json.empty()){
            CompileAndRun::Start(in_json, &out_json);
            resp.set_content(out_json, "application/json;charset=utf-8");
        } });

    svr.listen("0.0.0.0", atoi(argv[1])); //启动http服务
}

🌿5. 基于MVC 结构的oj 服务设计–oj_server

本质:建立一个小型网站

🍃 5.1 功能设计

  1. 获取首页,用题目列表充当
  2. 编辑区域页面
  3. 提交判题功能(编译并运行)
  • M: Model,通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL)
  • V: view, 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
  • C: control, 控制器,就是我们的核心业务逻辑

🍃 5.2 用户请求的服务路由功能

oj_server.cc实现用户请求的服务路由功能

#include <iostream>
#include "../comm/httplib.hpp"


using namespace httplib;

int main()
{
    //用户请求的服务路由功能
    Server svr;
    //获取所有的题目列表
    svr.Get("/all_questions",[](const Request &req,Response &resp){
        resp.set_content("这是所有题目的列表","text/plain;charset=utf-8");
    });
    //用户要根据题目编号,获取题目的内容
    //  /quetions/100
    //R"()",原始字符串raw string,保持字符串内容的原貌,不用做相关的转义
    svr.Get(R"(/question/(\d+))",[](const Request &req,Response &resp){
        std::string number = req.matches[1];
        resp.set_content("这是指定的一道题"+number,"text/plain;charset=utf-8");
    });
    //用户提交代码,使用我们的判题功能(1. 每道题的测试用例 2. compile_and_run)
    svr.Get(R"(/jude/(\d+))",[](const Request &req,Response &resp){
        std::string number = req.matches[1];
        resp.set_content("指定题目的判题"+number,"text/plain;charset=utf-8");
    });
    
    svr.set_base_dir("./wwwroot");
    svr.listen("0,0,0,0",8080);
    return 0;
}

🍃 5.3 题库设计(文件版)

🍁5.3.1 实现逻辑

  1. 题目的编号
  2. 题目的标题
  3. 题目的难度
  4. 题目的描述,题面
  5. 时间要求(内部处理)
  6. 空间要求(内部处理)

两批文件构成:

  • 第一个:questions.list:题目列表(不需要出现题目的内容)
  • 第二个:题目的描述,预设值的代码(hander.cpp),测试用例代码(tail.cpp)通过文件的编号,产生关联的

🍁5.3.2 实现例子

  • desc.txt

这里用来写题目描述

求一个数组中最大的值

示例 1:
输入: [1,2,3,4,5,6,7]
输出: 7

示例 2:
输入: [-1,1,2,3,4,5,6,7,9]
输出: 9

  • header.hpp

这里用来给用户写代码

#include <iostream>
#include <vector>
using namespace std;

class Solution
{
public:
    int FindMax(vector<int>& v)
    {
        
        return true;
    }
};

  • tail.hpp

这里用来做测试用例,设计测试用例是特别考验对于代码的理解能力的

#ifndef CompileOnline
// 这是为了编写用例的时候有语法提示. 实际线上编译的过程中这个操作是不生效的.
#include "header.cpp"
#endif

void Test1()
{
    vector<int> v={1,2,3,4,5,6,7};
    int ret = Solution().FindMax(v);
    if (ret==7)
    {
        std::cout << "Test1 ok!" << std::endl;
    }
    else
    {
        std::cout << "测试用例: {1,2,3,4,5,6,7} 未通过" << std::endl;
    }
}
void Test2()
{
    vector<int> v={-1,1,2,3,4,5,6,7,9};

    int ret = Solution().FindMax(v);
    if (ret==9)
    {
        std::cout << "Test2 ok!" << std::endl;
    }
    else
    {
        std::cout << "测试用例: {-1,1,2,3,4,5,6,7,9} 未通过" << std::endl;
    }
}
int main()
{
    Test1();
    Test2();
    return 0;
}

后续如果我们想要更新题库,只需要按照一样的逻辑编写代码即可,然后赋予唯一的题目编号即可


🍃 5.4 oj_model.hpp

  • 和数据进行交互,对外提供访问数据的接口
  • 根据题目.list文件,加载所有的题目信息到内存中
  • OJ需要的是 header.hpp+用户写的内容 + tail.cpp
#pragma once
//文件版本

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include <cstdlib>
#include <cassert>
#include "../comm/log.hpp"
#include "../comm/util.hpp"

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

namespace ns_model
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;

    struct Question{
        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 questins_list = "./questions/questions.list";
    const std::string questins_path = "./questons/"

    class Model{
    private:
        //题号:题目细节
        //题号 : 题目细节
        unordered_map<string, Question> questions;
    public:
        Model()
        {
            assert(LoadQuestionList())
        }

        bool LoadQuestionList(const std::string &question_list)
        {
            //加载配置文件:questions/questions.list + 题目编号文件
            ifstream in(question_list);
            if(!in.is_open())
            {
                LOG(FATAL) << " 加载题库失败,请检查是否存在题库文件" << "\n";
                return false;
            }

            std::string line;
            while(getline(in,line))
            {
                vector<string> 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.men_limit = atoi(tokens[4].c_str());

                string _path = questins_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.cc",&(q.tail),true);

                question.insert({q.number,q});
            }   
            LOG(INFO) << " 加载题库..成功" << "\n";
            in.close();
        }

        void GetAllQuestions(vector<Question> *out)
        {
            if(question.size() == 0)
            {
                LOG(ERROR) << "用户获取题目失败,题目编号:" << number << "\n";
                return false;
            }
            for(const auto &q : questions)
            {
                out->push_back(q.second);//first:key,second:value
            }
        }

        void GetOneQuestion(const std::string &number,Question *q)
        {
            const auto& iter = questions.find(number);
            if(iter == questions.end())
            {
                return false;
            }
            (*q) = iter->second;
            return true;
        }

        ~Model()
        {}
    };
}

🍃 5.5 oj_view.hpp

🍁5.5.1 ctemplate引入

ctemplate最初被称为谷歌模板,因为它起源于用于谷歌搜索结果页面的模板系统。ctemplate 用于linux下的web开发,可以动态生成一个html网页,这里的 “ 动态 ” 指的是网页的数据不是固定的,可以使用变量来填充网页内容。

可以在gitee上搜索 ctemplate,选择一个下载。gitee下载链接: ctemplate下载

  • 在Linux命令行输入: git clone 复制的链接

🍁5.5.2 ctemplate安装

  • 接下来开始安装,先进入到 ctemplate 目录下
  • 第一步,因为是源码安装,需要手动运行安装程序,输入: ./autogen.sh
  • 第二步,输入:./configure
  • 第三步,输入:make
  • 如果出现了编译报错,大概率是gcc编译器版本过低的问题,可以输入gcc -v查看一下版本,此时需要更新版本。
  • 输入:sudo make install

🍁5.5.3 ctemplate使用

现在这个库已经安装到了当前系统中,我们可以在任意 .cc 文件中调用这个库

  • test.cc
#include <ctemplate/template.h>
#include <string>
#include <iostream>
 
int main(){
  // 形成数据字典
  ctemplate::TemplateDictionary dic("test");
  dic.SetValue("name", "张三");                // 相当于插入了一个键值对(name会在下面的网页模板中出现)
 
  // 构建空网页模板对象
  std::string empty_html = "./test.html";     // 空的网页模板
  ctemplate::Template* tp = ctemplate::Template::GetTemplate(empty_html, ctemplate::DO_NOT_STRIP);
  
  // 渲染网页模板(将网页中的变量 name 替换成 "张三")
  std::string filled_html;
  tp->Expand(&filled_html, &dic);
 
  std::cout << filled_html << std::endl;
  return 0;
}
  • test.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>Document</title>
</head>
<body>
  <!-- 渲染时,变量name会被替换成对应的值 -->
  <h1>{{name}}</h1>
</body>
</html>

  • 编译时,需要添加第三方库的依赖 -lctemplate、-lpthread。因为ctemplate库用到了pthread库

在这里插入图片描述


🍁5.5.4 渲染网页

#pragma once

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

#include "oj_model.hpp"

namespace ns_view
{   
    using namespace ns_model;

    const std::string template_path = "./template_html/"

    class View
    {
    public:
        view_(){}
        ~view_(){}
    public:
        void AllExpandHtml(const vector<struct Question> &questions,std::string *html)
        {
            // 题目的编号 题目的标题 题目的难度
            // 推荐使用表格显示
            // 1. 形成路径
            std::string src_html = template_path + "all_questions.html";
            // 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. 获取被渲染的网页
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);

            // 4. 开始完成渲染功能
            tpl->Expand(html, &root);
        }
        
        void OneExpandHtml(const struct Question &q,std::string1 *html)
        {
            // 1. 形成路径
            std::string src_html = template_path + "one_question.html";

            // 2. 形成数字典
            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);

            //3. 获取被渲染的html
            ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
           
            //4. 开始完成渲染功能
            tpl->Expand(html, &root);

        }
        
    };
}

🍃 5.6 oj_control.hpp

oj_control的主要功能是将用户提交的代码进行反序列化,得到题目的编号,通过题目编号找到对应的题目,将用户代码和对应的测试用例拼接在一起,重新组合成新的代码,再进行序列化,形成新的json串,最后再根据负载均衡算法,选择对应的编译服务器进行编译运行。

🍁5.6.1 编译主机设计

因后台可能存在多台提供编译服务的主机,为了区分不同的主机,我们就需要一个结构来保存主机的相关信息,这些信息包括

在这里插入图片描述
当用户提交代码后,编译服务的负载将增加。当代码编译完成并运行成功后,编译服务的负载将减少。如果中途服务主机突然挂了,还需要清空对应主机的负载。

因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证我对负载操作的安全性,就需要一个mutex互斥锁保护对负载的操作。我们将主机信息和相关操作封装成一个类

// 提供服务的主机
class Machine
{
public:
    std::string ip;  //编译服务的ip
    int port;        //编译服务的port
    uint64_t load;   //编译服务的负载
    std::mutex *mtx; // mutex禁止拷贝的,使用指针
public:
    Machine() : ip(""), port(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();
    }

    //重置负载为0
    void ResetLoad()
    {
        if (mtx)
            mtx->lock();
        load = 0;
        if (mtx)
            mtx->unlock();
    }

    // 获取主机负载,没有太大的意义,只是为了统一接口
    uint64_t Load()
    {
        uint64_t _load = 0;
        if (mtx)
            mtx->lock();
        _load = load;
        if (mtx)
            mtx->unlock();

        return _load;
    }
};

🍁5.6.2 编译主机设计

因为后台存在多个主机提供编译服务,因此需要我们将这些主机有序的组织起来。并且我们需要为每台主机进行编号。

为此,采用vector作为存放主机的容器,因为vector的下标很好的与主机编号相匹配。在提供编译服务之前,我们需要知道有哪些主机能为我们提供服务,所以规定在当前路径下的conf文件夹下的一个.conf文件里面会存放所有的可以提供服务的主机信息,包括IP地址和端口号,中间采用":"号分割。当调用负载均衡模块时,它会自动读取该文件,并初始化vector中的主机信息。

主机存在多个,当一个服务请求主机时,选择了一个负载最低的主机,如果这个主机挂掉,那么它会选择其他负载最低的主机。同时也需要记录已经挂掉的主机。因为我们需要两个vector,一个用来存储当前可用主机,另一个用来存储已经挂掉的主机。

如果服务请求主机,然而所有主机全部挂掉,此时该请求服务就得不到任何响应,唯一能做的就是为后台开发人员提供相关的日志信息。

注意:当多个服务同时请求主机时,可能会导致负载不均衡的情况,所以也需要加锁控制

  • 负载均衡算法:

到这里我们已经有了一个vector,里面存放了所有可用的主机。当请求服务到来时,只需要通过遍历的方式,找到负载最下的主机即可

class LoadBlance
{
private:
    // 可以给我们提供编译服务的所有的主机
    // 每一台主机都有自己的下标,充当当前主机的id
    std::vector<Machine> machines;
    // 所有在线的主机id
    std::vector<int> online;
    // 所有离线的主机id
    std::vector<int> offline;
    // 保证LoadBlance它的数据安全
    std::mutex mtx;

public:
    LoadBlance()
    {
        assert(LoadConf(service_machine));
        LOG(INFO) << "加载 " << service_machine << " 成功" << std::endl;
    }

    ~LoadBlance()
    {
    }

public:
    bool LoadConf(const std::string &machine_conf)
    {
        std::ifstream in(machine_conf);
        if (!in.is_open())
        {
            LOG(FATAL) << " 加载: " << machine_conf << " 失败" << std::endl;
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            std::vector<std::string> tokens;
            StringUtil::SplitString(line, &tokens, ":");
            if (tokens.size() != 2)
            {
                LOG(WARNING) << " 切分 " << line << " 失败" << std::endl;
                continue;
            }
            Machine m;
            m.ip = tokens[0];
            m.port = 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;
    }

    // id: 输出型参数
    // m : 输出型参数
    bool SmartChoice(int *id, Machine **m)
    {
        // 1. 使用选择好的主机(更新该主机的负载)
        // 2. 我们需要可能离线该主机
        mtx.lock();
        // 负载均衡的算法
        // 轮询
        int online_num = online.size();
        if (online_num == 0)
        {
            mtx.unlock();
            LOG(FATAL) << " 所有的后端编译主机已经离线, 请运维的同事尽快查看" << std::endl;
            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();

        auto it = std::find(online.begin(), online.end(), which);
        if (it != online.end())
        {
            //要离线的主机已经找到啦
            machines[which].ResetLoad();
            online.erase(it);
            offline.push_back(which);
        }
        mtx.unlock();
    }

    void OnlineMachine()
    {
        mtx.lock();
        online.insert(online.end(), offline.begin(), offline.end());
        offline.erase(offline.begin(), offline.end());
        mtx.unlock();

        LOG(INFO) << "所有的主机有上线啦!" << std::endl;
    }

    // for test
    void ShowMachines()
    {
        mtx.lock();
        std::cout << "当前在线主机列表: ";
        for (auto &id : online)
        {
            std::cout << id << " ";
        }
        std::cout << std::endl;
        std::cout << "当前离线主机列表: ";
        for (auto &id : offline)
        {
            std::cout << id << " ";
        }
        std::cout << std::endl;
        mtx.unlock();
    }

    int GetOnlineMachine()
    {
        return online.size();
    }
};

🍁5.6.3 核心业务逻辑的控制器

用户的请求有多种,包括请求所有题目列表,请求单个题目和详细内容,用户提交代码,请求判题。

  • 如果是请求题目列表或者单个题目加详细信息,则需要调用oj_view模块,构建网页。
  • 如果是请求判题功能,需要对用户提交的代码进行反序列化,重新拼接成新的代码,选择负载最小的主机进行编译和运行,最后将运行结果返回给oj_server
class Control
{
private:
    Model _model;            //提供后台数据
    View _view;              //提供html渲染功能
    LoadBlance _load_blance; //核心负载均衡器
public:
    Control(){}
    ~Control(){}
public:
    void RecoveryMachine()
    {
        _load_blance.OnlineMachine();
    }
    //根据题目数据构建网页
    // html: 输出型参数
    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 Question(const string &number, string *html)
    {
        bool ret = true;
        struct Question q;
        if (_model.GetOneQuestion(number, &q))
        {
            // 获取指定题目信息成功,将所有的题目数据构建成网页
            _view.OneExpandHtml(q, html);
        }
        else
        {
            *html = "指定题目: " + number + " 不存在!";
            ret = false;
        }
        return ret;
    }

    // code: #include...
    // input: ""
    void Judge(const std::string &number, const std::string in_json, std::string *out_json)
    {
        // LOG(DEBUG) << in_json << " \nnumber:" << number << std::endl;

        // 0. 根据题目编号,直接拿到对应的题目细节
        struct Question q;
        _model.GetOneQuestion(number, &q);

        // 1. in_json进行反序列化,得到题目的id,得到用户提交源代码,input
        Json::Reader reader;
        Json::Value in_value;
        reader.parse(in_json, in_value);
        std::string code = in_value["code"].asString();

        // 2. 重新拼接用户代码+测试用例代码,形成新的代码
        std::string head;
        FileUtil::ReadFile("./questions/head.hpp", &head, true);
        Json::Value compile_value;
        compile_value["input"] = in_value["input"].asString();
        compile_value["code"] = head + "\n" + code + "\n" + q.tail;
        compile_value["cpu_limit"] = q.cpu_limit;
        compile_value["mem_limit"] = q.mem_limit;
        Json::FastWriter writer;
        std::string compile_string = writer.write(compile_value);

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

            // 4. 然后发起http请求,得到结果
            Client cli(m->ip, m->port);
            m->IncLoad();
            LOG(INFO) << " 选择主机成功, 主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 当前主机的负载是: " << m->Load() << std::endl;
            if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
            {
                // 5. 将结果赋值给out_json
                if (res->status == 200)
                {
                    *out_json = res->body;
                    m->DecLoad();
                    LOG(INFO) << "请求编译和运行服务成功..." << std::endl;
                    break;
                }
                m->DecLoad();
            }
            else
            {
                //请求失败
                LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线" << std::endl;
                _load_blance.OfflineMachine(id);
                if (_load_blance.GetOnlineMachine() != 0)
                {
                    goto END;
                }
                _load_blance.ShowMachines(); //仅仅是为了用来调试
                break;
            }
        }
    }
};

🌿6. 前端页面设计

前端页面分为三大部分:

  1. 首页页面
  2. 题目列表页面
  3. 指定题目的编写提交页面

由于我们的项目主要是研发后端,关于前端的代码就写的简单一点,如果后续需要美化的话再进行优化

🍃 6.1 丐版首页

<!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>
        /* 起手式, 100%保证我们的样式设置可以不受默认影响 */
        * {
            /* 消除网页的默认外边距 */
            margin: 0px;
            /* 消除网页的默认内边距 */
            padding: 0px;
        }

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

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续float带来的影响 */
            overflow: hidden;
        }

        .container .navbar a {
            /* 设置a标签是行内块元素,允许你设置宽度 */
            display: inline-block;
            /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: white;
            /* 设置字体的大小 */
            font-size: large;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 50px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置a标签中的文字居中 */
            text-align: center;
        }
        /* 设置鼠标事件 */
        .container .navbar a:hover {
            background-color: green;
        }
        .container .navbar .login {
            float: right;
        }

        .container .content {
            /* 设置标签的宽度 */
            width: 800px;
            /* 用来调试 */
            /* background-color: #ccc; */
            /* 整体居中 */
            margin: 0px auto;
            /* 设置文字居中 */
            text-align: center;
            /* 设置上外边距 */
            margin-top: 200px;
        }

        .container .content .font_ {
            /* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */
            display: block;
            /* 设置每个文字的上外边距 */
            margin-top: 20px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置字体大小
            font-size: larger; */
        }
    </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 class="login" href="#">登录</a>
        </div>
        <!-- 网页的内容 -->
        <div class="content">
            <h1 class="font_">欢迎来到Byih的OnlineJudge平台</h1>
            <p class="font_">这个我个人独立开发的一个在线OJ平台</p>
            <a class="font_" href="/all_questions">点击我开始编程啦!</a>
        </div>
    </div>
</body>

</html>

在这里插入图片描述


🍃 6.2 题目列表

<!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>
        /* 起手式, 100%保证我们的样式设置可以不受默认影响 */
        * {
            /* 消除网页的默认外边距 */
            margin: 0px;
            /* 消除网页的默认内边距 */
            padding: 0px;
        }

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

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续float带来的影响 */
            overflow: hidden;
        }

        .container .navbar a {
            /* 设置a标签是行内块元素,允许你设置宽度 */
            display: inline-block;
            /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: white;
            /* 设置字体的大小 */
            font-size: large;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 50px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置a标签中的文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件 */
        .container .navbar a:hover {
            background-color: green;
        }

        .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: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
            margin-top: 50px;
            background-color: rgb(243, 248, 246);
        }

        .container .question_list h1 {
            color: green;
        }
        .container .question_list table .item {
            width: 100px;
            height: 40px;
            font-size: large;
            font-family:'Times New Roman', Times, serif;
        }
        .container .question_list table .item a {
            text-decoration: none;
            color: black;
        }
        .container .question_list table .item a:hover {
            color: blue;
            text-decoration:underline;
        }
        .container .footer {
            width: 100%;
            height: 50px;
            text-align: center;
            line-height: 50px;
            color: #ccc;
            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 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="/question/{{number}}">{{title}}</a></td>
                    <td class="item">{{star}}</td>
                </tr>
                {{/question_list}}
            </table>
        </div>
        <div class="footer">
            <!-- <hr> -->
            <h4>@Byih</h4>
        </div>
    </div>

</body>

</html>

在这里插入图片描述


🍃 6.3 指定题目提交页面

<!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插件 -->
    <!-- 官网链接:https://ace.c9.io/ -->
    <!-- CDN链接:https://cdnjs.com/libraries/ace -->
    <!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 -->
    <!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ -->
    <!-- 引入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: 0;
            padding: 0;
        }

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

        .container .navbar {
            width: 100%;
            height: 50px;
            background-color: black;
            /* 给父级标签设置overflow,取消后续float带来的影响 */
        }

        .container .navbar a {
            /* 设置a标签是行内块元素,允许你设置宽度 */
            display: inline-block;
            /* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
            width: 80px;
            /* 设置字体颜色 */
            color: white;
            /* 设置字体的大小 */
            font-size: large;
            /* 设置文字的高度和导航栏一样的高度 */
            line-height: 50px;
            /* 去掉a标签的下划线 */
            text-decoration: none;
            /* 设置a标签中的文字居中 */
            text-align: center;
        }

        /* 设置鼠标事件 */
        .container .navbar a:hover {
            background-color: green;
        }

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

        .container .part1 .left_desc pre {
            padding-top: 10px;
            padding-left: 10px;
            font-size: medium;
            font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', 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 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" onclick="submit()">提交代码</button>
        </div>
    </div>
    <script>
        //初始化对象
        editor = ace.edit("code");

        //设置风格和语言(更多风格和语言,请到github上相应目录查看)
        // 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html
        editor.setTheme("ace/theme/monokai");
        editor.session.setMode("ace/mode/c_cpp");

        // 字体大小
        editor.setFontSize(16);
        // 设置默认制表符的大小:
        editor.getSession().setTabSize(4);

        // 设置只读(true时只读,用于展示代码)
        editor.setReadOnly(false);

        // 启用提示菜单
        ace.require("ace/ext/language_tools");
        editor.setOptions({
            enableBasicAutocompletion: true,
            enableSnippets: true,
            enableLiveAutocompletion: true
        });

        function submit(){
            // alert("嘿嘿!");
            // 1. 收集当前页面的有关数据, 1. 题号 2.代码
            var code = editor.getSession().getValue();
            // console.log(code);
            var number = $(".container .part1 .left_desc h3 #number").text();
            // console.log(number);
            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)
            {
                // console.log(data.status);
                // console.log(data.reason);
                // 拿到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>

在这里插入图片描述

  • 错误展示

在这里插入图片描述

  • 正确展示

在这里插入图片描述

到这里我们的文件版在线OJ就已经完成了,后续我们还要进行升级,将题目变成数据库版本


🌿7. 引入mysql

此前我们已经实现了文件版的数据库,现在我们来把文件版本升级为数据库版本,这里就需要下载Mysql了,由于篇幅问题,这里就不详细说明了。可以在网上自行查找相关资源

Mysql官网

  1. 在数据库中设计可以远程登陆的MySQL用户,并给他赋权
    oj_client
  2. 设计表结构
    数据库:oj, 表:oj_questions
  3. 开始编码
    连接访问数据库
    有可能你的系统中,已经默认安装了mysql的开发包
    这里我们使用第三方引入的方式,不安装
    我们的oj_server基于MVC模式的,和数据打交道的只有一个oj_model模块,只需要更改该文件即可!!

🍃 7.1 建表

这里推荐使用Mysql workbench,虽然对新手不友好,但是熟练运用的话还是可以提高一定的效率

Mysql Workbench

在这里插入图片描述

下载完成之后我们就可以连接我们的mysql使用了,记得把云服务器的防火墙打开,不然是无法访问的

CREATE TABLE IF NOT EXISTS `questions`(
	`number` int PRIMARY KEY AUTO_INCREMENT COMMENT '题目的ID',
	`title` VARCHAR(64) NOT NULL COMMENT '题目的标题',
	`star` VARCHAR(8) NOT NULL COMMENT '题目的难度',
	`desc` TEXT NOT NULL COMMENT '题目描述',
	`header` TEXT NOT NULL COMMENT '题目头部,给用户看的代码',
	`tail` TEXT NOT NULL COMMENT '题目尾部,包含我们的测试用例',
	`time_limit` int DEFAULT 1 COMMENT '题目的时间限制',
	`mem_limit` int DEFAULT 5000000 COMMENT '题目的空间限制'
)ENGINE=INNODB DEFAULT CHARSET=utf8;

  • 第一步:建表

在这里插入图片描述

  • 第二步:录题

这里只需要把对应的题目内容填入表中,然后点记apply自动生成即可

在这里插入图片描述

然后我们就可以看到题目已经成功录入进去了

在这里插入图片描述


🍃 7.2 引入第三方

因为我们这里是引用外部的库,所以我们是采用建立静态连接的方式来实现的

当我们make形成可执行程序的时候,ldd一下看是否有成功找到连接的库

在这里插入图片描述

这里可以看到我是找不到这个库的,所以我们需要手动添加路径,配置环境,我们需要进到/etc/ld.so.conf.d/路径下

然后创建一个.conf文件,因为设计系统层面,所以需要sudo才能创建

在这里插入图片描述

然后我们需要把引入静态库的路径写到oj_lib_search.conf下,相当于告诉系统,你应该去哪里找这个库,我们只需要pwd就可以找到当前路径了,然后复制粘贴进来我们就可以成功运行了

在这里插入图片描述


🍃 7.3 更改oj_model

#pragma once
//MySQL 版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include "include/mysql.h"

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include <cstdlib>
#include <cassert>


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

namespace ns_model
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;

    struct Question
    {
        std::string number; //题目编号,唯一
        std::string title;  //题目的标题
        std::string star;   //难度: 简单 中等 困难
        std::string desc;   //题目的描述
        std::string header; //题目预设给用户在线编辑器的代码
        std::string tail;   //题目的测试用例,需要和header拼接,形成完整代码
        int cpu_limit;      //题目的时间要求(S)
        int mem_limit;      //题目的空间要去(KB)
    };

    const std::string oj_questions = "oj_questions";
    const std::string host = "127.0.0.1";
    const std::string user = "";//自己的数据库名称
    const std::string passwd = "";//数据库👩
    const std::string db = "oj";
    const int port = 3306;

    class Model
    {
    public:
        Model()
        {}
        bool QueryMySql(const std::string &sql, vector<Question> *out)
        {
            // 创建mysql句柄
            MYSQL *my = mysql_init(nullptr);

            // 连接数据库
            if(nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(),db.c_str(),port, nullptr, 0)){
                LOG(FATAL) << "连接数据库失败!" << "\n";
                return false;
            }

            // 一定要设置该链接的编码格式, 要不然会出现乱码问题
            mysql_set_character_set(my, "utf8");

            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; i < rows; i++)
            {
                MYSQL_ROW row = mysql_fetch_row(res);
                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连接
            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))
            {
                if(result.size() == 1){
                    *q = result[0];
                    res = true;
                }
            }
            return res;
        }
        ~Model()
        {}
    };
} // namespace ns_model
 

🌿8. 顶部部署makefile

在顶层新建一个Makefile文件,该文件的功能就是可以make时可以同时编译CompilerServer服务和OJServer服务,当输入make submit时会自动形成一个output文件,里面包含了compiler_server和oj_server的应用程序和一些运行程序必须的文件,时间打包的功能。最后输入make clean不光会清理掉创建的可执行程序,还会清理掉output的内容。

.PHONY: all
all:
	@cd compile_server;\
	make;\
	cd -;\
	cd oj_server;\
	make;\
	cd -;

.PHONY:output
output:
	@mkdir -p output/compile_server;\
	mkdir -p output/oj_server;\
	cp -rf compile_server/compile_server output/compile_server;\
	cp -rf compile_server/temp output/compile_server;\
	cp -rf oj_server/conf output/oj_server/;\
	cp -rf oj_server/lib output/oj_server/;\
	cp -rf oj_server/questions output/oj_server/;\
	cp -rf oj_server/template_html output/oj_server/;\
	cp -rf oj_server/wwwroot output/oj_server/;\
	cp -rf oj_server/oj_server output/oj_server/;

.PHONY:clean
clean:
	@cd compile_server;\
	make clean;\
	cd -;\
	cd oj_server;\
	make clean;\
	cd -;\
	rm -rf output;

🌿9. 思维导图

在这里插入图片描述


评论 56
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

有缘再见了

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

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

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

打赏作者

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

抵扣说明:

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

余额充值