通过noVNC和websockify连接到QEMU、KVM

通过noVNC和websockify连接到QEMU、KVM

_

开源项目 QEMU、KVM、libvirt 实现了创建虚拟机,启动虚拟机,监控虚拟机。我们解决了从无到有的问题,这时就该考虑从有到优了。尽管我们能使用 SSH 的方式来登录使用虚拟机,但这种方式从感觉欠缺点什么,用户往往会更喜欢绚丽多彩的东西。

事实上 VNC 的客户端很多,诸如 VNC Viewer,TightVNC,RealVNC 等。然而我们需要的是 web 版的 VNC,自然而然我选择了 noVNC。

noVNC:HTML5 技术的 VNC 客户端

noVNC 是一个可以运行在众多浏览器的 HTML5 VNC 客户端,包括手机浏览器(iOS 和 Android)。

诸如 Ganeti Web ManagerOpenStackOpenNebulaLibVNCServerThinLinc 等众多厂商、项目和产品整合了 noVNC。

VNC Server

除非你使用的 VNC Server 支持 WebSocket 连接(比如 x11vnc/libvncserverQEMU 或者 MobileVNC),否则你需要使用一个 WebSocket 和 TCP socket 之间相互转换的代理。

幸运的是 noVNC 提供了一个代理器 websockify

尽管官方说 QEMU 支持 WebSocket 连接,但我仍然不知道如何在不使用 websockify 的情况下连接到 QEMU,如果有知道的朋友,分享出来吧。

Linux 系统环境

物理机系统版本:Windows7 64 位

宿主机:VMware 12.1.0
宿主机系统版本:Ubuntu 16.04 64 位

客户机(虚拟机):QEMU 2.5.0 libvirt 1.3.1
客户机(虚拟机)系统版本:Deepin 15.4 64 位

一眼看去,可能关系很复杂,其实很简单,因为笔者比较穷,只有一台物理台式机,宿主机是在该物理机上通过 VMware 虚拟出来,而客户机(虚拟机)是在宿主机上通过 QEMU、KVM 虚拟出来的。

测试环境的拓扑关系

宿主机有两台,分别命名为 Node1 和 Node2,这两台宿主机的系统环境完全一致。Node1 的 IP 为:192.168.10.231,Node2 的 IP 为:192.168.10.230。客户机(虚拟机)有一台,名为 Guest1,位于宿主机 Node1,也就是 VNC Server 位于 Node1。要用 noVNC 连接的远程机器就是客户机(虚拟机)Guest1,noVNC 和 websockify 位于宿主机 Node2。它们之间的关系如图所示。

%%{init: { 'theme': 'base', 'themeVariables': { 'background': '#ffffff', 'primaryColor': '#F0F4FF', 'primaryBorderColor': '#4A6CF7', 'primaryTextColor': '#1E293B', 'lineColor': '#94A3B8', 'secondaryColor': '#FEF3C7', 'tertiaryColor': '#F0FDF4', 'fontFamily': 'system-ui, -apple-system, "PingFang SC", sans-serif', 'fontSize': '14px', 'edgeLabelBackground': '#ffffff', 'nodeBorder': '2px' }, 'flowchart': { 'curve': 'basis', 'padding': 24, 'nodeSpacing': 50, 'rankSpacing': 60, 'useMaxWidth': true } }}%% graph TD %% 样式类定义 classDef client fill:#E3F2FD,stroke:#1E88E5,stroke-width:2px,color:#0D47A1,font-weight:bold,radius:10px; classDef proxy fill:#FFF3E0,stroke:#FB8C00,stroke-width:2px,color:#E65100,font-weight:bold,radius:10px; classDef host fill:#F3E5F5,stroke:#8E24AA,stroke-width:2px,color:#4A148C,font-weight:bold,radius:10px; classDef vnc fill:#FCE4EC,stroke:#D81B60,stroke-width:2px,color:#880E4F,font-weight:bold,radius:10px; classDef guest fill:#E8F5E9,stroke:#43A047,stroke-width:2.5px,color:#1B5E20,font-weight:bold,radius:12px; subgraph PC["💻 PC (192.168.10.100)"] Chrome["🌐 浏览器"] end subgraph Node2["🖥️ Node2 (192.168.10.230)"] WS["🔗 noVNC / Websockify<br>端口: 6080"] end subgraph Node1["🖥️ Node1 (192.168.10.231)"] VNC["📡 VNC Server<br>端口: 5900"] Guest1["🖥️ Guest1<br>192.168.10.110"] end %% 连线(双向 + 单向) Chrome --> WS WS --> Chrome WS --> VNC VNC --> WS VNC <-.-> Guest1 %% 应用样式 class Chrome client; class WS proxy; class VNC host; class Guest1 guest; %% 子图样式 style PC fill:#F5F9FF,stroke:#90CAF9,stroke-width:2px style Node2 fill:#FFF8F0,stroke:#FFCC80,stroke-width:2px style Node1 fill:#FBF0FF,stroke:#CE93D8,stroke-width:2px

