C++/Linux实战项目 - 负载均衡式在线OJ平台

本文介绍了如何设计一个基于C++和Linux的在线OJ平台,包括编译运行服务和MVC结构的oj服务器。重点讨论了编译服务器的编译与运行模块,oj服务器的模型、视图和控制器组件,以及前端页面的渲染。平台采用负载均衡,能够处理大量判题请求,提升服务性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

总览

简述

项目核心的三个模块

项目宏观结构

 compile_server 编译与运行服务

总览分析

compiler.hpp

runner.hpp

compile_run.hpp

compile_server.cc

oj_server 基于MVC结构的oj服务设计

总览分析

Model 提供对数据的操作方法

View 使用后端数据对前端页面进行渲染,获取渲染之后的html

Control 逻辑控制模块,oj_server.cc直接调用Control提供的方法

oj_server.cc

前端

index.html

all_question.html

one_question.html 

结尾


总览

简述

此项目,旨在设计出一款类似于leetcode的在线OJ平台,通过浏览器客户端获取服务端提供的http服务。主要提供的功能有两个: 1. 获取题目列表  2. 获取指定题目的详细信息,并获取一个在线编辑器,用于提交代码,提交后显示出此题的提交结果: 编译错误 / 运行时出错(程序崩溃) / 编译运行成功但没有通过测试用例 / 编译运行成功且通过测试用例

项目核心的三个模块

1. common 公共模块 : 用于提供一些第三方库文件,一些工具类,工具方法。

2. compile_server 编译与运行模块 : 通过提供网络服务的方式,获取通过网络请求发送来的源代码,仅提供编译运行功能,并将编译运行的结果通过网络返回回去。

3. oj_server 在线OJ模块 : 提供http服务,如获取题目列表,进入指定题目的OJ界面,负载均衡。

综上,common就是提供一些工具方法,用于另外两个模块使用。而oj_server相当于是一个在线oj平台的后端服务器,提供http服务,当用户获取列表,编写指定题目之后,将前端代码通过http请求发送给oj_server时,oj_server通过网络请求,负载均衡式地使用compile_server提供的编译运行服务,获取编译运行结果,再返回给用户的前端界面。

项目宏观结构

 compile_server 编译与运行服务

总览分析

compile_server模块旨在实现出一个通过网络请求的方式,提供编译并运行服务的后端。在此项目中此功能用于服务oj_server编译并运行用户提交的oj代码。注意:此模块,仅提供编译运行功能,提交的代码是否通过测试用例,此模块不负责。

 compiler.hpp提供编译服务,runner.hpp提供运行服务。compile_run.hpp整合下方两个功能,提供一个编译并运行功能的接口。而compile_server.cc通过网络请求,获取代码,调用compile_run.hpp提供的的接口。

这里有一个注意点:将来,oj_server模块,将用户提交的代码通过网络请求给compile_server,大体思路是:此模块创建临时文件,将代码写入文件中,也就成为了源文件。下方对源文件进行编译运行,此处文件名等并不重要,我们的目的是编译运行获取结果。那么,如何让compiler.hpp和runner.hpp对正确的源文件进行编译,和对正确的可执行程序进行运行呢?只要统一他们的文件名即可(不包含后缀)

compiler.hpp

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "./../common/util.hpp"
#include "../common/log.hpp"

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

namespace ns_compiler
{
    using namespace ns_util;
    using namespace ns_log;

    class Compiler
    {
    public:
        // 返回值: 编译成功true,编译失败false
        // 输入参数: 编译的文件名(不带路径,不带后缀)
        static bool compile(const std::string &file_name)
        {
            // 程序替换为g++编译,成功则没有输出,可执行程序生成
            // 失败则g++会向标准错误中输出错误信息,即编译失败的原因
            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(ERROR) << "内部错误,编译时创建子进程失败" << std::endl;
                return false;
            }
            else if (pid == 0)
            {
                // 子进程,进行程序替换g++,编译指定文件
                umask(0);
                int _compile_error_fd = open(PathUtil::CompileError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if (_compile_error_fd < 0)
                {
                    LOG(WARNING) << "内部错误, 编译时没有生成stderr文件" << std::endl;
                    exit(1); // 其实父进程不关心
                }
                // 程序替换,并不影响进程的文件描述符表
                dup2(_compile_error_fd, 2);
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),
                       PathUtil::Src(file_name).c_str(), "-D", "COMPILE_ONLINE", "-std=c++11", nullptr);
                exit(2);  // 其实父进程不关心
            }
            else
            {
                // 父进程
                waitpid(pid, nullptr, 0); // 不关心子进程的退出结果,只关心是否编译成功(exe是否生成)
                // std::cout << "flag" << std::endl;
                if (FileUtil::IsFileExists(PathUtil::Exe(file_name)) == true)
                {
                    // 可执行程序已生成,编译成功
                    // std::cout << "flag 2" << std::endl;
                    LOG(INFO) << PathUtil::Src(file_name) << "编译成功!" << std::endl;
                    return true;
                }
            }
            // 可执行程序没有生成,g++错误信息已打印到CompileError文件中。
            return false;
        }
    };
}

