这几天在自己的个人机器上装了个虚拟机VMWare。然后在程序中获得本地IP,发现,是错误的,发现获得的IP竟然不是原来的192.168.1.155,而是一个奇怪的192.168.233.1。后来发现,原来自己装了虚拟机,这个地址是自己虚拟机VMWare VMNet8的地址,而且,这个网络被命名为本地连接2。
看下我自己原来的代码,是用的gethostbyname方法获得的。于是,写了个Demo程序,进行运行
#include
#include
#include
#include
int main(int argc, char *argv[])
{
char *ptr, **pptr;
struct hostent *hptr;
struct hostent *pHPtr;
char str[32];
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested = MAKEWORD( 2, 0 );
char name[100];
ptr = name;
if (0 == WSAStartup( wVersionRequested, &wsaData ) )
{
if(0 != gethostname ( ptr, strlen(ptr)) )
{
printf("没有获得名称\n");
return -1;
}
if (NULL == (hptr = gethostbyname(ptr)))
{
printf("获得地址错误\n");
return -1;
}
printf("机器的正式名字是: %s\n", ptr);
for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
{
printf("别名是: %s\n", *pptr);
}
int i = 1;
switch(hptr->h_addrtype)
{
case AF_INET:
case AF_INET6:
pptr = hptr->h_addr_list;
for(; *pptr != NULL; pptr++)
{
printf("地址是: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
}
pHPtr = hptr;
for (pptr = hptr->h_addr_list; *pptr != NULL; pptr++)
{
//inet_ntop的实质
printf("第%d 个地址是: %d.%d.%d.%d\n", i, pHPtr->h_addr_list[i-1][0] & 0x00ff, pHPtr->h_addr_list[i-1][1] & 0x00ff,
pHPtr->h_addr_list[i-1][2] & 0x00ff, pHPtr->h_addr_list[i-1][3]& 0x00ff);
printf("第%d 个地址是: %s\n", i++, inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)) );
}
printf("第一个地址是: %s\n", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
break;
default:
printf("未知地址类型\n");
}
WSACleanup( );
}
return 0;
}
运行结果如图所示:
原来,第一个地址早已不是我自己的本地地址,而是VMNet8 的地址。
我查下相应函数的用法,相应信息如下:
struct hostent *gethostbyname(const char *name);
这个函数的传入值是域名或者主机名,例如"www.google.cn"等等。传出值,是一个hostent的结构。如果函数调用失败,将返回NULL。
返回hostent结构体类型指针
struct hostent
{
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
#define h_addr h_addr_list[0]
};
hostent->h_name
表示的是主机的规范名。例如www.google.com的规范名其实是www.l.google.com。
hostent->h_aliases
表示的是主机的别名.www.google.com就是google他自己的别名。有的时候,有的主机可能有好几个别名,这些,其实都是为了易于用户记忆而为自己的网站多取的名字。
hostent->h_addrtype
表示的是主机ip地址的类型,到底是ipv4(AF_INET),还是pv6(AF_INET6)
hostent->h_length
表示的是主机ip地址的长度
——————————————以上是分割线————————————————————————————
可见,通过gethostbyname 的方法获取IP的方法也是可以的,只是这个VMNet 8地址确实排在前面,我无法修改。
而且gethostbyname是递归通过DNS阻塞查询主机名和IP,很影响效率。据说好多公司做爬虫都是自己查询主机名和网站名的IP,不用gethostbyname,就是因为gethostbyname效率太低了。
继续查资料。
发现一个函数GetAdaptersInfo,getAdapterInfo相应参考资料如下:
GetAdaptersInfo函数:
DWORD GetAdaptersInfo(
// 接受数据的缓冲区,指向一个结构IP_ADAPTER_INFO
PIP_ADAPTER_INFO pAdapterInfo,
// 指向输出缓冲区的大小的指针
PULONG pOutBufLen
);
这个函数中最重要是第一个参数,看看这个结构有点啥?MSDN中说明如下:
typedef struct _IP_ADAPTER_INFO {
struct _IP_ADAPTER_INFO* Next; // 链表指针,指向下一个单元
DWORD ComboIndex;
char AdapterName[MAX_ADAPTER_NAME_LENGTH + 4]; // 接口信息物理名称
char Description[MAX_ADAPTER_DESCRIPTION_LENGTH + 4];// 接口描述信息
UINT AddressLength; // MAC地址的长度
BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH]; // MAC地址
DWORD Index;
UINT Type;
UINT DhcpEnabled;
PIP_ADDR_STRING CurrentIpAddress;
IP_ADDR_STRING IpAddressList; // IP地址列表
IP_ADDR_STRING GatewayList; // 网关地址列表
IP_ADDR_STRING DhcpServer; // DHCP地址
BOOL HaveWins;
IP_ADDR_STRING PrimaryWinsServer; // 首选WINS 服务器
IP_ADDR_STRING SecondaryWinsServer; // 备用WINS服务器
time_t LeaseObtained;
time_t LeaseExpires;
} IP_ADAPTER_INFO, *PIP_ADAPTER_INFO;
————————————————————以上是分割线————————————————————
于是,参照相应样例,写出如下代码:
#include
#include
#include
#pragma comment(lib, "IPHlpApi.Lib")
int main()
{
IP_ADAPTER_INFO IOInfo[20];
PIP_ADAPTER_INFO pIOInfo = NULL;
DWORD Result = 0;
unsigned long nLen = sizeof(IOInfo);
Result = GetAdaptersInfo(IOInfo, &nLen);
if (NO_ERROR != Result)
{
fprintf(stderr, "GetAdaptersInfo Error!\n");
return -1;
}
else
{
pIOInfo = IOInfo;
int Num = 0;
while (pIOInfo != NULL)
{
fprintf(stdout, "\n--------------------Num.%d-------------------------\n", Num);
printf("|Name:%s\n", pIOInfo->AdapterName);
printf("|Desc:%s\n", pIOInfo->Description);
printf("|IP:%s\n", pIOInfo->IpAddressList.IpAddress.String);
printf("|MAC:%02X:%02X:%02X:%02X:%02X:%02X\n", pIOInfo->Address[0], pIOInfo->Address[1], pIOInfo->Address[2],
pIOInfo->Address[3], pIOInfo->Address[4], pIOInfo->Address[5]);
printf("|GateWay:%s\n", pIOInfo->GatewayList.IpAddress.String);
printf("--------------------Num.%d-------------------------", Num++);
pIOInfo = pIOInfo->Next;
}
}
return 0;
}
运行结果如下:
我看到,运行程序确实区分出了VM的网卡和本地网卡;无奈本地网卡很有可能变,而本地网卡的Desc不足以填充足够的信息,从而为作为整个信息的区分。
我突然想到IPConfig命令,可以完全显示出网卡和网络的名称,于是,收启发,想进行IPConfig命令,然后获取信息,再获得IP地址。
在我的机器上运行ipconfig /all 命令,获得结果如下:
Windows IP 配置
主机名 . . . . . . . . . . . . . : USER-20151221DQ
主 DNS 后缀 . . . . . . . . . . . :
节点类型 . . . . . . . . . . . . : 混合
IP 路由已启用 . . . . . . . . . . : 否
WINS 代理已启用 . . . . . . . . . : 否
以太网适配器 本地连接 2:
连接特定的 DNS 后缀 . . . . . . . : localdomain
描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet8
物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-08
DHCP 已启用 . . . . . . . . . . . : 是
自动配置已启用. . . . . . . . . . : 是
本地链接 IPv6 地址. . . . . . . . : fe80::643d:e61d:7f3:fb4d%15(首选)
IPv4 地址 . . . . . . . . . . . . : 192.168.233.1(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
获得租约的时间 . . . . . . . . . : 2016年7月19日 9:26:32
租约过期的时间 . . . . . . . . . : 2016年7月19日 13:41:32
默认网关. . . . . . . . . . . . . :
DHCP 服务器 . . . . . . . . . . . : 192.168.233.254
DHCPv6 IAID . . . . . . . . . . . : 318787670
DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-1E-09-9D-1C-2C-41-38-8E-9C-F8
DNS 服务器 . . . . . . . . . . . : 192.168.233.2
主 WINS 服务器 . . . . . . . . . : 192.168.233.2
TCPIP 上的 NetBIOS . . . . . . . : 已启用
以太网适配器 VMware Network Adapter VMnet1:
连接特定的 DNS 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : VMware Virtual Ethernet Adapter for VMnet1
物理地址. . . . . . . . . . . . . : 00-50-56-C0-00-01
DHCP 已启用 . . . . . . . . . . . : 否
自动配置已启用. . . . . . . . . . : 是
本地链接 IPv6 地址. . . . . . . . : fe80::acef:74f3:abe2:7f54%13(首选)
IPv4 地址 . . . . . . . . . . . . : 192.168.254.1(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :
DHCPv6 IAID . . . . . . . . . . . : 302010454
DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-1E-09-9D-1C-2C-41-38-8E-9C-F8
DNS 服务器 . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
TCPIP 上的 NetBIOS . . . . . . . : 已启用
以太网适配器 本地连接:
连接特定的 DNS 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : Intel(R) 82579LM Gigabit Network Connection
物理地址. . . . . . . . . . . . . : 2C-41-38-8E-9C-F8
DHCP 已启用 . . . . . . . . . . . : 否
自动配置已启用. . . . . . . . . . : 是
本地链接 IPv6 地址. . . . . . . . : fe80::6822:7d71:8cda:2843%11(首选)
IPv4 地址 . . . . . . . . . . . . : 192.168.1.155(首选)
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.1.1
DHCPv6 IAID . . . . . . . . . . . : 237781304
DHCPv6 客户端 DUID . . . . . . . : 00-01-00-01-1E-09-9D-1C-2C-41-38-8E-9C-F8
DNS 服务器 . . . . . . . . . . . : fec0:0:0:ffff::1%1
fec0:0:0:ffff::2%1
fec0:0:0:ffff::3%1
TCPIP 上的 NetBIOS . . . . . . . : 已启用
隧道适配器 isatap.{A5D17715-BE7B-4932-9B76-1F96CFE71258}:
媒体状态 . . . . . . . . . . . . : 媒体已断开
连接特定的 DNS 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已启用 . . . . . . . . . . . : 否
自动配置已启用. . . . . . . . . . : 是
隧道适配器 isatap.{EFE41274-D6A2-40A2-B625-131EA423FCD6}:
媒体状态 . . . . . . . . . . . . : 媒体已断开
连接特定的 DNS 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #2
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已启用 . . . . . . . . . . . : 否
自动配置已启用. . . . . . . . . . : 是
隧道适配器 isatap.localdomain:
媒体状态 . . . . . . . . . . . . : 媒体已断开
连接特定的 DNS 后缀 . . . . . . . : localdomain
描述. . . . . . . . . . . . . . . : Microsoft ISATAP Adapter #3
物理地址. . . . . . . . . . . . . : 00-00-00-00-00-00-00-E0
DHCP 已启用 . . . . . . . . . . . : 否
自动配置已启用. . . . . . . . . . : 是
-E0
DHCP 已启用 . . . . .
通过这个,我可以清楚的看到,我的首个IP确实是命名为本地连接2 的 IP,而不是我的本地IP;但是这个上面的网络名称绝对不错,“本地连接”这个网络IP是:192.168.1.155
于是,用着执行这个ipconfig的方法的程序,发现,知道一个system函数。发现此函数可以运行命令,但是,显示不出来命令的回显。目前也找不到更好的方法
,于是,我想到的方法是,把命令回显存到相应文件里,然后,再进行读取后获得IP。
查询到一个函数GetCurrentDirectory,然后确保运行的程序创建的文件在相应的程序的路径下,可以进行灵活写入。
整个程序如下:
#include
#include
#include
#include
#include
//#include
#ifndef MAXTHPATH
#define MAXTHPATH (256)
#endif
#ifndef IPNUM
#define IPNUM (20)
#endif
#ifndef FILENAME
#define FILENAME ("/NetMask.txt")
#endif
char *getLocalIP(char *pszIP);
int main(int argc, char *argv[])
{
char pszIP[IPNUM] = {0};
getLocalIP(pszIP);
printf("本地IP为 %s\n", pszIP);
//delete []buf;
return 0;
}
char *getLocalIP(char *pszIP)
{
char pPath[MAXTHPATH];
GetCurrentDirectory(MAXTHPATH, pPath);
strcat(pPath, FILENAME);
char pszCmd[MAXTHPATH] = {0};
strcpy(pszCmd, "ipconfig /all > ");
strcpy(pszCmd + strlen(pszCmd), pPath);
//执行DOS命令,并把网络信息输出到文件夹下
system(pszCmd);
FILE *fp = fopen(pPath, "r");
fseek(fp, 0, SEEK_END);
int len = ftell(fp);
rewind(fp);
char *pBuf = new char[len+1];
memset(pBuf, 0, len +1);
fread(pBuf, 1, len, fp);
fclose(fp);
//根据一个机器获得,如果DOS命令变更,可能会发生更改
char *pLAN = strstr(pBuf, "本地连接:");
char *pIPV4 = strstr(pLAN, "IPv4 地址");
pIPV4 = strstr(pIPV4, ":");
pIPV4 += 2; //翻过“:”符号,获得真正的IP
char *pEnd = strstr(pIPV4, "(");
strncpy(pszIP, pIPV4, pEnd - pIPV4);
//避免内存泄露
delete []pBuf;
return pszIP;
}
运行结果如下:
显然,得到了正确的IP。
几个值得注意的事情是:
1)在Linux下,此命令应该改为ifconfig;
2)其他机器不知道相应的回显字符串格式是否正确,还有,机器的语言由中文换成英文后,是否能有正确的支持;
3)system函数无法把回显直接输出到程序里;
4)最后获得IP的过程,读取字符串可以用正则;正则匹配通用性更好,但在windows下不支持正则,要想进行支持,需要添加boost库进行支持
5)有没有更好的直接获取本地IP的方法,虽然此种方法可以获得多网卡,尤其是实际的物理多网卡的IP地址