一、需求描述
需求: 在计算机考试系统中,考生访问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