Compiler类提供编译功能接口,对参数传来的指定的文件(不包含后缀,路径),进行编译。编译成功返回true,对应的可执行生成。失败返回false,编译错误原因在对应的CompileError文件中。

思路:要编译,肯定要程序替换为g++,思路是让子进程进行程序替换,替换为g++,父进程通过对应的可执行是否生成来判断是否编译成功。编译失败时,g++会向标准错误中输出错误信息,标准错误原本为显示器,现通过dup2系统调用,将其重定向至CompileError文件中,则若编译失败,失败原因会在对应的CompileError文件中保存。

注意: 在编译运行模块中,会出现很多临时文件,也就是对应某一次编译运行请求所生成的源文件,编译错误文件,可执行等等。此处的策略为,在compile_server编译运行模块下下创建一个temp目录,用于存储临时文件。

runner.hpp

#pragma once
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <fcntl.h>
#include "../common/util.hpp"
#include "../common/log.hpp"

namespace ns_runner
{
    using namespace ns_util;
    using namespace ns_log;

    // 只负责运行编译好的可执行
    class Runner
    {
    public:
        /* 返回值 > 0,oj程序运行异常,收到了信号,返回值为信号编号
         * 返回值 = 0,oj程序运行成功,标准输出和标准错误信息在对应的文件中
         * 返回值 < 0,内部错误。如打开文件失败,创建子进程执行oj程序时失败。
         * cpu_limit: file_name程序运行时,可以使用的CPU资源上限(时间,秒)
         * mem_limit: file_name程序运行时,可以使用的内存资源上限(KB)
         */
        static int run(const std::string file_name, int cpu_limit, int mem_limit)
        {
            std::string _excute = PathUtil::Exe(file_name);
            std::string _stdin = PathUtil::Stdin(file_name);
            std::string _stdout = PathUtil::Stdout(file_name);
            std::string _stderror = PathUtil::Stderror(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);   // OJ程序的输出结果
            int _stderror_fd = open(_stderror.c_str(), O_CREAT | O_WRONLY, 0644); // OJ程序的运行时错误信息
            if (_stdin_fd < 0 || _stdout_fd < 0 || _stderror_fd < 0)
            {
                LOG(ERROR) << "内部错误,运行时打开文件失败" << std::endl;
                return -1; // 代表打开文件失败
            }

            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(ERROR) << "运行时创建子进程失败" << std::endl;
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderror_fd);
                return -2; // 代表创建子进程失败
            }
            else if (pid == 0)
            {
                // 子进程进行程序替换,执行可执行程序
                dup2(_stdin_fd, 0);
                dup2(_stdout_fd, 1);
                dup2(_stderror_fd, 2);
                SetProcLimit(cpu_limit, mem_limit);
                execl(_excute.c_str(), _excute.c_str(), nullptr);
                exit(1);
            }
            else
            {
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderror_fd);
                // 父进程获知程序的执行情况,仅关心成功执行or异常终止
                // 对于成功执行之后的执行结果并不关心,是上层的任务,需要根据测试用例判断
                int status = 0;
                waitpid(pid, &status, 0);
                LOG(INFO) << "OJ题运行完毕, 退出信号: " << (status & 0x7F) << std::endl;
                return status & 0x7F; // 将子进程的退出信号返回(并非退出码)
            }
        }
        // 设置进程占用资源大小的接口(CPU资源,内存资源)mem_limit的单位为KB
        static void SetProcLimit(int _cpu_limit, int _mem_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_limit;
            mem_limit.rlim_max = RLIM_INFINITY;
            mem_limit.rlim_cur = _mem_limit * 1024; // 转化为KB
            setrlimit(RLIMIT_AS, &mem_limit);
        }
    };
}

