工作记录(九)——判题沙盒实现

工作记录(9):判题沙箱的本地化实现

一、引言

继上文实现了判题沙箱的前期工作后,现在实现Java、C++、Python三种语言沙箱的本地化实现。项目采用模板方法模式,以JavaCodeSandboxTemplate为基类,定义了判题流程的通用步骤:

1. 保存用户代码到隔离文件夹

2. 编译代码(如有必要)

3. 执行代码,收集输出

4. 整理输出,生成判题响应

5. 清理临时文件

其他语言的沙箱(如CppNativeCodeSandbox、PythonNativeCodeSandbox)继承该模板,重写差异化方法,实现多语言支持。

 二、Java 沙箱实现详解

 1. 判题流程总览

public ExecuteCodeResponse executeCode(ExecuteCodeRequest request) {
    // 1. 保存用户提交的代码到文件
    代码文件 = 保存代码到文件(request.code);
    // 2. 编译代码文件
    编译结果 = 编译代码文件(代码文件);
    // 3. 针对每组输入,执行代码文件
    执行结果列表 = 执行代码文件(代码文件, request.inputList);
    // 4. 整理所有执行结果,生成输出响应
    输出响应 = 整理输出(执行结果列表);
    // 5. 删除临时代码文件,释放资源
    删除文件(代码文件);
    // 返回最终的执行结果
    return 输出响应;
}

 2. 实现逻辑与关键细节

 2.1 保存用户代码

     在saveCodeToFile方法中,系统首先获取当前用户目录,并检查是否存在全局代码目录tmpCode。如果不存在,则创建该目录。接着,系统为每次判题生成一个唯一的子目录(使用UUID),并将用户代码写入Main.java文件中。这一步的目的是确保每次判题的代码都隔离存放,防止不同用户的代码互相干扰。代码写入时使用UTF8编码,确保中文字符的正确处理。

  • 获取当前工作目录,拼接出全局代码存储路径。
  • 如果存储目录不存在则自动创建。
  • 为每次代码保存生成唯一的子目录,避免文件冲突。
  • 拼接出代码文件的完整路径,并将代码内容写入该文件。
  • 返回最终的代码文件对象,供后续编译和执行使用。
public File saveCodeToFile(String code) {
    // 获取当前工作目录
    String rootDir = 获取当前工作目录();
    // 拼接全局代码目录路径
    String codeDir = rootDir + "/code_dir";
    // 如果目录不存在则创建
    if (!目录存在(codeDir)) {
        创建目录(codeDir);
    }
    // 生成唯一子目录
    String userCodeDir = codeDir + "/" + 随机UUID();
    // 拼接代码文件路径
    String codeFilePath = userCodeDir + "/Main.java";
    // 写入代码内容到文件
    File codeFile = 写入文件(code, codeFilePath);
    // 返回代码文件对象
    return codeFile;
}

 2.2 编译代码

     在compileFile方法中,系统使用javac命令编译用户代码。编译命令中指定了UTF8编码,确保源代码中的中文字符能够正确编译。系统通过Runtime.getRuntime().exec启动编译进程,并调用ProcessUtils.runProcessAndGetMessage方法收集编译输出。如果编译过程中出现错误,系统会收集错误信息并提前返回,确保判题流程的及时终止。

public File saveCodeToFile(String code) {
    String rootDir = 获取当前工作目录();
    String codeDir = rootDir + "/code_dir";
    if (!目录存在(codeDir)) { 创建目录(codeDir); }
    String userCodeDir = codeDir + "/" + 随机UUID();
    String codeFilePath = userCodeDir + "/Main.java";
    File codeFile = 写入文件(code, codeFilePath);
    return codeFile;
}

 2.3 执行代码

      在runFile方法中,系统使用java命令运行编译后的Java文件。运行命令中指定了最大内存限制(256MB)、UTF8编码和类路径,确保代码在安全的环境中运行。系统支持多组输入,循环执行每组输入,并通过ProcessUtils.runProcessAndGetMessage方法收集执行输出。为了防止用户代码死循环或卡死,系统启动了一个新线程,在指定时间(5秒)后强制销毁进程,确保判题流程的及时终止。

