OpenFDE中融合(Fusion)概念的很重要部分是应用融合,即在OpenFDE桌面环境下,可以用户无感知的运行不同平台的程序,充分发挥各平台的特点。运行在linux系统上的OpenFDE,本身是一个安卓桌面,同时又与大量的linux程序共存,初步设想是OpenFDE提供入口,直接启动linux程序,现阶段是通过VNC来进行linux输入输出管理,在安卓桌面上实现一个VNC客户端进行C/S调用。
VNC 和 RFB(Remote Framebuffer Protocol)
VNC(Virtual Network Computing)是一种远程桌面控制技术,允许用户通过互联网或局域网远程访问和控制其他计算机。VNC的工作原理是在远程计算机上运行一个VNC服务器程序,用户通过VNC客户端程序连接到该服务器,然后可以在自己的计算机上看到和操作远程计算机的桌面。
VNC使用的技术是RFB(Remote Frame Buffer远程帧缓冲)协议的显示画面分享,他可以做到与操作系统无关,可跨平台,因为他的工作原理就是把图像在本地绘制好之后通过远程发送过客户端进行解码显示。
VNC大部分情况下用做远程控制,并且在效率和带宽上有一定限制。虽然都是在本机,目前我们选用这个方案也是因为项目成熟,开发周期短,能够快速实现需求,实现产品级使用体验效果。
RFB协议参见文档:
RFC 6143: The Remote Framebuffer Protocol
RFB协议简单来说就是在C/S端之间传递输入和输出,输入是键盘鼠标事件,输出是显示帧,协议内容不多,是基于socket连接通讯的。
协议比较简单明了,开源的项目也非常多,OpenFDE选用了TigerVNC方案,在此基础上,我们做的工作主要包括以下几个部分:
linux程序管理
安卓客户端实现
输入法三端打通
剪切板和其他交互优化
下面就重点部分详细说明一下。
VNC server 和 client
实现效果
使用TigerVNC的server,apt安装后,启动vncserver实际参数如下:
/usr/bin/Xtigervnc :2 -desktop firefox_web_browser -auth /home/warlice/.Xauthority -geometry 1900x1200 -depth 24 -rfbwait 30000 -rfbport 5902 -pn -SecurityTypes None -BlacklistThreshold=10000000 -BlacklistTimeout=0firefox_web_browser 脚本内容如下:
#!/bin/bash
fde-set-ime-engine firefox_web_browser & #启动输入法,后文会讲
export GDK_BACKEND=x11
export QT_QPA_PLATFORM=xcb
export GTK_IM_MODULE=ibus
export QT_IM_MODULE=ibus
export QT4_IM_MODULE=ibus
export im=ibus
firefox &输入法的内容在后面描述,此处vncserver的启动参数配置,指定了布局宽高,端口和连接超时,另外还指定了启动程序firefox,即用户想要打开使用的程序。在OpenFDE上,实现逻辑如下:
列出所有Linux系统上可用的程序,提供启动入口:
当用户点击linux程序图标的时候:
先按以上的参数,启动vncserver,拉起对应linux程序,启动完成后,在FDE的安卓应用中,新打开一个窗口,并建立vnc连接,显示RFB协议传输的帧数据,同时把键盘鼠标等输入事件按照协议发送给server。
这样就提供给了一个完整的用户体验,就像在安卓上直接启动一个linux程序一样。
源码方案
当把这些功能串起来的时候,不可避免的需要对这些功能做修改,所以需要对这些功能做定制化修改。经过大量的对比研究,使用的项目源码如下。
这个项目 bVNC、aRDP、aSPICE 和 Opaque(四个 Android 远程桌面客户端)的源代码,我们使用的是他的 bVNC 模块,虽然他还有捐赠的Pro版本,但是从使用介绍来看也只是增加了加密功能。
Server端:GitHub - TigerVNC/tigervnc: High performance, multi-platform VNC client and server
server端的源码处理会复杂一些,server的需要管理linux程序,真正处理输入输出,就需要真正处理linux程序的图形界面,linux上面使用的xorg-xserver程序,vncserver通过与平台相关的xvnc等库来调用xorg-xserver,经vncserver本身的逻辑处理后转成RFB协议,与client交互。
linux上面编译依赖与xorg-xserver的库,如果只是进行调试可以编译x0vncserver。
x0vncserver - an inefficient VNC server which continuously polls any X
他并不能使用所有vncserver的启动参数,启动执行后的效果是桌面输出。可以参考GitHub - TigerVNC/tigervnc: High performance, multi-platform VNC client and server 来了解server的实现逻辑。
编译TigerVNC Server
FDE使用的apt src源码,来修改编译TigerVNCServer deb包,具体步骤如下:
# 打开 /etc/apt/sources.list , 把所有 deb-src 的条目都放开,即解出注释状态。
sudo apt update
sudo apt source tigervnc-standalone-server
//进入源码目录后
sudo apt install equivs devscripts --no-install-recommends
sudo mk-build-deps -i -t "apt-get" -r
sudo DEB_BUILD_OPTIONS="parallel=8" dpkg-buildpackage -b -uc -us源码的管理用的quilt工具,具体使用可以参考 :
[quilt工具使用说明] f. quilt工具使用说明
编译成功后:
sudo dpkg -i tigervnc-standalone-server_1.10.1+dfsg-3_arm64.deb
输入处理和输入法
启动server和client进行连接后,就是按照RFB协议进行通讯,输出就是帧数据,显示什么帧数据由server端决定,基本也不会去改动,client的大部分业务逻辑基本都是在处理输入即鼠标键盘事件。最简单的方法就是在activity的dispatchKeyEvent将所有事直接透传给server端,bVNC就是这样处理的。
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return canvas.getKeyboard().keyEvent(event.getKeyCode(), event);
}按照报文格式发送数据包:
协议报文-鼠标事件的报文如下
+--------------+--------------+--------------+
| No. of bytes | Type [Value] | Description |
+--------------+--------------+--------------+
| 1 | U8 [5] | message-type |
| 1 | U8 | button-mask |
| 2 | U16 | x-position |
| 2 | U16 | y-position |
+--------------+--------------+--------------+协议报文-键盘事件的报文如下
+--------------+--------------+--------------+
| No. of bytes | Type [Value] | Description |
+--------------+--------------+--------------+
| 1 | U8 [4] | message-type |
| 1 | U8 | down-flag |
| 2 | | padding |
| 4 | U32 | key |
+--------------+--------------+--------------+具体可以参见
[RFB协议文档] 输入协议 · RFB 远程帧缓冲协议
需要注意的是安卓使用的键盘keysym与linux能识别的键盘布局是否统一,也就是说发送事件的keysym在server端能否正确识别。
输入法需求说明
在VNC的使用中,有一个需求一直没有被很好的解决,就是文本的输入,英文字符还好处理,直接使用按键即可输出需要的字符,但是中文字符等unicode字符集的字符,linux就没法直接生成了。经过验证linux可以接收少量的在xkeysyms的中文字符,你可以绑定一个中文字符到一个键盘按键上,但是数量也被限制为键盘按键的个数。
所以,如果你想输入中文,最好的办法是启动脚本加入/usr/bin/ibus-daemon -d &,再设置输入法为想要的输入法。但是这也会有一个问题,输入法在linux上启动,与FDE桌面的使用是割裂的,linux上输入法的安装卸载怎么处理,另外候选词框的显示也是一个问题,如果兼容做得不好,并不会显示在VNC中。
基于这些原因,FDE的VNC应用融合方案采用了一个更为完整的实现方式。对输入链路的各个环节都做了个改造,当然到ibus输入法就结束了,X程序只是作为接收文本的一方。
实现效果是,在安卓VNC client端中使用安卓输入法,就可以把中文字符输出到server端打开的linux程序中。
包含三个环节的改动:
VNC APP能够唤起输入法,把输入法生成的字符拦截和转发给vncserver,并且不影响原来的正常通路;
VNC server能够把接收的字符,发给ibus输入法,并且不影响原来的正常通路;
Ibus输入法能够正常调起,接收字符输入到X程序,并且不影响原来的正常输入。
VNC APP
APP实现唤起输入法,用的EditText.request()就可以,但是要拦截获取输入法的内容,需要重写EditText,注入自定义InputConnection,再通过committext接口获取输入的文本。
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if(DEBUG){
Log.d(TAG, "commitText() called with: text = [" + text + "], mTextView = [" + detectEventEditText + "]");
}
if (detectEventEditText == null) {
return super.commitText(text, newCursorPosition);
}
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
Reflector.invokeMethodExceptionSafe(mIMM, "registerSuggestionSpansForNotification",
new Reflector.TypedObject(spans, SuggestionSpan[].class));
}
Reflector.invokeMethodExceptionSafe(detectEventEditText, "resetErrorChangedFlag");
Reflector.invokeMethodExceptionSafe(detectEventEditText, "hideErrorIfUnchanged");
if(mInputModeFlag == INPUT_MODE_ONLY_KEYBOARD){
return false;
}
if(!TextUtils.isEmpty(text)){
detectEventEditText.getCanvas().getKeyboard().keyEvent(0xff, null, text.toString());
}
return true;
}其实可以直接使用#com.android.internal.inputmethod.EditableInputConnection,但是他是一个internal类,final类,笔者通过copy源码在APP集成的方式,部分不能直接调用的函数,用反射来完成。
非输入法生成的按键事件,在activity中调用接口直接按RFB协议发送。
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return canvas.getKeyboard().keyEvent(event.getKeyCode(), event);
}不同的是输入法生成字符用0xff作为keycode,在RfbProto中区分处理。
//正常按键
public synchronized void writeKeyEvent(int keySym, int metaState, boolean down) {
if (viewOnly)
return;
eventBufLen = 0;
if (down) {
writeModifierKeyEvents(metaState, down);
}
if (keySym > 0)
writeKeyEvent(keySym, down);
// Always release all modifiers after an "up" event
if (!down) {
writeModifierKeyEvents(metaState, down);
}
try {
os.write(eventBuf, 0, eventBufLen);
} catch (IOException e) {
Log.e(TAG, "Failed to write key event to VNC server.");
e.printStackTrace();
}
}
//输入字符
public synchronized void writeKeyStringEvent(int keycode, char c, boolean down) {
if (viewOnly || os == null )
return;
int keySym = c;
if (keySym > 0xff) {
keySym += 0x1000000; //out of keyboard
}
Log.d(TAG, "writeKeyStringEvent() called with: keySym = [" + keySym + "], down = [" + down + "]");
eventBufLen = 0;
if (keySym > 0)
writeSpacialKeyEvent(keycode, keySym, down);
try {
os.write(eventBuf, 0, eventBufLen);
} catch (IOException e) {
Log.e(TAG, "Failed to write key event to VNC server.");
e.printStackTrace();
}
}代码见:
VNC server
收到从VNC APP传过来的KeyEvent,按原协议,将保留位做为输入字符还是按键的标志来区分是字符还是按键。
common/rfb/SMsgReader.cxx · OpenFDE/fde_tigervncserver - Gitee
void SMsgReader::readKeyEvent()
{
// bool down = is->readU8();
// is->skip(2);
// rdr::U32 key = is->readU32();
// handler->keyEvent(key, 0, down);
bool down = is->readU8();
rdr::U32 keycode = is->readU16();
// is->skip(2); 使用这个保留位,来区分是输入字符还是按键 0xff
rdr::U32 key = is->readU32();
handler->keyEvent(key, keycode, down);
}server其他的改动是上文提到的quilt使用patch的方式,主要改动都在这个patch中。
debian/patches/fde_patch_001.diff · OpenFDE/fde_tigervncserver - Gitee
关键如下,简单说明就是将一个4位的unicode,拆成4次按键发送。比如要发送 “我”,unicode是 “6211”,就会分别发送 “6”,“2” ,“1”,“1”4个down、up事件,如果有 “0”, 需要发送一个特殊的keysym,不然ibus的协议会对他进行转换。
/*
* vncKeyboardEvent() - add X11 events for the given RFB key event
*/
void vncKeyboardEvent(KeySym keysym, unsigned xtcode, int down)
{
/* Simple case: the client has specified the key */
// xtcode = 0xff;
if (xtcode == 0xff) {
//hardcode keysym = 0xcccc , transfer IBUS_KEY_Adiaeresis maybe never used
if (down) {
vncKeysymKeyboardEvent(0xcccc, down);
int fakeSym = (keysym >> 12) & 0xf;
if(fakeSym == 0){
fakeSendKeycode(0xcccd, down, xtcode);
fakeSendKeycode(0xcccd, 0, xtcode);
}else{
fakeSendKeycode(fakeSym, down, xtcode);
fakeSendKeycode(fakeSym, 0, xtcode);
}
fakeSym = (keysym >> 8) & 0xf;
if(fakeSym == 0){
fakeSendKeycode(0xcccd, down, xtcode);
fakeSendKeycode(0xcccd, 0, xtcode);
}else{
fakeSendKeycode(fakeSym, down, xtcode);
fakeSendKeycode(fakeSym, 0, xtcode);
}
fakeSym = (keysym >> 4) & 0xf;
if(fakeSym == 0){
fakeSendKeycode(0xcccd, down, xtcode);
fakeSendKeycode(0xcccd, 0, xtcode);
}else{
fakeSendKeycode(fakeSym, down, xtcode);
fakeSendKeycode(fakeSym, 0, xtcode);
}
fakeSym = keysym & 0xf;
if(fakeSym == 0){
fakeSendKeycode(0xcccd, down, xtcode);
fakeSendKeycode(0xcccd, 0, xtcode);
}else{
fakeSendKeycode(fakeSym, down, xtcode);
fakeSendKeycode(fakeSym, 0, xtcode);
}
} else {
vncKeysymKeyboardEvent(0xcccc, down);
}
} else{
fakeSendKeycode(keysym, down, xtcode);
}
}这里可以理解成一个编码的过程,之所以需要这样处理,还是因为ibus协议的传输过程,keysym的类型似乎只在1个字节的时候,或者一些在X11的keysymdef.h中定义的才不会被转义。
Ibus输入法
最后文本的输入还是使用的linux上的输入法,它的启动是通过环境变量的方式,它也没有其他作用,就是解码vncserver传过来的内容,生成字符。
就是按ibus输入法的流程,注册按键回调:
g_signal_connect(engine, "process-key-event", G_CALLBACK(engine_process_key_event_cb), NULL);将vncserver发送来的keyval,keycode,state,4次事件生成一个unicode,再提交文本,正常的按键没有这个上面使用的标志位0xcccc,所以相当于没有经过这个输入法。
gboolean engine_process_key_event_cb(IBusEngine *engine,
guint keyval,
guint keycode,
guint state) {
LOG_INFO("engine_process_key_event");
ibus_engine_show_lookup_table(engine);
// ibus_engine_show_preedit_text(engine);
ibus_engine_show_auxiliary_text(engine);
std::thread::id t1_id = std::this_thread::get_id();
// char receiver[100]; //
// sprintf(receiver, "\n\r 接收数据 keycode:%d keyval:%d flag:%d text:%d ",keycode, keyval, flag, text);
// engine_commit_text(engine, ibus_text_new_from_string(receiver));
//1.unicode start
if (keyval == 0xcccc && !(state & IBUS_RELEASE_MASK)) {
// sprintf(receiver, "\n\r 1.unicode start keycode:%d maks:%d ",keycode, state & IBUS_RELEASE_MASK);
// engine_commit_text(engine, ibus_text_new_from_string(receiver));
flag = 3;
text = 0;
processing = 1;
return TRUE;
}
//2.add unicode
if (flag != -1 && !(state & IBUS_RELEASE_MASK)) {
// sprintf(receiver, "\n 2.add unicode flag:%d maks:%d ",flag, state & IBUS_RELEASE_MASK);
// engine_commit_text(engine, ibus_text_new_from_string(receiver));
if(keyval == 0xcccd){
text += (0 << (flag * 4));
}else{
text += (keyval << (flag * 4));
}
// char input_string[100]; //1
// sprintf(input_string, "\n\r 生成字符 位flag:%d keyval:%d text:%d",flag, keyval, text);
// engine_commit_text(engine, ibus_text_new_from_string(input_string));
flag--;
return TRUE;
}
//3.unicode over
if (keyval == 0xcccc && (state & IBUS_RELEASE_MASK)) {
// sprintf(receiver, "\n\r 3.unicode over keycode:%d maks:%d flag:%d text:%d",keycode, state & IBUS_RELEASE_MASK, flag, text);
// engine_commit_text(engine, ibus_text_new_from_string(receiver));
flag = -1;
wchar_t unicodeChar = static_cast<wchar_t>(text);
std::wstring_convert <std::codecvt_utf8_utf16<wchar_t>> converter;
std::string utf8Str = converter.to_bytes(unicodeChar);
// char input_string[100]; //
// sprintf(input_string, "\n\r 按键keyval:%d keycode:%d text:%d final====> ", keyval, keycode, text);
// engine_commit_text(engine, ibus_text_new_from_string(input_string));
engine_commit_text(engine, ibus_text_new_from_string(utf8Str.c_str()));
processing = 0;
return TRUE;
}
if (processing) {
return TRUE;
}
text = 0;
flag = -1;
// sprintf(receiver, "\n\r 4. maks:%d " , state & IBUS_RELEASE_MASK);
// engine_commit_text(engine, ibus_text_new_from_string(receiver));
if (state & IBUS_RELEASE_MASK) {
return FALSE;
}
return FALSE;
}
剪切板
RFB的有两个版本的剪切板协议,剪贴板 · RFB 远程帧缓冲协议
旧的没有支持中文,需要使用拓展剪贴板伪协议。
RFB 3.8 协议限制,剪贴板只能传输 Latin-1 字符集。 2016年,Cendio Ossman 将 Extended Clipboard Pseudo-Encoding 合入协议主分支,支持在剪贴板消息中传输 unicode 字符集。 UltraVNC/TigerVNC/RealVNC 服务端都支持此拓展协议,x11vnc 尚未提供支持(2021/8/11)。
拓展剪贴板伪协议需要客户端和服务端软件同时支持。报文拓展了 ServerCutText 和 ClientCutText, 如下:
+--------------+--------------+--------------+
| No. of bytes | Type [Value] | Description |
+--------------+--------------+--------------+
| 1 | U8 [3/6] | message-type |
| 3 | | padding |
| 4 | S32 | length |
| 4 | U32 | text-type |
| length-4 | U8 array | text |
+--------------+--------------+--------------+本项目使用的remote-desktop-clients也没有实现这个,VNC clipboard for utf8 text · Issue #285 · iiordanov/remote-desktop-clients 。
笔者通过拓展剪贴板伪协议,实现了文本的复制,因为这个协议支持了多种格式,所以使用了ZlibOutStream 来转换复制内容,代码修改见如下patch:
https://gitee.com/openfde/remote-desktop-clients/commit/dd23ad0d01b409a10e973fdb6ccf19dc94d7f35f
实际上他也可以实现文件的复制,笔者没有对他做进一步实现。remote-desktop-clients连中文(unicode)复制也没有实现,笔者推测原因是移动端的剪切板使用场景太少,而OpenFDE是有这个需求的。
总之,OpenFDE桌面环境的linux应用融合VNC方案的大部分原理如上文,细节处理还有很多,目前已经做成一个成熟的产品发布使用了,后续也在研究OpenFDE桌面中直接使用xserver运行linux程序的方法。