一、演⽰项⽬

项⽬源码链接:https://gitee.com/zhang-shiyang-h/l_code/tree/master/OnlineOJ

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
客户端1

客户端2

客户端3

服务端在这里插入图片描述

二、所⽤技术与开发环境

1、核心技术栈

后端开发

  • C++基础 + STL标准库
  • 多进程、多线程并发编程
  • 负载均衡架构设计

第三方库

  • Boost库(字符串切割等工具)
  • cpp-httplib HTTP 网络库
  • ctemplate 前端模板渲染库
  • jsoncpp JSON 序列化 / 反序列库

数据库

  • MySQL + C API 数据库连接

前端基础

  • HTML/CSS/JS/jQuery/AJAX
  • Ace 在线代码编辑器

2、开发环境

  • 服务器:Ubuntu 20.04 云服务器
  • 开发工具:VS Code
  • 数据库管理:MySQL Workbench

三、项⽬宏观结构

我的项⽬核⼼由三个模块组成:

  1. comm: 公共模块
  2. compile_server: 编译运行模块
  3. oj_server: 题目管理、负载均衡及业务功能

代码结构:

comm:
	httplib.h
	Mutex.hpp
	Log.hpp
	Util.hpp
	
compile_server:
	compile.hpp
	runner.hpp
	compile_run.hpp
	compile_server.cc
	temp 存放临时文件

oj_server
	oj_model.hpp 文件版
	oj_model2.hpp MySQL版
	oj_view.hpp
	oj_control.hpp
	oj_server.cc
	conf:
		service_machine.conf
	questions:
		questions.list
		1:
			desc.txt
			header.cpp
			tail.hpp
	template_html:
		all_questions.html
		one_question.html
	wwwroot
		index.html

makefile1

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

makefile2

oj_server:oj_server.cc
	g++ -o $@ $^ -I./include -L./lib -std=c++17 -lpthread -lctemplate -ljsoncpp -lmysqlclient
.PHONY:clean
clean:
	rm -rf oj_server 	

makefile3

.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/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;

四、公共模块comm

1、Mutex.hpp

#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    // 互斥锁封装类
    class Mutex
    {
    public:
        // 初始化锁
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }

        // 加锁
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
        }

        // 解锁
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
        }

        // 获取锁指针
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }

        // 销毁锁
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        pthread_mutex_t _mutex; // 原生互斥锁对象
    };

    // RAII 自动锁
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
            : _mutex(mutex)
        {
            _mutex.Lock();
        }

        ~LockGuard()
        {
            _mutex.Unlock();
        }

    private:
        Mutex &_mutex;
    };
}

2、Log.hpp

#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";

    // 日志策略基类(接口)
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 控制台日志输出(线程安全)
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cout << message << gsep;
        }

        ~ConsoleLogStrategy() {}

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "/var/log";
    const std::string defaultfile = "my.log";

    // 文件日志输出(自动建目录、线程安全)
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path), _file(file)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_path))
                return;

            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << std::endl;
            }
        }

        // 追加写入日志文件
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
            std::ofstream out(filename, std::ios::app);
            if (!out.is_open())
                return;

            out << message << gsep;
            out.close();
        }

    private:
        std::string _path;
        std::string _file;
        Mutex _mutex;
    };

    // 日志等级类
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 日志等级转字符串
    std::string LevelStr(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 获取格式化时间字符串(线程安全)
    std::string GetTimeStamp()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm; // 出参
        localtime_r(&curr, &curr_tm);
        char buf[128]; // 出参
        snprintf(buf, sizeof(buf), "%4d-%02d-%02d %02d:%02d:%02d",
                 curr_tm.tm_year + 1900,
                 curr_tm.tm_mon + 1,
                 curr_tm.tm_mday,
                 curr_tm.tm_hour,
                 curr_tm.tm_min,
                 curr_tm.tm_sec);
        return buf;
    }

    // 日志核心管理类
    class Logger 
    {
    public:
        Logger()
        {
            EnableConsoleLogStrategy();
        }

        // 切换为文件输出
        void EnableFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }

        // 切换为控制台输出
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 日志消息构造: 负责拼接内容, 析构时自动输出
        class LogMessage
        {
        public:
            // 构造日志头部(时间、等级、进程ID、文件名、行号)
            LogMessage(LogLevel level, std::string src_name, int line_number, Logger &logger)
                :  _logger(logger) 
            {
                std::stringstream ss;
                ss << "[" << GetTimeStamp() << "] "
                   << "[" << LevelStr(level) << "] "
                   << "[" << getpid() << "] "
                   << "[" << src_name << "] "
                   << "[" << line_number << "] - ";
                _loginfo = ss.str(); 
            }

            // 流方式拼接日志内容
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;           
                _loginfo += ss.str(); 
                return *this;         
            }

            // 析构自动输出日志
            ~LogMessage()
            {
                if (_logger._fflush_strategy) 
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }

        private:     
            std::string _loginfo;   
            Logger &_logger;        
        };

        // 仿函数接口, 创建日志消息
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }

    private:
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };

    Logger logger; 