public List<ExecuteMessage> runFile(File codeFile, List<String> inputList) {
    String codeDir = codeFile.getParent();
    List<ExecuteMessage> resultList = new ArrayList<>();
    for (String input : inputList) {
        // 构造运行命令,指定内存、编码、classpath和参数
        String cmd = "java -Xmx256m -Dfile.encoding=UTF8 -cp " + codeDir + " Main " + input;
        Process process = 执行命令(cmd);
        // 启动超时线程,超时后销毁进程
        启动线程(延迟TIME_OUT后销毁(process));
        // 获取执行结果
        ExecuteMessage msg = 获取进程输出(process, "运行");
        resultList.add(msg);
    }
    return resultList;
}

 2.4 整理输出与判题

      在getOutputResponse方法中,系统遍历所有执行结果,收集标准输出和错误输出。如果存在错误输出,系统会提前终止判题流程,并返回错误信息。系统统计所有执行结果的最大用时,并设置判题信息。如果所有执行结果均成功,系统将状态码设置为2(成功),否则设置为3(运行错误)。

public List<ExecuteMessage> runFile(File codeFile, List<String> inputList) {
    String codeDir = codeFile.getParent();
    List<ExecuteMessage> resultList = new ArrayList<>();
    for (String input : inputList) {
        // 构造命令,设置内存、编码、classpath和参数
        String cmd = "java -Xmx256m -Dfile.encoding=UTF8 -cp " + codeDir + " Main " + input;
        Process process = 执行命令(cmd);
        // 启动超时线程,超时后销毁进程
        启动定时销毁线程(process, TIME_OUT);
        // 获取执行结果
        ExecuteMessage msg = 获取进程输出(process, "运行");
        resultList.add(msg);
    }
    return resultList;
}

 2.5 文件清理

在deleteFile方法中,系统递归删除用户代码的临时目录,防止磁盘空间被占满,确保系统资源的及时释放。

public boolean deleteFile(File userCodeFile) {
    if (userCodeFile.getParentFile() != null) {
        String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
        boolean del = FileUtil.del(userCodeParentPath);
        return del;
    }
    return true;
}

 三、C++ 沙箱实现

 1. C++与Java的主要不同点

  • C++ 是编译型语言。用户提交的 .cpp 源代码,必须先用编译器(如 g++)编译成本地机器码的可执行文件(如 Windows 下的 .exe,Linux 下的无扩展名可执行文件)。运行时,直接执行生成的 Main.exe 文件即可
  • Java 是半编译半解释型语言。用户提交的 .java 源代码,需要先用 javac 编译成字节码文件(.class),但不是本地可执行文件。运行时,需要用 Java 虚拟机(JVM)通过 java 命令来解释执行字节码文件

所以C++沙箱CppNativeCodeSandbox继承自JavaCodeSandboxTemplate,但是要重写编译与执行方法,以适应C++的编译型特性。

 2. 主要逻辑实现

 2.1 编译命令

 使用g++编译,需指定输出文件、编码、栈大小等参数

public ExecuteMessage compileFile(File userCodeFile) {
    String usercodecompilepath = userCodeFile.getAbsolutePath();
    String usercodeexecutepath = usercodecompilepath.replaceFirst(GLOBAL_CLASS_NAME, GLOBAL_CPP_EXECUTE_NAME);
    String compileCmd = String.format("g++ o %s fexeccharset=GBK  Wl,stack=268435456 %s", usercodeexecutepath, usercodecompilepath);
    Process compileProcess = Runtime.getRuntime().exec(compileCmd);
    ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译");
    return executeMessage;
}

 2.2 运行命令

 直接运行可执行文件,传递输入参数

