目录

负载均衡式的在线OJ项目编写一

负载均衡式的在线OJ项目编写(一)

一.项目演示

本项目的功能为一个在线的 ,实现类似leetcode的题目列表、在线提交、编译、运行等功能。

代码持续开发中,下面是代码链接

二.所用到的技术与开发环境

所⽤技术:

• C++ STL 标准库

• Boost 准标准库(字符串切割)

 • cpp-httplib 第三⽅开源⽹络库

• ctemplate 第三⽅开源前端⽹⻚渲染库

• jsoncpp 第三⽅开源序列化、反序列化库

• 负载均衡设计

• 多进程、多线程

• MySQL C connect

• Ace前端在线编辑器(了解)

 • html/css/js/jquery/ajax (了解)

开发环境

• Centos 7 云服务器

• vscode

• Mysql Workbench

三.项⽬宏观结构

 我们的项⽬核⼼是三个模块

  1. comm :公共模块 

  2. compile_server :编译与运⾏模块

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

I.leetcode 结构

• 只实现类似 leetcode 的题⽬列表+在线编程功能

II.我们的项⽬宏观结构

https://i-blog.csdnimg.cn/direct/356839d7165f45ecb7ee0a8d5bf55982.png

III.编写思路

  1. 先编写 compile_server

  2. oj_server

  3. version1 基于⽂件版的在线OJ 

  4. 前端的⻚⾯设计

  5. version2 基于 MySQL 版的在线OJ

四.compiler 服务设计

提供的服务:编译并运⾏代码,得到格式化的相关的结果

https://i-blog.csdnimg.cn/direct/f22219d4978c4372b3e8034b2e8c1d08.png

https://i-blog.csdnimg.cn/direct/a3858c985f5d475e9d71ac3d38845e8e.png

还需要设计一个makefile自动编译模块

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

开始设计compile模块(只负责编译代码)

https://i-blog.csdnimg.cn/direct/f22219d4978c4372b3e8034b2e8c1d08.png

我们还需要加一个temp文件夹来保存代码文件

https://i-blog.csdnimg.cn/direct/16091e70e93b48278e7aa2775a3af80c.png

https://i-blog.csdnimg.cn/direct/c4a1deb77f624728b1d8c4a7112dd2f5.png

https://i-blog.csdnimg.cn/direct/251cbdbdec2644c3880fb638b48c9f5f.png

compile代码的大致思路(不完整的)

#pragma once

//只负责代码的编译


#include <iostream>
#include <unistd.h>



namespace ns_compiler
{
    class Compiler
    {
    public:
        Compiler()
        {

        }
        ~Compiler()
        {

        }
        //返回值: 编译成功: true, 否则: false
        //输入参数: 编译文件名
        //filename -> name
        //name -> ./temp/name.cpp
        //name -> ./temp/name.exe
        //name -> ./temp/name.stderr
        //文件路径存储在temp路径下
        static bool Compile(const std::string& file_name)
        {
            pid_t res = fork();
            if(res < 0)
            {
                //创建子进程失败
                return false;
            }
            else if(res == 0)
            {
                //子进程 执行编译代码
                //调用编译器,执行编译
                //g++ -o target src -std=c++11
                execlp("g++","-o")

            }
            else
            {
                //父进程
            }
        }
    };
}

可以发现我们不仅要生成.exe文件和.stderr文件,后续运行文件也要用到路径拼接,所以我们可以在comm(公共模块)中设计一个路径拼接util类

//路径拼接大致思路代码

#pragma once

#include <iostream>
#include <string>

namespace ns_util
{
    class PathUtil
    {
    public:
        //构建源文件路径 + 后缀的完整文件名
        static std::string Src(const std::string & file_name)
        {

        }
        //构建可执行程序的完整路径 + 后缀
        static std::string Exe(const std::string & file_name)
        {

        }
        //构建该程序对应的标准错误的完整路径 + 后缀
        static std::string Stderr(const std::string & file_name)
        {

        }
    };
}

