在远程协助中,请求协助的一方称为被协助方,或者(远程协助)服务端;响应的一方称为协助方,或者(远程协助)客户端;这里命名被协助方为Cv,而协助方命名为Cs,中转服务器为Sr。
TightVNC是免费为个人和商业用途,提供有用的,完整的源代码, 在管理,技术支持,教育和许多其他用途, 跨平台,可用于Windows和Unix,Java客户端,包括 与标准的VNC软件兼容,符合RFB协议规范。
TightVNC 分为Server和 Viewer两部分,Server作为请求协助端,Viewer协助端亦即是Cs和Cv 。
Repeater为中转服务器Sr,当Server连接Viewer是,通过Sr相互转发数据。对Cs的监控端口为5500,Cv的监控端口为5900,当然5500、5900都是vnc的默认端口,这样会引起其他一些VNC软件的冲突,所以Cs和Cv监控端口都需要修改。
整个设备架设可简化为如图1所示,分为情况
1) 协助用户Cv和请求协助用户Cs1在同一局域网内,这时Cv与Cs1可以直接进行TCP通信;
2) 协助用户Cv和请求协助用户Cs2不在同一局域网内,这时Cv与Cs2只能通过中转服务器相互转发数据进行通信。
整体布局
一、被协助用户Cs请求协助用户Cv的协助过程流程图如图2所示;
Cs请求Cv协助过程流程图
1) 被协助者
如果对方拒绝接受请求,显示信息窗口,提示“远程请求被对方拒绝”。
如果对方接受请求,则创建远程协助对象;是否允许对方查看,调用远程协助对象的StartServer,参数为对方用户ID。
申请受控请求
2) 协助者
窗口提示用户“xxxx发出远程协助邀请,您是否同意?”;如果“同意”则创建远程协助对象,调用远程协助对象Connect方法,连接对方的IP以及Port,建立TCP连接。调用用远程协助对象的SetHost(),参数为用户自己ID。
调用用远程协助对象的SetViewOnly(),参数为false;远程协助建立成功。
双方通过中转服务器或者直接建立TCP连接,完成远程协助以及数据交互。
二、Server两种请求协助方式Connect和Listen
1) 在局域网中Server端和 Viewer端如果能够直接进行TCP通信的时,这时就不需要通过中转服务器,Viewer直接连接Server。流程图如图3所示
Cv与Cs直接进行TCP通信流程图
2) 当Server端和 Viewer端不能直接的TCP通信时,亦就可能是在两个不同的局域网中,这时就需要通过中转服务器Repeater进行Server端和 Viewer端的消息转发。流程图如图4所示
Cv与Cs通过Repeater通信流程图
将Server和 Viewer封装在两个动态库中,分别为RemoteHelpServer和RemoteHelpViewer,外部调用RemoteHelpServer和RemoteHelpViewer时,应为单实例调用,即是已经作为一个客户端或者是服务器端时,不能再与其他人进行远程协助。
外部接口包括:连接方式、调节画面质量、画面模式和协助方式等
//参数:无
//返回值:Server端实例指针
//作用:创建远程协助Server端实例
RemoteHelpHandle *CreateRemoteHelpServer();
//参数:Server端实例指针
//返回值:无
//作用:销毁远程协助Server端实例
void DestoryRemoteHelpServer(RemoteHelpHandle *handle);
//参数:Server端实例指针,中转服务器IP及端口号
//返回值:成功时返回Socket指针
//作用:Server连接中转服务器
SocketIPv4 *ServerConnect(RemoteHelpHandle *handle,TCHAR *host,unsigned short port);
//参数:Server端实例指针,监听端口号
//返回值:成功时返回Socket指针
//作用:Server监听
SocketIPv4 *ServerListen(RemoteHelpHandle *handle,unsigned short port);
//参数:Server端实例指针,Socket指针,是否作为服务监听状态
//返回值:0,1
//作用:Server确认Viewer可以查看
int AddNewConnection(RemoteHelpHandle *handle,SocketIPv4 *socket,bool bIsListenMode);
//参数:Server端实例指针,连接凭证
//返回值:无
//作用:Server连接凭证
void SetServerConnectionTag(RemoteHelpHandle *handle,char *pszConnectionTag);
//参数:Server端实例指针
//返回值:无
//作用:Server端断开连接
void ServerDisconnect(RemoteHelpHandle *handle);
//参数:无
//返回值:Viewer端实例指针
//作用:创建远程协助Viewer端实例
RemoteHelpHandle *CreateRemoteHelpViewer();
//参数:Viewer端实例指针
//返回值:无
//作用:销毁远程协助Viewer端实例
void DestoryRemoteHelpViewer(RemoteHelpHandle *handle);
//参数:Viewer端实例指针,中转服务器IP及端口号
//返回值:关闭窗体时返回true,其它为false
//作用:Viewer连接中转服务器
bool ViewerConnect(RemoteHelpHandle *handle,char *host,int port);
//参数:Viewer端实例指针,中转服务器IP及端口号
//返回值:关闭窗体时返回true,其它为false
//作用:Viewer连接中转服务器
void SetViewerConnectionTag(RemoteHelpHandle *handle,char *pszConnectionTag);
//参数:Viewer端实例指针,是否使用8Bit
//返回值:无
//作用:远程协助图像质量,两种情况8Bit,24Bit
void Set8BitColor(RemoteHelpHandle *handle,bool use8Bit);
//参数:Viewer端实例指针,压缩率级数
//返回值:无
//作用:远程协助图像压缩率
void setJpegCompressionLevel(RemoteHelpHandle *handle,int level);
//参数:Viewer端实例指针, 是否为只查看
//返回值:无
//作用:设置远程协助只查看或可控
void setViewOnly(RemoteHelpHandle *handle,bool viewOnly);
//参数:Viewer端实例指针, 是否为全屏
//返回值:无
//作用:设置远程协助窗体为全屏或退出全屏
void setFullScreenMode(RemoteHelpHandle *handle,bool fullScreenMode);
//参数:Viewer端实例指针
//返回值:无
//作用:Viewer端断开连接
void ViewerDisconnect(RemoteHelpHandle *handle);
一、远程协助握手过程窗体界面显示:
请求远程协助
Viewer接受远程协助,等待确认Server确认查看
Server确认查看
Server已申请受控
二、客户端Cv:如图5所示,连接成功后分别有属性设置、最大化、断开等按钮;左下方为显示图像面板。
客户端Cv
三、属性设置:如图6所示,调节画面质量:1-9级别的lpeg图像压缩;画面模式:8位和24位画面质量
属性设置
四、客户端Cs:如图7所示,连接成功后,可以断开远程控制或者停止受控等操作。
客户端Cs
五、远程协助窗体:窗体模式分别有最大化,还原和最小化;快捷键分别有Shift+Ctrl+Alt+F 全屏、ESC退出全屏。当窗体最大化即全屏时,顶部出现工具条,可以控制窗体最大化,还原和最小化等操作,如图8所示,
远程协助全屏模式
注:
在这个功能刚开始做的时候,google 、百度了很多这方面的资料,无奈资料还是比较少的。其中有一篇论文《基于P2P的远程协助系统》,写得比较详细的方案,但最终没有实现P2P的方法,可能是一开始就走了弯路和现成的系统限制了,时间方面不允许和稳定性。
《基于P2P的远程协助系统》里面写是通过本地代理,中转服务器建立桥梁实现远程协助通信。其中本地代理可以不管VNC里面代码的逻辑具体实现,只是负责通信,对远程协助与VNC版本轻松解耦,没有依赖VNC版本,可以不断地使VNC更新换代有重要意义。还有一个作用是可以TCP打洞实现P2P《TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞》,不依赖VNC内部发送规则。
《TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞》一文中这位前辈写TCP实现P2P的方法,我想这对一些刚开始做TCP实现P2P通信有很大的帮助,感谢他的分享的精神,至少我对TCP实现P2P通信有所了解了。他所说的实现过程如下
1、 S启动两个网络侦听,一个叫【主连接】侦听,一个叫【协助打洞】的侦听。
2、 A和B分别与S的【主连接】保持联系。
3、 当A需要和B建立直接的TCP连接时,首先连接S的【协助打洞】端口,并发送协助连接申请。同时在该端口号上启动侦听。注意由于要在相同的网络终端上绑定到不同的套接字上,所以必须为这些套接字设置 SO_REUSEADDR 属性(即允许重用),否则侦听会失败。
4、 S的【协助打洞】连接收到A的申请后通过【主连接】通知B,并将A经过NAT-A转换后的公网IP地址和端口等信息告诉B。
5、 B收到S的连接通知后首先与S的【协助打洞】端口连接,随便发送一些数据后立即断开,这样做的目的是让S能知道B经过NAT-B转换后的公网IP和端口号。
6、 B尝试与A的经过NAT-A转换后的公网IP地址和端口进行connect,根据不同的路由器会有不同的结果,有些路由器在这个操作就能建立连接(例如我用的TPLink R402),大多数路由器对于不请自到的SYN请求包直接丢弃而导致connect失败,但NAT-A会纪录此次连接的源地址和端口号,为接下来真正的连接做好了准备,这就是所谓的打洞,即B向A打了一个洞,下次A就能直接连接到B刚才使用的端口号了。
7、 客户端B打洞的同时在相同的端口上启动侦听。B在一切准备就绪以后通过与S的【主连接】回复消息“我已经准备好”,S在收到以后将B经过NAT-B转换后的公网IP和端口号告诉给A。
8、 A收到S回复的B的公网IP和端口号等信息以后,开始连接到B公网IP和端口号,由于在步骤6中B曾经尝试连接过A的公网IP地址和端口,NAT-A纪录了此次连接的信息,所以当A主动连接B时,NAT-B会认为是合法的SYN数据,并允许通过,从而直接的TCP连接建立起来了。
整个过程跟UDP打洞很类似,我认为他文中写可能是没有注重的写出一个重点,就是“Sock.SetSockOpt ( SO_REUSEADDR, &nOptValue , sizeof(UINT) )”其中的SO_REUSEADDR,因为这样以上的过程才能够实现。
还有在OSChina里的一个讨论贴“C++实现TCP打洞的思想”,有些人说的tcp打洞无法实现,那些人我认为是根本没有去了解过吧,而且QQ的远程协助是TCP连接的,难道QQ的远程协助都是经服务器转发的?
知识有限,基本上想说的都说了!