天天看點

遠端開關機

http://download.csdn.net/detail/a514534316/6340523

作為機房管理者,要管理的計算機較多,經常面臨大量計算機要開啟或關閉,如果每次逐一去開啟或關閉,也是一項艱巨的任務,如果能從一台計算機上遠端開啟或關閉本區域網路内的一台或多台計算機,将是一件輕松快樂的事。

一、遠端開機

1.對被開啟計算機的硬體要求

要實作網絡遠端開機,對被開啟的計算機而言需要電源、主機闆、網卡3件裝置的支援。首先電源必須是符合ATX 2.03标準的ATX電源,而且其+5V的備用電流必須在600mA以上,以便能喚醒網卡。其次是主機闆和網卡都必須支援Wake-up On LAN(WOL)技術(即遠端喚醒)。可通過檢視主機闆網卡使用說明書确認,對主機闆而言可直接檢視BIOS設定中的“Power Management Setup”菜單中是否有“Wake on Lan”一項來确認,有則将“Wake on Lan”設定為“Enable”, 開啟遠端喚醒功能。另外檢視BIOS設定中是否有“Wake on PCI Card”,有則說明主機闆可通過PCI插槽直接向網卡供電,将其設定為“Enable”;沒有則需要在主機闆的WOL接口(3針)和網卡的WOL接口之間連一根三芯遠端喚醒電纜,以便主機闆給網卡供電。

2.遠端開機原理

遠端開機的實作,主要是向目标計算機發送特殊格式的資料包(包含有6個位元組的“FF”和重複16遍的目标計算機的MAC位址,共102個位元組的資料),目标計算機的網卡隻要檢測到資料包中某個片段含有這102個位元組的資料,便會将該計算機喚醒,它是AMD公司開發推廣的技術。是以遠端開機需要知道目标計算機的MAC位址,如果要開啟的計算機隻有一台,可直接在該計算機上檢視MAC位址并記錄下來,但是如果有多台計算機需要開啟,用這種方式麻煩且容易出錯,是以應考慮程式設計解決這個問題。

3.程式設計擷取區域網路内各計算機的MAC位址

怎麼擷取區域網路内各計算機的MAC位址呢?了解網絡通信原理的人都知道,網絡中兩台計算機要互相通信,看似隻要互相知道IP位址即可,但那隻是在網絡層上,在資料鍊路層上最終必須知道對方計算機網卡的實體位址,即MAC位址。那麼網絡通信時如何知道其它計算機的MAC位址呢?靠ARP(Address Resolution Protocol)即位址解析協定,通過在區域網路内廣播ARP請求包,對方即會響應,告知其MAC位址,雙方計算機都會将對方的MAC位址及IP位址對應儲存在一張位址映射表中,以備通信使用。是以程式設計時要發送一個ARP請求包來擷取指定計算機的MAC位址,Windows API中已提供現成的函數SendARP,其聲明如下:

DWORD SendARP(IPAddr DestIP, IPAddrSrcIP,PULONG pMacAddr, PULONG  PhyAddrLen);

第一個參數為要擷取其MAC位址的目标計算機機的IP位址,參數類型為IPAddr ,其實類型就是unsigned long (使用者輸入的目的主機的IP位址一般是字元串類型點式IP位址,需要将其轉換成一個3 2位的無符号長整數,可用inet_addr函數完成);第二個參數為源機的IP位址;第三個參數為存放目标計算機MAC位址的指針變量;第四個參數為存放目标計算機MAC位址位元組長度的指針變量。該函數的定義在iphlpapi.h頭檔案中,是以要包含#include<iphlpapi.h>;該函數的實作在Iphlpapi.lib庫檔案中,要在項目設定的連結中加入庫檔案Iphlpapi.lib。(注意:VC6.0不含這兩個檔案,需網上下載下傳,而VC7.0中含有。)關鍵代碼如下:

//将使用者輸入的目的主機的字元串類型點式IP位址轉換成一個3 2位的無符号長整數:

ULONG ULDestIP=inet_addr(strIPAddr);

//發送ARP請求包獲得遠端MAC位址:

iRusult=SendARP(ULDestIP,(unsignedlong)NULL,(PULONG)&ULMacAdd,&PhyAddrLen);

//由于獲得的MAC位址是6位元組的unsignedchar數值,不便閱讀,是以需要将其轉換為字元串:

