按理来说,本人不该发表此类专业文章,鄙人零星碎片化的开发经历,让本人斗胆向诸位网友,在远控方面做一点演示说明,谈论一点自己的认识。

程序工程代码地址:点击此处下载。

程序分为两个部分,控制端和被控端,他们之间通过网络来连接和交互,其工作过程大体如下:

被控端每隔20毫秒截屏,图像经过压缩,通过tcp网络传输给控制端,控制端对接收到的视频帧实时刷新显示;被控端实时接收控制端的对屏幕的操作消息(主要是键盘的按键、鼠标的位置和动作等),并在本端模拟这些键盘鼠标操作。

控制端代码主要在RemoteControlRecver.cpp和RemoteControlListener.cpp中,被控端代码主要在RemoteControlProc.cpp和RemoteControl.cpp中。

程序是自己对远控的一点探索和demo演示,实现过程吻合本文思路,虽然离商用有很远距离,但从实际的使用效果看,已经具备了远控的基础功能和效果。

本文已经被看雪学院引用链接地址:

(一)原理

经典的远控,比如国外的TeamViewer、国内近几年出现的ToDesk,功能强大且精密复杂,但这并不说明它的高不可攀(远控软件,除非算法上的突破,无论理论和工程技术,恐怕都无法为设计开发者赢得博士学位),它的原理无非就是:将被控端的屏幕实时复制到主控端并保持刷新,主控端就像使用本地的屏幕一样,使用菜单、键盘输入、鼠标点击等视觉交互,主控端在该屏幕上的键盘和鼠标操作,通过网络传输给被控端,转化为被控端相应的键盘和鼠标操作

当然这是极为简略的描述,实践中要考虑很多其他因素,比如对网络流量的考虑:如果每一帧画面都是截屏数据,要保证动画的连贯逼真,每秒至少要传输24帧以上的画面,每一帧画面如果采用24位真彩色、屏幕分辨率假定是最常用的1920x1080,此时,未压缩前的大小是6220800字节,压缩后一般最少也有150-200kb左右(压缩率跟画面的像素有关,一般来讲,常见的图像压缩算法,越是像素排列无规则、相对速度运动越快、变化率越大的像素值,压缩比越低),这时每秒的带宽压力要达到80M/b(10MB)以上,有些网络环境下,这是一个恐怖的数字,实际环境可能达不到,因此,如何压缩减少视频传输流量,提高控制反应速度和画面的丝滑控制,是此类软件的核心技术之一。

一般来说,要实现此类软件的敏捷开发,最快捷的方式是使用第三方开发包,比如大名鼎鼎的ffmpeg,此开发包中有多种方案可以实现高效的视频传输,如h264、h265协议接口,此类接口可以将传输数据减少1到2个数量级,实际测试数据流量在每秒几百kb甚至100kb/s,已经可以满足实际需要,但是,h264和h265的显示效果和网络流量的综合效果并不够优秀,从测试中发现,微软自带的远程控制软件mstsc.exe,在100kb以内的网速下,画面显示依然清晰、控制依然保持流畅,这就不是第三方开发包可以轻易达到的。另外此类第三方接口中没有键盘鼠标消息的处理,很多定制化需求不能被满足,还需要对开发包进一步定制和开发。

如果对第三方开发包不太满意,那就只有自己动手手撸代码了。

(二)实现细节

魔鬼隐藏在细节中。从经验上来说,就算很简单的理论描述,工程实践中也会有很多细节需要填坑夯实,理学在前挖坑,工程学在后填坑,这也许就是工程学(比如软件工程)存在的意义吧。

下面就是对思路的细节描述。

主控端的具体代码逻辑如下:
每一个被控客户端的远程连接,控制端需要创建两个线程,一个负责与被控端网络通信,一个负责窗口显示刷新和窗口消息。
网络通信线程有两个执行节点,一个节点是执行recv函数,接收被控端的截屏数据;另一个执行节点是执行send函数,发送显示窗口的键盘鼠标消息。窗口显示和消息处理线程主要是实时刷新和显示被控端的截屏,切入点是依靠窗口的WM_PAINT消息,每当网络通信线程接收到一帧截屏后会调用InvalidateRect(参数是显示窗口的HWND句柄),此时窗口程序会执行刷新过程。此线程另外一个功能是,捕获主控端的在显示窗口中的键盘鼠标消息,并存放在全局变量中,这样网络通信线程就可以读取和发送给被控端,被控端模拟点击和键盘输入,将收到的键盘鼠标消息转换为本地的键盘鼠标操作。

另外需要注意的是,两个线程中,资源申请和释放、连接控制等主要是在显示刷新窗口线程中完成的,在主控和被控之间因各种原因断开连接时,要保证所有的资源都有效释放。

两个线程共用的客户端结构体如下:


