Visual C++實現對電腦遠程監控
[日期: 2005-10-20 ] |
來源: 作者: |
[字型:大 中 小] |
摘要:本文講述了利用Socket通訊端進行網路編程的一般技術,並通過該技術實現了對電腦的遠程監控。
關鍵字:Socket通訊端、伺服器、用戶端、遠程監控
引言
在工程施工中經常遇到中心主控機房和工程現場相分離的情況,這就需要工程設計人員經常往返於中心機房與工程現場之間,有時甚至為了修改幾個資料也要相關人員的現場操作才能解決。而且也不能很好的對工程現場進行即時的監測,這就為工程施工與系統的維護帶來了極大的不便。現在區域網路的技術已相當成熟,在中心機房和工程現場之間構建一個區域網路也並不困難。所以我們可以在區域網路的物理架構基礎之上通過Socket通訊端來實現電腦之間的通訊,使維護人員能在中心機房內足不出戶就可以即時地監測、控制遠在工程現場的電腦的工作狀態。本文就對類似程式的實現方法進行簡單的介紹。
Socket網路程式的一般思路
Windows Sockets 規範定義了一個基於 Microsoft Windows 的網路編程介面,它源於加裡弗尼亞大學伯克利分校的伯克利軟體發布(BSD)。它既包括熟悉的伯克利 Socket 風格的常式,也包括了一組 Windows 特有的擴充,使程式員可以利用Windows 原有的訊息驅動機制進行網路方面的編程。而此類程式中最常用的一種模式就是客戶/伺服器模式。在這種架構中,客戶應用程式向伺服器應用程式請求服務。伺服器應用程式一般在一個周知地址上偵聽(listen)服務要求。就是說,直到一個客戶向伺服器發出聯結請求之前,伺服器處理序進程是休眠的。收到請求時,伺服器處理序"醒來(wake up)",完成客戶請求的相應的活動。
通訊端共有三種類型:流式通訊端,資料通訊端以及原始通訊端等。流式通訊端定義了一種可靠的連線導向的服務,實現了無差錯無重複的順序資料轉送;資料通訊端定義了一種不需連線的服務,資料通過相互獨立的報文進行傳輸,是無序的,並且不保證可靠;原始通訊端則允許對低層協議如IP或ICMP等協議進行直接存取,主要用於對新的網路通訊協定實現的測試等。無串連伺服器一般都是面向交易處理的,一個請求一個應答就完成了客戶程式與服務程式之間的相互作用。而連線導向伺服器處理的請求往往比較複雜,不是一來一去的請求應答所能解決的,而且往往是並發伺服器。
本文所採用的就是連線導向的通訊端,其工作過程如下:伺服器首先啟動,通過調用socket()建立一個通訊端,然後調用bind()將該通訊端和本網地址聯絡在一起,再調用listen()使通訊端做好偵聽的準備,並規定它的請求隊列的長度,之後就調用accept()來接收串連。客戶在建立通訊端後就可調用connect()和伺服器建立串連。串連一旦建立,客戶機和伺服器之間就可以通過調用read()和write()來發送和接收資料。最後,待資料傳送結束後,雙方調用close()關閉通訊端。其主要的流程時序可以通過圖1來表示:
伺服器端程式設計實現
由於我們的目的是通過在位於中心機房的用戶端來監控遠端伺服器端,而根據前面介紹的連線導向通訊端應用程式的工作方式,要求伺服器必須先於用戶端而運行。所以根據實際需要,我們應當讓伺服器程式能自啟動。一般有三種方法:在Autoexec.bat裡添加代碼;在Win.ini的Run項裡添加啟動路徑;在註冊表裡添加索引值。本文在此採用後一種方法,通過向註冊表的Software\\Microsoft\\Windows\\CurrentVersion\\Run下添加索引值的方式來實現,另外也可以在RunServer下添加索引值實現之:
…… //設定待添加的註冊表的路徑 LPCTSTR Rgspath="Software\\Microsoft\\Windows\\CurrentVersion\\Run" ; …… //擷取系統路徑 GetSystemDirectory(SysPath,size); GetModuleFileName(NULL,CurrentPath,size); …… //把服務程式從當前位置拷貝到系統目錄中 FileCurrentName = CurrentPath; FileNewName = lstrcat(SysPath,"\\System_Server.exe"); ret = CopyFile(FileCurrentName,FileNewName,TRUE); …… //開啟索引值 ret=RegOpenKeyEx(HKEY_LOCAL_MACHINE,Rgspath,0,KEY_WRITE, &hKEY); if(ret!=ERROR_SUCCESS) { RegCloseKey(hKEY); return FALSE; } //設定索引值 ret=RegSetValueEx(hKEY,"System_Server",NULL,type, (const unsigned char*)FileNewName,size); if(ret!=ERROR_SUCCESS) { RegCloseKey(hKEY); return FALSE; } //關閉索引值 RegCloseKey(hKEY); |
註冊完之後就完成了自啟動。下面進行本文的重點:對通訊端進行編程,首先初始化Socket連接埠,並在初始化成功的前提下通過調用socket()建立一個通訊端,然後調用bind()將該通訊端和本網地址聯絡在一起,再調用listen()使通訊端做好偵聽的準備,並規定它的請求隊列的長度。其中listen()函數主要用來建立一個socket通訊端以偵聽到來的聯結,而且僅用於支援聯結的 socket,即類型為 SOCK_STREAM 的 socket。該通訊端被設為"被動"模式,負責響應到來的聯結,並由進程將到來的聯結排隊掛起。該函數典型地用於需要同時有多個聯結的伺服器:如果一個聯結請求到達且隊列已滿,用戶端將收到一個 WSAECONNREFUSED 的錯誤。當沒有可用的描述符時,listen() 將試圖把函數合理地繼續下去。它將接受聯結直到隊列為空白。如果描述符變為可用,後來的對 listen() 或 accept() 調用將會把隊列填充到當前或最近的累積數(the current or most recent "backlog’’),可能的話,繼續偵聽到來的聯結。下面是這部分的主要代碼:
…… wMajorVersion = MAJOR_VERSION; wMinorVersion = MINOR_VERSION; wVersionReqd = MAKEWORD(wMajorVersion,wMinorVersion); …… Status = WSAStartup(wVersionReqd,&lpmyWSAData); if (Status != 0) return FALSE; …… //建立Socket通訊端 ServerSock = socket(AF_INET,SOCK_STREAM,0); if (ServerSock==INVALID_SOCKET) return FALSE; dstserver_addr.sin_family = PF_INET; dstserver_addr.sin_port = htons(7016); dstserver_addr.sin_addr.s_addr = INADDR_ANY;//BIND Status = bind(ServerSock,(struct sockaddr far *)&dstserver_addr,sizeof(dstserver_addr)); if (Status != 0) return FALSE; //LISTEN Status = listen(ServerSock,1); if (Status != 0) return FALSE; |
接下來需要調用accept()來接收串連。客戶在建立通訊端後就可調用connect()和伺服器建立串連。其函數原形為:SOCKET PASCAL FAR accept ( SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen );該常式從在 s 上掛起的聯結隊列中取出第一個聯結,用和 s 相同的特性建立一個新的 socket 並返回新 socket 的控制代碼。如果隊列中沒有掛起的聯結,並且 socket 也未標明是非阻塞的,則 accept() 阻塞調用者直到有一個聯結。已經接受聯結的 socket (accepted socket)不應用於接受更多的聯結。參數 addr 是一個返回參數,填入的是通訊層的聯結實體地址。地址參數 addr 的嚴格格式由進行通訊的地址族確定。addrlen 是一個返回參數值;該值在調用前包含 addr 指向的緩衝區空間長度;調用返回時包含返回地址的實際長度。
//ACCEPT int len = sizeof(dstserver_addr); NewSock = accept(ServerSock,(struct sockaddr far *)&dstserver_addr,&len); if (NewSock < 0) { closesocket(ServerSock); return FALSE; } //擷取螢幕大小 SysWidth = GetSystemMetrics(SM_CXSCREEN); SysHeight = GetSystemMetrics(SM_CYSCREEN); …… |
串連一旦建立,客戶機和伺服器之間就可以通過調用read()和write()來發送和接收資料。最後,待資料傳送結束後,調用close()關閉通訊端。下面的函數就負責將當前的螢幕狀態,以資料的形式通過send函數發送給客戶程式,以實現對遠程伺服器端的電腦的遠程監視:
…… //Send Falg FALG = US_FLAG; send(NewSock,(char*)&FALG,sizeof(FALG)+1,MSG_OOB); //Get Message length = recv(NewSock,(char*)&iMsg,sizeof(iMsg)+1,0); if (length < 0) { //Close Sock closesocket(NewSock); closesocket(ServerSock); return FALSE; } //GetMessageData if (iMsg < 4500) { send(NewSock,(char*)&SysWidth,sizeof(SysWidth)+1,MSG_OOB); send(NewSock,(char*)&SysHeight,sizeof(SysHeight)+1,MSG_OOB); } switch(iMsg) { case US_DESKTOPBIT: //發送當前螢幕映像 SendDesktop(); break; …… } |
其中,SendDesktop()函數負責將螢幕儲存成位元影像,然後再通過send()函數將其以資料的形式發送出去,這一部分牽扯較多的位元影像操作,比較繁瑣,由於本文重點並不在此,僅作為一個功能函數將其關鍵性代碼摘選如下:
void SendDesktop() { …… //建立電腦裝置環境控制代碼 hdcmy = CreateDC("DISPLAY",NULL,NULL,NULL); hbufferdc = CreateCompatibleDC(hdcmy); //建立位元影像 hBit = CreateCompatibleBitmap(hdcmy, BitWidth, BitHeight); hOldBitmap = (HBITMAP)SelectObject(hbufferdc, hBit); StretchBlt(hbufferdc, 0, 0, BitWidth, BitHeight, hdcmy, 0, 0,SysWidth,SysHeight, SRCCOPY); hBit = (HBITMAP)SelectObject(hbufferdc, hOldBitmap); …… //DDBtoDIB hPal = (HPALETTE) GetStockObject(DEFAULT_PALETTE ); // 擷取位元影像資訊 GetObject(bitmap,sizeof(bm),(LPSTR)&bm); //初始化位元影像資訊頭 bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = bm.bmWidth; bi.biHeight = bm.bmHeight; bi.biPlanes = 1; //bi.biBitCount = bm.bmPlanes * bm.bmBitsPixel; bi.biBitCount = 4; bi.biCompression = BI_RGB; bi.biSizeImage = 0; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrUsed = 0; bi.biClrImportant = 0; …… lpbi = (LPBITMAPINFOHEADER)hDib; *lpbi = bi; GetDIBits(hdc, bitmap, 0L, (DWORD)bi.biHeight,(LPBYTE)NULL, (LPBITMAPINFO)lpbi, (DWORD)DIB_RGB_COLORS ); bi = *lpbi; if (bi.biSizeImage == 0) { bi.biSizeImage = ((((bi.biWidth * bi.biBitCount) + 31) & ~31) / 8) * bi.biHeight; } dwLen += bi.biSizeImage; if (handle = GlobalReAlloc(hDib, dwLen, GMEM_MOVEABLE)) hDib = handle; …… lpbi = (LPBITMAPINFOHEADER)hDib; BOOL bgotbits = GetDIBits( hdc, bitmap0L, (DWORD)bi.biHeight,(LPBYTE)lpbi+ (bi.biSize + ncolors * sizeof(RGBQUAD)),(LPBITMAPINFO)lpbi, (DWORD)DIB_RGB_COLORS); SelectPalette(hdc,hPal,FALSE); …… send(NewSock,(char*)&bitSize,sizeof(bitSize)+1,MSG_OOB); recv(NewSock,(char*)&BitMsg,sizeof(BitMsg)+1,0); plmagePoint = (LPBYTE)hDib; for(WORD i=0;i<bitSize/US_MAXSIZE;i++) { send(NewSock,(char*)plmagePoint,sizeof(BYTE)*US_MAXSIZE,MSG_OOB); plmagePoint = plmagePoint + US_MAXSIZE; recv(NewSock,(char*)&BitMsg,sizeof(BitMsg)+1,0); } if (bitSize%US_MAXSIZE) { send(NewSock,(char*)plmagePoint,sizeof(BYTE)*GlobalSize(hDib)%US_MAXSIZE,MSG_OOB); recv(NewSock,(char*)&BitMsg,sizeof(BitMsg)+1,0); } …… } |
客戶機端程式設計實現
相比而言,用戶端程式的網路通訊部分的實現較為簡單,只需建立socket通訊端連接埠,並用connect()同伺服器建立起串連後就可以用recv()和send()同伺服器收發資料了。
初始化Socket連接埠部分同伺服器的實現部分類似,
…… wVersionrequested = MAKEWORD(2,0); //啟動通訊端 WSAStartup(wVersionrequested,&wsaData); …… SetTimer(hWnd,IDT_TIMER,US_TIME,NULL); |
在此,通過設定定時器來及時地把遠端電腦的當前螢幕以位元影像資料的形式傳到用戶端,並顯示在螢幕上,使維護人員能及時瞭解到遠端電腦的工作狀態。首先要用connect()先建立一個到對等端(peer)的聯結。connect()函數主要用於建立到指定的外部關聯的聯結。如果 socket 尚未綁紮(unbound),則由系統為本地關聯指定一個唯一值。注意如果名字結構的地址域(the address field of the name structure)為全 0,connect()將返回錯誤 WSAEADDRNOTAVAIL。
對流式 sockets (SOCK_STREAM 類),將啟動一個到使用名字(該 socket 名字空間的一個地址)的外部主機的活動聯結。當調用成功完成時,該 socket 已準備好發送/接收資料。下面是定時器訊息響應函數的部分主要代碼:
…… clientSock = socket(AF_INET,SOCK_STREAM,0); if (clientSock < 0) return FALSE; //建立串連 client.sin_family = PF_INET; client.sin_port = htons(7016); client.sin_addr.s_addr = inet_addr(client_address); …… msgsock = connect(clientSock,(struct sockaddr*)&client,sizeof(client)); if (msgsock!=0) return FALSE; …… //擷取螢幕尺寸 GetWindowRect(hWnd,&rect); BitWidth = rect.right - rect.left; BitHeight = rect.bottom - rect.top; recv(clientSock,(char*)&Flag,sizeof(Flag)+1,0); if (Flag == US_FLAG) { MouseEventFlag = false; //發送訊息 Msg = US_DESKTOPBIT; send(clientSock,(char*)&Msg,sizeof(Msg)+1,MSG_OOB); //Send Bit Height and Weidth send(clientSock,(char*)&BitWidth,sizeof(BitWidth)+1,MSG_OOB); send(clientSock,(char*)&BitHeight,sizeof(BitHeight)+1,MSG_OOB); //接收資料 GetDesktopBit(hWnd); MouseEventFlag = true; } //關閉通訊端,釋放資料 closesocket(clientSock); |
這裡的在從伺服器接收到資料後通過調用GetDesktopBit()來完成資料的顯示,使從遠端電腦傳來的資料能在本地客戶機中再現:
…… //Get Bit Size recv(clientSock,(char*)&bitSize,sizeof(bitSize)+1,0); send(clientSock,(char*)&Flag,sizeof(Flag)+1,MSG_OOB); //鎖定記憶體 hDib = GlobalAlloc(GMEM_MOVEABLE,bitSize); p = (LPBYTE)GlobalLock(hDib); p2 = p; for(WORD i=0;i<bitSize/US_MAXSIZE;i++) { len = recv(clientSock,buf,US_MAXSIZE,0); CopyMemory(p2,buf,US_MAXSIZE); p2 = p2 + US_MAXSIZE; send(clientSock,(char*)&Flag,sizeof(Flag)+1,MSG_OOB); } if (bitSize%US_MAXSIZE) { len = recv(clientSock,buf,bitSize%US_MAXSIZE,0); CopyMemory(p2,buf,len); p2 = p2 + bitSize%US_MAXSIZE; send(clientSock,(char*)&Flag,sizeof(Flag)+1,MSG_OOB); } p2 = p2 - bitSize; …… hdc = GetDC(hWnd); GetClientRect(hWnd,&rect); //定義顏色 int color = (1<<((LPBITMAPINFOHEADER)p2)->biBitCount); if(color>256) color = 0; //顯示 StretchDIBits(hdc, 0, 0, rect.right,rect.bottom,0,0, ((LPBITMAPINFOHEADER)p)->biWidth, ((LPBITMAPINFOHEADER)p)->biHeight, (LPBYTE)p+(sizeof(BITMAPINFOHEADER)+color*sizeof(RGBQUAD)), (LPBITMAPINFO)p,DIB_RGB_COLORS, SRCCOPY); …… |
不論是伺服器還是用戶端,對於資料的傳輸都頻繁地使用了recv和send函數。其中前者主要用於從一個 socket 接收資料、讀取收到的資料。對流式通訊端,將返回當前所有的儘可能多的資料,最長達到所提供的緩衝區的長度。如果該 socket 已經配置為線內(in-line)接收帶外資料且有未讀出的帶外資料,則僅返回帶外資料。應用程式可以使用 ioctlsocket() SIOCATMARK選項確定是否還有其它的帶外資料待讀。如果 socket 上沒有到來的資料,則除非 socket 是非阻塞的,recv() 調用會等待資料到達。send()用於已聯結的資料報或流式通訊端,用來在一個 socket 上寫出(write outgoing)資料。在這裡需要特別指出的是:一個 send() 的成功完成並不能表明資料已被成功傳遞。如果儲存(hold)待發送資料的傳輸系統中沒有緩衝區空間可用,send() 將會阻塞,除非 socket 已設定為非阻塞 I/O 模式。
小結
本文通過Socket流式通訊端實現了對電腦的遠程監控。隨著電腦網路化的深入,電腦網路已滲透到各種傳統行業中,電腦網路編程尤其是基於Windows Socket通訊端的網路程式的編程日益顯得重要。本文採用直接利用動態串連庫wsock32.dll,以WinSock API對程式進行設計講解,雖實現比較繁瑣,但能地對程式的設計實現有很好的理解。隨著經驗的豐富,也可以採用VC++的MFC類庫中提供的CAsyncSocket通訊端類來實現Socket編程,比較方便。