docker+noVNC实现实训平台


一、需求描述

需求: 在计算机考试系统中,考生访问web站点,点击开始考试,页面会打开一个新的ubuntu系统,考生在该系统中进行相应操作。

二、技术方案

技术要点: docker + novnc

具体实现:
1、创建镜像(ubuntu桌面版、拉入novnc项目、镜像启动时启动novnc)
2、前端点击开始开始 -> 发送请求给后台 -> 后台通过 docker-java 启动docker容器(随机端口) -> 返回novnc路径

三、需求实现

3.1 查看dockerapi版本

docker --version
docker version --format '{{.Server.APIVersion}}'

3.2 配置Java连接Docker

// A. 打开docker.service
vim /usr/lib/systemd/system/docker.service

// B. 配置ExecStart (末尾添加 -H tcp://0.0.0.0:2375)
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375
systemctl daemon-reload
sudo systemctl restart docker

// C. 关闭防火墙
systemctl disable firewalld.service

// D.尝试访问链接(需要可以访问)
192.168.18.216:2375/version

// E.使用docker-java3.3.0远程连接使用Docker

3.3 添加依赖

<dependency>
   <groupId>com.github.docker-java</groupId>
   <artifactId>docker-java</artifactId>
   <version>3.3.0</version>
</dependency>
<dependency>
   <groupId>com.github.docker-java</groupId>
   <artifactId>docker-java-transport-httpclient5</artifactId>
   <version>3.3.0</version>
</dependency>

3.4 添加文件DockerUtils.java

package com.ybt.course.service.utils;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
import com.github.dockerjava.transport.DockerHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.time.Duration;
import java.util.*;

@Slf4j
public class DockerUtils {

    private static final Logger log = LoggerFactory.getLogger(DockerUtils.class);

    private static volatile DockerClient dockerClient;
    private String dockerHost;
    private String dockerApiVersion;

    public DockerUtils(String dockerHost, String dockerApiVersion) {
        Objects.requireNonNull(dockerHost, "Docker 主机地址不能为空.");
        Objects.requireNonNull(dockerApiVersion, "Docker API 版本不能为空.");

        this.dockerHost = dockerHost;
        this.dockerApiVersion = dockerApiVersion;

        // 使用双重校验锁实现 Docker 客户端单例
        if (dockerClient == null) {
            synchronized (DockerUtils.class) {
                if (dockerClient == null) {
                    dockerClient = createDockerClient(dockerHost, dockerApiVersion);
                }
            }
        }
    }

