OJ沙盒判题系统

成果展示

访问路由:http://47.120.1.220:8801/

Snipaste_2024-03-04_21-40-52.png

Snipaste_2024-03-04_21-37-28.png

Snipaste_2024-03-04_21-38-00.png

Snipaste_2024-03-04_21-37-44.png

Snipaste_2024-03-04_21-38-21.png

Snipaste_2024-03-04_21-37-08.png

技术栈

全栈
前端:基于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;

/**
* 模板流程
* @param executeCodeRequest
* @return
*/
@Override
public ExecuteCodeResponse executeCode(ExecuteCodeRequest executeCodeRequest) {
List<String> inputList = executeCodeRequest.getInputList();
String code = executeCodeRequest.getCode();
String language = executeCodeRequest.getLanguage();

//1把用户代码保存为文件
File userCodeFile = saveCodeToFile(code);

//2编译文件
ExecuteMessage complieFileExecuteMessage = complieFile(userCodeFile);
System.out.println(complieFileExecuteMessage);
//3
List<ExecuteMessage> executeMessageList = runFile(userCodeFile, inputList);

//4
ExecuteCodeResponse outputResponse = getOutputResponse(executeMessageList);

//5
boolean b = deleteFile(userCodeFile);
if(!b){
log.error("deleteFile error,userCodeFilePath = {}",userCodeFile.getAbsolutePath());
}
return outputResponse;
}

/**
* 保存用户代码为文件
* code 用户代码
* @return
*/
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;
}

/**
* 编译文件
* @param userCodeFile
* @return
*/
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) {
//return getErrorResponse(e);
throw new RuntimeException(e);
}
}

/**
* 执行文件
* @return
*/
public List<ExecuteMessage> runFile(File userCodeFile,List<String> inputList){
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
List<ExecuteMessage> executeMessageList = new ArrayList<>();
for(String inputArgs : inputList){
//可以通过命令行的方式生成安全管理器的.class进行使用 需要指定安全管理器路径 但是安全管理器粒度太细 不建议使用 -》环境隔离 docker
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, "运行");
//ExecuteMessage executeMessage = ProcessUtils.runInteractProcessAndGetMessage(runProcess, "运行", inputArgs);
System.out.println(executeMessage);
executeMessageList.add(executeMessage);
} catch (Exception e) {
throw new RuntimeException("执行错误"+e);
}
}
return executeMessageList;
}

/**
* 获取响应结果
* @param executeMessageList
* @return
*/
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);
//要借助第三方库获取 非常麻烦 暂时不做实现
//judgeInfo.setMemory();
executeCodeResponse.setJudgeInfo(judgeInfo);
return executeCodeResponse;
}

/**
* 删除文件
* @param userCodeFile
* @return
*/
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;
}

/**
* 错误响应
* @param e
* @return
*/
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
/**
* Java 原生代码沙箱实现(直接复用模板方法)
*/
@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);
//String code = ResourceUtil.readStr("testCode/simpleCompute/Main.java",StandardCharsets.UTF_8);
//String code = ResourceUtil.readStr("testCode/unsafeCode/ReadFileError.java",StandardCharsets.UTF_8);

executeCodeRequest.setCode(code);
executeCodeRequest.setLanguage("java");
ExecuteCodeResponse executeCodeResponse = javaNativeCodeSandbox.executeCode(executeCodeRequest);
System.out.println(executeCodeResponse);
}

/**
* 多态 覆盖方法 自定义docker实现
* @param userCodeFile
* @param inputList
* @return
*/
@Override
public List<ExecuteMessage> runFile(File userCodeFile, List<String> inputList) {
String userCodeParentPath = userCodeFile.getParentFile().getAbsolutePath();
//dockerClient
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();

//docker exec keen_blackwell java -cp /app Main 1 3
//执行命令并获取结果
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);//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;
//return super.runFile(userCodeFile, inputList);
}

}

项目值得注意的亮点

引入了工厂模式,代理模式,策略模式,模板方法进行优化。
工厂模式:为三种判题机模式实现方式进行了解耦。
代理模式:增强了判题机功能,可以统计时间和记录日志信息。
策略模式:为判题策略,比如java语言有特殊要求进行了优化,拆解代码。

Snipaste_2024-03-04_21-21-24.png