基于Windows平台远程桌面 - 分块异或屏传算法的设计与实现

Windows项目完整代码

基于QT项目完整代码

注:为提高效率,完整代码中修改了部分算法,整体架构没变。

本文介绍一下基于Windows GDI的远程桌面传输。远程桌面方法其实很简单,无外乎GDI截屏->socket传输->GDI画图这三大步骤,但实际上比较麻烦,最难解决的就是截屏后图像太大的问题,以1920*1200像素的屏幕分辨率为例,截屏一张24位的bmp位图,大约需要8M左右字节,连续截图产生的数据量会给socket带来巨大的压力。也有人说可以在GDI截屏之后直接将数据压缩为jpg图像再传输,实践发现存在两个问题:1、压缩后一张图片的大小约为200K-300K,连续截屏的情况下,socket传输仍然有压力;2、jpg压缩算法cpu占用较高。目前比较成熟的解决方法有:

1、Mirror Driver

2、分块异或屏传

3、固定块隔行扫描/态分块隔行扫描

1、3两种方式本文不做介绍,只介绍第二种“分块异或屏传”算法,

一、原理

前后保存两次bmp位图,把屏幕分成若干块(局域网一般为4~6块,广域网一般为8~32块,本文例子使用的分为32块)并编号,前后两副位图分别按对应编号块逐个像素点做异或(XOR)操作,若异或后的结果全是零,证明两个被分块的位图相等,不为零则两个被分块的位图不相等,不相等则把异或的结果进行压缩,并发送。
每个块处理完后,则把后一副图像记为前一副图像,再保存一副bmp位图作为后一副图像,再执行前面的分块异或发送操作。
请输入图片描述
注意这里为什么要每个像素点做异或操作,再压缩发送,而不是每块图像做CRC32,不相同则直接发送图像呢?
因为屏幕上的变化在很短的时间内,往往都是小范围的变化,这就意味着有很多相同的像素点,那么两块图像异或的结果就会有很多的零。
这么多的零经过压缩算法压缩,数据量会减少很多。比如 经过压缩后,10000001 可以表示为10*61的形式,当然压缩算法不只是压缩连续的0或1。
经过以上算法优化传输屏幕变化,实际的网络传输量会变得很小,屏幕传输流畅,著名的灰鸽子屏幕传输就采用此算法。

二、实现

(一)客户端实现步骤(截图APP)

1、获取屏幕宽高等初始化信息并发送给服务端;同时将屏幕分为32块(共4行8列),每个块用[x,y]坐标表示

2、当接收到服务器准备完成信息后,开始循环截屏,一次完整的截屏需要获取32个块位图信息;

3、将获得的第[x,y]块位图数据和上一次位图数据按位做异或比较,每一个位都为0则说明图像无变化,跳过该块;否则说明图像有变化,将上一次的位图数据替换为新数据之后,将该位图块数据传输至服务端。发送给服务端的数据至少包括该位图块的x、y轴坐标和位图块

伪代码如下:

while(1)
{
    for(int y=0;y<4;y++)
    {
        for(int x=0;x<8;x++)
        {
            //截取第[x,y]块的位图数据new[x,y]
            if(XOR(new[x,y] , old[x,y]) != 0)       //异或比较新旧位图数据
            {
                old[x,y] = new[x,y];    //将旧的位图数据替换为新位图数据
                send(new[x,y]);         //将该位图数据发给服务端
            }
        }
    }
}

(二)服务端实现步骤(显示APP)

1、接收客户端宽高等初始化信息,初始化32个位图控件(共4行8列),每个位图控件用[x,y]坐标表示

2、通知客户端准备完毕

2、循环接受客户端位图信息,从中抽取x、y轴坐标,还原位图数据,根据坐标信息显示在对应的位图控件上

(三)关于数据压缩

传输的过程中作者发现,以1920*1200屏幕为例子,分为32个位图块,一个块大约为200-300K,对于桌面变化块高刷的要求情况下,网络传输压力任然非常大,那就必然要将位图数据进行压缩。作者在压缩率和CPU使用率两个维度反复测试,发现灰鸽子采用zlib压缩算法还是有道理的,具体大家可以选用几种压缩算法自己测试测试。关于这个算法的使用作者就不赘述了,参见zlib Home Site