由于 QEMU、KVM 本身对 VNC Server 支持很友好,因此不需要额外在宿主机 Node1 安装 tightvnc 或 x11vnc 。如果你要连接的机器没有 VNC Server,可直接安装 tightvnc 和 x11vnc 的任意一个。

准备工作

修改 /etc/libvirt/qemu.conf 配置文件。

sudo gedit /etc/libvirt/qemu.conf

打开后有如下文件内容(仅截取部分)展示。

# Master configuration file for the QEMU driver.
# All settings described here are optional - if omitted, sensible
# defaults are used.

# VNC is configured to listen on 127.0.0.1 by default.
# To make it listen on all public interfaces, uncomment
# this next option.
#
# NB, strong recommendation to enable TLS + x509 certificate
# verification when allowing public access
#
#vnc_listen = "0.0.0.0"

# Enable this option to have VNC served over an automatically created
# unix socket. This prevents unprivileged access from users on the
# host machine, though most VNC clients do not support it.
#
# This will only be enabled for VNC configurations that do not have
# a hardcoded 'listen' or 'socket' value. This setting takes preference
# over vnc_listen.
#
#vnc_auto_unix_socket = 1

# Enable use of TLS encryption on the VNC server. This requires
# a VNC client which supports the VeNCrypt protocol extension.
# Examples include vinagre, virt-viewer, virt-manager and vencrypt
# itself. UltraVNC, RealVNC, TightVNC do not support this
#
# It is necessary to setup CA and issue a server certificate
# before enabling this.
#
#vnc_tls = 1
......

将 vnc_listen = "0.0.0.0" 解禁,如图所示。

图2

默认情况下 VNC 监听 127.0.0.1,修改为 0.0.0.0 后适应性更高,如果你使用 libvirt 创建虚拟机,那么虚拟机 xml 配置文件可以向下面这样添加一个 graphics。

<graphics type='vnc' autoport='yes' keymap='en-us'
                  listen='0.0.0.0'/> <!--VNC配置,autoport="yes"表示自动分配VNC端口,推荐使用,listen="0.0.0.0"表示监听所有IP-->

取得客户机(虚拟机)Guest1 的端口号

获得客户机(虚拟机)Guest1 的 port(端口号),有两种方式获得,可通过调用 libvirt Java api 获得客户机(虚拟机)Guest1 的 xml 配置描述文件,然后从中取出 port,还可通过 virsh 控制台命令获得端口号,命令如下。

sudo virsh vncdisplay kvmdemo

kvmdemo 是客户机(虚拟机)Guest1 的名称,得到的结果如图所示。

图3

自动分配的 VNC 端口(自增)默认从 5900 开始,因此 kvmdemo 的 port 是 5900。

noVNC 快速开始

首先下载 noVNC,可通过 git 下载,也可到官网下载压缩包。
按照前面的拓扑关系,我们将 noVNC 下载到宿主机 Node2。

sudo git clone https://github.com/novnc/noVNC.git

下载完毕后,进入 noVNC 文件夹,执行如下命令。