参数:要执行的可执行程序的文件名(不包含路径,后缀),cpu限制,mem限制(OJ题一般会有时间空间限制)。程序执行结果,通过返回值进行判断,大于0,为程序异常终止(收到了信号),等于0,为程序运行成功,此时,标准输出和标准错误信息都在对应的文件中。小于0,为内部错误,如打开文件失败,创建子进程失败等等...

这里思路和compiler.hpp相似,让子进程进行程序替换,执行可执行(在temp目录下,也就是编译模块编译好的可执行),父进程通过waitpid,阻塞式等待子进程退出结果,通过信号判断其运行情况。若崩溃,则信号大于0,此时不考虑标准输出和标准错误。若等于0,则信号为0,表示程序运行成功,此时标准输出和标准错误在对应文件中。这里的关键其实还是,重定向,也就是子进程程序替换为可执行前要dup2,将标准输入输出错误进行重定向(此处不考虑输入),对应的临时文件在temp目录下。

compile_run.hpp

// 需定制通信协议
#pragma once
#include "../compile_server/compiler.hpp"
#include "../compile_server/runner.hpp"
#include "../common/log.hpp"
#include "../common/util.hpp"
#include "jsoncpp/json/json.h"

namespace ns_compile_run
{
    using namespace ns_compiler;
    using namespace ns_runner;
    using namespace ns_log;
    using namespace ns_util;

    class CompileAndRun
    {
    public:
        /***************************************
         * 应用层协议定制:
         * 输入json串:
         * code: 用户提交的OJ代码
         * input: 用户提交的代码对应的输入(不做处理)
         * cpu_limit: OJ程序的时间要求
         * mem_limit: OJ程序的空间要求
         *
         * 输出json串:
         * 必填:
         * status: 状态码
         * reason: 请求结果(状态码描述)
         * 选填:(当OJ程序编译且运行成功时)
         * stdout: OJ程序运行完的标准输出结果
         * stderr: OJ程序运行完的标准错误结果
         * (若编译且运行成功out_json中才会有stdout和stderr字段)
         * 参数:
         * in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
         * out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
         * ************************************/
        static void execute(const std::string &in_json, std::string *out_json)
        {
            // 随便写写:输入的json串中有代码,输入,时间空间限制等等
            // 提取出代码,写入到源文件中。
            // 编译源文件,看编译结果
            // 若编译成功,则运行可执行
            // 若一切顺利则status状态码为0,对应的输出结果也写入
            // 若某一步出现了错误,则status设置为对应的数字
            // reason也写好

            // 对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 = 0; // 状态码,最终要写入到out_json中
            std::string file_name;
            int run_result = 0;
            if (code.empty())
            {
                status = -1; // 用户输入的OJ代码为空(非服务端问题)
                goto END;
            }
            file_name = FileUtil::UniqueFileName();
            // 形成临时源文件,代码为传来的code,这个文件名不重要,唯一即可
            // 我们的目的是编译并运行这份代码
            if (FileUtil::WriteFile(PathUtil::Src(file_name), code) == false)
            {
                status = -2; // 未知错误
                goto END;
            }
            if (Compiler::compile(file_name) == false)
            {
                status = -3; // 编译失败,跳过后面的运行
                goto END;
            }
            run_result = Runner::run(file_name, cpu_limit, mem_limit);
            if (run_result < 0)
            {
                status = -2; // 未知错误(不管run内部是打开文件失败还是创建子进程失败,统一称之为内部错误,即服务端错误)
            }
            else if (run_result > 0)
            {
                status = run_result; // 运行错误,程序崩溃,此时status为程序退出信号
            }
            else
            {
                status = 0; // 运行成功(且编译成功)
            }
        END:
            Json::Value out_value;
            out_value["status"] = status;                          // 状态码
            out_value["reason"] = StatusToDesc(status, file_name); // 状态码描述
            if (status == 0)
            {
                // 只有当编译且运行成功时,才有stdout stderr字段
                std::string _stdout;
                FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true); // 读取OJ程序的标准输出结果
                out_value["stdout"] = _stdout;
                std::string _stderr;
                FileUtil::ReadFile(PathUtil::Stderror(file_name), &_stderr, true); // 读取OJ程序的标准错误结果
                out_value["stderr"] = _stderr;
            }
            Json::StyledWriter writer;
            *out_json = writer.write(out_value); // 将out_value进行序列化
            // RemoveTempFile
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值