RFB(The Remote Framebuffer Protocol) 协议介绍
RFB的中文名字叫做”远程帧缓冲区协议”, 它用于访问远程的用户界面, 允许客户端查看和操作远程的窗口系统。它主要是将远程窗口系统的帧缓冲区(可以理解我屏幕中的像素区)发送给客户端,因而客户端就可以看到远程窗口的界面了。客户端也会向运行RFB协议的服务端发送一些事件,比如鼠标事件,键盘事件,这样就客户端就可以操作远程的窗口系统了。VNC(Virtual Network Computing)就是基于的RFB协议,它用于远程控制桌面,类似于蒲公英。
RFB协议的监听端口
RFB协议的默认监听端口位 5190,对于一台主机运行多个RFB服务器的情况下,监听端口位 5900 + N 。一些基于浏览器的客户端使用Java应用程序来运行RFB协议。RFB服务器有时在端口5800上提供一个简单的HTTP服务器,该服务器提供必要的Java小程序。
在某些情况下,客户端和服务器的初始角色是相反的,RFB客户端监听端口5500,RFB服务器联系RFB客户端。一旦建立了连接,双方将扮演正常角色,RFB服务器将发送第一条握手消息。
请注意,IANA为RFB分配的唯一端口号是端口5900,因此RFB客户端和服务器应避免使用其他端口号,除非它们与已知使用非标准端口的服务器或客户端通信。
显示协议
协议的显示端基于一个图形原语:“在给定的x,y位置放置一个存放像素数据的矩形(可以理解为在屏幕的x,y坐标的位置放了宽w个像素,高h个像素的矩形)”。这似乎是一种绘制许多用户界面组件的低效方法。然而,允许对像素数据进行各种不同的编码使我们在如何权衡各种参数(如网络带宽、客户端绘制速度和服务器处理速度)方面具有很大的灵活性。
这些矩形的序列进行帧缓冲区更新(这里简称为“更新”)。更新表示从一个有效帧缓冲区状态到另一个有效帧缓冲区状态的更改(可以理解为屏幕上的像素刷新),因此在某些方面类似于视频帧。更新中的矩形通常是不相交的,但并不总是不相交的。
更新由RFB客户端驱动,比如我们在RFB客户端的界面上滑动鼠标,RFB客户端会将我们鼠标滑动时的坐标实时发送给RFB服务端,RFB服务端计算出鼠标指针的像素位置,将鼠标像素发送给RFB客户端,这样我们就会看到鼠标指针移动了。其他的比如按动键盘,鼠标按键这些都是一样的原理。
输入协议
当用户按下键或指针按钮,或者移动指针设备,客户端就会将输入事件发送到服务器。这些输入事件也可以从其他非标准I/O设备合成。例如,基于笔的手写识别引擎可能会生成键盘事件。
版本
RFB协议经过三个已发布版本的演变:3.3、3.7和3.8。本文主要记录最终版本3.8;在后面描述早期版本的细微差异。
协议消息
RFB协议可以在任何可靠的传输上运行,无论是字节流还是基于消息的传输。它通常通过TCP/IP连接进行操作。协议分为三个阶段。首先是握手阶段,其目的是商定协议版本和要使用的安全类型。第二阶段是初始化阶段,客户机和服务器在其中交换ClientInit和ServerInit消息。最后一个阶段是正常的协议交互。客户机可以发送它想要的任何消息,也会从服务器接收消息作为结果。所有这些消息都以消息类型字节开头,后跟消息特定数据。
以下协议消息的描述使用基本类型U8、U16、U32、S8、S16和S32。它们分别表示8位、16位和32位无符号整数以及8位、16位和32位有符号整数。所有多字节整数(像素值本身除外)都以大端顺序排列(最高有效字节优先)。有些消息使用基本类型的数组,数组中的条目数由数组前面的字段确定。
握手消息
协议版本握手
握手开始于服务器向客户端发送协议版本消息。这让客户端知道服务器支持的最高RFB协议版本号。然后,客户机回复类似的消息,给出实际应使用的协议版本号(可能与服务器引用的版本号不同)。客户机不应请求高于服务器提供的协议版本。客户机和服务器都可以通过这种机制提供某种程度的向后兼容性。
目前唯一发布的协议版本是3.3、3.7和3.8。其他版本号由一些服务器和客户端报告,但应解释为3.3,因为它们在3.7或3.8中没有实现不同的握手。添加新的编码或伪编码类型不需要更改协议版本,因为服务器可以忽略它不理解的编码。
ProtocolVersion消息由12个字节组成,解释为格式为“RFB xxx.yyy\n”的ASCII字符字符串,其中xxx和yyy是主版本号和次版本号
1 | RFB 003.003\n(十六进制52 46 42 20 30 30 33 2e 30 30 33 0a)3.3版本 |
安全握手
一旦确定了协议版本,服务器和客户端必须就连接上使用的安全类型达成一致。服务器列出了它支持的安全类型:
1 | +--------------------------+-------------+--------------------------+ |
如果服务器列出了客户端支持的至少一种有效的安全类型,则客户端会发回一个字节,指示要在连接上使用的安全类型:
1 | +--------------+--------------+---------------+ |
如果安全类型数为零,则由于某种原因连接失败(例如,服务器无法支持所需的协议版本)。后面是一个描述原因的字符串(其中一个字符串指定为长度,后跟那么多ASCII字符):
1 | +---------------+--------------+---------------+ |
服务器在发送原因字符串后关闭连接。
安全类型有:
1 | +--------+--------------------+ |
还存在其他的安全类型,但没有公开记录
确定了安全类型,就会进入安全结果握手阶段。
安全结果握手
首先看看上面安全类型为None(不进行密码验证)的握手过程。
由于不进行密码验证,所以当客户端和服务端在安全握手阶段确定了Node的安全类型后,服务端直接返回握手成功消息。
1 | +--------------+--------------+-------------+ |
VNC Authentication 安全类型会在客户端进行连接时要求输入密码,服务端验证通过才能连接。但依然是不安全的。因为数据的传输是明文的,抓包软件就可以抓到。具体握手过程如下:
服务器发送一个随机的16字节质询:
1 | +--------------+--------------+-------------+ |
客户端使用用户提供的密码作为密钥,使用DES对质询进行加密。要形成密钥,密码将被截断为八个字符,不足的话在右侧填充空字节。然后,客户端发送生成的16字节响应:
1 | +--------------+--------------+-------------+ |
服务端收到客户端的16字节响应后,用DES算法对其解密于指定的密码比较,从而判断用户输入的密码是否正确。
当用户输入的密码正确,则发送握手成功消息
1 | +--------------+--------------+-------------+ |
否则发送握手失败消息,并在发送一个消息来说明为什么失败,然后关闭连接。
1 | +--------------+--------------+-------------+ |
1 | +---------------+--------------+---------------+ |
不管安全类型是什么,安全结果握手主要做的就是发送是否握手成功,上面两种安全类型都是如此。即:如果成功发送握手成功消息
1 | +--------------+--------------+-------------+ |
发送握手失败(密码不正确等)发送握手失败消息和失败原因说明,然后关闭来连接
1 | +--------------+--------------+-------------+ |
1 | +---------------+--------------+---------------+ |
初始化消息
安全结果握手成功后,就进入初始化消息阶段,首先客户端发送一个ClientInit消息,然后服务端发送ServerInit消息,初始化阶段完成
ClientInit(客户端初始化消息)
1 | +--------------+--------------+-------------+ |
如果客户端自己连接上服务端后,也允许其他客户端连接这个服务端,那么shared-flag 为 1, 如果客户端向独占这个服务端,在它连接期间,不允许其他客户端连,shared-flag 为 0。
服务端初始化消息
在接收到ClientInit消息后,服务器发送一条ServerInit消息。这会告诉客户端服务器帧缓冲区的宽度和高度、像素格式以及与桌面关联的名称:
1 | +--------------+--------------+------------------------------+ |
像素格式结构
1 | +--------------+--------------+-----------------+ |
- bits-per-pixel:每个像素所占的bit数
- depth:表示在像素中有用的位数
- big-endian-flag:大端标志
- true-color-falg:真彩色标志,数字图像的表示方式,其他的还有索引颜色(一个索引代表一种颜色)等。
- red-max,green-max,blue-max:红,绿,蓝颜色通道的最大值
- red-shift,green-shift,blue-shift:红,绿,蓝三种颜色在像素中的起始bit距离像素开始位置的偏移bit数
- padding:填充,用于内存对齐
Client to Server Messages(客户端到服务端消息)
1 | +--------+--------------------------+ |
存在其他消息类型,但未公开记录。
Set Pixel Format 消息
SetPixelFormat消息设置在服务器到客户端消息中(后面说)FramebufferUpdate消息中发送像素值的格式。如果客户端未发送SetPixelFormat消息,则服务器将按照上面服务器初始化时发送给客户端的消息格式发送像素值。
如果真彩色标志为零(false),则表示将使用“颜色映射”。服务器可以使用SetColorMapEntries消息设置颜色映射中的任何条目。客户机发送此消息后,颜色映射的内容是未定义的,即使服务器以前已设置了条目。
1 | +--------------+--------------+--------------+ |
颜色格式同上。
Set Encodings 消息
SetEncodings消息设置服务器可以发送像素数据的编码类型。此消息中给出的编码类型顺序是客户机对其首选项的提示(指定的第一种编码是最首选的)。服务器可能会也可能不会选择使用此提示。如果此处未明确指定,像素数据可能始终以原始编码发送。
除了真正的编码之外,客户机还可以请求“伪编码(后面说)”向服务器声明它支持协议的某些扩展。不支持扩展的服务器将忽略伪编码。注意,这意味着客户机必须假设服务器不支持扩展,直到它从服务器获得一些特定于扩展的确认。
1 | +--------------+--------------+---------------------+ |
后面是客户端发送的具体编码
1 | +--------------+--------------+---------------+ |
Framebuffer Update Request 消息
FramebufferUpdateRequest消息通知服务器客户端对由x位置、y位置、宽度和高度指定的帧缓冲区区域感兴趣。服务器通常通过发送FramebufferUpdate来响应FramebufferUpdateRequest。可以发送单个FramebufferUpdate以响应多个FrameBufferUpdateRequest。
服务器假定客户端保留其感兴趣的帧缓冲区所有部分的副本。这意味着服务器通常只需要向客户端发送增量更新。
如果客户端丢失了它所需的特定区域的内容,则客户端发送增量设置为零(false)的FramebufferUpdateRequest。这要求服务器尽快发送指定区域的全部内容。该区域将不会使用CopyRect编码(后面说)进行更新。
如果客户端没有丢失它感兴趣的区域的任何内容,那么它将发送一个FramebufferUpdateRequest,增量设置为非零(true)。如果帧缓冲区的指定区域发生更改,服务器将发送帧缓冲区更新。请注意,FramebufferUpdateRequest和FramebufferUpdate之间可能有一段不确定的时间。
对于快速客户端,客户端可能希望调整其发送增量FramebufferUpdateRequests的速率,以避免过多的网络流量。
1 | +--------------+--------------+--------------+ |
Key Event 消息
KeyEvent消息表示按键或释放。如果现在按下该键,则向下标志为非零(true),如果现在松开该键,则向下标志为零(false)。即使客户机或服务器未运行X窗口系统,也使用X窗口系统定义的“keysym”值指定密钥本身。
1 | +--------------+--------------+--------------+ |
对于大多数普通键,keysym与相应的ASCII值相同。有关详细信息,请参阅[XLIBREF]或X Window系统发行版中的头文件<X11/keysymdef.h>。其他一些常用键包括:
1 | +-----------------+--------------------+ |
keysyms的解释是一个复杂的领域。为了尽可能广泛地进行互操作,应遵循以下准则:
- 在解释键符号时,“Shift state”(即,Shift键符号中的任何一个是否被按下)只能用作提示。例如,在美国键盘上“#”字符会移位,但在英国键盘上则不会。使用美国键盘的服务器从使用英国键盘的客户端接收“#”字符时,不会发送任何shift键。在这种情况下,服务器可能在内部需要在其本地系统上模拟shift键,以便获得“#”字符而不是“3”。
- 大写和小写键符之间的差异是很明显的。这与 X 窗口系统中的一些键盘处理不同,后者将它们视为相同。例如,服务器接收到一个大写的 ‘A’ 键符,而没有任何 shift 键按下,应该将其解释为一个大写的 ‘A’。这可能涉及内部模拟的 shift 键按下。
- 服务器应尽可能忽略“锁定”键符号,如CapsLock和NumLock。相反,他们应该根据其大小写解释每个基于字符的keysym。
- 与Shift不同,修改键(如Control和Alt)的状态应视为修改其他键符号的解释。请注意,ASCII控制字符(如Ctrl-A)没有键符号——这些字符应该由客户端先发送控制键,然后再发送“A”键来生成。
- 在某些情况下,客户端中的修改键(例如 Control 和 Alt)也可以用于生成字符键符(即与字符关联的键码)。例如,在德国的 PC 键盘上,按下 Ctrl-Alt-Q 可以生成 ‘@’ 字符。在这种情况下,客户端需要发送额外的释放事件,以便正确地解释 ‘@’ 字符。这是因为对于服务器来说,Ctrl-Alt-@ 可能代表着完全不同的意义,因此需要确保键符的正确解释。
- 在 X 窗口系统中,关于“反向制表符(backward tab)”并没有一个普遍的标准。在一些系统中,按下 shift+tab 会产生键符 “ISO_Left_Tab”,在另一些系统中会产生私有的 “BackTab” 键符,而在另一些系统中会产生 “Tab” 键符,并且应用程序会根据 shift 状态来确定它表示的是反向制表符还是正向制表符。在 RFB 协议中,更倾向于采用后一种方法。客户端应该生成一个带有 Shift 键的 Tab 键符,而不是 “ISO_Left_Tab”。然而,为了与现有客户端保持向后兼容,服务器也应该将 “ISO_Left_Tab” 解释为带有 Shift 键的 Tab。
- 现代版本的 X 窗口系统支持用于 Unicode 字符的键符(keysyms),其中包括具有十六进制 1000000 位设置的 Unicode 字符。为了最大的兼容性,如果一个键同时具有 Unicode 和传统编码,客户端应发送传统编码。
- 一些系统会对诸如 Ctrl-Alt-Delete 这样的键组合进行特殊解释。RFB(远程帧缓冲)客户端通常会提供一个菜单或工具栏功能,用于发送这样的键组合。RFB 协议并不对它们进行特殊处理;要发送 Ctrl-Alt-Delete,客户端发送左侧或右侧 Control、左侧或右侧 Alt 和 Delete 键的按下,然后发送释放按键的信息。许多 RFB 服务器接受 Shift-Ctrl-Alt-Delete 作为 Ctrl-Alt-Delete 的同义词,可以直接从键盘上输入。
Pointer Event 消息
鼠标事件的报文结构如下:
1 | +--------------+--------------+--------------+ |
其中, button-mask 表示鼠标按键按下的状态, button-mask 看作一个8位的二进制, 从0到7为每个二进制位编号(从低位到高位), 二进制 1表示鼠标被按下, 二进制 0 表示鼠标没有被按下, 编号为 1, 2, 3的位表示鼠标左键, 中键和右键.。例如:如果编号为1的二进制位为1, 表示鼠标左键被按下, 反之没有按下。如果编号为2的位为1,说明鼠标中键被按下,反之没被按下。编号为3的同理。如果编号1,2的为都为1,说明鼠标左键和中键都被按下,以此类推,不说了。如果鼠标带滚轮,如果向上滚动,由编号4的位控制,如果向下滚动,由编号为5的位控制。
x-position 和 y-position 表示鼠标的坐标
Client Cut Text 消息
RFB为在客户端和服务器之间同步所选文本的“剪切缓冲区”提供了有限的支持。此消息告诉服务器,客户端的剪切缓冲区中有新的ISO 8859-1(Latin-1)文本。行尾仅由换行符(十六进制0a)表示。未使用回车符(十六进制0d)。无法将文本传输到Latin-1字符集之外。
1 | +--------------+--------------+--------------+ |
总的来说,这个消息的作用就是讲客户端复制的文本可以在服务端进行粘贴。
Server to Client Messages (服务器到客户端消息)
1 | +--------+--------------------+ |
存在其他私有消息类型,但未公开记录。
Framebuffer Update 消息
帧缓冲区更新由一系列矩形像素数据组成,客户端应将这些数据放入其帧缓冲区。它是响应来自客户端的FramebufferUpdateRequest而发送的。请注意,FramebufferUpdateRequest和FramebufferUpdate之间可能有一段不确定的时间。
1 | +--------------+--------------+----------------------+ |
该消息头后面是 number-of-rectangles 个矩形的像素数据。每个矩形都以矩形头开始:
1 | +--------------+--------------+---------------+ |
主要说一下 encoding-type。为了进行高效的像素传输,可以对这些像素使用不同的编码(如压缩)来提供像素的传输效率。为了在客户端解析这些矩形的像素的时候,得知道它是由哪种编码进行传输的,以便进行解码。有关编码的内容后面说。
Set Color Map Entries 消息
当像素格式使用“颜色映射”时,此消息告诉客户端指定的像素值应映射到给定的RGB值。请注意,此消息可能仅更新部分颜色映射。只有在客户端发送了至少一个FramebufferUpdateRequest之后,并且只有在商定的像素格式使用颜色映射时,服务器才应发送此消息。
颜色映射值始终为16位,值的范围从0到65535,与使用的显示硬件无关。例如,白色的颜色贴图值为65535,65535,65535。
消息以描述要更新的colormap条目范围的头开始:
1 | +--------------+--------------+------------------+ |
这个头后面跟着的是 number-of-colors 个 RGB 值, 每个值的格式如下:
1 | +--------------+--------------+-------------+ |
Bell 消息
一个 “Bell” 消息会在客户端发出可听见的信号,如果客户端支持的话。
1 | +--------------+--------------+--------------+ |
Server Cut Text 消息
服务器的剪切缓冲区中有新的ISO 8859-1(Latin-1)文本。行尾仅由换行符(十六进制0a)表示。未使用回车符(十六进制0d)。无法将文本传输到Latin-1字符集之外。
1 | +--------------+--------------+--------------+ |
总的来说就是在服务端复制的文字,可以粘贴到客户端所在的电脑。
Encodings (编码)
这个就是上面说的对矩形像素进行编码的方式。
1 | +--------+-----------------------------+ |
存在其他编码类型,但未公开记录。
Raw Encoding
最简单的编码类型是原始像素数据。在这种情况下,数据由宽度*高度个像素值组成(其中宽度和高度是矩形的宽度和高度)。一行一行的填充到像素数组中(如下面的报文格式)。所有RFB客户端必须能够处理这种原始编码的像素数据,而RFB服务器只应生成原始编码,除非客户端明确要求使用其他编码类型。
1 | +----------------------------+--------------+-------------+ |
CopyRect Encoding
CopyRect(复制矩形)编码是一种非常简单和高效的编码,当客户端在其帧缓冲区中的其他位置已经具有相同的像素数据时,可以使用这种编码。导线上的编码仅由X,Y坐标组成。这在帧缓冲区中提供了一个位置,客户端可以从中复制像素数据的矩形。这可以用于多种情况,最常见的情况是用户在屏幕上移动窗口,以及滚动窗口内容。
1 | +--------------+--------------+----------------+ |
为获得最大兼容性,CopyRect的源矩形不应包含由同一FramebufferUpdate消息中的先前条目更新的像素。否则, 可能服务端发过来的坐标上的像素可能被这次的FramebufferUpdate消息更改,导致复制的是错误的像素值。
版本差异(和 3.8相比)
3.3 版本
Protocol Version 是:
1
RFB 003.003\n(十六进制52 46 42 20 30 30 33 2e 30 30 33 0a)3.3版本
在安全握手阶段, 安全类型由服务器发送给客户端, 而不是双向协商。
1
2
3
4
5+--------------+--------------+---------------+
| No. of bytes | Type [Value] | Description |
+--------------+--------------+---------------+
| 4 | U32 | security-type |
+--------------+--------------+---------------+安全类型只能采用 0、1、2 。0为连接失败,后面是原因字符串。
如果安全类型为1,则不发送SecurityResult消息,直接进入初始化阶段。
VNC Authentication 中, 如果验证失败,服务器发送 SecurityResult消息,不发送原因字符串。
3.7版本
Protocol Version 是:
1
RFB 003.003\n(十六进制52 46 42 20 30 30 33 2e 30 30 37 0a)3.7版本
如果安全类型为1,则不发送SecurityResult消息,直接进入初始化阶段。
VNC Authentication 中, 如果验证失败,服务器发送 SecurityResult消息,不发送原因字符串。