typedef struct {SOCKET                  hSockClient;        //被控客户端socketsockaddr_in                stAddrClient;       //被控地址HWND                  hwndWindow;         //窗口线程的窗口句柄,据此可以保存和找到该线程,并交互鼠标键盘消息char* lpClientBitmap;                       //被控的屏幕像素地址char* dibits;                                //被控像素处理内存缓冲int                     bufLimit;           //像素地址块分配大小int                      lpbmpDataSize;//像素实际大小int                       dataType;   //像素块的类型,有两种,一种是截屏,一种是屏幕刷新值UNIQUECLIENTSYMBOL      unique;     //被控的信息STREMOTECONTROLPARAMS    param;      //被控的屏幕宽度高度位数等显示信息
}REMOTE_CONTROL_PARAM, * LPREMOTE_CONTROL_PARAM;

被控端程序比较简单,主要是在一个循环中,获取截屏数据,发送给控制端,然后接收控制端的键盘鼠标消息,并将这些键盘鼠标消息转换为本地的键盘鼠标消息。

代码中的api采用了函数指针的调用方式,去掉前面的lp前缀就可以理解了。主要的功能模块如下:

  1. 被控端截屏发送给控制端。从网上的资料来看,截屏功能的实现方法如下:

int GetScreenFrame(int ibits, char* szScreenDCName, int left, int top, int ScrnResolutionX, int ScrnResolutionY, char* lpBuf, char** lppixel, int* pixelsize) {int iRes = 0;HWND hwnd = lpGetDesktopWindow();HDC hdc = lpGetDC(hwnd);//HDC hdc = lpCreateDCA(szScreenDCName, 0, 0, 0);//HDC hdc = lpGetDC(0);if (hdc == 0){writeLog("GetScreenFrame lpCreateDCA error:%d\r\n", GetLastError());return FALSE;}HDC hdcmem = lpCreateCompatibleDC(hdc);HBITMAP hbitmap = lpCreateCompatibleBitmap(hdc, ScrnResolutionX, ScrnResolutionY);lpSelectObject(hdcmem, hbitmap);iRes = lpBitBlt(hdcmem, 0, 0, ScrnResolutionX, ScrnResolutionY, hdc, 0, 0, SRCCOPY);if (hbitmap == 0){lpReleaseDC(0, hdc);lpDeleteDC(hdcmem);lpDeleteObject(hbitmap);writeLog("GetScreenFrame lpCreateCompatibleBitmap error:%d\r\n", GetLastError());return FALSE;}int wbitcount = 0;if (ibits <= 1) {wbitcount = 1;}else if (ibits <= 4) {wbitcount = 4;}else if (ibits <= 8) {wbitcount = 8;}else if (ibits <= 16) {wbitcount = 16;}else if (ibits <= 24) {wbitcount = 24;}else {wbitcount = 32;}DWORD dwpalettesize = 0;if (wbitcount <= 8){dwpalettesize = (1 << wbitcount) * sizeof(RGBQUAD);}DWORD dwbmbitssize = ((ScrnResolutionX * wbitcount + 31) / 32) * 4 * ScrnResolutionY;DWORD dwBufSize = dwbmbitssize + dwpalettesize + sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER);LPBITMAPFILEHEADER bmfhdr = (LPBITMAPFILEHEADER)lpBuf;bmfhdr->bfType = 0x4d42;bmfhdr->bfSize = dwBufSize;bmfhdr->bfReserved1 = 0;bmfhdr->bfReserved2 = 0;bmfhdr->bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwpalettesize;LPBITMAPINFOHEADER lpbi = (LPBITMAPINFOHEADER)(lpBuf + sizeof(BITMAPFILEHEADER));lpbi->biSize = sizeof(BITMAPINFOHEADER);lpbi->biWidth = ScrnResolutionX;lpbi->biHeight = ScrnResolutionY;lpbi->biPlanes = 1;lpbi->biBitCount = wbitcount;lpbi->biCompression = BI_RGB;lpbi->biSizeImage = 0;lpbi->biXPelsPerMeter = 0;lpbi->biYPelsPerMeter = 0;lpbi->biClrUsed = 0;lpbi->biClrImportant = 0;char* lpData = lpBuf + sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER) + dwpalettesize;iRes = lpGetDIBits(hdcmem, hbitmap, 0, ScrnResolutionY, lpData, (BITMAPINFO*)lpbi, DIB_RGB_COLORS);lpDeleteDC(hdcmem);lpDeleteObject(hbitmap);lpReleaseDC(0, hdc);if (iRes == 0){writeLog("lpGetDIBits error:%d\r\n", GetLastError());return FALSE;}*lppixel = lpData;*pixelsize = dwbmbitssize;return dwBufSize;
}

上面有几点需要啰嗦几句:
(1) windows上gdi二维图像api都是用DC句柄来实现的。测试发现,GetDC(0)等同于CreateDC(“display”,0, 0, 0),也等同于GetDC(GetDesktopWindow()),这几种用法都是用来获取桌面的DC。

