远程控制和原理和实践

news2024/11/9 9:44:48

按理来说,本人不该发表此类专业的文章,但是从鄙人的开发经历出发,让本人斗胆在此对远控软件做一些论述,谈论一点自己的认识。

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

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

被控端每隔20毫秒截屏,图像压缩后,网络传输给控制端并实时显示;被控端时刻接收控制端的控制消息(主要是键盘的按键、鼠标的位置和动作),并模拟实现这些键盘鼠标操作。

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

本程序只是自己对远控的探索和demo演示,其实现跟本文思路一样,距离商用还有很大距离。是从实际的效果看,程序已经具备了远控软件的基本功能。

(一)原理

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

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

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

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

(二)实现和细节

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

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

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


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,在256位模式下,要给调色板留下空间,调色板一般需要另外的1024字节大小的空间,否则会发生内存越界异常。另外,此函数如果不知道如何填写位图参数,可以在第一次调用时,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个参数,测试中发现扫描线的行数并不需要dword对齐。
函数中的hbitmap不能被SelectObject选中,测试中发现,hbitmap使用SelectObject被选中,这样调用没有问题。

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

  1. 数据压缩传输。采用zip压缩,参数中设置为尽可能最大化压缩,大概的压缩估值是6-15倍的压缩比。此时,如果是8位的位图数据帧,压缩后大约是60-120kb,如果是16位,压缩后大约为100-600kb,32位的话,大约是200-600kb。当然这样的压缩比难以满足实际需求,后面会详细介绍减少视频流量的其他措施,可以将平均网络流量降低到平均100kb。
  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显示在当前窗口中的客户区中。

  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;