public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
    String userCodeParentPath = userCodeFile.getAbsolutePath();
    String usercodeexecutepath = userCodeParentPath.replaceFirst(GLOBAL_CLASS_NAME, GLOBAL_CPP_EXECUTE_NAME);
    List<ExecuteMessage> executeMessageList = new ArrayList<>();
    for (String inputArgs : inputList) {
        String runCmd = String.format(" %s  %s", usercodeexecutepath, inputArgs);
        Process runProcess = Runtime.getRuntime().exec(runCmd);
        //与Java一致.....

 2.3 编码与栈大小

 fexeccharset=GBK:防止中文输出乱码(Windows下常见问题)

 Wl,stack=268435456:设置栈大小,防止递归爆栈

四、Python 沙箱实现与Java的异同

 1. 不同点

Python语言是解释型语言,代码直接由Python解释器逐行执行,无需编译。Python沙箱PythonNativeCodeSandbox同样继承自JavaCodeSandboxTemplate,重写了编译与执行方法,适应Python解释型语言的特性。

 2. 主要逻辑实现

2.1编译步骤

 Python为解释型语言,无需编译,compileFile直接返回null

@Override
public ExecuteMessage compileFile(File userCodeFile) {
    //python 无需编译
   return null;
}

 2.2 运行命令

 直接用python命令运行脚本,传递输入参数

@Override
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
    String userCodePath = userCodeFile.getAbsolutePath();
    List<ExecuteMessage> executeMessageList = new ArrayList<>();
    for (String inputArgs : inputList) {
        String runCmd = String.format("python %s %s", userCodePath, inputArgs);
        Process runProcess = Runtime.getRuntime().exec(runCmd);
        new Thread(() > {
            try {
                Thread.sleep(TIME_OUT);
                runProcess.destroy();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
        ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行");
        executeMessageList.add(executeMessage);
    }
    return executeMessageList;
}

 五、C++ 测试用例详解

以CppCodeExecuteTest为例,详细说明整个判题流程:

 @Test
    void executeCode() {
        CodeSandbox codeSandbox =new CppNativeCodeSandbox();
        String code = "        #include <iostream>\n" +
                "        #include <cstdlib>\n" +
                "\n" +
                "        int main(int argc, char **argv)\n" +
                "        {\n" +
                "\n" +
                "        \tint sum = 0;\n" +
                "        \tfor (int i = 1; i < argc; ++i)\n" +
                "        \t{\n" +
                "        \t\tint num_i = atoi(argv[i]);\n" +
                "        \t\tsum += num_i;\n" +
                "        \t}\n" +
                "        \tstd::cout << sum;\n" +
                "\n" +
                "        \treturn 0;\n" +
                "        }";

        String language = "cpp";
        // 输入参数列表,两组
        List<String> inputList = Arrays.asList("1 2", "3 4");
        ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
                .code(code)
                .language(language)
                .inputList(inputList)
                .build();
        ExecuteCodeResponse executeCodeResponse = codeSandbox.executeCode(executeCodeRequest);

        Assertions.assertNotNull(executeCodeResponse);
    }

1. 外部调用流程

初始化沙箱:创建CppNativeCodeSandbox实例,准备接收用户代码和输入。

准备代码:用户提交的C++代码,功能为计算命令行参数之和。代码逻辑为遍历命令行参数并累加,最后输出结果。

准备输入:两组输入"1 2"、"3 4",用于测试代码功能。

构建请求:将代码、语言(cpp)和输入列表封装为ExecuteCodeRequest对象。

执行判题:调用codeSandbox.executeCode(executeCodeRequest),启动判题流程。

验证结果:断言返回的ExecuteCodeResponse不为null,确保判题流程正常执行。


2. 内部执行流程

保存代码:系统将用户代码保存到tmpCode/UUID/Main.cpp文件中。确保每次判题的代码隔离存放,防止不同用户的代码互相干扰。

编译代码:系统使用g++命令编译Main.cpp文件,生成可执行文件Main.exe。编译命令中指定了输出文件、编码(GBK)和栈大小(268435456),确保代码在安全的环境中运行。

执行代码:系统循环执行每组输入,调用Main.exe并传递输入参数。第一组输入"1 2":执行Main.exe 1 2,代码计算1 + 2 = 3,输出3。第二组输入"3 4":执行Main.exe 3 4,代码计算3 + 4 = 7,输出7。

整理输出:系统收集所有执行结果,生成outputList = ["3", "7"],状态码为2(成功),表示判题成功。

清理文件:判题结束后,系统递归删除tmpCode/UUID目录,释放磁盘空间,确保系统资源的及时释放。

 六、实现难点与最佳实践

 1. 多语言兼容与扩展

 难点:不同语言的编译、运行方式差异大,需统一接口、便于扩展

 实践:模板方法模式+多态,最大化代码复用,最小化差异化实现

 2. 安全与隔离

 难点:防止恶意代码危害系统,保障判题公平

 实践:每次判题独立目录,结合操作系统权限、SecurityManager等手段

 3. 编码与平台兼容

 难点:中文乱码、路径分隔符、可执行文件后缀等跨平台问题

 实践:统一编码参数,动态拼接路径,Windows下注意.exe后缀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值