tips:warn 这里填写的是宿主机 Node1 的 ip,而不是客户机(虚拟机)Guest1 的 ip,VNC Server 通过端口映射的方式找到位于宿主机上的客户机(虚拟机)。
sudo ./utils/launch.sh --vnc 192.168.10.231:5900

如果不指定端口,noVNC 默认的访问端口是 6080。执行过程中,noVNC 会去 GitHub 下载 websockify,如果觉得下载太慢,可先将 websockify 下载下来后,解压到 utils 文件夹下。
如此,一个 noVNC 就启动起来了。

图4

oh,it's work.

现在你可以在浏览器输入:

http://192.168.10.230:6080/vnc.html?host=192.168.10.230&port=6080

就可以访问到客户机(虚拟机)Guest1 了。

图5

我们来梳理一下用户发出请求到得到响应的流程:

%%{init: { 'theme': 'base', 'themeVariables': { 'background': '#ffffff', 'primaryColor': '#F0F4FF', 'primaryBorderColor': '#4A6CF7', 'primaryTextColor': '#1E293B', 'lineColor': '#94A3B8', 'secondaryColor': '#FEF3C7', 'tertiaryColor': '#F0FDF4', 'fontFamily': 'system-ui, -apple-system, "PingFang SC", sans-serif', 'fontSize': '13px', 'edgeLabelBackground': '#ffffff', 'nodeBorder': '2px' }, 'flowchart': { 'curve': 'basis', 'padding': 24, 'nodeSpacing': 50, 'rankSpacing': 60, 'useMaxWidth': true } }}%% flowchart LR %% 样式类定义 classDef client fill:#E3F2FD,stroke:#1E88E5,stroke-width:2px,color:#0D47A1,font-weight:bold,radius:12px; classDef proxy fill:#FFF3E0,stroke:#FB8C00,stroke-width:2px,color:#E65100,font-weight:bold,radius:12px; classDef server fill:#F3E5F5,stroke:#8E24AA,stroke-width:2px,color:#4A148C,font-weight:bold,radius:12px; classDef guest fill:#E8F5E9,stroke:#43A047,stroke-width:2.5px,color:#1B5E20,font-weight:bold,radius:16px; PC["💻 PC Chrome<br><small>192.168.10.100</small>"] subgraph Node2["🖥️ Node2<br><small>192.168.10.230</small>"] WS["🔗 websockify <small>:6080</small>"] end subgraph Node1["🖥️ Node1<br><small>192.168.10.231</small>"] VNC["📡 VNC Server <small>:5900</small>"] Guest1["🖥️ Guest1<br><small>192.168.10.110</small>"] end %% 连线 PC -->|📤 WebSocket 请求| WS WS -->|📥 WebSocket 响应| PC WS -->|📤 TCP socket 转发| VNC VNC -->|📥 TCP socket 响应| WS VNC <-.->|🔗 VNC 连接| Guest1 %% 应用样式 class PC client; class WS proxy; class VNC server; class Guest1 guest; %% 子图样式 style Node2 fill:#FFF8F0,stroke:#FFCC80,stroke-width:2px style Node1 fill:#FBF0FF,stroke:#CE93D8,stroke-width:2px

PC Chrome 通过 WebSocket 请求 Node2 上的 websockify 服务, 将 WebSocket 请求通过 TCP socket 转发到指定的 Node1 上的 VNC Server 服务(192.168.10.231:5900),VNC Server 服务收到请求处理完成后返回 TCP socket 响应,在 Node2 上的 websockify 代理器这里被转成 WebSocket 的响应返回给PC Chrome。

整个过程 websockify 代理器是关键,noVNC 可以被放在浏览器端。从流程来看 websockify 可以被部署在任何地方,下一节就将实现 websockify 与 noVNC 分离。

noVNC 进阶

tips:info 独立 websockify 实现一个端口,多个代理

上一节,我们已经通过 noVNC 连上了 KVM,然而这种连接方式并不实用。在实际应用中,不可能为每台虚拟机都架一个代理,这种方式对端口号的消耗也是巨大的,同时 noVNC 通常是集成在前端页面。那有没有可能仅开一个端口,而实现代理多台虚拟机呢,答案自然是可以。