三、部分核心代码

(一)客户端

typedef struct BmpBlock
{
	BYTE * pBmpBits;		//位图数据
	DWORD size;				//数据字节数
}BmpBlock;

class RDPCore
{
public:
	int ScreenWidth;		//屏幕宽度
	int ScreenHeight;		//屏幕高度
	int bmpBit;				//截图位信息
private:
	int blockWidth;			//块宽度
	int blockHeight;		//块高度
	BmpBlock bp[32];		//分块位图数据
public:
	RDPCore();
	~RDPCore();

	// 功能:获取上一次截屏指定块的位图数据
	// 参数[int]indexX:X轴块序号(0-7)
	// 参数[int]indexY:Y轴块序号(0-3)
	// 返回值[BmpBlock]:位图数据和字节数
	BmpBlock * GetBmpBlockByte(int indexX, int indexY);

	// 功能:截取指定块位图,并与上一次的位图数据进行对比,如有变化替换上一次位图信息,并返回新的位图大小,无变化返回0(屏幕等分成4*8=32块)
	// 参数[int]indexX:X轴块序号(0-7)
	// 参数[int]indexY:Y轴块序号(0-3)
	// 返回值[DWORD]:字节数
	DWORD BmpBlockCmp(int indexX, int indexY);

	// 功能:测试函数,用于将内存数据保存为bmp文件
	// 参数[TCHAR]filename:文件名
	// 参数[BYTE]hBmp:内存数据
	// 参数[DWORD]bmpsize:数据大小
	// 参数[int]width:图片宽度
	// 参数[int]height:图片高度
	void SaveBmpToFile(TCHAR * filename, BYTE * hBmp, DWORD bmpsize, int width, int height);

private:
	// 功能:XOR异或比较两个BYTE是否相同
	// 参数[BYTE]srcByte:BYTE数据
	// 参数[BYTE]dstByte:BYTE数据
	// 参数[DWORD]bit:srcByte字节数
	// 参数[DWORD]bit:dstByte字节数
	// 返回值[BOOL]:相同返回TRUE,否则返回FALSE
	BOOL ByteCmp(BYTE * srcByte, BYTE * dstByte, DWORD srcsize, DWORD dstsize);
};
BmpBlock * RDPCore::GetBmpBlockByte(int indexX, int indexY)
{
	return &bp[indexY * 8 + indexX];
}

