作为机房管理员,要管理的计算机较多,经常面临大量计算机要开启或关闭,如果每次逐一去开启或关闭,也是一项艰巨的任务,如果能从一台计算机上远程开启或关闭本局域网内的一台或多台计算机,将是一件轻松快乐的事。
一、远程开机
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, IPAddr SrcIP,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,(unsigned long)NULL,(PULONG)&ULMacAdd,&PhyAddrLen);
//由于获得的MAC地址是6字节的unsigned char数值,不便阅读,所以需要将其转换为字符串:
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=(struct hostent*)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控件中:
int iItemNumber=m_ListHostInfo.GetItemCount();
char strNumber[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地址,接下来便可发送远程开机的数据包了,采用广播形式发送。关键代码如下:
SOCKET SocketData=socket(AF_INET, SOCK_DGRAM, 0); //创建套接字
bool bOptVal=true;
int iRusult=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)
{ int nItem=m_AddrListCtrl.GetNextSelectedItem(pos);//获取选择项
strMacAddr=m_ListHostInfo.GetItemText(nItem,3);//获取选择项的第四列数据MAC地址
BYTE ByteMacAddr[6];
//将字符串型式MAC地址转换为6个字节的数值:
sscanf(strMacAddr, "%2x-%2x-%2x-%2x-%2x-%2x",&ByteMacAddr[0], &ByteMacAddr[1], &ByteMacAddr[2], &ByteMacAddr[3], &ByteMacAddr[4], &ByteMacAddr[5]);
//构造远程开机数据包
BYTE bDataPacket[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,(char FAR *)bDataPacket,102,0,(SOCKADDR *)&RecvAddr, sizeof(RecvAddr));
}
程序运行的主界面如图1所示。
图1 程序主界面
二、远程关机
远程关机的方法分两种:一种需要在被控制的计算机上编写软件(适用于任何系统)、一种不需要在被控制的计算机上编写软件(只适用于Windows2000、WindowsXP以上任何系统)。
1.有被控端软件
需要编写控制端软件和被控端软件,由控制端软件发送自定义的关机命令字符串,被控端软件收到相应命令后关闭本机。通信方式有TCP、UDP两种,TCP是面向连接的,为了保证可靠的传输可采用它,UDP是无连接的,为了提高传输速度可采用它。由于篇幅限制且UDP方式相对简单,我这里只谈TCP方式。
TCP方式需要通信的一端作为服务端,进行监听(Listen),等待接受(accept)另一端即客户端的连接(connect)。如果仅仅用于关机,将控制端或被控端作为服务端均无不可,但是为了软件的可扩展性,我将控制端作为服务端,关键代码如下:
(1)服务端:
先设定服务端地址和端口,创建套接字并绑定,然后将套接字置为监听模式,启动一个线程处理接收。
sockaddr_in ServerSockAddr;
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("绑定错误");
else listen(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)
{
SOCKET SockAccept;
struct sockaddr_in clientaddr;
int iAddrLen=sizeof(struct sockaddr);
ULONG ulClientIpAddr;
CString strIpAddr;
CRemoteOnOffDlg *PowerDlg=(CRemoteOnOffDlg*)AfxGetApp()->GetMainWnd();
while(1)
{
SockAccept=accept(PowerDlg->m_SockListen,(sockaddr*)&clientaddr,&iAddrLen);
ulClientIpAddr=clientaddr.sin_addr.s_addr;
for(int i=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;
}
}
}
}
最后在用户点击关机按钮或菜单时发送自定义的关机命令字符串:
POSITION pos=m_ListHostInfo.GetFirstSelectedItemPosition();
while(pos)
{
int nItem=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创建与服务器的连接。最后等待接收关机命令字符串:
CString strServerIPAddr="192.168.1.1";//此处为服务端的IP地址
SOCKET SockClient;
sockaddr_in ServerSockAddr;
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);
int iAllRecvLen=0,iThisRecvLen=0;
char strRecvBuf[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,PHANDLE TokenHandle);
第一参数是要修改权限的进程句柄;第二个参数为对该令牌的访问类型;第三个参数即获得的进程访问令牌的句柄。
为了修改进程令牌权限,还要先定义一个令牌权限TOKEN_PRIVILEGES类型结构变量,该结构定义如下:
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount;
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES
第一个成员变量为权限数量;第二个成员变量为LUID_AND_ATTRIBUTES类型结构变量数组。该结构定义如下:
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid;
DWORD Attributes;
} LUID_AND_ATTRIBUTES;
第一个成员变量为某权限的本地唯一标识;第二个成员变量为该权限属性。
为了获取某权限的本地唯一标识,需要通过LookupPrivilegeValue函数,该函数声明如下:
BOOL LookupPrivilegeValueA(LPCSTR lpSystemName,LPCSTR lpName,PLUID lpLuid);
第一个参数为系统名,本地系统为NUL;第二个参数为权限名;第三个参数为返回的权限本地唯一标识。
定义好了令牌权限TOKEN_PRIVILEGES结构变量后,最后通过AdjustTokenPrivileges函数修改访问令牌权限,该函数声明如下:
BOOL AdjustTokenPrivileges (HANDLE TokenHandle,BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState,DWORD BufferLength,PTOKEN_PRIVILEGES reviousState, PDWORD ReturnLength);
第一个参数为访问令牌句柄;第二个参数为是否取消所有权限;第三个参数为前面定义好的令牌权限;后面三个参数针用于保存修改前的令牌权限,分别为用于保存的内存长度、保存的内存地址、实际保存的内存长度。
具体代码如下:
if(strcmp("POWOFF",strRecvBuf)==0)
{
closesocket(SockClient);
WSACleanup();
if(GetVersion()<0x80000000)//判断WINDOWS系统版本号
{
HANDLE hProcessToken;
TOKEN_PRIVILEGES tkp;
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以上的系统本身支持远程关机,所以也可不编写被控端软件,Windows API中已提供现成的函数InitiateSystemShutdown,其声明如下:
BOOL InitiateSystemShutdownA(LPSTR lpMachineName,LPSTR lpMessage,DWORD dwTimeout,BOOL bForceAppsClosed,BOOL bRebootAfterShutdown);
第一个参数为机器名,本机为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都能调试通过,并投入使用。
|