在RFB协议中,定义了剪贴板的支持。分别是ClientCutText和ServerCutText两条消息。前者是将客户端的剪贴板数据发送到服务端,后者正好相反。既然RFB协议中已经有了这两条消息,是不是意味着我们就可以使用剪贴板啦。很遗憾,不是。
目前的linux下的vnc服务器(如tightvnc server)只支持cut buffer这种剪贴板形式,我想将来也不会有其它的支持。原因如下:cut buffer是一种“消极”的Peer-to-Peer的通信方式,在整个过程中,内容的提供者只要将所提供的内容放入cut buffer中,它的任务就完成了。当内容的请求者想要得到数据时,它只要从cut buffer中取出数据,而不用理会内容的提供者是否已经不存在;相比之下,采用“积极”的Peer-to-Peer的通信方式就要复杂得多了。它要求内容的提供者要一直存在,并且当内容的请求者想要得到数据时,请求者与提供者必须进行通信,这个过程涉及多次的应答,更重要的是,要通过XServer来作为中间媒介。单从这一点,vnc就不可能支持如Clipboard或者是Primary这类的selections来进行Peer-to-Peer通信了。因为有谁会为了实现了一小小的剪贴板而把x协议(那怕是一部份)加进rfb协议里呢?
可是cut buffer这个东西太旧啦,技术太落后啦,效率太低啦,格式太单一啦,以至于没有多少软件支持它了。比如gedit,google-chrome等都不支持cut buffer了。这就造成了我们的错觉:vnc的剪贴板使用不了。当客户端拷贝下一串字符串后,你可以在服务器上的相应display下执行xprop -root CUT_BUFFER0,可以看到,事实上数据是有传送到服务端的。
解决这个问题的办法有点"土",就是使用一个工具,在cut buffer与selections之间进行数据的“搬运”。这个程序叫做autocutsel。把如下两行命令加入到xstartup脚本中,然后再拷贝点东西试试。
autocutsel -s PRIMARY -cutbuffer 0 -f
autocutsel -s CLIPBOARD -cutbuffer 0 -f
是不是就万事大吉啦?如果你不需要在多字节字符环境下工作,那的确是万事大吉。可是万一想拷贝中文,如“终端”二字,问题就来了,显示出来的东西变成了如“//u7ec8//u7aef”。原因还是出在cut buffer身上。上面已经说了,vnc的剪贴板的数据在服务器端是存放在cut buffer中的(准确来的是cut buffer 0),而根据icccm对cut buffer的定义,它只有一种类型:STRING,它的格式是8位二进制。也就是说,在cut buffer中只能存在Latin-1字符集中的字符,中文?想都别想!这一点在rfb协议中也可以看出。
有没有办法呢?有的,不过跟使用autcutsel一样“土”,而且是土上加土。以下讲的东西有关于代码的修改,再也没有什么工具能帮我们了,我们只能自己打造工具了。
首先,我们要承认一个事实,多字节字符数据在cut buffer中一定是如//uxxxx这种形式,不用指望那一天它会变成你能看懂的字符。
第一步先解决从服务器拷贝数据到客户端的问题。当客户端收到//uxxxx后,我们该怎么办?其实,我们收到的是对应的字符的unicode编码的字符串表示。这下好办多了。我们只要把这个编码转成字符就可以解决问题。步骤如下:
a.使用正则表达式将所有//u后面的4个字符提取出来。正则表达式如:(////u([[:xdigit:]]{4}));
b.提取出来的字符串转换成数字;
c.使用wcrtomb得到一个代表对应多字节字符的字符数组;
d.将这个字符数组的数据转换成utf8格式;
e.重复b、c、d三步,处理所用提取出来的字符,形式一个新的字符串。(用gtk就不用这么麻烦了,使用g_regex_replace_eval,java中也类似的函数);
f.最后将e中得到的字符串设置到本地的剪贴板中。(如果设置函数的参数不要求是utf8类型,而只是本地字符编码的话,(d)项可以不用。)
代码就不贴出来了,按照这个思路,用java或者gtk来实现都不是太复杂。
完成了以上的工作后,我们就可以从服务器拷贝中文(当然不止中文了)到客户端。
第二步解决从客户端到服务器的数据拷贝问题。我们知道,cut buffer中的多字节字符一直就是以//uxxxx方式存在的,就是说,autocutsel在作“搬运”的时候,拿到的东西就只能是Latin-1中的字符。如果autocutsel能把拿到的东西转换一下,再提供给请求者,那问题就解决了。没办法,又得自己改造工具了,开源就是好!
autocutsel的代码结构是这样子的:它定时去检查cut buffer中的内容是否已经改变了,如果改变,那将新内容设置到相应的selections中;如果selections中的数据更新,也会设置到cut buffer中。对于我们来说,只关注数据是怎样设置进cut buffer中的就可以了。
在CheckBuffer函数中,会调用OwnSelectionIfDiffers函数,在OwnSelectionIfDiffer中又会调用到ConvertSelection函数。ConverSelection函数也就是我们修改的主要地方了。在这个函数中,对数据进行转换,可以看到里面只支持STRING得几种简单的类型,现在,我们就要加进另外的一个类型。该怎样入手呢?我是参考vim的源代码的,在vim的ui.c中,也有一个类似ConverSelection的转换函数要实现,名字叫做clip_x11_convert_selection_cb,里面有如下代码:
2277 else if (*target == compound_text_atom
2278 || *target == text_atom)
2279 {
2280 XTextProperty text_prop;
2281 char *string_nt = (char *)alloc((unsigned)*length + 1);
2282
2283 /* create NUL terminated string which XmbTextListToTextProperty wants */
2284 mch_memmove(string_nt, string, (size_t)*length);
2285 string_nt[*length] = NUL;
2286 XmbTextListToTextProperty(X_DISPLAY, (char **)&string_nt, 1,
2287 XCompoundTextStyle, &text_prop);
2288 vim_free(string_nt);
2289 XtFree(*value); /* replace with COMPOUND text */
2290 *value = (XtPointer)(text_prop.value); /* from plain text */
2291 *length = text_prop.nitems;
2292 *type = compound_text_atom;
2293 }
我们在autocutsel也依样完成一个类似的类型转换。代码如下:
191 if(*target == XA_COMPOUND_TEXT(d))
192 {
.... ....
219 char *string_nt = (char *)XtMalloc((unsigned)*length + 1);
220 memmove(string_nt, *value, (size_t)*length);
221 string_nt[*length] = '/0';
222 Xutf8TextListToTextProperty(dpy, (char **)&string_nt, 1,
223 XCompoundTextStyle, &text_prop);
224 XtFree(string_nt);
225 XtFree(*value); /* replace with COMPOUND text */
226 *value = (XtPointer)(text_prop.value); /* from plain text */
227 *length = text_prop.nitems;
228 *type = XA_COMPOUND_TEXT(d);
229 *format = 8;
230
231 if (options.debug) {
232 printf("Returning ");
233 PrintValue((char*)*value, *length);
234 printf("Length:%ld/n",*length);
235 printf("/n");
236 //printf("XA_UTF8_STRING:%s,%ld/n",(char *)*value,*length);
237 }
238 return True;
239 }
中间省去的部分是对传进来的字符串进行解释,取得含//uxxxx字符串的子串,然后对这些子串转换成相应的多字节字符的过程,代码是用glib完成的,做法与第一步的相关内容完全一样。这里关键是用Xutf8TextListToTextProperty函数把string_nt的内容转换成compound text类型,然后将结果存入text_prop中。
到这里,autocutsel改造完了。
现在,我们终于可以在copy/paste中使用中文了。貌似一切正常。
可是,问题真的完全解决了吗?不是的。如果你写下一串字符串"//u1234",你的本意是拷贝字符串本身,也就是“//u1234”,可是,你会发现,你们vnc剪贴板下是出现问题,它会把"//u1234"转换成一个字符。这该怎么办好呢?
现在的答案是:真的没办法了!正则表达式很好用,可是还不能聪明到能够知道我们的意图,对于"//u1234"来说,它只会把它当成一个多字节字符来处理。事实就是这样,不知还有没有更好的办法来实现令人讨厌的vnc剪贴板呢??