一个端口,多个代理原理,引入 token 文件

在 websockify 项目的 Wiki 主页介绍了实现一个端口,多个代理的方法。
实现的原理就是 websocketproxy.py 这个代理从一个指定的 token 目录读取 token 文件,一个 token 文件通常对应一台客户机(虚拟机)。token文件内容形如 ​ token1: host1:port1 ,这里的 token1 是全局唯一的一个字符串标识,host1 是客户机(虚拟机)所在的宿主机的 ip 地址,本例中就是 Node1 的 ip,而 port1 是客户机(虚拟机) VNC Server 的端口号,本例中就是 Guest1 的 VNC Server 的端口号。因此,本例中名为 generic 的客户机(虚拟机)Guest1 的 token 文件内容为:​ generic: 192.168.10.231:5901

注意: 一个 token 文件可以对应一台客户机(虚拟机),一个 token 文件也可以对应多台客户机(虚拟机)。为了方便编程,通常是一对一的关系。

分离 noVNC 与 websockify

在 Github 上 noVNC 和 websockify 本来就是独立的两个项目。

首先下载 websockify,可通过 git 下载。
按照前面的拓扑关系,我们将 websockify 下载到宿主机 Node2。

sudo git clone https://github.com/novnc/websockify.git

下载完毕后,进入 websockify 文件夹,将上面的 generic 的 token 文件移动到 ./token/目录下,然后执行如下命令。

sudo python2.7 ./run --token-plugin TokenFile --token-source ./token/ 6080

这里的 6080 就是 websockify 代理器的端口号。

图6

现在可以打开 noVNC 的 vnc_lite.html,并在末尾加上
?host=192.168.10.230&port=6080&path=websockify/?token=generic 就可以访问远程客户机(虚拟机)Guest1 了。

图7

如果要连接其他客户机(虚拟机)只需往 ./token/ 目录下添加对应的 token 文件,然后改变 url 的 token 就可以通过 noVNC 访问客户机(虚拟机)。

图8

容器化 websockify

微服务现在是一个很火的概念,提到微服务,自然少不了 docker。下面就提供一种将 websockify 去状态并容器化运行的方案。

1.编写 Dockerfile 文件

FROM python

 MAINTAINER kyyee "kyyee.com"

 RUN apt-get update -y

 RUN apt-get upgrade -y

# Installing the fundamental package for scientific computing with Python: numpy
 RUN apt-get install -y python-numpy

# clean the backup
 RUN apt-get clean

# Copy the files into the container
 ADD ./websockify/ /websockify/

# start the java application
 CMD ["python2.7", "/websockify/run", "--token-plugin", "TokenFile", "--token-source", "/websockify/token/", "6080"]

# usage volume
# VOLUME ["/websockify/token/"]

2.生成 docker 镜像

sudo docker build -t websockify .

3.运行 docker 镜像

sudo docker run -p 6080:6080 --name websockify -it websockify -v /home/kyyee/websockify/token/:/websockify/token/

-v 为 docker 挂载命令,: 前是宿主机上的目录,: 后是 docker 容器中的目录。这里将 /websockify/token/ 目录挂载到宿主机上的 /home/kyyee/websockify/token/ 目录,在 /home/kyyee/websockify/token/ 目录操作与在 /websockify/token/ 目录操作没有区别。

可能存在的问题

某些情况下,你可能连不上 VNC Server,如图所示。

图9

这个时候请查看启动 websockify 的控制台,如果是 handler exception: [Errno 111] Connection refused,通常是由于远端要连接的宿主机 ip 或映射 port 填写错误,或者你远端要连接的客户机(虚拟机)压根没开机。如果没有错误提示,但仍然无法连接,那么可能是远端要连接的客户机(虚拟机)在 virt-manager 或者其他工具中已经打开。

未完待续,后续将讲解 noVNC 加密传输。

libvirt Java API用法连载之libvirt Java API使用详解(四) 2024-08-08
kolla构建openstack镜像 2025-05-03

评论区