// 简化调用宏
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

3、Util.hpp

#pragma once

#include <iostream>
#include <string>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <atomic>
#include <fstream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <boost/algorithm/string.hpp>

const std::string temp_path = "./temp/";

namespace UtilModule
{
    // 时间工具类
    class TimeUtil
    {
    public:
        // 获取秒级时间戳
        static std::string GetTimeStamp()
        {
            struct timeval _time; // 出参
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec);
        }

        // 获取毫秒级时间戳
        static std::string GetTimeMs()
        {
            struct timeval _time; // 出参
            gettimeofday(&_time, nullptr);
            return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
        }
    };

    // 路径工具类: 拼接编译/运行所需临时文件路径
    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;
        }

        // 源文件
        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 CompileError(const std::string &file_name)
        {
            return AddSuffix(file_name, ".compile_error");
        }

        // 标准输入输出错误文件
        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");
        }
    };

    // 文件工具类: 文件判断、读写、唯一文件名生成
    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;
        }

        // 生成唯一文件名(时间戳+原子自增ID)
        static std::string UniqFileName()
        {
            static std::atomic_uint id(0);
            id++;
            std::string ms = TimeUtil::GetTimeMs();
            return ms + "_" + std::to_string(id);
        }

        // 写入文件
        static bool WriteFile(const std::string &target, const std::string &content)
        {
            std::ofstream out(target);
            if (!out.is_open())
                return false;

            out.write(content.c_str(), content.size());
            out.close();
            return true;
        }

        // 读取文件, keep控制是否保留换行符
        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;              // 出参
            while (std::getline(in, line)) // 读取一行
            {
                (*content) += line;
                (*content) += (keep ? "\n" : "");
            }
            in.close();
            return true;
        }
    };

    // 字符串工具类: 字符串分割
    class StringUtil
    {
    public:
        // 分隔字符串,合并连续分隔符    target出参
        static void SplitString(const std::string &src, std::vector<std::string> *target, const std::string &sep)
        {
            boost::split(*target, src, boost::is_any_of(sep), boost::algorithm::token_compress_on);
        }
    };
}

五、编译运行模块compile_server

提供服务:代码编译运行,输出格式化执行结果

1、compiler.hpp

它是整个 OJ 系统的「代码编译器模块」,专门负责把用户提交的 C++ 源代码,调用 g++ 编译成可执行程序,并且捕获编译错误、保证编译过程安全、不影响主服务。

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"

namespace ns_compiler
{
    using namespace LogModule;
    using namespace UtilModule;

    // 编译模块: 负责调用g++编译用户代码
    class Compiler
    {
    public:
        Compiler() = default;
        ~Compiler() = default;