DWORD RDPCore::BmpBlockCmp(int indexX, int indexY)
{
	HDC hScreenDC = CreateDC(_T("DISPLAY"), NULL, NULL, NULL);	 //通过指定DISPLAY来获取一个显示设备上下文环境
	HDC hmemDC = CreateCompatibleDC(hScreenDC);			 //该函数创建一个与指定设备兼容的内存设备上下文环境(DC)
	HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, blockWidth, blockHeight);//创建一张长width,宽height的画布,用于后面绘制图形
	HBITMAP hOldBM = (HBITMAP)SelectObject(hmemDC, hBitmap);//该函数选择一对象到指定的设备上下文环境中,该新对象替换先前的相同类型的对象。
	BitBlt(hmemDC, 0, 0, blockWidth, blockHeight, hScreenDC, indexX * blockWidth, indexY * blockHeight, SRCCOPY);//该函数对指定的源设备环境区域中的像素进行位块(bit_block)转换,以传送到目标设备环境。
	hBitmap = (HBITMAP)SelectObject(hmemDC, hOldBM);

	BITMAPINFO bitmapInfo = { 0 };
	bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader);
	bitmapInfo.bmiHeader.biWidth = blockWidth;
	bitmapInfo.bmiHeader.biHeight = blockHeight;
	bitmapInfo.bmiHeader.biPlanes = 1;
	bitmapInfo.bmiHeader.biBitCount = 32;
	bitmapInfo.bmiHeader.biCompression = BI_RGB;
	bitmapInfo.bmiHeader.biSizeImage = 0;
	bitmapInfo.bmiHeader.biXPelsPerMeter = 0;
	bitmapInfo.bmiHeader.biYPelsPerMeter = 0;
	bitmapInfo.bmiHeader.biClrUsed = 0;
	bitmapInfo.bmiHeader.biClrImportant = 0;
	
	if (GetDIBits(hmemDC, hBitmap, 0, blockHeight, NULL, &bitmapInfo, DIB_RGB_COLORS) == 0)//第一次调用 GetDIBits函数,并将参数五置为NULL,且bitmapInfo的BITMAPINFOHEADER结构的biSize已经初始化过,。
	{
		return -1;
	}
	BYTE * bitmapBits = new BYTE[bitmapInfo.bmiHeader.biSizeImage];
	if (bitmapBits == NULL)
	{
		return -1;
	}
	memset(bitmapBits, 0, bitmapInfo.bmiHeader.biSizeImage);
	if (GetDIBits(hmemDC, hBitmap, 0, blockHeight, bitmapBits, &bitmapInfo, DIB_RGB_COLORS) == 0)//第二次调用函数,并且第五个参数为有效的指针且可写入
	{
		return -1;
	}
	

	//判断是否有变化
	DWORD retSize = 0;
	int blockIndex = indexY * 8 + indexX;
	if (ByteCmp(bp[blockIndex].pBmpBits, bitmapBits, bp[blockIndex].size, bitmapInfo.bmiHeader.biSizeImage))
	{
		//删除原来的数据
		delete[] bp[blockIndex].pBmpBits; bp[blockIndex].pBmpBits = NULL;	
		bp[blockIndex].size = 0;
		//覆盖为新的数据
		bp[blockIndex].pBmpBits = new BYTE[bitmapInfo.bmiHeader.biSizeImage];
		memset(bp[blockIndex].pBmpBits, 0, bitmapInfo.bmiHeader.biSizeImage);
		memcpy(bp[blockIndex].pBmpBits, bitmapBits, bitmapInfo.bmiHeader.biSizeImage);
		bp[blockIndex].size = bitmapInfo.bmiHeader.biSizeImage;
		retSize = bitmapInfo.bmiHeader.biSizeImage;
	}
	delete[] bitmapBits; 
	bitmapBits = NULL;
	DeleteObject(hBitmap);
	DeleteDC(hmemDC);
	DeleteDC(hScreenDC);
	return retSize;
}
BOOL RDPCore::ByteCmp(BYTE * srcByte, BYTE * dstByte, DWORD srcsize, DWORD dstsize)
{
	if (srcsize != dstsize)
		return TRUE;
	for (DWORD i = 0; i < srcsize; i++)
	{
		if ((*(srcByte + i) ^ *(dstByte + i)))		//按位异或,全为0表示无变化
			return TRUE;
	}
	return FALSE;
}

 (二)服务端(以MFC为例)