https://i-blog.csdnimg.cn/direct/e151ea9adb7c4a49adff9afd4ea854bc.png

https://i-blog.csdnimg.cn/direct/afddd469f1bb4ad0806db3698fd59cf8.png

util类的具体实现

#pragma once

#include <iostream>
#include <string>

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;
        }
        //构建源文件路径 + 后缀的完整文件名
        //name -> ./temp/name.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");
        }
    };
}

https://i-blog.csdnimg.cn/direct/9ab3cb7d55ae4d5b8cf246a19031dfbc.png

compile的具体实现

static bool Compile(const std::string& file_name)
        {
            pid_t pid = fork();
            if(pid < 0)
            {
                //创建子进程失败
                return false;
            }
            else if(pid == 0)
            {
                //子进程 执行编译代码
                //调用编译器,执行编译
                //g++ -o target src -std=c++11
                execlp("g++","-o",PathUtil::Exe(file_name).c_str(),\
                PathUtil::Src(file_name).c_str(),"-std=c++11",nullptr);
                exit(1);
            }
            else
            {
                //父进程
                //nullptr:表示不关心子进程退出的状态
                //0:表示阻塞等待
                waitpid(pid,nullptr,0);
                //编译是否成功,就看是否形成了可执行的程序
                if(FileUtil::IsFileExists(PathUtil::Exe(file_name)))
                {
                    return true;
                }
            }
            return false;
        }

要判断文件是否存在,这个函数使用的频率也较高(也放到comm模块中)

新的判断文件是否存在的接口介绍

https://i-blog.csdnimg.cn/direct/b968794e58bb4b4b9ccdbe44d76d8389.png

https://i-blog.csdnimg.cn/direct/f0107a9c66294afca5d603fe417e9420.png

https://i-blog.csdnimg.cn/direct/5b38e92523b84d65a83972affdb14bfc.png

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

编译成功已经写好了,那编译出错呢?

编译出错的话,我们将错误重定向到标准错误文件中写入错误信息

创建并打开文件

https://i-blog.csdnimg.cn/direct/c3b8654b1dbe4d39828f62e202f9ded2.png

重定向接口

https://i-blog.csdnimg.cn/direct/ef7649434e5f4a37b481be4031ed5c4e.png

https://i-blog.csdnimg.cn/direct/f444e662f3a748969d9b1c02afcaac73.png

五.log日志服务的编写

上面初步完成了我们编译的过程,必要时还是要打印日志的(所以我们来写一个日志功能)

#pragma once


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

