UltraVNC(TightVNC、WinVNC)源码分析文档

Posted on 2005-10-10 09:56 Piccolo Goo 阅读( 3248) 评论( 1)   编辑 收藏

                                    

Content List: System Shell :
1.1 IActiveDesktop
    Allows a client program to manage the desktop items and wallpaper on a local computer.
#include 

IActiveDesktop* active_desktop = 0;
CoCreateInstance(CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER,
		IID_IActiveDesktop, (void**)&active_desktop);

1.2 SetProcessShutdownParameters
    The SetProcessShutdownParameters function sets shutdown parameters for the currently calling process. This function sets a shutdown order for a process relative to the other processes in the system.
// Set this process to be the last application to be shut down.
SetProcessShutdownParameters(0x100, 0);

1.3 One Instance Running by Mutex
    Use Win32 Mutex object to insure that only one instance is currently running in our OS.
const char mutexname [] = "WinVNC_Win32_Instance_Mutex";

BOOL vncInstHandler::Init()
{
	// Create the named mutex
	HANDLE mutex = CreateMutex(NULL, FALSE, mutexname);
	if (mutex == NULL)
		return FALSE;

	// Check that the mutex didn't already exist
	if (GetLastError() == ERROR_ALREADY_EXISTS)
		return FALSE;

	return TRUE;
}

1.4 sscanf
    Read formatted data from a string.
// Check the protocol version
int major, minor;
sscanf((char *)&protocol_ver, "RFB %03d.%03d\n", &major, &minor);

1.5 Kill Screen Saver
// How to kill the screen saver depends on the OS
switch (osversioninfo.dwPlatformId)
case VER_PLATFORM_WIN32_WINDOWS:
	HWND hsswnd = FindWindow ("WindowsScreenSaverClass", NULL);
	if (hsswnd != NULL)
		PostMessage(hsswnd, WM_CLOSE, 0, 0); 
	break;
 
case VER_PLATFORM_WIN32_NT:
	HDESK hDesk = OpenDesktop(
				"Screen-saver",
				0,
				FALSE,
				DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS
				);
	if (hDesk != NULL)
	{
		EnumDesktopWindows(hDesk, (WNDENUMPROC) &KillScreenSaverFunc, 0);
		CloseDesktop(hDesk);
		// Pause long enough for the screen-saver to close
		//Sleep(2000);
		// Reset the screen saver so it can run again
		SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, TRUE, 0, SPIF_SENDWININICHANGE); 
	}
	break;
}	

1.6 Disable Nagle Algorithm
    Nagle Algorithm主要是用于优化小数据包的发送,用于在IP栈中缓冲小数据包,积累一定数量的小数据包一起发送。这样可以减少系统发包的数量,Nagle Algorithm实现时内部也有一个Timeout机制,但这个Timeout时间长度不能在外部设置。而且对每个IP栈的实现,这个Timeout Span 都有一个实现相关的值。
// Disable Nagle's algorithm
BOOL nodelayval = TRUE;
setsockopt(m_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&nodelayval, sizeof(BOOL));


Screen Capture :
2.1 Poll Schema
    Poll方式就是最简单的一种方式了,也是大家在考虑截屏时最先想到一个解决方案。为取得系统的屏幕,poll方案每33ms轮询一次系统屏幕,将变化的部分添加加到一个UpdateRegion中。为获取这些屏幕变化的Rect,系统还采取了一种优化Polling算法,将屏幕分为32*32 pixel的小矩形块,算法给出了一个各个矩形块轮询的Order:
const int pollingOrder[32] = {
		0, 16,  8, 24,  4, 20, 12, 28,
		10, 26, 18,  2, 22,  6, 30, 14,
		1, 17,  9, 25,  7, 23, 15, 31,
		19,  3, 27, 11, 29, 13,  5, 21
};
获取当前屏幕鼠标图像的思路:
第一步:获取屏幕鼠标的HCURSOR
    GetCursorPos获取屏幕鼠标位置,然后WindowFromPoint获取鼠标的拥有者窗口,GetWindowThreadProcessId获取相关线程ID,比较该ID和GetCurrentThreadId返回的ID,如果相同,通过GetCursor直接获取鼠标的HCURSOR;否则,通过AttachThreadInput连接上目标线程的Input Mechanism,再调用GetCursor获取鼠标的HCURSOR。