UINT ListenThreadFunc(LPVOID pParam)
{
	CServerDlg * pDlg = (CServerDlg *)pParam;
	WSADATA wsaData;
	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (iResult != NO_ERROR)
		AfxMessageBox(_T("WSAStartup Error"));

	SOCKET hServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (hServer == INVALID_SOCKET)
	{
		AfxMessageBox(_T("socket Error"));
		WSACleanup();
		return 0;
	}
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(8000);
	sin.sin_addr.S_un.S_addr = INADDR_ANY;
	if (bind(hServer, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
	{
		AfxMessageBox(_T("bind error\n"));
		return 0;
	}

	if (listen(hServer, 2) == SOCKET_ERROR)
	{
		AfxMessageBox(_T("listen error"));
		return 0;
	}

	sockaddr_in remoteAddr;
	int nAddrLen = sizeof(remoteAddr);
	SOCKET hListen = accept(hServer, (SOCKADDR *)&remoteAddr, &nAddrLen);
	if (hListen == SOCKET_ERROR)
	{
		AfxMessageBox(_T("accept error"));
		return 0;
	}


	//第一步 接收客户端配置信息(包括位信息、客户端屏幕宽、高)
	char * pData = NULL;
	int bit, ClientWidth, ClientHeight;
	int len = RecvString(hListen, pData);
	if (len > 0)
	{
		memcpy(&ClientWidth, pData, sizeof(int));
		memcpy(&ClientHeight, pData + sizeof(int), sizeof(int));
		memcpy(&bit, pData + sizeof(int) * 2, sizeof(int));
	}
	else
	{
		AfxMessageBox(_T("获取客户端信息错误"));
		return 0;
	}
	TCHAR cTitle[256] = { 0 }; char cIP[20]; TCHAR wIP[20];
	inet_ntop(AF_INET, &remoteAddr.sin_addr , cIP , 20);
	swprintf(wIP, 20, _T("%hs"), cIP);
	wsprintf(cTitle, _T("\\\\%s [width:%d / height:%d / %dbit]"), wIP, ClientWidth, ClientHeight, bit);
	pDlg->SetWindowText(cTitle);
	pDlg->CreatePicControl(ClientWidth , ClientHeight);    //创建32个位图控件
	free(pData); pData = NULL;

	//第二步 通知客户端准备完毕开始传送位图流数据
	char pSendData[6] = "Ready";
	len = SendString(hListen, pSendData , 5);
	if (len <= 0)
	{
		AfxMessageBox(_T("通知客户端开始传送错误"));
		return 0;
	}

	//第三步 接收客户端位图流数据
	while (1)
	{
		len = RecvString(hListen, pData);
		if (len > 0)
		{
			int x = 0; int y = 0; DWORD bmpSize = 0;
			memcpy(&x, pData, sizeof(int));						//第一个int为块x轴坐标
			memcpy(&y, pData + sizeof(int), sizeof(int));		//第二个int为Y轴坐标
			memcpy(&bmpSize, pData + sizeof(int) * 2, sizeof(DWORD));		//第三个个DWORD为原始位图字节数
			char * pzlibBmpBits = pData + sizeof(int) * 2 + sizeof(DWORD);			//剩余的是压缩后的位图数据

			CDC *pDC = pDlg->pBmpBlock[y * 8 + x]->GetDC();
			HDC hdc = pDC->GetSafeHdc();

			HBITMAP hBitmap = CreateCompatibleBitmap(hdc, pDlg->BlockWidth, pDlg->BlockHeight);

			BITMAPINFO bitmapInfo = { 0 };
			bitmapInfo.bmiHeader.biSize = sizeof(bitmapInfo.bmiHeader);
			bitmapInfo.bmiHeader.biWidth = pDlg->BlockWidth;
			bitmapInfo.bmiHeader.biHeight = pDlg->BlockHeight;
			bitmapInfo.bmiHeader.biPlanes = 1;
			bitmapInfo.bmiHeader.biBitCount = bit;
			bitmapInfo.bmiHeader.biCompression = BI_RGB;
			bitmapInfo.bmiHeader.biSizeImage = 0;
			bitmapInfo.bmiHeader.biXPelsPerMeter = 0;
			bitmapInfo.bmiHeader.biYPelsPerMeter = 0;
			bitmapInfo.bmiHeader.biClrUsed = 0;
			bitmapInfo.bmiHeader.biClrImportant = 0;

			//解压
			char * pBmpBits = NULL;
			if ((pBmpBits = (char *)malloc(bmpSize)) == NULL)
			{
				free(pData); pData = NULL;
				continue;
			}
			ZeroMemory(pBmpBits, bmpSize);
			if (uncompress((Bytef *)pBmpBits, &bmpSize, (Bytef *)pzlibBmpBits, len - sizeof(int) * 2 - sizeof(DWORD)) != Z_OK)
			{
				free(pBmpBits); pBmpBits = NULL;
				free(pData); pData = NULL;
				continue;
			}

			SetDIBits(hdc, hBitmap, 0, pDlg->BlockHeight, pBmpBits, &bitmapInfo, DIB_RGB_COLORS);
			pDlg->pBmpBlock[y * 8 + x]->SetBitmap(hBitmap);
			pDlg->pBmpBlock[y * 8 + x]->ReleaseDC(pDC);
			free(pBmpBits); pBmpBits = NULL;
			free(pData); pData = NULL;
		}
		else if (len < 0)
		{
			AfxMessageBox(_T("客户端断开连接"));
			break;
		}
	}
	closesocket(hListen);
	closesocket(hServer);
	WSACleanup();
	return 1;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值