vnc 键盘慢
诸如Kimchi和Ovirt之类的基于Web的KVM管理工具可帮助用户轻松创建和管理虚拟机(VM),甚至可以从移动设备进行管理。 此类工具依赖于虚拟桌面计算 (VNC)等远程桌面共享技术,而使用VNC的工具则需要基于Web的VNC客户端,例如noVNC 。
VNC的最初目标是使物理PC可以远程访问。 因为虚拟化不是VNC的问题,所以将VNC与VM一起使用时,对键击的解释和操作需要特殊处理。 Web技术带来了其他挑战:Web应用程序必须解决浏览器支持方面的差异,否则将使用范围限制为某些选定的浏览器。 Web应用程序对PC硬件的访问受到浏览器API的限制,而台式机应用程序则具有更直接的访问权限。
本文旨在帮助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相同的键码。 由于从scancode到keycode的转换( 键盘驱动程序的首要任务),应用程序不需要处理所有已知的键盘类型。
scancode和keycode之间的转换是可逆的。 任何键码都可以转换回生成它的确切硬件扫描码。 例如,在标准的美国102键键盘上按下标有Q的键并不能解释为按下了Q,而是按下 了位于第三行第二列中的键 。
按键符号(keysyms)
对于应用程序而言,使用键码仍然不是理想的选择,因为相同的物理键可以根据键盘布局表示不同的符号。 例如,在法语键盘中,位于第三行第二列的键是A,而不是Q。大多数应用程序(例如,文本编辑器)都想知道用户按下了Q,而不是按下的键所在的位置。在布局中。
键符号( keysym )是在考虑了键盘映射( keymap )之后一次或多次按键操作/释放所生成的符号。 从键码到键符的转换是操作系统进行的最后一次转换,将确切的键符传递给应用程序。
图1展示了我刚刚描述的XT兼容键盘的翻译顺序,该键盘从美国或法国键盘布局向基于Linux的系统发送按键。
图1.按键如何从键盘传播到应用程序
与scancode到keycode的转换不同,从keycode到keysym的转换不可逆,原因有两个。 首先,翻译涉及了解用于生成密钥符号的密钥映射,并且此信息并非在所有情况下都可用。 其次,无法知道使用哪个组合键来创建按键符号。 例如,可以通过按Shift + a或在启用Caps Lock的情况下按a来产生A的键符号。 这种歧义是QEMU在RFB中遇到的问题的根源。
RFB协议,QEMU / KVM虚拟化和VNC
RFB (远程帧缓冲区)是VNC用于远程访问GUI的协议。 在协议及其扩展中定义的RFB客户端到服务器消息的几种类型中,这里感兴趣的是KeyEvent
,即按下或释放键时从RFB客户端发送到服务器的消息。 图2显示了消息格式。
图2. RFB KeyEvent
客户端消息的格式
-
message-type
指定message-type
。KeyEvent
消息是类型4。 -
down-flag
指示键的状态。 如果按下该键,则值为1;否则为0。 如果释放,则值为0。 -
padding
是一个零填充的两字节字段。 -
keysym
是已按下或释放的键的keyym。
接收到KeyEvent
消息时,RFB服务器将根据down-flag
的值在按下或释放时复制远程桌面中的keyym。 在此消息中使用keysym值是早期QEMU版本与VNC客户端/服务器一起进行虚拟化的设计问题的根本原因。
QEMU是一个硬件仿真器。 当您连接到在QEMU VM中运行的VNC服务器时,该服务器不仅会接收并显示按键,还会显示按键。 它模拟它们,就像有人在虚拟机中的真实键盘上按下键一样。 结果,QEMU在接收到RFB KeyEvent
消息后,尝试将发送的密钥转换为将生成该密钥的XT扫描代码。 但是, KeyEvent
消息发送keyym。 QEMU面临着一个挑战,即如何基于键符号从按下或释放的键中检索实际的XT扫描代码。
在QEMU最初尝试解决此问题失败(请参阅“首次尝试”侧栏)之后, GTK-VNC和QEMU社区合作创建了RFB协议的正式扩展,该扩展添加了一个新的KeyEvent
消息,该消息不仅包括keyym ,还有在VNC客户端中按下的键码。 图3显示了消息格式。
图3. QEMU扩展的KeyEvent
RFB消息的格式
-
message-type
指定message-type
。 扩展QEMUKeyEvent
消息的类型为255。 - 如果为零,则
submessage-type
默认值为一字节。 -
down-flag
指示键的状态。 如果按下该键,则值为1;否则为0。 如果释放,则值为0。 -
keysym
是已按下或释放的键的keyym。 -
keycode
是生成密钥符号的密钥代码。
有了额外的keycode
信息,QEMU可以将密钥代码反转为扫描代码并进行仿真。 此功能还使VNC服务器不知道VNC客户端正在使用哪个键映射。 如果客户端的键盘映射与来宾OS(在VM中运行的OS)中配置的键盘映射相同,则键盘可以正常工作。
Web技术和按键处理
桌面应用程序和Web应用程序之间键盘事件处理的差异为完全实现基于Web的VNC客户端增加了一层复杂性。 该层是浏览器。 桌面应用程序可以更直接地访问底层硬件,而Web应用程序受浏览器支持的限制。
浏览器中按键处理的基础
浏览器之间缺乏标准化,尽管现在总体上比2000年代初期少了一个问题,但仍然存在。 在键盘处理方面,差异可能很大。
浏览器为Web应用程序提供了三个键盘事件:
-
keydown
:按下一个键。 -
keyup
:释放一个键。 -
keypressed
:按下字符键。
keydown
和keyup
事件类似于OS处理的键盘事件。 仅当生成keypressed
符号时, keypressed
事件才会发生。 特殊键(例如Shift或Alt)不会生成keypressed
事件。 为了使Web应用程序可靠地获取生成的字符,它必须依赖keypressed
事件。
每个事件至少具有以下三个属性:
-
keyCode
属性是指按下时没有修饰键(例如Shift或Alt)的键。 当按下a键时,即使所生成的keyym为A,keyCode
也是相同的。许多网站和Web教程都误导了此属性为键的scancode。 -
charCode
属性是由按键事件生成的按键符号的ASCII码(如果有)。 -
which
属性通常在大多数情况下返回与keyCode
相同的值,从而提供所按键的Unicode值。
您可以使用“ Javascript按键事件测试脚本”页面来查看按下某些按键时键盘事件的行为。 例如,按左Shift键将产生:
keydown keyCode=16 which=16 charCode=0
keyup keyCode=16 which=16 charCode=0
按a键产生:
keydown keyCode=65 (A) which=65 (A) charCode=0
keypress keyCode=0 which=97 (a) charCode=97 (a)
keyup keyCode=65 (A) which=65 (A) charCode=0
按住a键将产生:
keydown keyCode=65 (A) which=65 (A) charCode=0
keypress keyCode=0 which=97 (a) charCode=97 (a)
keydown keyCode=65 (A) which=65 (A) charCode=0
keypress keyCode=0 which=97 (a) charCode=97 (a)
keydown keyCode=65 (A) which=65 (A) charCode=0
keypress keyCode=0 which=97 (a) charCode=97 (a)
keyup keyCode=65 (A) which=65 (A) charCode=0
该浏览器对键盘事件的支持使VNC Web客户端得以实现。 一些VNC客户项目开始尝试解决多键盘布局问题。 但是noVNC项目并未实现QEMU VNC扩展来解决该问题,因此我在2015年决定尝试。 当然,我认为,这只是使用keyCode
(浏览器提供的所谓扫描代码)并将其放入QEMU扩展KeyEvent
消息中的问题。 可能出什么问题了?
keyCode,几乎是扫描码
使用keyCode
属性在noVNC中实现QEMU扩展不能解决键盘布局问题。 我了解到,尽管keyCode
属性具有位置行为,但它与布局无关,因此不能在QEMU KeyEvent
消息中用作键代码。
下面的简单实验显示了keyCode
属性在不同布局中的行为。 再次使用Javascript Key Event Test Script页面显示键盘事件,这是在美国布局键盘中按q键时的输出:
keydown keyCode=81 (Q) which=81 (Q) charCode=0
keypress keyCode=0 which=113 (q) charCode=113 (q)
keyup keyCode=81 (Q) which=81 (Q) charCode=0
将布局更改为法语,这是相同键的输出:
keydown keyCode=65 (A) which=65 (A) charCode=0
keypress keyCode=0 which=97 (a) charCode=97 (a)
keyup keyCode=65 (A) which=65 (A) charCode=0
请注意,当布局更改时, keyCode
值将从81变为65。 在法语AZERTY布局键盘中,第三行的第二个键是a,并且keyCode
反映此布局更改。
在我尝试在noVNC项目中实现QEMU扩展时,浏览器没有属性来描述JavaScript中的物理位置(键的与布局无关的键代码)。 结果,我不得不搁置工作。
抢救KeyboardEvent.code
在2016年初, Chrome浏览器稳定版本48中包含了一个名为code
的新KeyboardEvent
属性。( Firefox较早引入了该属性,后来它在Opera中也可用。)Mozilla开发人员网络对该属性的描述如下:
KeyboardEvent.code
包含一个字符串,该字符串标识所按下的物理键。
该值不受当前键盘布局或修改器状态的影响,因此特定键将始终返回相同的值。
有了这个新属性,我可以恢复并完成我的实现。
工作实施
扩展的QEMU KeyEvent
扩展已在多个桌面VNC客户端中很好地建立和实现。 既然KeyboardEvent.code
属性使恢复被按下的物理键成为可能,VNC Web客户端就没有理由不效仿并实施扩展。 我为noVNC项目实现的解决方案可用于任何基于Web的VNC客户端。
忽略按键事件
我选择忽略解决方案中的keypressed
事件。 仅当一个或多个keypressed
事件产生可读字符( keypressed
符号)时才触发这些事件。 当检测到来自支持QEMU VNC扩展的客户端的连接时,QEMU VNC服务器将(大部分时间,如我稍后将讨论的那样)仅忽略消息的keysym
字段,仅依靠keycode
字段来模拟该消息。 VM中的XT scancode。
代码实施
我设计的完整的工作实现可在GitHub上找到。
在这里,我将重点介绍一些特别值得注意的细节。
如何将KeyboardEvent.code转换为xt_scancode
KeyboardEvent.code
给出了KeyboardEvent.code
的物理位置,但没有提供可直接在RFB消息中使用的格式。 这是此属性的可能值的示例:
'Esc' key: xt_scancode 0x0001 keyboardevent.code = "Escape"
Spacebar: xt_scancode 0x0039 keyboardevent.code = "Space"
'F1' key: xt_scancode 0x003B keyboardevent.code = "F1"
我的实现使用此Mozilla开发人员网络文章中KeyboardEvent.code
上提供的表创建一个哈希表,该哈希表将KeyboardEvent.code
值转换为相应的xt_scancode
,例如:
XT_scancode["Escape"] = 0x0001;
XT_scancode["Space"] = 0x0039;
XT_scancode["F1"] = 0x003B;
创建QEMU RFB KeyEvent消息
将buff
视为大小为12的字节数组:
buff[offset] = 255; // msg-type
buff[offset + 1] = 0; // sub msg-type
buff[offset + 2] = (down >> 8);
buff[offset + 3] = down;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
var RFBkeycode = getRFBkeycode(keycode)
buff[offset + 8] = (RFBkeycode >> 24);
buff[offset + 9] = (RFBkeycode >> 16);
buff[offset + 10] = (RFBkeycode >> 8);
buff[offset + 11] = RFBkeycode;
数据的结构类似于图3并非偶然。 在该代码中, keycode
是xt_scancode
从翻译keyboardevent.code
值,以及keysym
是零场(在大多数情况下)。
getRFBkeycode()
函数将XT_scancode
转换为QEMU VNC扩展定义的格式:
function getRFBkeycode(xt_scancode) {
var upperByte = (keycode >> 8);
var lowerByte = (keycode & 0x00ff);
if (upperByte === 0xe0 && lowerByte < 0x7f) {
lowerByte = lowerByte | 0x80;
return lowerByte;
}
return xt_scancode
}
NumLock的一个奇怪案例:当按键符号很重要时
我提到键盘符几乎被忽略。 在至少一种情况下,QEMU VNC服务器会考虑键盘符号:使用数字键盘(Numpad)中的键时。
在该解决方案的第一个实现中-忽略QEMU KeyEvent消息的keysym字段-发生了一个奇怪的行为:当任何多用途数字小键盘键-0、1、2、3、4、6、7、8、9或十进制分隔符(在en_US
布局中为句点)—即使在VM和客户端上都将NumLock状态设置为ON
的情况下,也是如此,QEMU VNC服务器将:
- 将VM NumLock状态更改为
OFF
(如果为ON
) - 按下键
例如,在客户端和VM上都将NumLock状态设置为ON
时按Numpad 8键将在VM中将NumLock状态设置为OFF
,然后执行向上箭头键。 在NumLock状态为OFF
按Numpad 8键会按预期工作。
通过可靠地同步客户端和VM的NumLock状态,可以解决此问题。 但是远程QEMU VNC服务器不可能知道客户端键盘的NumLock状态。 服务器可以看到何时按下/释放NumLock键,但是它没有有关当前NumLock状态的信息,因为QEMU VNC KeyEvent
消息未传递该信息。
在对桌面VNC客户端进行了广泛的测试之后,我意识到在这种情况下会发送keyym。 尽管键代码不会基于NumLock状态而更改,但键符会受到影响。 结论是QEMU VNC服务器使用keyym字段来猜测客户端的NumLock状态,并据此采取行动以尝试同步VM状态。 在该键符被发送为零的实施,服务器解释这是“客户端的数字锁定状态为OFF
,”强制客户端的NumLock状态OFF
,然后发送键码压制。
由于不发送任何键符,默认情况下NumLock状态为OFF
,因此解决方案是仅在NumLock状态为ON
时发送键符。
发送数字键的键盘符
产生的键符键盘事件是keypressed
事件,我的解决办法忽略。 那么如何将键盘符号提供给QEMU KeyEvent
消息?
幸运的是, keypressed
事件对于确定keypressed
符号不是必需的。 数字小键盘在所有布局中都是标准的(否则,如果没有小键盘,QEMU VNC服务器将无法猜测NumLock状态)。 因此,可以预先确定Numpad键的keyym值。
剩下的问题是,如何(不使用keypress
事件)将Numpad 7用作Home的情况与如何将其用作数字7有所区别。我的实现使用了在keydown
事件中设置的KeyboardEvent.keyCode
属性,进行区分,如以下代码摘录所示。
以下函数接收键盘事件evt
,并将KeyboardEvent.code
值与属于数字KeyboardEvent.code
值进行比较:
function isNumPadMultiKey(evt) {
var numPadCodes = ["Numpad0", "Numpad1", "Numpad2",
"Numpad3", "Numpad4", "Numpad5", "Numpad6",
"Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
return (numPadCodes.indexOf(evt.code) !== -1);
}
我使用前面的函数来查看指定的键盘事件是否需要任何特殊处理。
以下函数接收键盘事件evt
,并将其keyboardevent.keyCode
属性与称为numLockOnKeyCodes
的预定义值集进行numLockOnKeyCodes
:
function getNumPadKeySym(evt) {
var numLockOnKeySyms = {
"Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
"Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
"Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
"Numpad9": 0xffb9, "NumpadDecimal": 0xffac
};
var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102,
103, 104, 105, 108, 110];
if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {
return numLockOnKeySyms[evt.code];
}
return 0;
numLockOnKeyCodes
值对应于数字小键盘键0到9以及处于NumLock ON
状态的小数点分隔符。 如果evt.keyCode
是这些值之一,则该函数返回numLockOnKeySyms
给出的等效keysym; 否则,它返回零。
在代码内调用这些函数的方式如下:
result.code = evt.code;
result.keysym = 0;
if (isNumPadMultiKey(evt)) {
result.keysym = getNumPadKeySym(evt);
}
在此代码中, result
是传递给进行处理的对象。 这样,解决方案可确保正确处理NumLock键。
AltGR和Windows
当我在Windows 10上运行的所有支持的浏览器(Chrome,Firefox和Opera)中测试noVNC解决方案时,出现了另一个异常:AltGR修饰符键在Linux VM上无法正常工作。
通过调试代码,我发现AltGR密钥是通过两个KeyEvent
消息(而不是一个)发送到QEMU VNC服务器的。 第一条消息是向左Ctrl键; 第二条消息是向右Alt键-与您期望某人按下向左Ctrl键并紧接着向右Alt键一样的效果。 当客户端在Linux PC上运行时,AltGR密钥作为正确的Alt发送。
此行为的原因是历史性的。 长话短说:较旧的美国键盘没有AltGR键,Windows开始使用左Ctrl +右Alt来模拟它。 该解决方案适用于没有AltGR键的键盘,但在使用带有AltGR的键盘时可能会产生误导。
一种解决方案是记录此行为,并强迫用户删除此默认映射。 另一个是我选择的解决方案-处理noVNC中的行为。 该代码包括对组合的左Ctrl和右Alt的特殊处理:
if (state.length > 0 && state[state.length-1].code == 'ControlLeft') {
if (evt.code !== 'AltRight') {
next({code: 'ControlLeft', type: 'keydown', keysym: 0});
} else {
state.pop();
}
}
(...)
if (evt.code !== 'ControlLeft') {
next(evt);
}
此代码告诉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问题的属性之一:
if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {
KeyboardEvent.keyCode
(连同which
和charCode
,你读的属性“ Web技术和击键处理 ”一节),已被弃用 ,因为2015年。然而,这应该在自己的位置要使用的财产, KeyboardEvent.key
当时并未在大多数浏览器中实现(目前,在所有Safari版本和Chrome移动版中仍不支持该功能)。 所有这些不赞成使用的属性已在noVNC和需要键盘控制的任何其他应用中广泛使用。 我不希望浏览器很快删除这些属性,但也不建议依赖已弃用的属性。 我强烈建议受影响的应用程序开发人员重构keyCode
, which
,和charCode
新KeyboardEvent.key
API。
结论
调查VNC Web应用程序中的键盘布局问题,设计建议的解决方案,在noVNC项目中实施该解决方案以及处理无法预料的问题是一个艰巨而有益的过程。
自从噩梦开始以来,Web开发已经走了很长一段路。 浏览器兼容性的提高使大多数Web应用程序仅能编码一次并在所有主要浏览器中按预期运行。 但是,当应用程序需要更高级的API(例如键盘处理甚至移动设备的加速度计)时,问题就开始出现。 在这些API中,浏览器支持可能会有所不同,从而直接影响应用程序的开发,而这些应用程序将通过响应式Web设计和HTML5来运行,这些应用程序应使用相同的代码库在多个设备中运行。
当遇到键盘布局问题时,VNC Web客户端会受到此类跨浏览器差异的影响。 尚未实现QEMU VNC KeyEvent扩展的项目存在问题,例如如何在不知道如何使用键映射的情况下如何在非美国键盘上解释给定的键符号。 除非KeyboardEvent.code
属性对所有浏览器都可用,否则实现扩展的项目(如我对noVNC所做的那样)将发现自己支持两种不同的键盘处理方案。
vnc 键盘慢