        // 编译指定源文件, 生成可执行程序
        static bool Compile(const std::string &file_name)
        {
            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(LogLevel::ERROR) << "创建子进程失败";
                return false;
            }
            else if (pid == 0)
            {
                // 子进程: 执行编译逻辑
                umask(0);
                int _stderr = open(PathUtil::CompileError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if (_stderr < 0)
                {
                    LOG(LogLevel::WARNING) << "打开stderr文件失败";
                    exit(1);
                }
                
                // 标准错误重定向到文件
                dup2(_stderr, 2);

                // 替换为g++执行编译
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(),
                       "-D", "COMPILER_ONLINE", "-std=c++11", nullptr);

                LOG(LogLevel::ERROR) << "启动编译器g++失败";
                exit(2);
            }
            else
            {
                // 父进程: 等待子进程完成
                waitpid(pid, nullptr, 0);
                if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
                {
                    LOG(LogLevel::INFO) << PathUtil::Src(file_name) << "编译成功";
                    return true;
                }
            }
            LOG(LogLevel::ERROR) << "编译失败, 未生成可执行程序";
            return false;
        }
    };
}

详细功能:

1. 调用 g++ 编译用户代码
1)接收一个唯一文件名(如 时间戳_ID)
2)找到对应的 .cpp 源文件
3)调用系统编译器 g++ 编译生成可执行文件 .exe

execlp("g++", "g++", "-o", 可执行文件, 源文件, "-D COMPILER_ONLINE", "-std=c++11", nullptr);

2. 创建子进程执行编译,实现安全沙箱(最重要!)

pid_t pid = fork();
if (pid == 0) {
    // 子进程执行编译
}

1)编译操作放在子进程里做
2)编译崩溃、卡死、出错不会影响主服务进程
3)这是安全沙箱的核心体现之一:进程隔离

3. 捕获编译错误信息,返回给前端

int _stderr = open(错误文件, O_CREAT | O_WRONLY, 0644);
dup2(_stderr, 2);

1)把 g++ 的标准错误(编译报错) 重定向到文件
2)用户代码语法错误、链接错误都会被保存下来
3)最后返回给网页显示:xxx 行缺少分号、变量未定义 等

4. 给编译添加关键宏定义:COMPILER_ONLINE

"-D", "COMPILER_ONLINE"

1)告诉编译器在线编译模式生效
2)让测试用例文件 tail.cpp 不重复引入头文件
3)实现用户代码 + 测试用例自动拼接

5. 等待编译完成,并判断是否成功

waitpid(pid, nullptr, 0);
if (可执行文件存在) 编译成功

1)父进程阻塞等待子进程编译完成
2)通过是否生成 exe 文件判断编译成功 / 失败
3)成功返回 true,失败返回 false

2、runner.hpp

它是 OJ 系统的「代码运行沙箱模块」,专门负责安全、受控地执行用户编译好的程序 ,限制 CPU 和内存资源,重定向输入输出,并返回程序运行状态。

#pragma once

#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"

namespace ns_runner
{
    using namespace LogModule;
    using namespace UtilModule;

    // 运行模块: 执行用户程序, 限制资源, 重定向输入输出
    class Runner
    {
    public:
        Runner() = default;
        ~Runner() = default;

        // 设置进程资源限制(CPU时间、内存大小)
        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);

            // 设置内存大小限制(KB)
            struct rlimit mem_rlimit;
            mem_rlimit.rlim_max = RLIM_INFINITY;
            mem_rlimit.rlim_cur = _mem_limit * 1024;
            setrlimit(RLIMIT_AS, &mem_rlimit);
        }

        // 运行可执行程序, 返回运行状态码
        static int Run(const std::string &file_name, int cpu_limit, int mem_limit)
        {
            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(LogLevel::ERROR) << "运行时打开标准文件失败";
                return -1;
            }

            pid_t pid = fork();
            if (pid < 0)
            {
                LOG(LogLevel::ERROR) << "运行时创建子进程失败";
                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);
                close(_stdin_fd);
                close(_stdout_fd);
                close(_stderr_fd);

                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(LogLevel::INFO) << "运行完毕, info: " << (status & 0x7F);
                return status & 0x7F;
            }
        }
    };
}

1. 受控运行用户程序(沙箱核心)
1)调用 fork() 创建独立子进程
2)在子进程中通过 execl() 替换执行用户的可执行程序。
3)用户程序崩溃、死循环只会杀死子进程,绝对不会影响主服务进程
,实现进程隔离。