第二步:获取HCURSOR的Bitmap
int :GetCursorSendBuffer(BYTE *pBuffer, int nSize)
{
	....
	ICONINFO IconInfo;
	GetIconInfo(hcursor, &IconInfo);
	
	BITMAP bmMask;
	GetObject(IconInfo.hbmMask, sizeof(BITMAP), (LPVOID)&bmMask);	
	GetBitmapBits(IconInfo.hbmMask,bmMask.bmWidthBytes * bmMask.bmHeight, m_mBits);
	....
}

现在有必要对HRGN操作相关API做一下介绍
GetRgnBox      获取HRGN的边界矩形
GetRegionData  可以将一个HRGN分解为一个RECT数组,见RGNDATA结构体说明
CombineRgn     对HRGN操作RGN_AND,RGN_COPY,RGN_DIFF,RGN_OR或是RGN_XOR


2.2 TightVNC
    TightVNC(Tight Virtual Network Computing)是一个远程桌面控制的开源软件,详情请参考 http://www.tightvnc.com.下载了TightVNC的代码,分析了一下其Server部分的代码, WinVNC下的文件很多,但我们按照它们各自的功能做一下划分,其结构如下:
KernelvncBuffer.cpp vncClient.cpp vncDesktop.cpp vncServer.cpp WinVNC.cpp
GUIvncAbout.cpp vncAcceptDialog.cpp vncAdvancedProperties.cpp vncConnDialog.cpp vncMenu.cpp vncProperties.cpp vncTimedMsgBox.cpp
Miscd3des.c Log.cpp MinMax.cpp RectList.cpp stdhdrs.cpp tableinitcmtemplate.cpp tableinittctemplate.cpp tabletranstemplate.cpp translate.cpp vncauth.c vncInstHandler.cpp vncKeymap.cpp vncRegion.cpp< vncService.cpp
NetworkVSocket.cpp vncSockConnect.cpp vncHTTPConnect.cpp rfbproto.h
EncodingvncEncodeCoRRE.cpp vncEncodeHexT.cpp vncEncoder.cpp vncEncodeRRE.cpp vncEncodeTight.cpp vncEncodeZlib.cpp vncEncodeZlibHex.cpp




其服务端的主要功能模块结构如下:
其核心框架就是四个类vncClient,vncServer,vncDesktop和vncBuffer.下面我就这四个类之间的联系和用途来作一下简单的分析:
vncServer:
    vncServer主要是做如下的一些工作:容许vncClient动态的添加和删除;将本地vncDesktop对象内部状态的任何改变"传播"到各个客户端;传播客户端的鼠标和键盘事件到本地的vncDesktop对象。同时,其还创建了vncSockConnect,vncCORBAConnect和vncHTTPConnect来接受Socket,Corba和HTTP的连接。 vncServer为每个连接上来的客户端分配了一个ClientID(其实就是内部客户对象数组的Index),并且提供了对客户端管理的众多函数:
virtual void DisableClients(BOOL state);
virtual void KillClient(vncClientId client);
virtual void KillAuthClients();
virtual void KillUnauthClients();

virtual vncClient* GetClient(vncClientId clientid);
vncClientId AddClient(VSocket *socket, BOOL auth, BOOL shared);
virtual void RemoveClient(vncClientId client);
同时,vncServer还提供了对客户Teleport,Capability,KeyboardEnabled,PointerEnabled,Name,Authenticated属性的get/set方法。
下面我们来看一下vncServer对客户端连接上来和客户端认证成功这两个事件的处理流程:
vncServer::AddClient:
    首先vncServer在其内部的vncClient *m_clientmap[MAX_CLIENTS]数组中为新连接上的客户端分配一个空闲的slot,并将其作为此客户的 clientID. 然后,为此连接分配一个vncClient对象,根据传递过来的参数,设置vncClient对象的相关属性,然后调用vncClient::Init方法将vncServer的实例指针和 clientID传给vncClient实例。接着,m_clientmap[clientid] = client并将此用户加入vncServer的未认证用户链表。