sprintf(strMacAddr,"%.2x-%.2x-%.2x-%.2x-%.2x-%.2x",ULMacAdd[0],ULMacAdd[1],ULMacAdd[2],ULMacAdd[3],ULMacAdd[4],ULMacAdd[5]);

為了實作擷取機房内所有機器的MAC位址,可以采取循環的辦法發送ARP請求包獲得所有機器的MAC位址,考慮機房内機器的IP位址一般都是連續的,是以先擷取IP位址最小的那台機器的MAC位址,然後逐一增加IP位址, 循環擷取其它機器的IP位址。

//注意IP位址加一前先要将ULONG類型的IP位址從網絡位元組順序轉換為主機位元組順序,加一後再從主機位元組順序轉換為網絡位元組順序。

ULDestIP=htonl(ntohl(ULDestIP)+1);

為了使使用者能對比觀察及關機的需要,程式中還擷取了遠端機的機器名,并與IP位址、MAC位址一起顯示在一個ListCtrl控件中。

//擷取遠端機器名:

    struct hostent*RemoteHost;  

    RemoteHost=(structhostent*)malloc(sizeof(struct hostent));

    RemoteHost=gethostbyaddr((char*)&ULDestIP,4,AF_INET);  

    strcpy(strRemoteHostName,RemoteHost->h_name); 

//将3 2位的無符号長整數IP位址轉換成字元串類型點式IP位址:

struct in_addr sAddr;  

    sAddr.s_addr=ULDestIP;

    strcpy(strIPAddr,inet_ntoa(sAddr));

//将遠端機的機器名、IP位址、MAC位址一起顯示在一個ListCtrl控件中:

    intiItemNumber=m_ListHostInfo.GetItemCount();

    charstrNumber[4];

    sprintf(strNumber,"%d",iItemNumber+1);

    m_ListHostInfo.InsertItem(iItemNumber,strNumber);//第一列顯示序号

    m_ListHostInfo.SetItemText(iItemNumber,1,strRemoteHostName);//第二列顯示機器名

    m_ListHostInfo.SetItemText(iItemNumber,2,strIPAddr);//第三列顯示IP位址

    m_ListHostInfo.SetItemText(iItemNumber,3,strMacAddr);//第四列顯示MAC位址

為了下次開機的需要,要将ListCtrl控件中顯示的機器名、IP位址、MAC位址一一對應儲存在一個檔案中。遠端開機前,需要将檔案中的機器名、IP位址、MAC位址讀出來顯示在ListCtrl控件中,在程式啟動後(比如在OnInitDialog函數中)就讀出來顯示,以便開機和關機都可以使用。檔案讀寫的代碼比較簡單,這裡就不再贅述。

4.發送遠端開機資料包

已經知道了要開啟計算機的MAC位址,接下來便可發送遠端開機的資料包了,采用廣播形式發送。關鍵代碼如下:

SOCKETSocketData=socket(AF_INET, SOCK_DGRAM, 0); //建立套接字

   bool bOptVal=true;

   intiRusult=setsockopt(SocketData,SOL_SOCKET,SO_BROADCAST,(char FAR*)&bOptVal,sizeof(bOptVal));//設定發送方式為廣播發送

   SOCKADDR_IN RecvAddr;

   RecvAddr.sin_family = AF_INET;

   RecvAddr.sin_port = htons(0);

   RecvAddr.sin_addr.s_addr=htonl(INADDR_BROADCAST);

為了将ListCtrl控件中所選擇的計算機都開啟,需要擷取所有選擇項中的MAC位址,然後構造遠端開機資料包,逐機發送。關鍵代碼如下:

POSITION  pos=m_ListHostInfo.GetFirstSelectedItemPosition(); 

while(pos)  

{   intnItem=m_AddrListCtrl.GetNextSelectedItem(pos);//擷取選擇項

    strMacAddr=m_ListHostInfo.GetItemText(nItem,3);//擷取選擇項的第四列資料MAC位址

    BYTEByteMacAddr[6];

    //将字元串型式MAC位址轉換為6個位元組的數值:

    sscanf(strMacAddr,"%2x-%2x-%2x-%2x-%2x-%2x",&ByteMacAddr[0], &ByteMacAddr[1],&ByteMacAddr[2], &ByteMacAddr[3], &ByteMacAddr[4],&ByteMacAddr[5]);

    //構造遠端開機資料包

    BYTEbDataPacket[102];

    memset(bDataPacket,0xFF,6);//先寫入6個位元組的FF

    for (int i=1;i<=16; i++)//然後循環16次寫入6位元組的MAC位址

       memcpy(bDataPacket+i*6,ByteMacAddr,6);

    //發送遠端開機資料包

    iRusult=sendto(SocketData,(charFAR *)bDataPacket,102,0,(SOCKADDR *)&RecvAddr, sizeof(RecvAddr));

}