2. 严格限制资源(防止恶意代码)

setrlimit(RLIMIT_CPU, &cpu_rlimit); // 限制CPU时间
setrlimit(RLIMIT_AS, &mem_rlimit);  // 限制内存大小

1)限制 CPU: 防止死循环耗尽服务器性能(超时系统自动杀死)。
2)限制内存: 防止无限 new 内存把服务器打挂(超限系统自动杀死)。
3)作用: 从操作系统内核层面限制资源,是资源隔离

3. 重定向输入输出(文件隔离)

dup2(_stdin_fd, 0);  // 标准输入 <- 文件
dup2(_stdout_fd, 1); // 标准输出 -> 文件
dup2(_stderr_fd, 2); // 标准错误 -> 文件

1)用户代码的所有输入、输出、报错全部写入临时文件
2)程序不能直接访问控制台,也不能访问系统文件。
3)作用:文件系统隔离,用户代码无法破坏服务器环境。

4. 捕获程序运行结果
1)父进程 waitpid 等待子进程退出。
2)获取子进程退出状态码 status。
3)提取 status & 0x7F 得到终止信号
0:正常运行结束
6(SIGABRT):内存溢出
24(SIGXCPU):CPU 超时
11(SIGSEGV):段错误

5. 文件描述符安全管理
1)打开文件后,无论成功失败,都会及时 close
2)子进程 dup2 后立即关闭无用的 fd,防止文件描述符泄漏

3、compile_run.hpp

它是整个编译运行服务的「总调度核心」,负责把 Compiler(编译)和 Runner(运行)组装起来,提供一套完整的、对外可用的「代码编译 + 运行 + 结果返回 + 自动清理」服务接口。

#pragma once

#include <signal.h>
#include <jsoncpp/json/json.h>
#include "../comm/Log.hpp"
#include "../comm/Util.hpp"
#include "compiler.hpp"
#include "runner.hpp"

namespace ns_compile_and_run
{
    using namespace LogModule;
    using namespace UtilModule;
    using namespace ns_compiler;
    using namespace ns_runner;

    // 编译运行模块: 整合编译、运行、结果返回、临时文件清理
    class CompileAndRun
    {
    public:
        // 清理本次请求产生的所有临时文件
        static void RemoveTempFile(const std::string &file_name)
        {
            unlink(PathUtil::Src(file_name).c_str());
            unlink(PathUtil::Exe(file_name).c_str());
            unlink(PathUtil::CompileError(file_name).c_str());
            unlink(PathUtil::Stdin(file_name).c_str());
            unlink(PathUtil::Stdout(file_name).c_str());
            unlink(PathUtil::Stderr(file_name).c_str());
        }

        // 状态码转描述信息
        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:
                FileUtil::ReadFile(PathUtil::CompileError(file_name), &desc, true);
                break;
            case SIGABRT:
                desc = "内存超过限制";
                break;
            case SIGXCPU:
                desc = "CPU使用超时";
                break;
            case SIGFPE:
                desc = "浮点数溢出";
                break;
            default:
                desc = "未知错误: " + std::to_string(code);
                break;
            }
            return desc;
        }

        // 编译运行服务入口
        // 输入: JSON字符串(代码、输入、资源限制)
        // 输出: JSON字符串(状态、描述、运行结果)
        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;
            int run_result = 0;
            Json::Value out_value; // 出参
            std::string file_name;

            // 代码为空
            if (code.size() == 0)
            {
                status_code = -1;
                goto END;
            }

            // 生成唯一文件名
            file_name = FileUtil::UniqFileName();

            // 写入源文件
            if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
            {
                status_code = -2;
                goto END;
            }

            // 编译
            if (!Compiler::Compile(file_name))
            {
                status_code = -3;
                goto END;
            }

            // 运行
            run_result = Runner::Run(file_name, cpu_limit, mem_limit);
            if (run_result < 0)
                status_code = -2;
            else if (run_result > 0)
                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_str; // 出参
                FileUtil::ReadFile(PathUtil::Stdout(file_name), &stdout_str, true);
                out_value["stdout"] = stdout_str; 

                std::string stderr_str;
                FileUtil::ReadFile(PathUtil::Stderr(file_name), &stderr_str, true);
                out_value["stderr"] = stderr_str; 
            }

            // 构造返回JSON
            Json::StyledWriter writer;
            *out_json = writer.write(out_value);

            // 清理临时文件
            RemoveTempFile(file_name);
        }
    };
}