`
该部分主要有以下几个变量描述:键盘按键信息,鼠标左键、中键、右键是否有点击动作,鼠标滚轮的滚动距离和坐标、鼠标的坐标位置。窗口处理程序监听鼠标键盘消息,将这些消息填充为上述结构体,并由通信线程发送给被控端。

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

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

  1. 客户端截屏动态延时。当服务端发现没有鼠标键盘等控制信息后,发送延时消息REMOTE_DUMMY_PACKET给客户端,客户端每收到一个该数据包,截屏延时增加10ms,最长到3000ms为止,而收到键盘鼠标等控制消息后,延时消息立刻恢复到默认的REMOTE_CLIENT_SCREEN_MIN_INTERVAL值,这样可以显著的减少网络流量。
  2. 将截屏数据帧的格式由bmp转化为jpeg。bmp是原始像素值格式,jpeg是压缩后的像素格式,jpeg压缩算法是对每一块的像素块进行压缩,经测试,在保证图像质量%80以上的条件下,其压缩比大约在10-40倍,这对zip压缩的优势并不明显(zip的压缩比平均在6-30倍左右),而且使用和转码也较为繁琐,弃之未用,当然,如果改为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那一行的执行,原因因该是,两边的窗口大小不一致,若被控的窗口比较大,而主控端窗口比较小,而主控端的截屏处理缓冲区是按照主控的的窗口大小分配的,转换坐标处理时,有可能发生内存溢出。

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

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

相关文章

【Note7】uboot,

文章目录 1.uboot1.U-Boot命令之常用命令&#xff1a;7.U-Boot命令之EMMC和SD卡操作命令&#xff1a;一般EMMC和SD卡是同一个东西&#xff0c;没特殊说明&#xff0c;统一MMC来代指EMMC和SD卡8.U-Boot命令之内存操作命令&#xff1a;直接对DRAM进行读写操作&#xff0c;uboot命…

CMake学习(7): CMake的嵌套

博客参考自&#xff1a;爱编程的大丙: https://subingwen.cn/cmake/CMake-primer/ ,仅供学习分享使用 如果项目很大&#xff0c;或者项目中有很多的源码目录&#xff0c;在通过 CMake 管理项目的时候如果只使用一个 CMakeLists.txt&#xff0c;那么这个文件相对会比较复杂&…

【嵌入式环境下linux内核及驱动学习笔记-(16)linux总线、设备、驱动模型之input框架】

目录 1、Linux内核输入子系统概念导入1.1 输入设备工作机制1.2 运行框架1.3 分层思想 2、驱动开发步骤2.1 在init()或probe()函数中2.2 在exit&#xff08;&#xff09;或remove&#xff08;&#xff09;函数中&#xff1a;2.3 上报事件2.4 input驱动要素导图2.5 input驱动的总…

代码生成器实现

代码生成器实现 实现封装元数据的工具类实现代码生成器的代码编写掌握模板创建的 构造数据模型 需求分析 借助Freemarker机制可以方便的根据模板生成文件&#xff0c;同时也是组成代码生成器的核心部分。对于Freemarker而 言&#xff0c;其强调 数据模型 模板 文件 的思…

chatgpt赋能python:Python与硬件结合的现实价值

Python与硬件结合的现实价值 Python是当今最受欢迎和广泛使用的编程语言之一&#xff0c;因其易学易用、开放源代码和灵活性而备受欢迎。但是当我们将它与硬件相结合&#xff0c;它能做到什么&#xff1f; 在这篇文章中&#xff0c;我们将向您介绍如何将Python与硬件结合&…

戴尔外星人m16r1国行中文原厂Windows11系统自带Support Assist OS Recovery恢复出厂设置

戴尔外星人m16r1国行中文原厂系统自带Support Assist OS Recovery恢复出厂设置 文件地址https://pan.baidu.com/s/1Pq09oDzmFI6hXVdf8Vqjqw?pwd3fs8 提取码:3fs8 支持Support Assist OS recovery型号: 戴尔外星人m18r1国行中文版Windows11系统 戴尔外星人x16r1国行中文版…

2023/6/9总结

CSS Less嵌套 子元素的选择器可以直接写在父元素里面。 如果不是它的后代元素&#xff0c;比如你想写伪类选择器、交集选择器&#xff0c;需要在前面加&号。 Less运算&#xff1a; 加减乘除都可以&#xff0c;运算符必须用空格隔开。如果俩个元素都有单位&#xff0…

【SpringBoot 3.x】使用starter整合Druid

Druid介绍 Druid是阿里巴巴的一个开源项目&#xff0c;号称为监控而生的数据库连接池&#xff0c;在功能、性能、扩展性方面都超过其他例如DBCP、C3P0、BoneCP、Proxool、JBoss DataSource等连接池,而且Druid已经在阿里巴巴部署了超过600个应用&#xff0c;通过了极为严格的考…

网络作业9【计算机网络】

网络作业9【计算机网络】 前言推荐网络作业9一. 单选题&#xff08;共12题&#xff0c;36分&#xff09;二. 多选题&#xff08;共1题&#xff0c;3分&#xff09;三. 填空题&#xff08;共2题&#xff0c;10分&#xff09;四. 阅读理解&#xff08;共1题&#xff0c;17分&…

C++【STL】之string的使用

STL简介 STL是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架。STL由六大组件构成&#xff1a;仿函数、算法、迭代器、空间配置器、容器和配接器。 其中各种容器可以很大帮助的提升我们编写程序的效率&#…

静态NAT配置与验证实验

静态NAT配置与验证实验 【实验目的】 部署静态NAT。熟悉静态NAT的应用方法。验证配置。 【实验拓扑】 实验拓扑如图所示。 实验拓扑 设备参数如表所示。 设备参数表 设备 接口 IP地址 子网掩码 默认网关 R1 f0/0 192.168.10.1 255.255.255.0 N/A S1/0 10.0.0.1…

GlyphControl: Glyph Conditional Control for Visual Text Generation

GlyphControl: Glyph Conditional Control for Visual Text Generation (Paper reading) Yukang Yang, Microsoft Research Asia, arXiv2023, Cited: 0, Code, Paper 1. 前言 最近&#xff0c;人们对开发基于扩散的文本到图像生成模型的兴趣日益增长&#xff0c;这些模型能够…

软件工程开发文档写作教程(11)—需求分析书的编写

本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl本文参考资料&#xff1a;电子工业出版社《软件文档写作教程》 马平&#xff0c;黄冬梅编著 需求分析书主要内容 按照国家《软件需求说明书GB8567-88》所定义的标准&#xff0c;软件需求…

2023去水印小程序saas系统源码修复独立版v1.0.3+uniapp前端

&#x1f388; 限时活动领体验会员&#xff1a;可下载程序网创项目短视频素材 &#x1f388; &#x1f389; 有需要的朋友记得关赞评&#xff0c;阅读文章底部来交流&#xff01;&#xff01;&#xff01; &#x1f389; ✨ 源码介绍 一个基于uniapp写的小程序&#xff0c;后端…

MATLAB | 绘图复刻(九) | 泰勒图及组合泰勒图

有粉丝问我这个图咋画&#xff1a; 我一看&#xff0c;这不就泰勒图嘛&#xff0c;就fileexchange上搜了一下泰勒图绘制代码&#xff0c;但是有的代码比较新的版本运行要改很多地方&#xff0c;有的代码需要包含一些压缩包没并没有的别人写的函数&#xff0c;于是我干脆自己写了…

JAVA-八种基础数据类型和包装类型及相关面试题

文章目录 前言一、基本数据类型1.1 分类1.2 概念1.3 代码1.4 二维表 二、各基本数据类型间强制转换2.1 为什么Java中有强制转换&#xff1f;2.2 示例代码 三、包装类型3.1 为什么有包装类型&#xff1f;3.2 基本概念3.3 转换方法 四、转换过程中使用的自动装箱和自动拆箱4.1 来…

Redis Lua脚本原理

Lua脚本执行过程 创建并修改Lua环境 1 创建基础Lua环境2 载入函数库3 创建全局表格Lua4 替换随机函数5 创建排序辅助函数6 创建redis.pcall函数7 全局环境保护8 修改后的Lua环境保存到服务器状态的Lua属性&#xff0c;等待脚本执行 Redis中带有不确定性的命令&#xff1a; …

RK3588平台开发系列讲解(以太网篇)PHY状态机

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、PHY状态机定义二、PHY的状态变化三、PHY的状态变化打印沉淀、分享、成长,让自己和他人都能有所收获!😄 一、PHY状态机定义 phy状态机: 目录:include/linux/phy.h enum phy_state {PHY_DOWN = 0,</

开源模型的力量

2 月&#xff0c;Meta 发布了其大型语言模型&#xff1a;LLaMA。与 OpenAI 及其 ChatGPT 不同&#xff0c;Meta 不仅仅为世界提供了一个可以玩的聊天窗口。 相反&#xff0c;它将代码发布到开源社区&#xff0c;此后不久模型本身就被泄露了。研究人员和程序员立即开始修改、改…

Protobuf实战:通讯录

网络版通讯录 需求 Protobuf常⽤于通讯协议、服务端数据交换场景。接下来将实现⼀个⽹络版本的通讯录&#xff0c;模拟实现客⼾端与服务端的交互&#xff0c;通过Protobuf来实现各端之间的协议序列化。 需求如下&#xff1a; 客⼾端可以选择对通讯录进⾏以下操作&#xff1a;…