程式運作的主界面如圖1所示。

圖1 程式主界面

二、遠端關機

遠端關機的方法分兩種:一種需要在被控制的計算機上編寫軟體(适用于任何系統)、一種不需要在被控制的計算機上編寫軟體(隻适用于Windows2000、WindowsXP以上任何系統)。  

1.有被控端軟體

需要編寫控制端軟體和被控端軟體,由控制端軟體發送自定義的關機指令字元串,被控端軟體收到相應指令後關閉本機。通信方式有TCP、UDP兩種,TCP是面向連接配接的,為了保證可靠的傳輸可采用它,UDP是無連接配接的,為了提高傳輸速度可采用它。由于篇幅限制且UDP方式相對簡單,我這裡隻談TCP方式。

TCP方式需要通信的一端作為服務端,進行監聽(Listen),等待接受(accept)另一端即用戶端的連接配接(connect)。如果僅僅用于關機,将控制端或被控端作為服務端均無不可,但是為了軟體的可擴充性,我将控制端作為服務端,關鍵代碼如下:

(1)服務端:

先設定服務端位址和端口,建立套接字并綁定,然後将套接字置為監聽模式,啟動一個線程處理接收。

sockaddr_inServerSockAddr;

ServerSockAddr.sin_addr.s_addr=htonl(INADDR_ANY);

ServerSockAddr.sin_family=AF_INET;

ServerSockAddr.sin_port=htons(SERVER_PORT);

m_SockListen=socket(AF_INET,SOCK_STREAM,0);

if(bind(m_SockListen,(sockaddr*)&ServerSockAddr,sizeof(ServerSockAddr)))

       MessageBox("綁定錯誤");

elselisten(m_SockListen,5);

AfxBeginThread(&thread,0);

線上程函數中接受用戶端的連接配接,得到一個新的套接字,用于和剛接受連接配接的那個客戶機通信。為了使使用者能将在ListCtrl控件上所選擇的計算機正确關機,需要将ListCtrl控件的行号與該行客戶機的連接配接套接字對應,将與各客戶機連接配接的所有套接字存放在一個套接字數組m_SockClient[]中,是以隻要将客戶機資訊在ListCtrl控件中所在行号作為套接字數組m_SockClient []中的下标來對應該客戶機的套接字即可。在accept函數的第二個參數中傳回了發出連接配接請求的那個客戶機的I P位址資訊,是以隻要将該I P位址與ListCtrll控件上所列出的所有客戶機的I P位址一一比較,找到該客戶機資訊所在行号,然後将該客戶機的套接字儲存在以該行号為下标的數組套接字元素中。關鍵代碼如下:

UINT thread(LPVOID p)

{  

SOCKETSockAccept;

    structsockaddr_in clientaddr;

    intiAddrLen=sizeof(struct sockaddr);

    ULONGulClientIpAddr;

    CStringstrIpAddr;

    CRemoteOnOffDlg*PowerDlg=(CRemoteOnOffDlg*)AfxGetApp()->GetMainWnd();

    while(1)

    {  

SockAccept=accept(PowerDlg->m_SockListen,(sockaddr*)&clientaddr,&iAddrLen);

       ulClientIpAddr=clientaddr.sin_addr.s_addr;

       for(inti=0;i<PowerDlg->m_ListHostInfo.GetItemCount();i++)

       {  

strIpAddr=PowerDlg->m_ListHostInfo.GetItemText(i,2);

           if(ulClientIpAddr==inet_addr(strIpAddr))

           {  

PowerDlg->m_SockClient[i]=SockAccept;

//為了知道哪些客戶機已建立了連接配接,我順便在ListCtrll控件中對應連接配接客戶機那一行的第五列打"√"作為标記:

              PowerDlg->m_ListHostInfo.SetItemText(i,4,"√");

              break;

           }

       }

    }

}

最後在使用者點選關機按鈕或菜單時發送自定義的關機指令字元串:

POSITIONpos=m_ListHostInfo.GetFirstSelectedItemPosition(); 

 while(pos)  

 { 

intnItem=m_ListHostInfo.GetNextSelectedItem(pos);//擷取選擇項

    send(m_SockClient[nItem],"POWOFF",COM_STR_LEN,0);

    closesocket(m_SockClient[nItem]);//關閉套接字

    m_ListHostInfo.SetItemText(nItem,4,"×");

}  