1. 整合编译 + 运行两大模块(总控)
1)调用 Compiler::Compile() 编译代码
2)调用 Runner::Run() 运行程序
3)对外只暴露一个入口:Start()
4)作用: 模块化解耦,上层不用关心内部细节,直接调用即可。

2. 解析输入 JSON,构造输出 JSON
1)输入: 前端 / 业务服务传过来的 JSON(代码、输入、CPU 限制、内存限制)
2)输出: 封装好的 JSON(状态码、原因、标准输出、标准错误)
3)作用: 统一服务间通信格式,方便 HTTP 接口传输。

3. 生成唯一文件名,保证多并发安全

file_name = FileUtil::UniqFileName();

1)毫秒时间戳 + 原子自增 ID
2)多线程 / 多请求同时编译运行不会文件名冲突
3)作用: 高并发安全,文件不覆盖、不混乱。

4. 状态码统一管理,错误信息人性化

CodeToDesc(status_code, file_name)

0:成功
-1:代码为空
-2:未知错误
-3:编译错误(直接读取编译错误文件)
SIGABRT:内存超限
SIGXCPU:CPU 超时

5. 自动清理所有临时文件(非常关键)

RemoveTempFile(file_name);

1)无论成功 / 失败,最后一定删除:
.cpp / .exe
.compile_error / .stdin / .stdout / .stderr
2)作用: 不残留垃圾文件,不占磁盘,安全不留痕迹。

6. 流程控制:失败直接跳转结束(goto)
1)代码为空 → 结束
2)写文件失败 → 结束
3)编译失败 → 结束
4)运行异常 → 结束
5)作用: 流程清晰,避免多层 if/else 嵌套。

4、compile_server.cc

它是整个编译运行服务的HTTP 服务器启动入口 ,本质是一个网络服务框架,负责接收前端网络请求,并把请求交给 compile_run.hpp 处理,最后把结果返回给前端。

#include"compile_run.hpp"
#include"../comm/httplib.h"

using namespace ns_compile_and_run;
using namespace httplib;

// 帮助提示: 参数错误时打印用法
void Usage(std::string proc)
{
    std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}

// HTTP服务主入口: 提供在线编译运行接口
int main(int argc,char* argv[])
{
    // 校验启动参数
    if(argc!=2)
    {
        Usage(argv[0]);
        return 1;
    }
    
    Server svr;

    // 注册服务接口: 接收JSON, 处理后返回JSON
    svr.Post("/compile_and_run",[](const Request& req, Response& resp)
    {
        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]));
    return 0;
}

1. 搭建 HTTP 服务,提供网络接口
1)使用第三方 httplib 库搭建 http 服务器
2)对外暴露 http 接口,让前端 / 其他服务可以远程调用
3)作用: 让编译运行功能变成可远程访问的网络服务,而不是只能本地运行

2. 接收前端 POST 请求

svr.Post("/compile_and_run", ...)

1)接口地址:/compile_and_run
2)接收前端发来的 JSON 格式请求(代码、输入、资源限制)
3)作用: 完成前后端交互

3. 调用核心编译运行逻辑

CompileAndRun::Start(in_json, &out_json);

1)把前端传来的 json 字符串直接交给 compile_run 模块
2)由它完成:编译 → 运行 → 结果封装
3)作用:网络层与业务层解耦,网络只负责收发数据,业务负责编译运行

4. 返回 JSON 结果给前端

resp.set_content(out_json, "application/json;charset=utf-8");

1)把处理好的结果 json 发回前端
2)格式标准:application/json
3)作用: 前端可以直接解析并展示给用户

更多推荐