(2) CreateCompatibleBitmap函数中的HDC要使用桌面的HDC,而不能是新创建的内存hdcmem,这是一个隐蔽的知识盲点,microsoft的解释如下:

大意是,CreateCompatibleBitmap产生的hBitmap位图中的位数和颜色跟使用的hdc参数中的保持一致,而使用CreateCompatibleDC函数创建的HDC默认都是2位的位图。

(3) GetDIBits函数有文档中未指明的知识盲点。比如lpbi参数指向的BITMAPINFO,在8位256色模式下,要给调色板留下空间,调色板一般需要另外的1024字节大小的空间,否则调用此api会发生内存越界异常。另外,此函数如果不知道如何填写BITMAPINFO位图参数,可以在第一次调用时,lpData参数为空,调用后,函数会自动填充BITMAPINFO结构的参数,然后第二次调用此函数,即可得到相应参数的位图数据。

BITMAPINFO结构体定义如下:

typedef struct tagBITMAPINFOHEADER{DWORD      biSize;LONG       biWidth;LONG       biHeight;WORD       biPlanes;WORD       biBitCount;DWORD      biCompression;DWORD      biSizeImage;LONG       biXPelsPerMeter;LONG       biYPelsPerMeter;DWORD      biClrUsed;DWORD      biClrImportant;
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;typedef struct tagBITMAPINFO {BITMAPINFOHEADER    bmiHeader;RGBQUAD             bmiColors[1];
} BITMAPINFO, FAR *LPBITMAPINFO, *PBITMAPINFO;

该函数的官方文档如下:

注意这里的描述,如果lpvBits参数有效,那么前6个参数必须初始化,并且扫描线的数值必须是Dword对齐。前6个参数是biSizeImage之前的6个参数,biSizeImage的计算比较复杂,不论位图的颜色深度是多少位,扫描线长度必须要4字节对齐。测试中还发现扫描线的行数并不需要dword对齐。

文档中说,函数调用时hbitmap参数不能被SelectObject选中,测试中发现,hbitmap即使已经被调用了SelectObject函数被选中,调用时也可以成功。

截图支持8位、16位、24位、32位颜色值,测试程序使用的16色。从视觉效果上,16位色跟24位,观看起来区别很小,特别是24位色(32位色相对于24位色只是增加了alpha值透明度),已经超过人的眼睛对颜色的识别程度的上限,再高的颜色值已经没有意义。由于传输的是像素值,而不是跟jpeg或者其他视频流算法中使用的近似压缩值(或者近似压缩块),所以画面的清晰度是很好的,这也是相比较于ffmpeg等第三方开发包使用h264、h265压缩视频流的优势。

另外,建议查看文档的英文版,中文版好多翻译不准确或者非常不严谨,长期依赖中文翻译,会导致开发水平得不到提高。

  1. 数据压缩传输。采用zip压缩,压缩参数设置为最大化压缩,压缩比估值大概是6-20倍。如果是8位的位图帧,分辨率1920x1080,每一帧压缩后大约是60-120KB;如果是16位,压缩后大约为100-300KB;32位的话,大约是150-600KB。当然这样的压缩比仍然难以满足实际需求,此文在第三个话题中会详细介绍如何减少视频流量,最终可以将网络流量降低到平均100KB/S。
  2. 控制端对截屏帧的显示刷新。主要代码如下:
     else if (mapit->second->dataType == REMOTE_CLIENT_SCREEN){char* lpClientBitmap = mapit->second->lpClientBitmap;HDC  hdcScr = CreateDCA("DISPLAY", NULL, NULL, NULL);HDC hdcSource = CreateCompatibleDC(hdcScr);LPBITMAPFILEHEADER pBMFH = (LPBITMAPFILEHEADER)lpClientBitmap;void* pDibts = (void*)(lpClientBitmap + pBMFH->bfOffBits);LPBITMAPINFOHEADER pBMIH = (LPBITMAPINFOHEADER)(lpClientBitmap + sizeof(BITMAPFILEHEADER));DWORD dwDibtsSize = ((pBMIH->biWidth * pBMIH->biBitCount + 31) / 32) * 4 * pBMIH->biHeight;char* pRemoteSrnData = 0;HBITMAP hRemoteBM = CreateDIBSection(0, (LPBITMAPINFO)pBMIH, DIB_RGB_COLORS, (void**)&pRemoteSrnData, 0, 0);if (hRemoteBM){memcpy(pRemoteSrnData, pDibts, dwDibtsSize);HBITMAP hSrcBM = (HBITMAP)SelectObject(hdcSource, hRemoteBM);int iX = pBMIH->biWidth;int iY = pBMIH->biHeight;RECT stRect = { 0 };int iRet = GetClientRect(hWnd, &stRect);//iRet = BitBlt(hdcDst, 0, 0, stRect.right - stRect.left, stRect.bottom - stRect.top,hdcSrc, 0, 0, SRCCOPY);iRet = StretchBlt(hdcDst, 0, 0, stRect.right - stRect.left, stRect.bottom - stRect.top, hdcSource, 0, 0, iX, iY, SRCCOPY);DeleteObject(hSrcBM);}else {WriteLog("RemoteControl CreateDIBSection error:%u\r\n", GetLastError());}DeleteObject(hRemoteBM);DeleteDC(hdcScr);DeleteDC(hdcSource);mapit->second->dataType = 0;EndPaint(hWnd, &stPS);return TRUE;}

代码中,lpClientBitmap指向接收到的内存中的bmp文件,调用CreateDIBSection函数是为了创建一个类似于此bmp文件格式和参数的hbitmap句柄后,然后将bmp文件的像素值拷贝到句柄指向的像素内存地址中,并使用StretchBlt将像素值显示在当前窗口中的客户区中,因为客户端和服务器的窗口大小可能不一样,所以使用StretchBlt实现缩放而不是BitBlt函数。
同时要注意,CreateDIBSection函数第一个参数为0,即相当于GetDC(0),0代表桌面窗口的HDC,这点在官方文档中并未说明,但是可以直接使用。

  1. 处理键盘鼠标消息。控制消息的收发和视频帧是顺序关系,而不是异步关系,被控端每发送一帧截屏后接收控制端的键盘鼠标消息,并将此消息模拟为本机的键盘鼠标操作;与此同时,控制端每接收一帧截屏后发送键盘鼠标消息。由于网络通信使用阻塞模式,此时一定要保证,被控端的先发送和后接收、控制端先接受再发送,主控和被控任何的执行分支都要分别执行这两对代码节点,否则会造成网络收发的死锁。另外,控制端如果发现,键盘鼠标的位置和动作跟上次的值相同,就会向被控端发送一个REMOTE_DUMMY_PACKET数据包,告诉被控端,控制端没有控制消息给你,你可以适当的增加截屏延时(一次增加10毫秒),以便减少网络消耗。主要的键盘鼠标结构体如下:

typedef struct {int screenx;int screeny;int bitsperpix;
}STREMOTECONTROLPARAMS, * LPSTREMOTECONTROLPARAMS;typedef struct {POINT pos;POINT size;
}REMOTECONTROLMOUSEPOS, * LPREMOTECONTROLMOUSEPOS;typedef struct {unsigned char key;unsigned char shiftkey;
}REMOTECONTROLKEY, * LPREMOTECONTROLKEY;typedef struct {int delta;int xy;
}REMOTECONTROLWHEEL, * LPREMOTECONTROLWHEEL;typedef struct {DWORD       dwType;POINT        stPT;DWORD      dwTickCnt;int           iDelta;
}STMOUSEACTION, * LPMOUSEACTION;

该结构由几个全局变量存放:键盘按键值,鼠标左键、中键、右键是否有点击动作,鼠标滚轮的滚动距离,鼠标的坐标位置等。控制端的窗口程序监听鼠标键盘消息,将这些消息填充为上述结构体,并由通信线程发送给被控端。

被控端通过keybd_event和mouse_event将收到的控制信息转换为本机的键盘鼠标消息。例子代码如下:

     if (dwCommand == REMOTE_MOUSE_POS){DWORD dwDataSize = lphdr->packlen;if (dwDataSize = sizeof(POINT) + sizeof(POINT)){LPREMOTECONTROLPOS pos = (LPREMOTECONTROLPOS)(lpBuf + sizeof(NETWORKPACKETHEADER));POINT stServerCurrent = pos->pos;POINT stPtServerMax = pos->size;int iXLocalMax = ScrnResolutionX;int iYLocalMax = ScrnResolutionY;POINT stPtLocalCurrent = { 0 };if (stPtServerMax.x != 0 && stPtServerMax.y != 0){stPtLocalCurrent.x = (iXLocalMax * stServerCurrent.x) / stPtServerMax.x;stPtLocalCurrent.y = (iYLocalMax * stServerCurrent.y) / stPtServerMax.y;//mouse_event(MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_MOVE,stPtLocalCurrent.x,stPtLocalCurrent.y,0,0);lpSetCursorPos(stPtLocalCurrent.x, stPtLocalCurrent.y);}}actionInterval(&dwSleepTimeValue);//checkTime(&dwSleepTimeValue);continue;}else if (dwCommand == REMOTE_KEYBOARD){if (lphdr->packlen == 2){LPREMOTECONTROLKEY lpkey = (LPREMOTECONTROLKEY)(lpBuf + sizeof(NETWORKPACKETHEADER));unsigned char key = lpkey->key;unsigned char keyshift = lpkey->shiftkey;//unsigned char szKeyboardState[256];//memmove(szKeyboardState,pData,256);//iRet = SetKeyboardState(pData);if (keyshift){keybd_event(VK_SHIFT, 0, 0, 0);keybd_event(key, 0, 0, 0);keybd_event(key, 0, KEYEVENTF_KEYUP, 0);keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);}else {keybd_event(key, 0, 0, 0);keybd_event(key, 0, KEYEVENTF_KEYUP, 0);}}actionInterval(&dwSleepTimeValue);//checkTime(&dwSleepTimeValue);continue;}else if (dwCommand == REMOTE_LEFTBUTTONDOWN || dwCommand == REMOTE_LEFTBUTTONDOUBLECLICK ||dwCommand == REMOTE_RBUTTONDOWN || dwCommand == REMOTE_RBUTTONDOUBLECLICK){DWORD dwDataSize = lphdr->packlen;if (dwDataSize = sizeof(POINT) + sizeof(POINT)){LPREMOTECONTROLPOS pos = (LPREMOTECONTROLPOS)(lpBuf + sizeof(NETWORKPACKETHEADER));POINT stServerCurrent = pos->pos;POINT stPtServerMax = pos->size;int iXLocalMax = ScrnResolutionX;int iYLocalMax = ScrnResolutionY;POINT stPtLocalCur = { 0 };if (stPtServerMax.x != 0 && stPtServerMax.y != 0){stPtLocalCur.x = (iXLocalMax * stServerCurrent.x) / stPtServerMax.x;stPtLocalCur.y = (iYLocalMax * stServerCurrent.y) / stPtServerMax.y;lpSetCursorPos(stPtLocalCur.x, stPtLocalCur.y);if (dwCommand == REMOTE_LEFTBUTTONDOWN){mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);}else if (dwCommand == REMOTE_RBUTTONDOWN){mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);}else if (dwCommand == REMOTE_RBUTTONDOUBLECLICK){mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);lpSleep(0);mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);}else if (dwCommand == REMOTE_LEFTBUTTONDOUBLECLICK){mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);lpSleep(0);mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);}}}actionInterval(&dwSleepTimeValue);//checkTime(&dwSleepTimeValue);continue;}else if (dwCommand == REMOTE_MOUSEWHEEL){DWORD dwDataSize = lphdr->packlen;if (dwDataSize = sizeof(DWORD) + sizeof(DWORD)){LPREMOTECONTROLWHEEL wheel = (LPREMOTECONTROLWHEEL)(lpBuf + sizeof(NETWORKPACKETHEADER));int key = wheel->delta & 0xffff;int delta = wheel->delta >> 16;int x = wheel->xy & 0xffff;int y = wheel->xy & 0xffff0000;mouse_event(MOUSEEVENTF_WHEEL, x, y, delta, 0);lpSleep(0);}actionInterval(&dwSleepTimeValue);//checkTime(&dwSleepTimeValue);continue;//to action more faster not to sleep}else if (dwCommand == RECV_DATA_OK || dwCommand == REMOTE_DUMMY_PACKET){freeInterval(&dwSleepTimeValue);}else if (dwCommand == REMOTECONTROL_END){writeLog("remotecontrol shutdown by server\r\n");break;}else{writeLog("RemoteControlProc unrecognized command:%u\r\n", dwCommand);//break;}checkTime(&dwSleepTimeValue);

(三)减少网络流量的努力

测试中发现,实际的网速有可能比想象中偏低,比如很多服务器网络带宽只有几百kb/s,上述依靠传输截屏帧的显示方式,按照40ms一帧的延时,8位位图数据帧,经过zip压缩后,网络流量可以平均减少10倍左右也就是大约1~2MB/s左右,依然无法满足实际需求,经过考虑,采用了如下几种改善措施:

  1. 客户端每次截屏后动态延时。当服务端没有鼠标键盘等控制信息后,发送延时消息REMOTE_DUMMY_PACKET给客户端,客户端每收到一个这样的数据包,截屏延时自动增加10ms,最长到2000ms为止,而收到键盘鼠标等控制消息后,延时立刻恢复到默认的REMOTE_CLIENT_SCREEN_MIN_INTERVAL值,这样可以显著的减少网络流量。
  2. 将截屏数据帧的格式由bmp转化为jpeg。bmp是原始像素值格式,jpeg是压缩后的像素格式,同时jpeg压缩算法是对每一块像素块进行压缩,经测试,在保证图像质量80%以上的条件下,其压缩比大约在10-40倍,但是这对zip压缩的优势并不明显(zip的压缩比平均在6-30倍左右),而且jpeg使用起来也较为繁琐,所以弃之未用。当然,如果改为jpeg格式的话,每一帧截屏的网络流量还能减少大约25%。
  3. 发送截屏帧前,执行屏幕像素比对操作。对每一帧截屏的所有像素值保存副本,每次截屏后跟上次的截屏数值比对,如果改变的像素值数量超过一帧像素总数的一半,就发送整个的截屏帧,否则只发送发生变化的像素值(格式是像素位置和像素的值,所有像素点在发送缓冲区中线性排列,并在zip压缩后传输);如果跟上一帧相比所有,像素点均未发生任何变化(这才是绝大多数情况),则发送延时消息。测试发现,在很短的截屏周期内,被控屏幕每一帧画面,不可能每一个像素都发生变化,改变的只有很少一小部分,甚至80%的监控周期内,整个屏幕的所有像素值未发生任何变化,此时不需要传送任何像素信息。这里还有一个常识,在windows下的截屏是没有鼠标的,因此,鼠标的点击和移动,也不会导致截屏像素点的任何改变。经测试发现,中度操作时,平均每个截屏周期,屏幕上变化的像素值,少的只有几十几百个字节,多的也只有几十kb,此种方法将发送截屏的概率下降了90%以上。当然,我觉得还有一种类似的方法,就是监听WM_PAINT消息,将重绘的像素块发送给主控端,这种方式跟像素值比对原理相同,只是实现方式不同。此段主要代码逻辑如下:
     if (mapit->second->dataType == REMOTE_PIXEL_PACKET){char* lpClientBitmap = mapit->second->lpClientBitmap;RECT rect;GetClientRect(hWnd, &rect);HDC  hdcScr = CreateDCA("DISPLAY", 0, 0, 0);HDC hdcSource = CreateCompatibleDC(hdcScr);HBITMAP hbmp = CreateCompatibleBitmap(hdcScr, mapit->second->param.screenx, mapit->second->param.screeny);SelectObject(hdcSource, hbmp);result = StretchBlt(hdcSource, 0, 0, mapit->second->param.screenx, mapit->second->param.screeny, hdcDst, 0, 0,rect.right - rect.left, rect.bottom - rect.top, SRCCOPY);if (result){char buf[0x1000];LPBITMAPINFO lpbmpinfo = (LPBITMAPINFO)buf;memset(lpbmpinfo, 0, sizeof(BITMAPINFO));lpbmpinfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);lpbmpinfo->bmiHeader.biBitCount = mapit->second->param.bitsperpix;lpbmpinfo->bmiHeader.biPlanes = 1;lpbmpinfo->bmiHeader.biWidth = mapit->second->param.screenx;lpbmpinfo->bmiHeader.biHeight = mapit->second->param.screeny;//DWORD dwbmbitssize = ((lpbmpinfo->bmiHeader.biWidth * lpbmpinfo->bmiHeader.biBitCount + 31) / 32) * 4 * lpbmpinfo->bmiHeader.biHeight;lpbmpinfo->bmiHeader.biSizeImage = 0;char* data = mapit->second->dibits;result = GetDIBits(hdcSource, hbmp, 0, lpbmpinfo->bmiHeader.biHeight, data, lpbmpinfo, DIB_RGB_COLORS);if (result && result != ERROR_INVALID_PARAMETER){int byteperpix = mapit->second->param.bitsperpix / 8;int itemsize = (sizeof(DWORD) + mapit->second->param.bitsperpix / 8);int cnt = mapit->second->lpbmpDataSize / itemsize;for (int i = 0; i < cnt; i++){int index = itemsize * i;int offset = *(DWORD*)(lpClientBitmap + index);if (offset > mapit->second->bufLimit){WriteLog("pixel offset error :%u\r\n", offset);break;}if (byteperpix == 4){DWORD color = *(DWORD*)(lpClientBitmap + index + sizeof(DWORD));*(DWORD*)(data + offset) = color;}else if (byteperpix == 3){char* color = lpClientBitmap + index + sizeof(DWORD);memcpy(data + offset, color, 3);}else if (byteperpix == 2){WORD color = *(WORD*)(lpClientBitmap + index + sizeof(DWORD));*(WORD*)(data + offset) = color;}else if (byteperpix == 1){unsigned char color = *(lpClientBitmap + index + sizeof(DWORD));*(data + offset) = color;}}result = SetDIBits(hdcSource, hbmp, 0, mapit->second->param.screeny, data, lpbmpinfo, DIB_RGB_COLORS);if (result){result = StretchBlt(hdcDst, 0, 0, rect.right - rect.left, rect.bottom - rect.top, hdcSource, 0, 0,mapit->second->param.screenx, mapit->second->param.screeny, SRCCOPY);if (result){}else {WriteLog("RemoteControl StretchBlt error:%u\r\n", GetLastError());}}else {WriteLog("RemoteControl SetDIBits error:%u\r\n", GetLastError());}}else {WriteLog("RemoteControl GetDIBits error:%u\r\n", GetLastError());}DeleteObject(hbmp);DeleteDC(hdcScr);DeleteDC(hdcSource);}else {WriteLog("RemoteControl StretchBlt error:%d\r\n", GetLastError());}mapit->second->dataType = 0;EndPaint(hWnd, &stPS);return TRUE;}

此处第一个StretchBlt函数的作用是,将主控端显示窗口大小转化为适合被控端宽度高度的大小,原因是,被控端无法得知主控端显示窗口的大小,被控发送的像素值位置是对本窗口的偏移值,如果两边窗口大小不一致,那么被控端的像素位置值就失去了意义。此时通过StretchBlt函数转换后,就可以将像素值直接写入转换后的hbitmap,并再次调用StretchBlt函数,将客户端的窗口大小调整为主控端显示窗口的大小。此处也用到了SetDIBits和GetDIBits函数,该函数上边已经讲过了,功能比较强大,但是使用起来比较复杂。此处有个内存越界的bug,也就是像素的偏移值会大于整个截屏的像素个数总数,会导致WriteLog那一行的执行,原因应该是,两边的窗口大小不一致,若被控的窗口比较大,而主控端窗口比较小,主控端的缓冲区是按照主控窗口大小分配的,转换坐标后,有可能发生内存溢出。

像素值比对函数如下:

int ScreenFrameChecker(char* backup, char* color, int colorlen, char* buf, int bytesperpix) {int counter = 0;if (bytesperpix == 4){DWORD* lpback = (DWORD*)backup;DWORD* lpcolor = (DWORD*)color;for (int i = 0; i < colorlen / bytesperpix; i++){if (lpback[i] != lpcolor[i]) {lpback[i] = lpcolor[i];char* lppixel = buf + counter * (sizeof(DWORD) + bytesperpix);*(DWORD*)lppixel = i * 4;*(DWORD*)(lppixel + sizeof(DWORD)) = lpcolor[i];counter++;}}}else if (bytesperpix == 3){char* lpback = (char*)backup;char* lpcolor = (char*)color;for (int i = 0; i < colorlen / bytesperpix; i += 3){if (lpback[i] != lpcolor[i] || lpback[i + 1] != lpcolor[i + 1] || lpback[i + 2] != lpcolor[i + 2]) {lpback[i] = lpcolor[i];lpback[i + 1] = lpcolor[i + 1];lpback[i + 2] = lpcolor[i + 2];char* lppixel = buf + counter * (sizeof(DWORD) + bytesperpix);*(DWORD*)lppixel = i;*(lppixel + sizeof(DWORD)) = lpcolor[i];*(lppixel + sizeof(DWORD) + 1) = lpcolor[i + 1];*(lppixel + sizeof(DWORD) + 2) = lpcolor[i + 2];counter++;}}}else if (bytesperpix == 2){WORD* lpback = (WORD*)backup;WORD* lpcolor = (WORD*)color;for (int i = 0; i < colorlen / bytesperpix; i++){if (lpback[i] != lpcolor[i]) {lpback[i] = lpcolor[i];char* lppixel = buf + counter * (sizeof(DWORD) + bytesperpix);*(DWORD*)lppixel = i * 2;*(WORD*)(lppixel + sizeof(DWORD)) = lpcolor[i];counter++;}}}else if (bytesperpix == 1){char* lpback = (char*)backup;char* lpcolor = (char*)color;for (int i = 0; i < colorlen / bytesperpix; i++){if (lpback[i] != lpcolor[i]) {lpback[i] = lpcolor[i];char* lppixel = buf + counter * (sizeof(DWORD) + bytesperpix);*(DWORD*)lppixel = i;*(char*)(lppixel + sizeof(DWORD)) = lpcolor[i];counter++;}}}else {return FALSE;}return counter;}

ScreenFrameChecker函数分8位、16位、24位、32位4种颜色深度值,跟上一帧比对截屏像素值,返回发生变化的像素值个数。为了加快速度,也可以按照每种规则,比如每次8个字节比对,或者使用simd指令优化,每次比对16字节。

(四)不足和缺陷

  1. 实例代码中未实现主控端和被控端的剪贴板的共享,以及文件复制粘贴操作
  2. 使用截屏方式抓取屏幕内容,然后压缩通过网络发送,这种机制太慢了。通过QueryPerformanceCounter高精度时钟测试,一帧截屏时间开销大概率在20-40毫秒之间,加上解压和网络传输的时间损耗,这样的一帧画面时间消耗可能超过50毫秒甚至100毫秒。这样的速度对应于每秒20帧以上的速率来说,将是一个致命缺陷,无法达到设计要求,这可能是因为线程切换或者API接口本身的问题,最好能将一帧抓屏时间消耗减少到10毫秒以内。
  3. 压缩算法使用zip,zip本身时间消耗非常少,测试几百kb的数据、压缩率最高时平均时间消耗在5毫秒左右,zip压缩是无损压缩,图像数据并不需要无损压缩,假如有其他合适的压缩算法,有较小的像素损失和较好的时间消耗,可能会有几毫秒的效率提高,但是不能算一个核心问题
  4. 其他很多细节处理不足。

远程控制之原理和实战相关推荐

  1. 深度学习Anchor Boxes原理与实战技术

    深度学习Anchor Boxes原理与实战技术 目标检测算法通常对输入图像中的大量区域进行采样,判断这些区域是否包含感兴趣的目标,并调整这些区域的边缘,以便更准确地预测目标的地面真实边界框.不同的模型 ...

  2. Java并发编程原理与实战六:主线程等待子线程解决方案

    Java并发编程原理与实战六:主线程等待子线程解决方案 参考文章: (1)Java并发编程原理与实战六:主线程等待子线程解决方案 (2)https://www.cnblogs.com/pony1223 ...

  3. DNS tunnel的原理及实战

    DNS tunnel的原理及实战 摘自:http://netsec.ccert.edu.cn/zhengming/2011/11/01/%E8%BD%AC%E8%BD%BD%EF%BC%9Adns-t ...

  4. 【赠书】五一假期福利,OpenCV4最新原理与实战书籍

    五一假期快要到了,本次给大家赠送3本本月新书,这次赠送的书籍是<OpenCV 4机器学习算法原理与编程实战>. 这是一本什么样的书 OpenCV(Open Source Computer ...

  5. Zookeeper原理和实战开发经典视频教程 百度云网盘下载

    Zookeeper原理和实战开发 经典视频教程 百度云网盘下载 资源下载地址:http://pan.baidu.com/s/1o7ZjPeM   密码:r5yf    转载于:https://www. ...

  6. 深度学习attention原理_深度学习Anchor Boxes原理与实战技术

    深度学习Anchor Boxes原理与实战技术 目标检测算法通常对输入图像中的大量区域进行采样,判断这些区域是否包含感兴趣的目标,并调整这些区域的边缘,以便更准确地预测目标的地面真实边界框.不同的模型 ...

  7. Oracle特殊恢复原理与实战(DSI系列)

    1.深入浅出Oracle(DSI系列Ⅰ) 2.Oracle特殊恢复原理与实战(DSI系列Ⅱ) 3.Oracle SQL Tuning(DSI系列Ⅲ)即将开设 4.Oracle DB Performan ...

  8. spark任务shell运行_Spark原理与实战(七)部署模式与运行机制

    导读:Spark的运行模式指的是Spark应用程序以怎样的方式运行,单节本地点运行还是多节点集群运行,自己进行资源调度管理还是依靠别人进行调度管理.Spark提供了多种多样,灵活多变的部署模式. 作者 ...

  9. MySQL闪回原理与实战

    文章来源 https://github.com/danfengcao/binlog2sql https://github.com/danfengcao/binlog2sql/blob/master/e ...

最新文章

  1. 站立会议报告(12)
  2. 在Eclipse添加Android兼容包( v4、v7 appcompat )
  3. TCP/IP协议栈在MSP430单片机上的实现
  4. 如何基于多线程队列简单实现mq
  5. MySQL 数据库常用存储引擎的特点
  6. Java行为参数化(一)
  7. 【渝粤题库】广东开放大学 面向对象方法精粹 形成性考核
  8. 无法获取未定义或 null 引用的属性“text”_【CSS】是时候开始用 CSS 自定义属性了...
  9. 一个程序掌握C++带参构造函数、带有默认参数的构造函数【C++类的经典使用案例】
  10. Perl语言入门14-17
  11. 安装sphinx的心得和错误处理
  12. c语言文件归档,Go语言tar归档文件的读写操作
  13. java的六大框架_常用的java开发框架介绍 (初学者必备的六大框架)
  14. 教师招聘面试视频 计算机,怎么准备教师招聘面试试讲?(附视频)
  15. 不写一行代码(三):实现安卓基于i2c bus的Slaver设备驱动
  16. 翻译管理协作翻译平台-crowdin
  17. win10计算机亮度无法调节,Win10电脑无法调节亮度怎么办 Win10系统不能调节屏幕亮度解决方法...
  18. word之插入图表题、表标题,图目录和表目录
  19. dij算法堆优化_迪杰斯特拉算法(Dijkstra) (基础dij+堆优化) BY:优少
  20. netstat 的各个 state 什么意思

热门文章

  1. i913980hx和i913900hx区别 酷睿i9 13980hx和i9 13900hx选哪个
  2. 保姆级教程超硬核包会,SystemVerilog SV 覆盖率
  3. 我的128天创作纪念日
  4. IOST 糖果领取领取步骤 笑来
  5. Java抽象类和接口的区别
  6. 【MCtalk Live】5大维度拆解在线抓娃娃爆红背后的产品逻辑
  7. zynq 7000 SDK下的流水灯实验
  8. phpstorm相关问题(不断更新)
  9. 玩转Eclipse — 快捷键设置及汇总
  10. Vue3+TypeScript从入门到精通系列之:泛型接口