基于 Web 的 KVM 管理工具(比如 Kimchi 和 Ovirt)可帮助用户轻松创建和管理虚拟机 (VM),甚至是从移动设备创建和管理虚拟机。这些工具依靠远程桌面共享技术,比如 虚拟网络计算 (VNC),而使用 VNC 的技术需要一个基于 Web 的 VNC 客户端,比如 noVNC。
VNC 最初的目的是使物理 PC 能够从远程进行访问。因为虚拟化不是 VNC 关注的问题,所以在将 VNC 用于 VM 时,需要经过特殊处理才能解释和操作击键。Web 技术也带来了额外的挑战:Web 应用程序必须解决浏览器支持上的差异,否则仅能用于某些选定的浏览器。Web 应用程序只能通过浏览器 API 访问 PC 硬件,而桌面应用程序能够更直接地进行访问。
本文旨在帮助 JavaScript 开发人员理解和解决相关挑战,让基于 Web 的 VNC 客户端(或其他任何面临同样问题的基于 Web 的硬件模拟器)能够准确地响应从多种键盘布局生成的击键信息。我首先将解释桌面操作系统如何处理键盘信号。然后,您将学习 RFB(VNC 使用的协议)如何将击键信息从 VNC 客户端发送到 VNC 服务器,还将了解此过程在虚拟化场景中涉及到哪些问题,以及 QEMU 社区如何为桌面 VNC 客户端解决这些问题。然后,我将介绍如何使用一个相对较新的浏览器 API 为基于 Web 的 VNC 客户端实现 QEMU 解决方案。
操作系统如何处理击键
键盘是一种硬件设备,对于每个按下或释放的键,它都会发送一个信号。这些信号称为扫描码,由一个或多个字节组成,用于唯一地标识按下或释放实体键的操作。
IBM 在 IBM XT 中设立了第一个扫描码标准。大部分制造商都遵循 XT 标准来确保设备与 IBM 硬件兼容。但是,扫描码不是一种容易供应用程序使用的好的键盘表示,因为不同的键盘类型可能使用不同的扫描码。举例而言,USB 键盘遵循与 XT 标准不同的扫描码标准。
键码
为了使应用程序能够处理任何类型的键盘,操作系统将扫描码转换为与不依赖于键盘的键码。例如,在 PS2 键盘中按 Q,会得到与在 USB 键盘中按 Q 相同的键码。得益于从扫描码到键码的转换(键盘驱动程序 的第一个任务),应用程序不需要处理所有已知的键盘类型。
扫描码与键码之间的转换是可逆的。任何键码都可以转换回生成它的准确的硬件扫描码。例如,在标准美国 102 键键盘上按下标为 Q 的键,不会解释为 Q 键被按下,而是解释为位于第三行第二列的键被按下。
键符 (keysyms)
对应用程序而言,使用键码仍不是很理想,因为根据不同的键盘布局,同一个实体键可能表示不同的符号。例如,在法国键盘中,位于第三行第二列的键是 A,不是 Q。大部分应用程序(例如文本编辑器)都希望获知用户按下了 Q,而不是按下的键在布局中的位置。
键符 (keysym) 是在考虑键盘布局图 (keymap) 后从一次或多次按键/释放键的操作生成的符号。从键码到键符的转换是操作系统执行的最后一次转换,该操作会向应用程序提供准确的键符。
图 1 演示了一个兼容 XT 的键盘将一个从美国或法国键盘布局将按键信号发送到基于 Linux 的系统的转换顺序。
图 1. 按键信号如何从键盘发送到应用程序
不同于扫描码到键码的转换,从键码到键符的转换是不可逆的,原因有两个。首先,这种转换需要知道用于生成键符的键盘布局图,而且不是所有场景都可以获得此信息。其次,无法知道使用了哪种键组合来创建键符。例如,A 的键符可通过按 Shift + a 或在锁定大写时按 a 来生成。这种模糊性是 QEMU 在使用 RFB 时遇到的问题的来源。
RFB 协议、QEMU/KVM 虚拟化和 VNC
RFB(远程帧缓冲区)是 VNC 用于远程访问 GUI 的协议。在该协议及其扩展协议中定义的多种 RFB 客户端到服务器消息类型中,本文关注的是 KeyEvent
,也就是在按下或释放一个键时从 RFB 客户端发送到服务器的消息。图 2 显示了该消息格式。
图 2. RFB KeyEvent
客户端消息的格式
点击查看大图
message-type
指定消息类型。KeyEvent
消息为类型 4。down-flag
指定键的状态。如果按下该键,该值为 1;如果释放,该值为 0。padding
是一个填充了 0 的 2 字节字段。keysym
是按下或释放的键的键符。
当收到 KeyEvent
消息时,依据 down-flag
的值,RFB 服务器将按下或释放键时的键符复制到远程桌面中。在此消息中使用键符,是早期 QEMU 版本在用于虚拟化的 VNC 客户端/服务器上遇到设计问题的根源。
首次尝试
QEMU 项目首次尝试引入 keymap
选项,以告诉 QEMU 生成键符的 VNC 客户端中使用了哪个键盘布局图。有了此信息,QEMU 就可以尝试从键符转换回键码;如果未指定键盘布局图,它会使用默认的美国布局。此方法不足以解决非美国键盘的 QEMU 问题。QEMU 需要能够支持 VNC 客户端使用的任何键盘布局(针对 100 多种语言的布局),而且从上一节中我们已经知道,不同的组合键可能生成相同的键符。
QEMU 是一个硬件模拟器。当您连接到在 QEMU 虚拟机中运行的 VNC 服务器时,服务器不会单纯地接收和显示击键;它会模拟它们,就像有人在虚拟机中的一个真实键盘上按键一样。结果,在收到 RFB KeyEvent
消息时,QEMU 会尝试着将已发送的键转换为生成该键的 XT 扫描码。但是,KeyEvent
消息发送的是键符。QEMU 曾经面临着如何根据键符利用已按下或已释放的键来获取实际 XT 扫描码的挑战。
在 QEMU 最初尝试解决此问题失败后(参见 “首次尝试” 边栏),GTK-VNC 和 QEMU 社区合作创建了 RFB 协议的一个官方扩展,该扩展添加了一条新的 KeyEvent
消息,其中不仅包含键符,还包含在 VNC 客户端中按下的键码。图 3 显示了该消息格式。
图 3. QEMU 扩展 KeyEvent
RFB 消息的格式
点击查看大图
message-type
指定消息类型。扩展的 QEMUKeyEvent
消息为类型 255。submessage-type
有一个一字节的默认值 0。down-flag
指定键的状态。如果按下该键,该值为 1;如果释放,该值为 0。keysym
是已按下或释放的键的键符。keycode
是生成该键符的键码。
借助额外的 keycode
信息,QEMU 可将键码转换回扫描码并进行模拟。此能力还使 VNC 服务器不知道 VNC 客户端使用了哪个键盘布局图。只要客户端的键盘布局图与来宾操作系统(在虚拟机中运行的操作系统)中配置的键盘布局图相同,键盘就会按预期工作。
Web 技术和击键处理
在桌面应用程序和 Web 应用程序之间的键盘事件处理差异,为全面实现基于 Web 的 VNC 客户端增加了一个复杂性层。该层是浏览器。桌面应用程序可更直接地访问底层硬件,而 Web 应用程序受到浏览器支持的限制。
浏览器中的击键处理基本知识
现在出现的问题总数比 2000 年代初期更少,但浏览器之间仍然缺乏标准化。而且谈到键盘处理,差异可能很大。
浏览器提向 Web 应用程序提供了 3 种键盘事件:
keydown
:按下一个键。keyup
:释放一个键:keypressed
:按下一个字符键。
keydown
和 keyup
事件与操作系统处理的键盘事件类似。keypressed
事件仅在生成键符时发生。Shift 或 Alt 等特殊键不会生成 keypressed
事件。Web 应用程序要可靠地获得生成的字符,则必须依靠 keypressed
事件。
每个事件拥有至少以下 3 个属性:
keyCode
属性指按下的键,不含修饰键,比如 Shift 或 Alt。当按下 a 键时,甚至在生成的键符为 A 时,keyCode
也是相同的。许多网站和 Web 教程会误导性地将此属性称为键的扫描码。charCode
属性是键事件(如果有)生成的键符的 ASCII 码。which
属性返回的值在大多数时候与keyCode
相同,提供按下的键的 Unicode 值。
可以使用 Javascript 键事件测试脚本 页面查看在按下某个键时键盘事件有何行为。例如,按下左 Shift 键会得到:
|
按下 a 键会得到:
|
按住 a 键不放会得到:
|
这种对键盘事件的浏览器支持使实现 VNC Web 客户端成为可能。一些 VNC 客户端项目已开始试验解决多键盘布局问题。但 noVNC 项目没有实现 QEMU VNC 扩展来处理该问题,所以在 2015 年,我们决定尝试一下。毫无疑问,我曾认为解决该问题仅需使用 keyCode
(浏览器提供的所谓的扫描码)并将其放在 QEMU 扩展的 KeyEvent
消息中。哪里可能出错了?
keyCode,所谓的扫描码
在 noVNC 中使用 keyCode
属性实现 QEMU 扩展,没有解决键盘布局问题。我了解到,尽管 keyCode
属性拥有定位行为,但它依赖于布局,因此无法在 QEMU KeyEvent
消息中用作键码。
下面的简单试验展示了不同布局中的 keyCode
属性的行为。我们再次使用 Javascript 键事件测试脚本 页面来展示键盘事件,以下是在美国布局键盘中按下 q 键时的输出:
|
将布局更改为法国,以下是同一个键的输出:
|
请注意,当布局发生更改时,keyCode
值从 81 变为了 65。在法国 AZERTY 布局键盘中,第三行第二个键是 a,keyCode
反映了这一布局变化。
在我尝试在 noVNC 项目中实现 QEMU 扩展时,浏览器的 JavaScript 中没有描述物理位置的属性 — a 键的不依赖于布局的键码。所以,我必须暂时搁置这项工作。
KeyboardEvent.code 成为了救星
2016 年初,Chrome 浏览器稳定版 48 中包含一个名为 code
的新 KeyboardEvent
属性。(Firefox 之前已引入此属性,Opera 随后也提供了它。)Mozilla Developer Network 对此属性进行了如下描述:
KeyboardEvent.code
包含一个标识所按下的实体键的字符串。该值不受当前键盘布局或修饰键状态的影响,所以特定的键将始终返回相同的值。
借助这个新属性,我可以继续并完成我的实现。
有效的实现
浏览器支持状况
截至编写本文时,Chrome、Firefox、Firefox for Android 和 Opera 都支持 KeyboardEvent.code
属性。Microsoft Edge 路线图 将此 API 列入 “考虑” 范围。对于任何不支持 KeyboardEvent.code
的浏览器,VNC Web 客户端必须禁用 QEMU VNC 扩展,退而使用默认的 RFB KeyEvent
消息,或者找到另一种方法来使用可用的 KeyboardEvent
属性获取不依赖于布局的实体键。
扩展的 QEMU KeyEvent
消息已在多个桌面 VNC 客户端中良好地建立和实现。既然 KeyboardEvent.code
属性使恢复按下的实体键成为可能,那么 VNC Web 客户端就没有理由不采用相同方式实现该扩展。我为 noVNC 项目实现的解决方案可供任何基于 Web 的 VNC 客户端使用。
忽略 keypressed 事件
我在解决方案中选择了忽略 keypressed
事件。这些事件仅在一个或多个 keypressed
事件生成一个可读字符(一个键符)时触发。检测到来自支持 QEMU VNC 扩展的客户端的连接时,QEMU VNC 服务器会(在大多数时候,我稍后将讨论)忽略消息的 keysym
字段,仅依靠 keycode
字段在虚拟机中模拟 XT 扫描码。
代码实现
我设计的完整、有效的实现可在 GitHub 上获得。
在这里,我将重点介绍一些特别值得注意的细节。
如何将 KeyboardEvent.code 转换为 xt_scancode
KeyboardEvent.code
提供了键的物理位置,但未使用可直接用在 RFB 消息中的格式。以下是该属性的可能值的一个示例:
|
我的实现使用了这篇有关 KeyboardEvent.code
的 Mozilla Developer Network 文章 中提供的表,创建一个将 KeyboardEvent.code
值转换为相应的 xt_scancode
的哈希表,例如:
|
创建 QEMU RFB KeyEvent 消息
将 buff
视为一个大小为 12 的字节数组:
|
数据结构与 图 3 类似,这绝非偶然。在本代码中,keycode
是从 keyboardevent.code
值转换得到的 xt_scancode
,keysym
是一个 0 字段(大部分情况下如此)。
getRFBkeycode()
函数将 XT_scancode
转换为 QEMU VNC 扩展定义的格式:
|
NumLock 的独特情况:键符发挥作用的时刻
我提到过键符基本上被忽略。在至少一种情况下,QEMU VNC 服务器会考虑键符:当使用数字键盘 (Numpad) 中的键时。
在我的解决方案的第一个实现中(忽略 QEMU KeyEvent 消息的键符字段),出现了一种奇怪的行为:当按下任何多用途数字键盘键时,比如 0、1、2、3、4、6、7、8、9 或小数点(en_US
布局中的句点),即使虚拟机和客户端上的 NumLock 状态为 ON
,QEMU VNC 服务器也会:
- 将虚拟机的 NumLock 状态更改为
OFF
(如果它为ON
) - 按键
例如,在客户端和虚拟机上的 NumLock 状态为 ON
时按数字键盘键 8,会将虚拟机中的 NumLock 状态更改为 OFF
,然后执行向上箭头键的操作。在 NumLock 状态为 OFF
时按数字键盘键 8 的行为才是符合预期的。
此问题可通过可靠方式利用客户端和虚拟机的 NumLock 状态来解决。但远程 QEMU VNC 服务器不可能知道客户端键盘的 NumLock 状态。服务器可以看到何时按下/释放 NumLock 键,但无从了解当前的 NumLock 状态,因为 QEMU VNC KeyEvent
消息未传递该信息。
经过在桌面 VNC 客户端上广泛测试后,我认识到在这些环境中发送了键符。尽管键码不会基于 NumLock 状态而发生更改,但键符会受到影响。结论是,QEMU VNC 服务器使用键符���段来猜测客户端的 NumLock 状态,并采取相应行动来尝试同步虚拟机状态。在实现中,发送的键符为 0 时,服务器将此解释为 “客户端的 NumLock 状态为 OFF
”,强制将客户端 NumLock 状态更改为 OFF
,然后发送按下的键码。
因为如果不发送键符,会默认为 NumLock 状态为 OFF
,所以解决方案是仅在 NumLock 状态为 ON
时发送键符。
发送数字键盘的键符
生成键符的键盘事件是 keypressed
事件,我的解决方案忽略了该事件。那么如何将键符应用于 QEMU KeyEvent
消息?
幸运的是,确定键符不是一定需要 keypressed
事件。数字键盘在所有布局中都是标准的(否则,如果没有键盘布局图,QEMU VNC 服务器就无法猜测 NumLock 状态)。所以,数字键盘键的键符值可预先确定。
这就留下了一个问题,如果不使用 keypress
事件,如何区分数字键 7 用作 Home 键的情况和用作数字 7 的情况。我的实现使用了 KeyboardEvent.keyCode
属性(在 keydown
事件上设置)来进行区分,如下面的代码片段所示。
下面的函数接收一个键盘事件 evt
,并将 KeyboardEvent.code
值与属于数字键盘的值相比较:
|
我使用前面的函数来查看是否需要对某个指定的键盘事件进行任何特殊处理。
下面的函数接收一个键盘事件 evt
,并将它的 keyboardevent.keyCode
属性与一个名为 numLockOnKeyCodes
的预定义值集相比较:
|
在 NumLock ON
状态下,numLockOnKeyCodes
值对应于数字键盘键 0 到 9 和小数点。如果 evt.keyCode
是这些值之一,那么该函数会返回 numLockOnKeySyms
提供的等效键符;否则,它会返回 0。
以下是在代码内调用这些函数的方式:
|
在此代码中,result
是在处理过程中传递的对象。这样,解决方案就可以确保正确处理 NumLock 键。
AltGR 和 Windows
我在 Windows 10 上运行的所有支持的浏览器(Chrome、Firefox 和 Opera)中测试 noVNC 解决方案时出现了另一个异常:AltGR 修饰键在 Linux 虚拟机上未按预期工作。
通过调试代码,我发现, AltGR 键通过两条 KeyEvent
消息发送到 QEMU VNC 服务器,而不是一条消息。第一条消息是一个左 Ctrl 键;第二条消息是一个右 Alt 键 — 与您期望某人按下左 Ctrl 后立即按右 Alt 的效果相同。当客户端在 Linux PC 中运行时,发送 AltGR 键作为右 Alt。
出现此行为 是有历史原因的。长话短说:旧的美国键盘没有 AltGR 键,Windows 最初使用左 Ctrl + 右 Alt 来模拟它。此解决方案适合没有 AltGR 键的键盘,但在使用有 AltGR 的键盘时可能带来误导。
一个解决方案是记录此行为,并强制用户删除此默认映射。另一个是我选择的解决方案 — 用于处理 noVNC 中的行为。我的代码包含对按左 Ctrl 后按右 Alt 的组合的特殊处理:
|
此代码告诉 noVNC:在 keydown
事件中,如果 KeyboardEvent.code
等于 ControlLeft
,则不要立即转发该事件。等待第二个 keydown
事件,并验证它的代码是否等于 AltRight
,这意味着浏览器收到了一个左 Ctrl + 右 Alt 的组合,这可能意味着在 Windows 浏览器中按下了 AltGR 键。在这种情况下,丢弃左 Ctrl,仅转发右 Alt,这是 Linux 中的默认行为。这种处理使 AltGR 键能按预期工作,甚至在 Windows 浏览器中也是如此。
此方法的缺点是,即使用户合理地按下了左 Ctrl + 右 Alt 的组合,也不会转发该组合。我将此视为可接受的缺点,因为左 Ctrl + 右 Alt 不是一种常用的组合键(左 Ctrl + 左 Alt 和右 Ctrl + 右 Alt 容易键入得多)。适用性影响极小,而且用户不需要在 Windows 重新配置键盘布局图。
弃用的属性
我的实现的另一个已知缺陷,是一个用于处理 NumLock 问题的属性:
|
KeyboardEvent.keyCode
(连同 which
和 charCode
,可在 “Web 技术和击键处理” 部分看到)自 2015 年以来已被 弃用。但是,当时在大部分浏览器中没有实现应在它们的位置使用的属性 KeyboardEvent.key
(而且在编写本文时,所有 Safari 版本和 Chrome 移动版本仍不支持它)。所有这些弃用的属性被广泛用在 noVNC 和其他任何需要键盘控制的应用程序中。我不希望浏览器很快丢弃这些属性,但依靠一个弃用的属性不是推荐做法。我强烈建议受影响应用程序的开发人员将 keyCode
、which
和 charCode
重构为新的 KeyboardEvent.key
API。
结束语
桌面与 Web VNC 客户端
尽管更方便使用,但基于 Web 的 VNC 客户端比桌面客户端更慢。Kimchi 的一个社区要求是,使用名为 Virt Viewer 的 Linux VNC 客户端让桌面 VNC 客户端更容易连接到 Kimchi 管理的虚拟机。作为 Kimchi 项目的活跃贡献者,我开发了一个 Kimchi 特性,让用户可以选择使用 Virt Viewer 还是 noVNC 来连接到 Kimchi 虚拟机的 VNC 服务器。
调查 VNC Web 应用程序中的键盘布局问题,在 noVNC 项目中实现解决方案并处理未预见的问题,是一个虽艰苦但有益的过程。
Web 开发自早期的噩梦时代以来已有了很大改善。浏览器兼容性的提高使得大部分 Web 应用程序都只需编码一次,即可在所有主要浏览器中按预期运行。但当应用程序需要更高级的 API 时,比如键盘处理或者甚至移动设备加速计,问题就出现了。在这些 API 中,浏览器支持缓冲区,这直接影响了应使用相同代码库在多个设备上运行(借助响应式 Web 设计和 HTML5)的应用程序的开发。
在面临键盘布局问题时,VNC Web 客户端就会受到这类跨浏览器差异的影响。未实现 QEMU VNC KeyEvent 扩展的项目无法摆脱一些问题,比如如何在不知道使用的键盘布局图的情况下解释非美国键盘中的给定键符。除非 KeyboardEvent.code
属性可用于所有浏览器,否则实现该扩展的项目(正如我为 noVNC 所做的一样)需要支持两种不同的键盘处理模式。
本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-12/137810.htm