远程控制之原理和实战

news2024/11/22 21:46:59

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

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

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

被控端每隔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;		//被控客户端socket
	sockaddr_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. 压缩算法使用zip不太合适,但是暂时未找到更好的解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/648980.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(三)Kafka 生产者

文章目录 1. Kafka 发送消息的主要步骤2.创建 Kafka 生产者3.发送消息到 Kafka&#xff08;1&#xff09;发送并忘记&#xff08;2&#xff09;同步发送&#xff08;3&#xff09;异步发送 4.生产者配置&#xff08;1&#xff09;client.id&#xff08;2&#xff09;ack&#x…

查看P端日志操作步骤

1.登录PUTTY,这里以联调环境103.160.139.82为例。 2.登录&#xff0c;查看用户名&#xff1a;hxb或zzkpt,密码&#xff1a;用户名01动态口令。 例如hxb, sunmenglei01888888 3.进入P端日志存放目录&#xff0c; cd /home/zzkpt/logs/bcip 4.比如我要查看2023年5月5日&#xf…

索引常见问题

被问到SQL和索引优化问题&#xff0c;如何切入&#xff1f; 可以用 explain 进行分析 思考流程&#xff1a;找到哪些运行时间长浪费性能的sql&#xff0c;然后再用explain分析 慢查询日志 MySQL可以设置慢查询日志&#xff0c;当SQL执行的时间超过我们设定的时间&#xff0…

在UE中使用SVT(VirtualTexture)功能

前几年VT技术非常的火&#xff0c;这项技术主要运用在地形上&#xff0c;可以达到更高级别的精细度和更多次数的纹理混合&#xff0c;但实际非地形也可以用&#xff0c;特别是对于贴图尺寸比较大且多维度子材质比较多的模型&#xff0c;做了材质合并以及VT优化后&#xff0c;可…

二、线性神经网络

文章目录 前言一、线性回归1. 线性回归的基本元素1.1 线性模型1.2 损失函数1.3 解析解1.4 梯度下降1.5 用模型进行预测 2. 正态分布与平方损失3. 从线性回归到深度网络 二、线性回归的代码实现1. 生成数据集2. 读取数据集2.1 手动实现读取数据集2.2 简洁实现读取数据集 3. 初始…

便携补光LED化妆镜方案

近段时间&#xff0c;现代科技的衍生产品&#xff0c;智能化妆镜很受爱美女士的喜爱。为此&#xff0c;宇凡微推出无极调光的LED化妆镜方案。主控芯片采用宇凡微YF单片机&#xff0c;根据LED化妆镜方案的不同功能&#xff0c;支持定制开发。 一、LED化妆镜方案介绍 在日常过程中…

Html span标签的详细介绍

HTML &#xff1c;span&#xff1e;标签_span标签_allway2的博客-CSDN博客 一、span标签的定义及用法 在html中&#xff0c;span标签是使用来组合文档中的行内元素&#xff0c;以便使用样式来对它们进行格式化。 span标签本身并没有什么格式表现&#xff08;比如&#xff1a;换…

利用Matlab和cadence实现离散傅里叶分析(DFT)

例1&#xff1a; 采样定律&#xff0c;取100个点&#xff0c;信号频率是100HZ&#xff0c;采样频率是1000HZ&#xff0c;相当于采样十个周期&#xff0c;每个周期采样十个点。 cos&#xff08;2πT&#xff09;函数是以Ts1/fs为时间间隔对样本进行采样&#xff0c;取N个采样样…

Mini热风枪 制作过程

首先引个流吧 立创开源广场&#xff1a;https://oshwhub.com/abby_qi/mini-re-feng-qiang 哔哩哔哩&#xff1a; 实物图 然后说一下硬件的选型和图 风扇&#xff1a;3010无刷风扇 额定电压3.7V&#xff08;其实这个风扇还有其他额定电压的&#xff0c;比如9V12V&#xff0c;…

PyTorch 深度学习 || 专题九:PyTorch 全连接自编码网络的无监督学习

PyTorch 全连接自编码网络的无监督学习 文章目录 PyTorch 全连接自编码网络的无监督学习1. 数据去噪1.1 计算库和数据准备工作1.2 构建自编码网络1.3 调用主函数1.4 可视化 2. 数据的重建与降维2.1 计算模块和数据的准备2.2 自编码网络数据准备2.3 自编码网络的构建2.4 自编码网…

