Windows下程式修改IP的三種方法
以下討論的平台依據是Window XP + SP1, 不考慮Windows其它版本的相容性問題, 但對NT系列的系統, 理論上是通用的.
方法一: 網卡重啟
更改Windows網卡屬性選項中IP地址, 通過對比前後註冊表, 可以發現以下幾處發生變化
[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters/Interfaces/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}]
"IPAddress"
"SubnetMask"
"DefaultGateway"
"NameServer"
[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}/Parameters/Tcpip]
"IPAddress"
"SubnetMask"
"DefaultGateway"
[HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Services/Tcpip/Parameters/Interfaces/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}]
"IPAddress"
"SubnetMask"
"DefaultGateway"
"NameServer"
[HKEY_LOCAL_MACHINE/SYSTEM/ControlSet001/Services/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}/Parameters/Tcpip]
"IPAddress"
"SubnetMask"
"DefaultGateway"
其中{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}是網卡名稱(AdapterName), 不同的網卡, 不同的接入位置, 不同的接入的時間, 對應的值都不一樣, 它的值是第一次接入系統時, 由系統產生的GUID值.
此處CurrentControlSet實際是ControlSet001的別名.
[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters/Interfaces/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}]
"IPAddress"
"SubnetMask"
"DefaultGateway"
"NameServer"
是主要的設定處.
[HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}/Parameters/Tcpip]
"IPAddress"
"SubnetMask"
"DefaultGateway"
對一些服務有影響, 如不設定, 用netstat可以看到原來的IP地址仍處於監聽狀態(?).
但為了使設定生效, 還有很重要的一步, 即重啟網卡.
更改網卡的配置, 一般而言需要重啟網卡, 如
Linux系統, 只需運行
#ifconfig eth0 down
#ifconfig eht0 up
就可以實現網卡的重啟.
Windows環境下的步驟與之類似: 先禁用本地串連(網卡), 再啟用本地串連(網卡). 但沒有相應的命令或者直接的API. 所幸的是DDK提供一套裝置安裝函數, 用於控制系統裝置, 包括控制裝置的狀態改變.
/****************************************************************************************
Purpose: change state of the selected device
Input : hDevInfo device info set
pDeviceInfoData selected device info
NewState one of enable/disable
Output : TRUE for success, FALSE for failed
****************************************************************************************/
BOOL ChangeDeviceState(HDEVINFO hDevInfo, PSP_DEVINFO_DATA pDeviceInfoData, DWORD NewState)
{
SP_PROPCHANGE_PARAMS PropChangeParams = {sizeof(SP_CLASSINSTALL_HEADER)};
SP_DEVINSTALL_PARAMS devParams;
if (!pDeviceInfoData) {
return FALSE;
}
PropChangeParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
PropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
PropChangeParams.Scope = DICS_FLAG_CONFIGSPECIFIC;
PropChangeParams.StateChange = NewState;
PropChangeParams.HwProfile = 0;
if (!SetupDiSetClassInstallParams(hDevInfo,pDeviceInfoData,
(SP_CLASSINSTALL_HEADER *)&PropChangeParams,sizeof(PropChangeParams))
|| !SetupDiCallClassInstaller(DIF_PROPERTYCHANGE,hDevInfo,pDeviceInfoData)) {
return FALSE;
}
reutrn TRUE;
}
/* hDevInfo如何得到***********************************************************/
m_hDevInfo = SetupDiGetClassDevs(
(LPGUID) &GUID_DEVCLASS_NET, /* GUID_DEVCLASS_NET表示僅列出網路裝置 */
NULL,
this->m_hWnd,
DIGCF_PRESENT);
if (INVALID_HANDLE_VALUE == m_hDevInfo) {
return FALSE;
}
/* pDeviceInfoData如何得到**************************************************/
k = 0;
while (SetupDiEnumDeviceInfo(m_hDevInfo, k ,&DeviceInfoData)) {
k++;
if (CR_SUCCESS != CM_Get_DevNode_Status(&Status, &Problem,
DeviceInfoData.DevInst,0)) {
continue;
}
if ((Status & DN_NO_SHOW_IN_DM)) {
continue;
}
if (GetRegistryProperty(m_hDevInfo,
&DeviceInfoData,
SPDRP_FRIENDLYNAME,
&pBuffer,
&Length)) {
m_Adapter[adapter_num].index = k - 1; /* 當前網卡在裝置資訊集中的索引 */
_tcscpy(m_Adapter[adapter_num].desc, pBuffer); /* 當前網卡 */
GetRegistryProperty(m_hDevInfo,
&DeviceInfoData,
SPDRP_DRIVER,
&pBuffer,
&Length);
_tcscpy(m_Adapter[adapter_num].driver, pBuffer);
adapter_num++;
}
}
/* GetRegistryProperty是對SetupDiGetDeviceRegistryProperty封裝***************/
BOOL GetRegistryProperty(HDEVINFO DeviceInfoSet,
PSP_DEVINFO_DATA DeviceInfoData,
ULONG Property,
LPTSTR* Buffer,
PULONG Length)
{
while (!SetupDiGetDeviceRegistryProperty(
DeviceInfoSet,
DeviceInfoData,
Property,
NULL,
(PBYTE)(*Buffer),
*Length,
Length
))
{
if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
if (*(LPTSTR *)Buffer)
LocalFree(*(LPTSTR *)Buffer);
*Buffer = (LPTSTR)LocalAlloc(LPTR, *Length);
}else {
return FALSE;
}
}
return TRUE;
}
/* m_Adapter的資料結構 */
typedef struct adapter_info_s {
char name[NAME_LEN]; /* 內部裝置名稱, UUID的字串形式 */
char desc[NAME_LEN]; /* 網卡描述 */
char driver[NAME_LEN]; /* 網卡在註冊表中的位置, 如{4D36E972-E325-11CE-BFC1-08002BE10318}/0011
實際完整的鍵名為System//CurrentControlSet//Control//Class/{4D36E972-E325-11CE-BFC1-08002BE10318}/0011
該鍵包含的內容與SetupDiGetDeviceRegistryProperty得到的裝置屬性基本相同
如NetCfgInstanceId即為內部裝置名稱 DriverDesc為裝置描述 */
int index;
}adapter_info_t;
/*****************************************************************************
用何名稱區分不同的網卡
有如下名稱可供選擇
本地串連名, 這是系統使用的方法, 調用的是netman.dll中的未公開函數HrLanConnectionNameFromGuidOrPath(其原型筆者正在調試之中, 成功之後會另行撰文); 其實也可從註冊表中獲得HKLM/System/CurrentControlSet/Control/Network/{4D36E972-E325-11CE-BFC1-08002BE10318}/{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}/Connection/Name
網卡類型描述
裝置易記名稱 它與網卡類型描述基本相同, 當存在同種類型的網卡, 它會加#n(n = 2, 3, 4...)以示區分
如本程式中筆者即以裝置易記名稱區分網卡
*****************************************************************************/
/* 重啟網卡的過程************************************************************/
k = pAdapter->GetCurSel(); /* m_Adapter[k]即當前網卡 */
if (SetupDiEnumDeviceInfo(m_hDevInfo, m_Adapter[k].index ,&DeviceInfoData))
{
hCursor = SetCursor(LoadCursor(NULL, IDC_WAIT));
ChangeDeviceState(m_hDevInfo, &DeviceInfoData, DICS_DISABLE); /* 禁用網卡 */
ChangeDeviceState(m_hDevInfo, &DeviceInfoData, DICS_ENABLE); /* 啟用網卡 */
/* 重啟網卡, 一般情況下, 以下命令相當於前兩命令的組合. 但我仍建議使用前者 */
// ChangeDeviceState(m_hDevInfo, &DeviceInfoData, DICS_PROPCHANGE);
SetCursor(hCursor);
}
/* 掃尾工作 */
SetupDiDestroyDeviceInfoList(m_hDevInfo);
總結: 通過網卡重啟更改IP的方法有兩個步驟: 修改註冊表, 重啟網卡. 重啟網卡的全過程上面已作描述. 註冊表修改的內容為文中列出四個主要項, 如{97EFDAD8-EB2D-4F40-9B07-0FCD706FCB6D}的網卡名稱即是內部裝置名稱, 在adapter結構中已給出. 整個註冊表修改的過程比較簡單, 本文不加敘述.
方法二:未公開函數
Windows系統中, 更改Windows網卡屬性選項中IP地址, 可以即時使更改生效, 並且沒有重啟網卡的過程. 系統內建的netsh也能通過命令列或指令檔的形式, 完成更改IP的功能時, 也不需要重啟網卡
同時也有很多共用軟體, 可以實現同樣的功能, 常見IP地址更改軟體有IPFreeSet, IPChanger, IPProfile, IPHelp, IPSet, SNet等.
筆者通過分析netsh發現一個未公開函數, 即用netcfgx.dll封裝的dhcpcsvc.dll中DhcpNotifyConfigChange函數
具體的方法參見VCKB 25期 王駿先生的 "不重起Windows直接更改IP地址", 他得到的函數原型比我準確, 思路也很清晰.
分析上述共用軟體時, 發現其技術要點不外乎三種: 使用未公開函數, 調用netsh命令, 重啟網卡硬體. 調用netsh命令的實質還是使用未公開函數
使用未公開函數的有: IPFreeSet, IPChanger
調用netsh命令的有 : IPHelp, IPSet. 兩者都是用Delphi開發的.
重啟網卡硬體: IPSwitcher
速度比較: 因為netsh本身的實現是調用netcfgx.dll, netcfgx.dll封裝了對未公開函數的使用, 故效率相對較低. 在一台CPU:PIII500/RAM:256/XP的系統中, IPHelp需要6~7秒才能完成, 而IPFreeSet只需要1~2秒.
方法三:一卡多IP
除以上兩個方法外, 筆者再介紹一種方法. 無論是在Windows下還是在Linux下, 一塊網卡都可同時具有多個IP地址. 根據TCP/IP原理, 在網路層標識通訊節點是IP地址, 在鏈路層上的則是MAC地址. 只要通過ARP, 將多個IP與一個MAC對應起來, 就可實現一網卡多IP(其實是一MAC多IP). 系統本身也有相應的設定選項, 如windows是通過TCP/IP屬性的進階選項添加的, Linux下可由ifconfig命令添加.
iphlpapi提供AddIPAddress和DelIPAddress. 如果能先加入新的IP, 再去除原來的IP, 即可實現IP地址的更改.
具體內容參見我下篇文章"iphlpapi"的使用.