vncServer::Authenticated(vncClientId clientid):
    首先从未认证用户列表中根据clientid获取vncClient对象,并将其从unauth list 中删除。如果是vncServer的第一个用户,创建vncDesktop对象,并调用m_desktop->Init(this)来初始化该vncDesktop对象。接下来,为这个用户分配一个vncBuffer *buffer = new vncBuffer(m_desktop);并通过调用vncClient::SetBuffer为vncClient设置这个Buffer,最后将此用户添加到auth list中。
    vncServer提供了一个用户列表的操作接口,这些接口通过将vncServer的方法调用映射到对auth list中各个客户的同样的方法的函数调用,这些方法有:
virtual void TriggerUpdate();
virtual void UpdateRect(RECT &rect);
virtual void UpdateRegion(vncRegion ®ion);
virtual void CopyRect(RECT &dest, POINT &source);
virtual void UpdateMouse();
virtual void UpdateClipText(LPSTR text);
virtual void UpdatePalette();
vncDesktop:
    vncDesktop是一个全局唯一的对象,根据注释,vncDesktop主要是处理从display buffer中获取数据;同时,它还利用RFBLib DLL为vncServer提供诸如鼠标移动和屏幕更新等信息。上面提到,vncServer在第一个用户连接上来时发现其m_desktop为空时就创建一个vncDesktip对象,并调用 vncDesktop::Init(this)对其初始化.在vcnDesktop::Init的实现中我们发现其创建了一个vncDesktopThread,vncDesktop的方法调用大部分都在这个vncDesktopThread里完成的.下面我们来分析一下这个线程都做了些什么:
vncDesktopThread::run_undetached(void *arg):
    首先调用vncDesktop::Startup初始化,vncDesktop对象(见vncDesktop::Startup),然后就是处理桌面消息,调用 m_server->UpdateMouse()和m_server->UpdateRegion(rgncache) ,接下来调用vncServer::TriggerUpdate来发送屏幕更新到每个vncClient.然后就是处理 RFB_SCREEN_UPDATERFB_MOUSE_UPDATE这两个注册消息。

vncClient:
    vncClient做了数据发送的工作,在vncClient::SendUpdate函数的实现中,我们可以看到vncClient调用 SendRFBMsg首先发送 ,然后 SendCursorShapeUpdate发送鼠标形状更新, SendCursorPosUpdate发送鼠标Pos更新,发送 SendCopyRect,最后调用 SendRectangles发送需要更新的矩形的相关数据。其实每个客户端vncClient在调用vncClient::Init初始化的时候都开了一个线程,客户端的行为基本上都是在vncClientThread::run里完成的。该线程在跟客户端交互完成了认证,Pixel格式,Encoding算法等信息的协商后,就进入一个loop循环开始接受和处理远程客户端发过来的rfbSetPixelFormat,rfbSetEncodings,rfbFramebufferUpdateRequest,rfbKeyEvent,rfbPointerEvent,rfbClientCutText消息。

vncBuffer:
    vncBuffer主要处理发送数据的Encoding工作,其提供了远程客户的本地视图,其主要是利用内部的vncDesktop指针来获取相关的数据。






1. 首先得讲一下它用到的omni_thread库

google搜出来的链接:http://omniorb.sourceforge.net/omni40/omnithread.html,介绍
了omni_thread库的基本使用,足够了,这里还是把它用中文啰嗦一下:

1) 它的接口与用C语言实现的POSIX threads接口是类似的。作为线程库,它提供了基本的
 同步对象:互斥量,条件变量,信号量;还提供了线程对象;也提供了线程私有数据。

2) 互斥量(Mutex)
 omni_mutex对象,操作它的方法有lock(),unlock()。acquire()和release()分别与
 lock(), unlock()有相同的语义。

