OJ沙盒判题系统
成果展示
访问路由:http://47.120.1.220:8801/
技术栈
全栈
前端:基于Vue框架+arco实现
前端特点:运用了很多第三方插件,比如openapi,mdEditor,CodeEditor等等。
后端:基于Springboot实现
后端特点:自主实现了一个从0到1的代码沙箱,包括原生java和docker两种实现方式,这里主要展示的代码为如何实现这两种沙箱模式。
核心流程基本架构
模板方法
核心依赖:java进程类
1.把用户的代码保存为文件
2.编译代码,得到class文件
3.执行代码,得到输出结果
4.收集整理输出结果
5.文件清理
6.错误处理
- java程序异常情况
- 执行超时
- 占用内存
- 读文件
- 写文件
- 运行文件
- 执行高危操作
下面代码争对上面问题一一解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
| @Slf4j public abstract class JavaCodeSandboxTemplate implements CodeSandbox{ private static final String GLOBAL_CODE_DIR_NAME = "tmpCode";
private static final String GLOBAL_JAVA_CLASS_NAME = "Main.java";
private static final long TIME_OUT = 5000L;
@Override public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) { List<String> inputList = executeCodeRequest.getInputList(); String code = executeCodeRequest.getCode(); String language = executeCodeRequest.getLanguage();
File userCodeFile = saveCodeToFile(code);
ExecuteMessage complieFileExecuteMessage = complieFile(userCodeFile); System.out.println(complieFileExecuteMessage); List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);
ExecuteCodeResponse outputResponse = getOutputResponse(executeMessageList);
boolean b = deleteFile(userCodeFile); if(!b){ log.error("deleteFile error,userCodeFilePath = {}",userCodeFile.getAbsolutePath()); } return outputResponse; }
public File saveCodeToFile(String code){
String userDir = System.getProperty("user.dir"); String globalCodePathName = userDir + File.separator + GLOBAL_CODE_DIR_NAME; if(!FileUtil.exist(globalCodePathName)){ FileUtil.mkdir(globalCodePathName); }
String userCodeParentPath = globalCodePathName + File.separator + UUID.randomUUID(); String userCodePath = userCodeParentPath + File.separator + GLOBAL_JAVA_CLASS_NAME; File userCodeFile = FileUtil.writeString(code, userCodePath, StandardCharsets.UTF_8); return userCodeFile; }
public ExecuteMessage complieFile(File userCodeFile){ String compileCmd = String.format("javac -encoding utf-8 %s", userCodeFile.getAbsolutePath()); try { Process compileProcess = Runtime.getRuntime().exec(compileCmd); ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(compileProcess, "编译"); if(executeMessage.getExitValue() != 0){ throw new RuntimeException("编译错误"); } return executeMessage; } catch (Exception e) { throw new RuntimeException(e); } }
public List<ExecuteMessage> runFile(File userCodeFile,List<String> inputList){ String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath(); List<ExecuteMessage> executeMessageList = new ArrayList<>(); for(String inputArgs : inputList){ String runCmd = String.format("java -Xmx256m -Dfile.encoding=UTF-8 -cp %s Main %s", userCodeParentPath, inputArgs); try { Process runProcess = Runtime.getRuntime().exec(runCmd);
new Thread(() -> { try{ Thread.sleep(TIME_OUT); System.out.println("超时了,中断进程"); runProcess.destroy(); }catch (InterruptedException e){ throw new RuntimeException(e); } }).start();
ExecuteMessage executeMessage = ProcessUtils.runProcessAndGetMessage(runProcess, "运行"); System.out.println(executeMessage); executeMessageList.add(executeMessage); } catch (Exception e) { throw new RuntimeException("执行错误"+e); } } return executeMessageList; }
public ExecuteCodeResponse getOutputResponse(List<ExecuteMessage> executeMessageList){ ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse(); List<String> outputList = new ArrayList<>(); long maxTime = 0; for (ExecuteMessage executeMessage : executeMessageList) { String errorMessage = executeMessage.getErrorMessage(); if(StrUtil.isNotBlank(errorMessage)){ executeCodeResponse.setMessage(errorMessage); executeCodeResponse.setStatus(3); break; } outputList.add(executeMessage.getMessage()); Long time = executeMessage.getTime(); if(time != null){ maxTime = Math.max(maxTime,time); } } if(outputList.size() == executeMessageList.size()){ executeCodeResponse.setStatus(1); } executeCodeResponse.setOutputList(outputList); JudgeInfo judgeInfo = new JudgeInfo(); judgeInfo.setTime(maxTime); executeCodeResponse.setJudgeInfo(judgeInfo); return executeCodeResponse; }
public boolean deleteFile(File userCodeFile){ if(userCodeFile.getParentFile() != null){ String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath(); boolean del = FileUtil.del(userCodeParentPath); System.out.println("删除"+(del ? "成功":"失败")); return del; } return true; }
private ExecuteCodeResponse getErrorResponse(Throwable e){ ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse(); executeCodeResponse.setOutputList(new ArrayList<>()); executeCodeResponse.setMessage(e.getMessage()); executeCodeResponse.setStatus(2); executeCodeResponse.setJudgeInfo(new JudgeInfo()); return executeCodeResponse; } }
|
基于java原生实现
1 2 3 4 5 6 7 8 9 10
|
@Component public class JavaNativeCodeSandbox extends JavaCodeSandboxTemplate { @Override public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) { return super.executeCode(executeCodeRequest); } }
|
基于docker实现
进一步考虑在系统层进行隔离,通过远程连接,引入docker技术。
docker可以实现程序和宿主机的隔离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| @Component public class JavaDockerCodeSandbox extends JavaCodeSandboxTemplate {
private static final long TIME_OUT = 5000L;
private static final Boolean FIRST_INIT = true;
public static void main(String[] args) { JavaDockerCodeSandbox javaNativeCodeSandbox = new JavaDockerCodeSandbox(); ExecuteCodeRequest executeCodeRequest = new ExecuteCodeRequest(); executeCodeRequest.setInputList(Arrays.asList("1 2", "3 4")); String code = ResourceUtil.readStr("testCode/simpleComputeArgs/Main.java", StandardCharsets.UTF_8);
executeCodeRequest.setCode(code); executeCodeRequest.setLanguage("java"); ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest); System.out.println(executeCodeResponse); }
@Override public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) { String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath(); DockerClient dockerClient = DockerClientBuilder.getInstance().build(); String image = "openjdk:8-alpine"; if (FIRST_INIT) { PullImageCmd pullImageCmd = dockerClient.pullImageCmd(image); PullImageResultCallback pullImageResultCallback = new PullImageResultCallback() { @Override public void onNext(PullResponseItem item) { System.out.println("下载镜像:" + item.getStatus()); super.onNext(item); } }; try { pullImageCmd.exec(pullImageResultCallback) .awaitCompletion(); } catch (InterruptedException e) { System.out.println("拉取镜像异常"); throw new RuntimeException(e); } } System.out.println("下载完成");
CreateContainerCmd containerCmd = dockerClient.createContainerCmd(image); HostConfig hostConfig = new HostConfig(); hostConfig.withMemory(100 * 1000 * 1000L); hostConfig.withMemorySwap(0L); hostConfig.withCpuCount(1L); hostConfig.withSecurityOpts(Arrays.asList("seccomp=安全配置字符串")); hostConfig.setBinds(new Bind(userCodeParentPath, new Volume("/app"))); CreateContainerResponse createContainerResponse = containerCmd .withHostConfig(hostConfig) .withNetworkDisabled(true) .withReadonlyRootfs(true) .withAttachStderr(true) .withAttachStdin(true) .withAttachStdout(true) .withTty(true) .exec(); System.out.println(createContainerResponse); String containerId = createContainerResponse.getId();
dockerClient.startContainerCmd(containerId).exec();
List<ExecuteMessage> executeMessageList = new ArrayList<>(); for (String inputArgs : inputList) { StopWatch stopWatch = new StopWatch(); String[] inputArgsArray = inputArgs.split(" "); String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray); ExecCreateCmdResponse execkedCreateCmdResponse = dockerClient.execCreateCmd(containerId) .withCmd(cmdArray) .withAttachStderr(true) .withAttachStdin(true) .withAttachStdout(true) .exec(); System.out.println("创建执行命令" + execkedCreateCmdResponse); ExecuteMessage executeMessage = new ExecuteMessage(); final String[] message = {null}; final String[] errorMessage = {null}; long time = 0L; final boolean[] timeout = {true}; String execId = execkedCreateCmdResponse.getId(); ExecStartResultCallback execStartResultCallback = new ExecStartResultCallback() { @Override public void onComplete() { timeout[0] = false; super.onComplete(); }
@Override public void onNext(Frame frame) { StreamType streamType = frame.getStreamType(); if (StreamType.STDERR.equals(streamType)) { errorMessage[0] = new String(frame.getPayload()); System.out.println("输出错误结果为:" + errorMessage[0]); } else { message[0] = new String(frame.getPayload()); System.out.println("输出结果为:" + message[0]); } super.onNext(frame); } };
final long[] maxMemory = {0L}; StatsCmd statsCmd = dockerClient.statsCmd(containerId); ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() { @Override public void onStart(Closeable closeable) {
}
@Override public void onNext(Statistics statistics) { System.out.println("内存占用:" + statistics.getMemoryStats().getUsage()); maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]); }
@Override public void onError(Throwable throwable) {
}
@Override public void onComplete() {
}
@Override public void close() throws IOException {
} }); statsCmd.exec(statisticsResultCallback);
try { stopWatch.start(); dockerClient.execStartCmd(execId) .exec(execStartResultCallback) .awaitCompletion(TIME_OUT, TimeUnit.MILLISECONDS); stopWatch.stop(); time = stopWatch.getLastTaskTimeMillis(); statsCmd.close(); } catch (InterruptedException e) { System.out.println("程序执行异常"); throw new RuntimeException(e); } executeMessage.setMessage(message[0]); executeMessage.setErrorMessage(errorMessage[0]); executeMessage.setTime(time); executeMessage.setMemory(maxMemory[0]); executeMessageList.add(executeMessage); } return executeMessageList; }
}
|
项目值得注意的亮点
引入了工厂模式,代理模式,策略模式,模板方法进行优化。
工厂模式:为三种判题机模式实现方式进行了解耦。
代理模式:增强了判题机功能,可以统计时间和记录日志信息。
策略模式:为判题策略,比如java语言有特殊要求进行了优化,拆解代码。