0


OJ在线评测系统 Docker代码沙箱实现 基于模版方法模式实现在容器内交互

Docker代码沙箱一

刚刚我们熟悉了Docker的基本操作

我们在远程连接的项目里

写一个新的类JavaDockerCodeSandbox

把之前的类粘过去

docker负责运行java程序 并且得到结果

1.把用户代码保存为文件

2.编译代码 得到class文件

3.把编译好的文件上传到容器环境内

3.启动docker容器 在容器中执行代码 得到输出的结果

4.收集整理输出结果

5.文件清理 释放空间

6.错误处理 提高程序的健壮性

模版方法设计模式 定义同一套实现流程 让不同的子类去实现不同流程中的具体实现 执行步骤一样 每个步骤的实现方式不一样

创建容器 上传编译文件

自定义容器的两种方式

1.在已有镜像的基础上再进行扩充 比如说拉取现成的java环境 包含jdk 再把编译后的文件复制到文件里 适合新的项目跑通流程

2.完全自定义容器 适合比较成熟的项目 比如封装多个语言环境和实现

我们这边还是跟刚才的方法一样

  // 获取默认的 Docker Client
        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);
            }
        }

拉取的是java 的镜像 我们推荐的是 openjdk8

String image = "openjdk:8-alpine";

但是为了避免重复更新 拉取镜像

我们选择用布尔值判断

直接定义一个开关 直接写死

这边是因为 我们的镜像只需要拉取一次 如果要拉取多次 就需要单独写一个类判断

private static final Boolean FIRST_INIT = true;

拉取镜像

 // 拉取镜像
        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("下载完成");

接下来创建镜像

这边我们要思考一下创建容器的方式

我们是每一个测试用例都要单独创建一个容器 每个容器只执行一次java命令?

这种很显然不现实 太拉低性能了

我们要创建一个可交互的容器

把docker容器和我们的终端建立一个链接

设置主机配置

创建容器时 可以指定文件路径映射 作用是把本地的文件同步到容器中 可以让容器访问

也可以叫文件挂在目录

// 创建容器
        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)
                .withAttachStdin(true)
                .withAttachStderr(true)
                .withAttachStdout(true)
                .withTty(true)
                .exec();
        System.out.println(createContainerResponse);
        String containerId = createContainerResponse.getId();

Docker代码沙箱二

接下来就是启动容器 执行代码

和已经启动的容器进行交互

    // 启动容器
    dockerClient.startContainerCmd(containerId).exec();

我们先尝试拉取一下这个镜像

服务器还是响应超时啊

找了一个新的镜像源

hub.atomgit.com/library/openjdk:21-bullseye

看一下目前docker里面有那些镜像

发现成功启动了

把开关关掉

这是docker运行的命令

示例执行

docker exec keen_blackwell java -cp /app Main 1 3

能输出结果

注意要把命令拆分 作为数组传递

这样做是为了把每一个命令按空格拆开

我们来看这段代码

            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);
                }
            };

解释

这段代码是一个 Java 匿名内部类的实现,主要用于处理某个执行过程的回调。具体来看:

  1. ExecStartResultCallback: 这是一个回调接口,用于处理执行结果。
  2. onComplete(): 当执行完成时调用该方法,表示没有超时。它将 timeout[0] 设为 false,表示执行过程正常结束。
  3. onNext(Frame frame): 当有新的输出(例如标准输出或错误输出)时调用该方法。- StreamType streamType:用于判断输出的类型(标准输出或错误输出)。- 如果是标准错误输出 (STDERR),将错误信息存储在 errorMessage[0] 中并打印。- 否则,将正常输出存储在 message[0] 中并打印。

这个回调的作用是实时接收和处理命令执行过程中的输出信息,以及在执行完成时进行相应的处理。

异步”的意思是指程序在执行某些操作时,不需要等待该操作完成就可以继续执行后续代码。这种方式通常用于处理耗时的任务,比如网络请求或文件I/O,以避免阻塞主线程。