3) 条件变量(Conditional variable)
 omni_condition对象。wait()方法使线程在条件变量上等待,signal()唤醒至少一个正等
 待该条件变量的线程(pthread_cond_wait()是只唤醒一个正等待的线程,不知道是不是这
 个 文档写错了,竟然与pthread语义不一样),broadcast()唤醒所有正等待该条件变量
 的线程。
 一旦omni_condition对象创建,一个互斥量就与该条件变量对象在对象的整个生命周期
 中绑定,而pthread的条件变量只在wait期间进行绑定。

4) 信号量(semaphore)
 omni_semaphore对象。wait()等待,即Dijkstra所说的P操作;post()唤醒,即V操作。

5) 线程对象(thread object)
 有两种方法使用线程对象。第一种,以需要执行的函数为参数创建对象。第二种,从omni
 _thread继承产生一个新类,omni_thread库将会执行该新类的run()或run_undetached()
 函 数 
 一旦线程对象创建,即可调用start()或start_undetached()使线程开始运行。若调用sta
 rt()函数,omni_thread库会以run()函数为执行例程,并detached模式运行。若调用
 start_undetached()函数,omni_thread库会以run_undetached()函数为执行例程,但以u
 ndetached模式运行。








vncDesktop处理所有与桌面相关的消息,如用户操作,Hook DLL的消息,等等。vncClient代表了一个vnc客户。vncServer是VNC服务器类。vncSockConnect代表了网络连接,比listen, accept就是它做的。vncEncodexxx的一系列类代表了各种编码方式。vncRegion代表了任意的屏幕区域,它提供了方法得到组成该区域的一系列矩形。基本上,都是先new xxx类,再调用它的Init函数,而该函数就会new一个xxxThread对象,并调用xxxThread的Init函数,xxxThread的Init函数就会创建线程了,线程的入口函数是run()或run_undetached()。

程序基本流程是,vncSockConnectThread对象accept一个客户后,就new一个vncClient对象,并调用其Init函数,该函数new一个vncClientThread对象并调用它的Init函数,该函数会启动与客户端进行RFB交互的线程,线程入口函数是:vncClientThread::run()。

源码中使用到了HOOK,一般翻译成挂钩,不过个人觉得某本书上翻译的异常分支比较直观一点。Tight VNC源码中提供的Hook源码比较简单,只是有一点要注意,Hook无法得到所有屏幕变化的区域,最终还是得靠全屏幕扫描来得到所有的屏幕变化区域。另外,Ultra VNC源码中提供的Hook源码用PostThreadMessage函数来发送消息,用这份Hook源码作测试,除了调用SetWindowsHook函数的进程外,其他的进程根本收不任何消息。MSDN讲该函数往GUI 线程发消息容易丢失,但我是发送到普通线程,并非GUI线程。用Process Viewer查看,确实每个进程都把DLL映射到自己的地址空间中了。还是Tig ht VNC中的Hook比较好一点,使用PostMessage函数,消息不会丢失。

关于全屏幕扫描,前面讲到过,Hook无法得到屏幕的所有变化区域,还得辅之以屏幕扫描。Tight VNC中的屏幕区域扫描算法如下:(见PollArea函数)扫描方法是一次一行,由上至下,扫描线间距为32像素,依次取如下数组中的元素作为第 一行扫描线的位置:

 const int vncDesktop::m_pollingOrder[32] = {

  0, 16, 8, 24,  4, 20, 12, 28,

 10, 26, 18, 2, 22,  6, 30, 14,

  1, 17, 9, 25,  7, 23, 15, 31,

 19,  3,27, 11, 29, 13,  5, 21};

 对每行扫描线,以32像素长为单位,依次与前一次的屏幕比较,若有变化,则认为两条扫描线间32x32的矩形区域均有变化,记录到变化矩形列表中。若无变化,则认为两条扫描线间32x32的矩形区域均无变化。同样,通过Hook得到的屏幕区域变化也需要作记录。记录由Hook得到的屏幕变化区域见vncDesktopThread::run_undetached函数。


  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值