    /**
     * 初始化dockerClient
     * @param dockerHost
     * @param dockerApiVersion
     * @return
     */
    private DockerClient createDockerClient(String dockerHost, String dockerApiVersion) {

        String startStr = "tcp://";
        int startIndex = dockerHost.indexOf(startStr) + startStr.length();
        int endIndex = dockerHost.lastIndexOf(":2375");
        String ip = dockerHost.substring(startIndex, endIndex);

        log.info("Docker连接远程主机-" + ip);
        DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost)
                .withApiVersion(dockerApiVersion)
                //如果开启安全连接,需要配置这行
                .withDockerTlsVerify(false)
                .build();
        DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
                .dockerHost(config.getDockerHost())
                .sslConfig(config.getSSLConfig())
                .maxConnections(100)
                .connectionTimeout(Duration.ofSeconds(60))
                .responseTimeout(Duration.ofMinutes(30))
                .build();
        return DockerClientImpl.getInstance(config, httpClient);
    }

    /**
     * ip地址是否是本机
     */
    private static boolean isLocalAddress(String ipAddress) {
        try {
            InetAddress address = InetAddress.getByName(ipAddress);
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            while (interfaces.hasMoreElements()) {
                NetworkInterface networkInterface = interfaces.nextElement();
                Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress addr = addresses.nextElement();
                    if (addr.equals(address)) {
                        return true;
                    }
                }
            }
            return false;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 加载tar镜像文件
     * @param tarFilePath tar文件路径
     * @return
     */
    public String loadTarToImage(String tarFilePath) {
        log.info("Docker加载Tar镜像-" + tarFilePath);
        try {
            File tarFile = new File(tarFilePath);
            dockerClient.loadImageCmd(new FileInputStream(tarFile)).exec();
            log.info("Docker加载Tar镜像成功-" + tarFilePath);
            return "加载成功";
        } catch (Exception e) {
            log.info("Docker加载Tar镜像失败-" + e.getMessage());
            return "加载失败: " + e.getMessage();
        }
    }

    /**
     * 运行镜像, 随机端口映射80端口
     * @param imageName 镜像名称
     * @param tag 镜像Tag
     */
    public String runImage(String imageName, String tag) {
        String imageNameTag = tag == null ? imageName : imageName + ":" + tag;
        PortBinding portBinding = PortBinding.parse("0.0.0.0:0:80");
        CreateContainerResponse exec = dockerClient.createContainerCmd(imageNameTag)
                .withPortBindings(portBinding)
                .exec();
        String containerId = exec.getId();
        dockerClient.startContainerCmd(containerId).exec();
        return containerId;
    }

    /**
     * 镜像是否存在
     * @param imageId 容器id
     */
    public static boolean isExistImage(String imageId) {
        try {
            InspectImageResponse inspectImageResponse = dockerClient.inspectImageCmd(imageId).exec();
            return inspectImageResponse != null;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * tar镜像文件信息获取
     * @param imageName 镜像名称
     * @param tag 镜像Tag
     */
    public Map<String, Object> getTarFileInfo(String tarFilePath) {
        try {
            File tarFile = new File(tarFilePath);
            InputStream tarFileStream = new FileInputStream(tarFile);

            // 解压tar、获取manifest.json
            TarArchiveInputStream tin = new TarArchiveInputStream(tarFileStream);
            TarArchiveEntry entry = tin.getNextTarEntry();
            String json = null;
            while (entry != null) {
                // 只读取manifest.json
                if(entry.getName().equals("manifest.json")){
                    ByteArrayOutputStream result = new ByteArrayOutputStream();
                    int count;
                    byte data[] = new byte[1024];
                    while ((count = tin.read(data, 0, 1024)) != -1) {
                        result.write(data, 0, count);
                    }
                    result.close();
                    json = result.toString();
                    break;
                }
                entry = tin.getNextTarEntry();
            }
            // 不存在则抛出异常
            if(json == null){
                throw new RuntimeException("tar文件缺少manifest.json文件");
            }

            // 组建数据
            String imageId = JSONArray.parseArray(json).getJSONObject(0).getString("Config").replace(".json", "");
            Map<String, Object> tarInfo = new HashMap<String, Object>();
            tarInfo.put("imageId", imageId);
            return tarInfo;
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap<String, Object>();
        }
    }

    /**
     * 停止容器
     * @param id 容器id
     */
    public void stopContainer(String id) {
        dockerClient.stopContainerCmd(id).exec();
    }

    /**
     * 启动容器
     * @param id 容器id
     */
    public void startContainer(String id) {
        dockerClient.startContainerCmd(id).exec();
    }

    /**
     * 重启容器
     * @param id 容器ID
     */
    public void restartContainer(String id) {
        dockerClient.restartContainerCmd(id).exec();
    }

    /**
     * 停止并删除容器
     * @param id 容器ID
     */
    public void removeContainer(String id) {
        if (!isExistContainer(id)) {
            return;
        }
        if (isRunContainer(id)) {
            stopContainer(id);
        }
        dockerClient.removeContainerCmd(id).exec();
    }

    /**
     * 容器列表
     */
    public List<Container> listContainers() {
        try {
            ListContainersCmd listContainersCmd = dockerClient.listContainersCmd();
            return listContainersCmd.exec();
        } catch (Exception e) {
            throw new RuntimeException("获取所有 Docker 容器信息失败: " + e.getMessage());
        }
    }

    /**
     * 容器是否运行中
     * @param containerId 容器id
     */
    public boolean isRunContainer(String containerId) {
        InspectContainerResponse exec = dockerClient.inspectContainerCmd(containerId).exec();
        if (exec != null) {
            return Boolean.TRUE.equals(exec.getState().getRunning());
        }
        return false;
    }

    /**
     * 容器是否存在
     * @param containerId 容器id
     */
    private static boolean isExistContainer(String containerId) {
        try {
            InspectContainerResponse inspectContainerResponse = dockerClient.inspectContainerCmd(containerId).exec();
            return inspectContainerResponse != null;
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取noVNC地址
     * @param containerId 容器id
     */
    public String getNoVNCAddress(String containerId) {
        // 获取docker主机地址
        String[] parts = this.dockerHost.split(":");
        String ipAddress = parts[1].substring(2);

        // 获取容器80端口绑定的宿主主机端口
        String hostPortSpec = "";
        InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec();
        Map<ExposedPort, Ports.Binding[]> bindings = containerInfo.getNetworkSettings().getPorts().getBindings();
        for (Map.Entry<ExposedPort, Ports.Binding[]> entry : bindings.entrySet()) {
            ExposedPort exposedPort = entry.getKey();
            Ports.Binding[] portBindings = entry.getValue();
            if (exposedPort.getPort() == 80) {
                for (Ports.Binding binding : portBindings) {
                    hostPortSpec = binding.getHostPortSpec();
                    if (hostPortSpec != null) {
                        break;
                    }
                }
            }
        }

        // 拼凑noVNC地址
        return "http://" + ipAddress + ":" + hostPortSpec;
    }

    public static void main(String[] args) throws InterruptedException {

        String dockerHost = "tcp://192.168.17.126:2375";
        String dockerApiVersion = "1.41";
        DockerUtils dockerUtil = new DockerUtils(dockerHost, dockerApiVersion);

        Info info = dockerClient.infoCmd().exec();
        String infoStr = JSONObject.toJSONString(info);
        System.out.println("docker的环境信息如下:=================");
        System.out.println(infoStr);

        // String containerId = dockerUtil.runImage("dorowu/ubuntu-desktop-lxde-vnc", null);
        // dockerUtil.stopContainer("cc86df37c76b");
        // dockerUtil.removeContainer("cc86df37c76b");
        // String noVNCAddress = dockerUtil.getNoVNCAddress("f5feadab7a4e");
        // boolean isFinish = dockerUtil.loadTarToImage("F:\\vscfile\\videos\\b7d6f36e9b884663bd1cd8e3188c2914.tar");
        // System.out.println("==============导入Tar为镜像,执行结果:" + isFinish);

        // 判断是否是本机
        // boolean b1 = dockerUtil.isLocalAddress("127.0.0.1");
        // System.out.println("====127.0.0.1是否是本机: " + b1);
        // boolean b2 = dockerUtil.isLocalAddress("192.168.19.140");
        // System.out.println("====192.168.19.140是否是本机: " + b2);
        // boolean b3 = dockerUtil.isLocalAddress("192.168.19.141");
        // System.out.println("====192.168.19.141是否是本机: " + b3);

        // ubuntu测试
        String loadRes = dockerUtil.loadTarToImage("/data/nfs/vscfile/videos/5fd96493d07d48eb9d80907a60ca4eef_hello-world2.tar");
        System.out.println("加载镜像: " + loadRes);
    }
}

四、附录

  • github-novnc
  • 参考镜像
  • 启动镜像命令
    docker run -P dorowu/ubuntu-desktop-lxde-vnc
    docker run -p 6080:80 -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc
    docker run -p 6080:80 -e RESOLUTION=1920x1080 -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc
    
    // 访问: 192.168.18.216:6080

文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录