(2)用戶端

先解析伺服器名,然後用s o c k e t建立一個套接字,再用c o n n e c t建立與伺服器的連接配接。最後等待接收關機指令字元串:

CStringstrServerIPAddr="192.168.1.1";//此處為服務端的IP位址

SOCKETSockClient;

sockaddr_inServerSockAddr;

ServerSockAddr.sin_addr.s_addr=inet_addr(strServerIPAddr);

ServerSockAddr.sin_family=AF_INET;

ServerSockAddr.sin_port=htons(SERVER_PORT);

SockClient=socket(AF_INET,SOCK_STREAM,0);

while(connect(SockClient,(sockaddr*)&ServerSockAddr,sizeof(ServerSockAddr))!=0);

intiAllRecvLen=0,iThisRecvLen=0;

charstrRecvBuf[COM_STR_LEN+1]="";

while(iThisRecvLen!=SOCKET_ERROR&&iAllRecvLen<COM_STR_LEN)

//循環接收資料

   iThisRecvLen=recv(SockClient,strRecvBuf+iAllRecvLen,COM_STR_LEN,0);

   iAllRecvLen+=iThisRecvLen;

}

被控端收到指令字元串後,調用ExitWindowsEx函數關閉或重新開機本客戶機。ExitWindowsEx函數在Windows9x系統中可直接使用,在Windows2000或WindowsXP以上系統中預設的情況下程序不具有關機權限,是以要将目前程序的關機權限Enabled。先通過OpenProcessToken函數獲得目前程序通路令牌的句柄,該函數聲明如下:

BOOL OpenProcessToken (HANDLE ProcessHandle,DWORD DesiredAccess,PHANDLETokenHandle);

第一參數是要修改權限的程序句柄;第二個參數為對該令牌的通路類型;第三個參數即獲得的程序通路令牌的句柄。

為了修改程序令牌權限,還要先定義一個令牌權限TOKEN_PRIVILEGES類型結構變量,該結構定義如下:

typedef struct _TOKEN_PRIVILEGES {

    DWORDPrivilegeCount;

   LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];

} TOKEN_PRIVILEGES

第一個成員變量為權限數量;第二個成員變量為LUID_AND_ATTRIBUTES類型結構變量數組。該結構定義如下:

typedef struct _LUID_AND_ATTRIBUTES {

    LUID Luid;

    DWORDAttributes;

    }LUID_AND_ATTRIBUTES;

第一個成員變量為某權限的本地唯一辨別;第二個成員變量為該權限屬性。

為了擷取某權限的本地唯一辨別,需要通過LookupPrivilegeValue函數,該函數聲明如下:

BOOL LookupPrivilegeValueA(LPCSTR lpSystemName,LPCSTR lpName,PLUIDlpLuid);

第一個參數為系統名,本地系統為NUL;第二個參數為權限名;第三個參數為傳回的權限本地唯一辨別。

定義好了令牌權限TOKEN_PRIVILEGES結構變量後,最後通過AdjustTokenPrivileges函數修改通路令牌權限,該函數聲明如下:

BOOL AdjustTokenPrivileges (HANDLE TokenHandle,BOOLDisableAllPrivileges,   PTOKEN_PRIVILEGES NewState,DWORD BufferLength,PTOKEN_PRIVILEGES reviousState,    PDWORDReturnLength);

第一個參數為通路令牌句柄;第二個參數為是否取消所有權限;第三個參數為前面定義好的令牌權限;後面三個參數針用于儲存修改前的令牌權限,分别為用于儲存的記憶體長度、儲存的記憶體位址、實際儲存的記憶體長度。

具體代碼如下:

if(strcmp("POWOFF",strRecvBuf)==0)

{  

closesocket(SockClient);

    WSACleanup();

    if(GetVersion()<0x80000000)//判斷WINDOWS系統版本号

    {  

HANDLEhProcessToken;

       TOKEN_PRIVILEGEStkp;

OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,&hProcessToken);

    LookupPrivilegeValue(NULL,SE_SHUTDOWN_NAME,&tkp.Privileges[0].Luid);

       tkp.PrivilegeCount= 1;

       tkp.Privileges[0].Attributes= SE_PRIVILEGE_ENABLED;

    AdjustTokenPrivileges(hProcessToken,FALSE,&tkp,0,(PTOKEN_PRIVILEGES)NULL,0);

    }

