工作记录(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后缀
5665

被折叠的 条评论
为什么被折叠?