namespace ns_log
{
    using namespace ns_util;
    //日志等级
    enum {
        INFO,
        DEBUG,
        WARNING,
        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本质 内部是包含缓存区的
        //没有endl就不会将缓存区内部的内容全部刷新出来
        std::cout << message;

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

}

https://i-blog.csdnimg.cn/direct/3dceeaa31dd24f2ab1013852189aad9f.png

https://i-blog.csdnimg.cn/direct/24ed6e549ebb4b7b8a5e872ffea45fb9.png

https://i-blog.csdnimg.cn/direct/8e8ce9b5d34f4324bb61ae8ed5e61249.png

https://i-blog.csdnimg.cn/direct/503b43dae7b34923984e16d5c8a6d828.png

六.测试编译模块

https://i-blog.csdnimg.cn/direct/a0337ccdbc454beebf20bb21df8f023c.png

https://i-blog.csdnimg.cn/direct/10af4bb67f7f449fb5eb6582637ac0e0.png

https://i-blog.csdnimg.cn/direct/caec86063e0547d69d89fcd40066b347.png

https://i-blog.csdnimg.cn/direct/470b99c9031949dfb02a2c6551e283df.png

还有一个比较重要的一点,在vsocde创建的文件,是root的,所有不能用其他普通用户去执行(会出错)

https://i-blog.csdnimg.cn/direct/d0a31c46c9694391b7e957caf35e2675.png

https://i-blog.csdnimg.cn/direct/8c3ffb35611e4cec8386371acd9404ca.png

报错测试

https://i-blog.csdnimg.cn/direct/c199fbbe5de343d1beb6243f24622b01.png

https://i-blog.csdnimg.cn/direct/5bb0c8883bbe411782d685da33e0a4f8.png

补充https://i-blog.csdnimg.cn/direct/785b0a273137446ea5c25f29a530b0f3.png

七.运行功能开发

子进程来执行代码,父进程来进行等待

可以思考以下的问题:

/******************************

            * 程序运行:

            * 1. 代码跑完,结果正确

            * 2. 代码跑完,结果不正确

            * 3. 代码没跑完,异常了

            * Run模块需要考虑代码跑完,结果正确与否吗??? 不考虑

            * 结果正确与否: 是由我们的测试用例来决定的!

            * 我们只考虑: 是否正确运行完毕

            *

            *

            * 我们必须知道可执行程序是谁? ./temp/code.exe

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

            * 标准输入: 不处理(用户自测)

            * 标准输出: 程序运行完成,输出结果是什么

            * 标准错误: 运行时错误信息

            * 全部写道同名文件中

            ******************************/

https://i-blog.csdnimg.cn/direct/b3b6bbdc5f8948a380579c0e9555850b.png

https://i-blog.csdnimg.cn/direct/b2bccc71475945c08cb4912ec13e9b93.png

https://i-blog.csdnimg.cn/direct/e20c92ef877f4e669e195e1b801405bd.png

https://i-blog.csdnimg.cn/direct/318098832f21445fba15429940a09648.png

#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/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模块需要考虑代码跑完,结果正确与否吗??? 不考虑
            * 结果正确与否: 是由我们的测试用例来决定的!
            * 我们只考虑: 是否正确运行完毕
            * 
            * 
            * 我们必须知道可执行程序是谁? ./temp/code.exe
            * 一个程序在默认启动的时候
            * 标准输入: 不处理(用户自测)
            * 标准输出: 程序运行完成,输出结果是什么
            * 标准错误: 运行时错误信息
            * 全部写道同名文件中
            ******************************/

            std::string _execute = PathUtil::Exe(file_name);
            std::string _stdin   = PathUtil::Stdin(file_name);
            std::string _stdout  = PathUtil::Stdout(file_name);
            std::string _stderr  = PathUtil::Stderr(file_name);

            umask(0);
            int _stdin_fd = open(_stdin.c_str(),O_CREAT | O_RDONLY,0644);
            int _stdout_fd = open(_stdout.c_str(),O_CREAT | O_WRONLY,0644);
            int _stderr_fd = open(_stderr.c_str(),O_CREAT | O_WRONLY,0644);

            if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0)
            {
                LOG(ERROR) << "运行时打开标准文件失败" << "\n";
                return -1;//代表打开文件失败
            }

            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(ERROR) << "运行时创建子进程失败" << "\n";
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);
                return -2;//代表创建子进程失败
            }
            else if(pid == 0)
            {
                dup2(_stdin_fd,0);
                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; //将程序是否受到异常(信号 1-30)进行返回
            }
        }
    };
}

https://i-blog.csdnimg.cn/direct/c85ecf5e6f294c88875e0f53487ece24.png

八.测试运行模块

#include "compiler.hpp"
#include "runner.hpp"

using namespace ns_compiler;
using namespace ns_runner;




int main()
{
    std::string code = "code";
    Compiler::Compile(code);

    Runner::Run(code);

    return 0;
}

编译完之后,就直接运行程序

https://i-blog.csdnimg.cn/direct/f43a04f816fd4482be3283f48a047f17.png

https://i-blog.csdnimg.cn/direct/d0e20bf1054e431a8e27e70d8ae8a8e5.png

未完待续