//強迫所有程序退出、關閉計算機并切斷電源

    ExitWindowsEx(EWX_FORCE|EWX_POWEROFF,0);

}

在Windows2000或WindowsXP以上系統中,也可用InitiateSystemShutdown函數代替ExitWindowsEx函數關閉本機,InitiateSystemShutdown函數的使用在下面介紹。

2.無被控端軟體

由于Windows2000、WindowsXP以上的系統本身支援遠端關機,是以也可不編寫被控端軟體,WindowsAPI中已提供現成的函數InitiateSystemShutdown,其聲明如下:

BOOL InitiateSystemShutdownA(LPSTRlpMachineName,LPSTR lpMessage,DWORD dwTimeout,BOOL bForceAppsClosed,BOOLbRebootAfterShutdown);

第一個參數為機器名,本機為NULL;第二個參數為關機提示對話框中顯示的消息;第三個參數為關機前提示的時間;第四個參數為是否強制關閉應用程式;第五個參數為是否重新開機。

遠端關機需要具有相應權限,如果在域環境下可以直接以域管理者身份登入系統獲得遠端關機的權限,一般在機房或網吧中都是工作組環境,無法直接獲得遠端關機的權限,怎麼辦呢?通過試驗我找到了一個辦法,具體步驟如下:

(1)在被控機上通過“計算機管理”建立一個使用者,然後在“組政策”中給該使用者配置遠端關機權限,具體操作為:運作“gpedit.msc”打開“組政策編輯器”。在“組政策”左側樹視窗中依次打開“計算機配置”、“Windows 設定”、“安全設定”、“本地政策”、“使用者權利指派”。在“組政策”右側清單視窗選擇“從遠端系統強制關機”政策添加該使用者。

(2)在工作組環境中無法直接以該使用者賬号從控制機登入被控機,需要在控制機上建立與被控機上一緻的使用者賬号(使用者名和密碼都需相同),可預先通過“計算機管理”完成,也可在調用關機代碼前臨時通過下面的代碼來建立:

NET_API_STATUS retStatus = 0;

DWORD dwError = 0;

USER_INFO_1 structUserInfo;

ZeroMemory(&structUserInfo,sizeof(structUserInfo));

structUserInfo.usri1_name = szUserName;

structUserInfo.usri1_password= szPassword;

structUserInfo.usri1_priv = USER_PRIV_USER;

structUserInfo.usri1_flags =UF_NORMAL_ACCOUNT;

retStatus =NetUserAdd(NULL, 1, (LPBYTE)(&structUserInfo), &dwError); //建立使用者

    //将該使用者加到"Administrators"組

_LOCALGROUP_MEMBERS_INFO_3 memberUser;

memberUser.lgrmi3_domainandname =structUserInfo.usri1_name;

retStatus = NetLocalGroupAddMembers(NULL,L"Administrators", 3, (LPBYTE)(&memberUser), 1);

在調用關機代碼後如果要删除上面所建立的使用者可通過下面代碼完成:

NetUserDel(NULL,structUserInfo.usri1_name);

(3)在控制機和被控機的“組政策”中找到“本地政策”的“安全選項”的“網絡通路:本地賬号的共享和安全模式”,設定為“經典-本地使用者以自己的身份驗證”。

(4)如果在控制機上已用上面建立的使用者賬号登入,此時可直接調用InitiateSystemShutdown函數遠端關機,否則先要通過下面代碼登入:

// LogonUser函數接受登入資訊并傳回有效登入的安全性通路令牌。

LogonUser(m_strUserName,strMachineName,m_strPassword,LOGON32_LOGON_INTERACTIVE,LOGON32_PROVIDER_DEFAULT,&hLogonToken);          

// ImpersonateLoggedOnUser函數接收LogonUser的安全性通路令牌,并将該令牌用到目前的執行中。

    ImpersonateLoggedOnUser(hLogonToken );

(5)調用InitiateSystemShutdown函數遠端關機

InitiateSystemShutdown(strMachineName,"關機!",30,TRUE,FALSE);

三、結語

本文實作了區域網路中的遠端開機功能和有用戶端無用戶端兩種方式下的關機功能,在此基礎上還可根據需要進行功能擴充。本程式在WindowsXP作業系統下用VC6.0或VS.NET都能調試通過,并投入使用。

繼續閱讀