在你的代码中,

ExecStartResultCallback

的使用意味着以下几点:

  1. 非阻塞: 当你发起某个执行任务时,程序不会停下来等这个任务完成,而是会继续执行后面的代码。
  2. 回调机制: 一旦任务有输出(比如标准输出或错误输出),或者任务完成,相关的回调方法(onNextonComplete)会被自动调用。这种机制允许程序在任务执行的同时处理输出。
  3. 提高效率: 通过异步处理,可以在等待I/O操作时执行其他任务,提高程序的效率和响应性。

总的来说,异步编程模式适合处理需要时间的操作,同时保持应用的流畅和响应。

创建命令

StopWatch stopWatch = new StopWatch();
            String[] inputArgsArray = inputArgs.split(" ");
            String[] cmdArray = ArrayUtil.append(new String[]{"java", "-cp", "/app", "Main"}, inputArgsArray);
            ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId)
                    .withCmd(cmdArray)
                    .withAttachStderr(true)
                    .withAttachStdin(true)
                    .withAttachStdout(true)
                    .exec();

执行命令

  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);
                }
            };

尽量复用之前的ExecuteMessage模式

在异步接口中填充正常 异常信息

这样我们流程就能跑通

但是最重要的是我们要去优化

我们的docker是否真的安全呢

如何获取程序执行时间? 和原生一样

StopWatch类

获取执行时间

try {
                stopWatch.start();
                dockerClient.execStartCmd(execId)
                        .exec(execStartResultCallback)
                        .awaitCompletion(TIME_OUT, TimeUnit.MICROSECONDS);
                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);
            executeMessageList.add(executeMessage);
        }

获取程序占用内存

程序每时每刻都能查看内存 不可能获取每一个时间点的内存

我们要定义一个周期 定期的获取内存

这是 Java 中使用 Docker Java API 获取容器内存使用情况的写法。代码定义了一个

StatsCmd

命令来获取指定

containerId

的统计信息,并通过

ResultCallback

处理获取到的内存数据。在

onNext

方法中,它输出内存使用量并更新最大内存值。其他方法用于处理流的开始、错误和完成事件。你想更深入地了解哪部分吗?

这是一个匿名内部类的写法。使用匿名内部类可以在需要的地方直接实现接口,避免创建一个单独的类。这使代码更加简洁,同时能够直接访问外部类的变量,如

maxMemory

  // 获取占用的内存
            StatsCmd statsCmd = dockerClient.statsCmd(containerId);
            ResultCallback<Statistics> statisticsResultCallback = statsCmd.exec(new ResultCallback<Statistics>() {

                @Override
                public void onNext(Statistics statistics) {
                    System.out.println("内存占用:" + statistics.getMemoryStats().getUsage());
                    maxMemory[0] = Math.max(statistics.getMemoryStats().getUsage(), maxMemory[0]);
                }

                @Override
                public void close() throws IOException {

                }

                @Override
                public void onStart(Closeable closeable) {

                }

                @Override
                public void onError(Throwable throwable) {

                }

                @Override
                public void onComplete() {

                }
            });
            statsCmd.exec(statisticsResultCallback);

这一步 是把回调传递给命令

   statsCmd.exec(statisticsResultCallback);

获取到内存的占用

executeMessage.setMessage(message[0]);
executeMessage.setErrorMessage(errorMessage[0]);
executeMessage.setTime(time);
executeMessage.setMemory(maxMemory[0]);
executeMessageList.add(executeMessage);
标签: docker 容器 运维

本文转载自: https://blog.csdn.net/qq_30500575/article/details/142596395
版权归原作者 朱道阳 所有, 如有侵权,请联系我们删除。

“OJ在线评测系统 Docker代码沙箱实现 基于模版方法模式实现在容器内交互”的评论:

还没有评论