1.5 掌握Scala内建控制结构(一)

一、条件表达式 &#xff08;一&#xff09;语法格式 if (条件) 值1 else 值2 &#xff08;二&#xff09;执行情况 条件为真&#xff0c;结果是值1&#xff1b;条件为假&#xff0c;结果是值2。如果if和else的返回结果同为某种类型&#xff0c;那么条件表达式结果也是那种…

微信小程序开发20__第三方UI组件 ColorUI 的应用

ColorUI 有鲜艳的高饱和色彩&#xff0c; 是专注视觉的微信小程序组件库。 gitee 网址 &#xff1a;ColorUI: 鲜亮的高饱和色彩&#xff0c;专注视觉的小程序组件库 一 使用方法 在微信小程序中使用 ColorUI 需要两个步骤&#xff1a; 第一步&#xff1a; 下载源码解压…

【Linux】详解环境变量与命名行参数

目录 环境变量了解PATH什么是环境变量&#xff1f;使用环境变量系统自带环境变量示例 命名行参数argc与argvenvenviron 环境变量 了解PATH 提出问题&#xff1a; 我写的可执行程序&#xff0c;与系统的可执行程序都是可执行程序&#xff0c;那么为什么执行系统的可执行程序…

Dokcer安装---Mqtt

1、拉取镜像 docker pull registry.cn-hangzhou.aliyuncs.com/synbop/emqttd:2.3.6 老版本 2、运行 docker run -it --name emq -p 18083:18083 -p 1883:1883 -p 8084:8084 -p 8883:8883 -p 8083:8083 -d registry.cn-hangzhou.aliyuncs.com/synbop/emqttd:2.3.6 –name 容器…

佩戴舒适度极好的蓝牙耳机推荐,久戴不累的蓝牙耳机分享

​听歌、刷剧、游戏&#xff0c;运动、吃饭、睡觉等&#xff0c;要说现在年轻人除了离不开手机之外&#xff0c;还有就是蓝牙耳机了&#xff01;当然&#xff0c;随着蓝牙耳机的快速发展&#xff0c;各种各样的蓝牙耳机都有&#xff0c;导致很多人不知道耳机怎么选了&#xff0…

管理类联考——逻辑——知识篇——第五章 假言命题(必考)(最重要的基础)

第五章 假言命题&#xff08;必考&#xff09;&#xff08;最重要的基础&#xff09; 假言命题&#xff1a;陈述某一事物情况是另一件事物情况的条件的命题。假言命题中的充分条件假言命题和必要条件假言命题是联考逻辑最重要的必考考点。1 *本质为&#xff1a;充分必要&#…

Vue中如何进行分布式鉴权与认证

Vue中如何进行分布式鉴权与认证 随着前后端分离的趋势不断加强&#xff0c;前端应用的安全性问题也日益受到关注。在Vue应用中&#xff0c;我们通常需要实现分布式鉴权和认证&#xff0c;以确保用户的安全性和数据的保密性。本文将介绍在Vue中如何进行分布式鉴权与认证。 什么…

闲聊下最近哦

随便聊聊 聊聊最近工作或日常上一家公司一直比较忙,人也比较懒,一直没有写博客,最近换了下工作,争取坚持写博客吧 聊聊最近工作或日常 上一家公司一直比较忙,人也比较懒,一直没有写博客,最近换了下工作,争取坚持写博客吧 上家公司做了几年多了,上半年离职换了个工作,现阶段这…

《六》TypeScript 中的泛型

泛型&#xff1a;宽泛的类型&#xff0c;其实就是类型的参数化&#xff0c;让类型像参数一样&#xff0c;不预先指定&#xff0c;而是在使用的时候再让别人传入进来。 在定义函数、类或者接口时&#xff0c;如果遇到类型不明确的时候&#xff0c;就可以使用泛型。 平常开发中可…

Qt5.15.10+msvc2019_x86+qwebengine(含mp4)源码编译

系统要求: win10 64bit 英文版(或者把“区域”->“管理”->“非Unicode程序中所使用的当前语言”->改为"英语(美国)") 内存16g够用,cpu性能越高越好,硬盘在安装环境、下载源码后,至少还有100g可用空间 下载源码: https://download.qt.io/archiv…