《Windows网络与通信程序设计(第3版)》——2.2 Winsock的寻址方式和字节顺序...

    xiaoxiao2024-05-25  114

    本节书摘来自异步社区《Windows网络与通信程序设计(第3版)》一书中的第2章,第2.2节,作者: 陈香凝 , 王烨阳 , 陈婷婷 , 张铮 更多章节内容可以访问云栖社区“异步社区”公众号查看。

    2.2 Winsock的寻址方式和字节顺序

    本节讲述在Winsock中主机地址信息的表示方法,以及相关的操作函数。

    2.2.1 Winsock寻址因为Winsock要兼容多个协议,所以必须使用通用的寻址方式。TCP/IP使用IP地址和端口号来指定一个地址,但是其他协议也许采用不同的形式。如果Winsock强迫使用特定的寻址方式,添加其他协议就不大可能了。Winsock的第一个版本使用sockaddr结构来解决此问题。

    struct sockaddr { u_short sa_family; char sa_data[14]; };

    在这个结构中,第一个成员sa_family指定了这个地址使用的地址家族。sa_data成员存储的数据在不同的地址家族中可能不同。本书仅仅使用Internet地址家族(TCP/IP),Winsock已经定义了sockaddr结构的TCP/IP版本——sockaddr_in结构。它们本质上是相同的结构,但是第2个更容易操作。

    在Winsock中,应用程序通过SOCKADDR_IN结构来指定IP地址和端口号,定义如下。

    struct sockaddr_in { short sin_family; // 地址家族(即指定地址格式),应为AF_INET u_short sin_port; // 端口号 struct in_addr sin_addr; // IP地址 char sin_zero[8]; // 空字节,要设为0 };

    (1)sin_family域必须设为AF_INET,它告诉Winsock程序使用的是IP地址家族。

    (2)sin_port域指定了TCP或UDP通信服务的端口号。应用程序在选择端口号时必须小心,因为有一些端口号是保留给公共服务使用的,如FTP和HTTP。基本上,端口号可分成如下3个范围:公共的、注册的、动态的(或私有的)。

    0~1 023由IANA(Internet Assigned Numbers Authority)管理,保留为公共的服务使用。1 024~49 151是普通用户注册的端口号,由IANA列出。49 152~65 535是动态和/或私有的端口号。普通用户应用程序应该选择1 024~49 151的注册了的端口号,以避免使用了一个其他应用程序或者系统服务已经使用的端口号。在49 152~65 535之间的端口号也可以自由地使用,因为没有服务注册这些端口号。

    (3)sin_addr域用来存储IP地址(32位),它被定义为一个联合来处理整个32位的值,两个16位部分或者每个字节单独分开。描述32位IP地址的in_addr结构定义如下。

    struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; // 以4个u_char来描述 struct { u_short s_w1,s_w2; } S_un_w; // 以2个u_short来描述 u_long S_addr; // 以1个u_long来描述 } S_un; };

    用字符串“aa.bb.cc.dd”表示IP地址时,字符串中由点分开的4个域是以字符串的形式对in_addr结构中的4个u_char值的描述。由于每个字节的数值范围是0~255,所以各域的值都不可超过255。

    (4)最后一个域sin_zero没有使用,是为了与SOCKADDR结构大小相同才设置的。

    应用程序可以使用inet_addr函数将一个由小数点分隔的十进制IP地址字符串转化成由32位二进制数表示的IP地址。inet_ntoa是inet_addr函数的逆函数,它将一个网络字节顺序的32位IP地址转化成字符串。

    unsigned long inet_addr(const char* cp); // 将一个"aa.bb.cc.dd"类型的IP地址字符串转化为32位的二进制数 char * inet_ntoa (struct in_addr in); // 将32位的二进制数转化为字符串

    注意,inet_addr返回的32位二进制数是用网络顺序存储的,下一小节详细讲述字节顺序。

    2.2.2 字节顺序字节顺序是长度跨越多个字节的数据被存储的顺序。例如,一个32位的长整型0x12345678跨越4个字节(每个字节8位)。Intel x86机器使用小尾顺序(little-endian),意思是最不重要的字节首先存储。因此,数据0x12345678在内存中的存放顺序是0x78、0x56、0x34、0x12。大多数不使用小尾顺序的机器使用大尾顺序(big-endian),即最重要的字节首先存储。同样的值在内存中的存放顺序将是0x12、0x34、0x56、0x78。因为协议数据要在这些机器间传输,所以就必须选定其中的一种方式做为标准,否则会引起混淆。

    TCP/IP统一规定使用大尾方式传输数据,也称为网络字节顺序。例如,端口号(它是一个16位的数字)12345(0x3039)的存储顺序是0x30、0x39。32位的IP地址也是以这种方式存储的,IP地址的4部分存储在4个字节中,第一部分存储在第一个字节中。

    上述sockaddr和sockaddr_in结构中,除了sin_family成员(它不是协议的一部分)外,其他所有值必须以网络字节顺序存储。Winsock提供了一些函数来处理本地机器的字节顺序和网络字节顺序的转换。

    u_short htons(u_short hostshort); // 将u_short类型变量从主机字节顺序转化到TCP/IP网络字节顺序 u_long htonl(u_long hostlong); // 将u_long类型变量从主机字节顺序转化到TCP/IP网络字节顺序 u_short ntohs(u_short netshort); // 将u_short类型变量从TCP/IP网络字节顺序转化到主机字节顺序 u_long ntohl(u_long netlong); // 将u_long类型变量从TCP/IP网络字节顺序转化到主机字节顺序

    这些API是平台无关的。使用它们可以保证程序正确地运行在所有机器上。

    下面代码示例了如何初始化sockaddr_in结构。

    sockaddr_in sockAddr; // 设置地址家族 sockAddr.sin_family = AF_INET; // 转化端口号6789到网络字节顺序,并安排它到正确的成员 sockAddr.sin_port = htons(6789); // inet_addr函数转化一个"aa.bb.cc.dd"类型的IP地址字符串到长整型 // 它是以网络字节顺序记录的IP地址 sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 也可以用下面的代码设置IP地址(通过设置4个字节部分,设置sockAddr的地址) /* sockAddr.sin_addr.S_un.S_un_b.s_b1 = 127; sockAddr.sin_addr.S_un.S_un_b.s_b2 = 0; sockAddr.sin_addr.S_un.S_un_b.s_b3 = 0; sockAddr.sin_addr.S_un.S_un_b.s_b4 = 1; */

    2.2.3 获取地址信息通常,主机上的接口被静态地指定一个IP地址,或者是由配置协议来分配,如动态主机配置协议(DHCP)。如果DHCP服务器不能到达,系统会使用Automatic Private IP Addressing (APIPA)自动分配169.254.0.0/16范围内的地址。

    1.获取本机IP地址获取本机的IP地址比较简单,下面的GetAllIps例子打印出了本机使用的所有IP(一个适配器一个IP地址),程序代码如下。

    #include "../common/InitSock.h" // GetAllIps工程 #include <stdio.h> CInitSock initSock; // 初始化Winsock库 void main() { char szHost[256]; // 取得本地主机名称 ::gethostname(szHost, 256); // 通过主机名得到地址信息 hostent *pHost = ::gethostbyname(szHost); // 打印出所有IP地址 in_addr addr; for(int i = 0; ; i++) { char *p = pHost->h_addr_list[i]; // p指向一个32位的IP地址 if(p == NULL) break; memcpy(&addr.S_un.S_addr, p, pHost->h_length); char *szIp = ::inet_ntoa(addr); printf(" 本机IP地址:%s \n ", szIp); } }

    GetAllIps先调用gethostname取得本地主机的名称,然后通过主机名得到其地址信息。

    2.获取MAC地址有时为了检测网络,或者为了一些其他特殊的目的,需要自己来直接操作原始数据帧(第9章再具体讲述),这就需要获取自己和LAN中其他主机的MAC地址。

    获取本地机器的MAC地址很容易,使用帮助函数GetAdaptersInfo即可。此函数的作用是获取本地机器的适配器信息,用法如下。

    DWORD GetAdaptersInfo( PIP_ADAPTER_INFO pAdapterInfo, // 指向一个缓冲区,用来取得IP_ADAPTER_INFO结构的列表 PULONG pOutBufLen // 用来指定上面缓冲区的大小。如果大小不够,此参数返回所需大小 ); // 函数调用成功返回 ERROR_SUCCESS IP_ADAPTER_INFO结构包含了本地计算机上网络适配器的信息,定义如下。 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地址的长度(应为6个字节) BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH]; // MAC地址 DWORD Index; // 适配器索引 UINT Type; // 适配器类型,如MIB_IF_TYPE_ETHERNET等 UINT DhcpEnabled; // 指定此适配器是否使DHCP(动态主机配置)协议有效了 PIP_ADDR_STRING CurrentIpAddress; // 保留字段 IP_ADDR_STRING IpAddressList; // 与此适配器相关的IP地址列表 IP_ADDR_STRING GatewayList; // 网关地址列表 IP_ADDR_STRING DhcpServer; // HDCP服务器 BOOL HaveWins; // 指定此适配器是否使用WINS(Windows Internet名称服务) IP_ADDR_STRING PrimaryWinsServer; // WINS服务器的主IP地址 IP_ADDR_STRING SecondaryWinsServer; // WINS服务器的第二IP地址 time_t LeaseObtained; // 获取当前DHCP租用的时间 time_t LeaseExpires; // 当前DHCP租用期满的时间 } IP_ADAPTER_INFO, *PIP_ADAPTER_INFO;

    下面的例子LocalHostInfo打印出了本机的IP地址、网络(内部LAN)的子网掩码、网关的IP地址和本机的MAC地址。本书第9章讲述网络扫描与检测时还要使用本例中的代码。

    include // 完整代码在配套光盘的LocalHostInfo工程下

    #include <stdio.h> #include "Iphlpapi.h" // 包含了对IP帮助函数的定义 #pragma comment(lib, "Iphlpapi.lib") #pragma comment(lib, "WS2_32.lib") // 全局数据 u_char g_ucLocalMac[6]; // 本地MAC地址 DWORD g_dwGatewayIP; // 网关IP地址 DWORD g_dwLocalIP; // 本地IP地址 DWORD g_dwMask; // 子网掩码 BOOL GetGlobalData() { PIP_ADAPTER_INFO pAdapterInfo = NULL; ULONG ulLen = 0; // 为适配器结构申请内存 ::GetAdaptersInfo(pAdapterInfo,&ulLen); pAdapterInfo = (PIP_ADAPTER_INFO)::GlobalAlloc(GPTR, ulLen); // 取得本地适配器结构信息 if(::GetAdaptersInfo(pAdapterInfo,&ulLen) == ERROR_SUCCESS) { if(pAdapterInfo != NULL) { memcpy(g_ucLocalMac, pAdapterInfo->Address, 6); g_dwGatewayIP = ::inet_addr(pAdapterInfo->GatewayList.IpAddress.String); g_dwLocalIP = ::inet_addr(pAdapterInfo->IpAddressList.IpAddress.String); g_dwMask = ::inet_addr(pAdapterInfo->IpAddressList.IpMask.String); } } printf(" \n -------------------- 本地主机信息 -----------------------\n\n"); in_addr in; in.S_un.S_addr = g_dwLocalIP; printf(" IP Address : %s \n", ::inet_ntoa(in)); in.S_un.S_addr = g_dwMask; printf(" Subnet Mask : %s \n", ::inet_ntoa(in)); in.S_un.S_addr = g_dwGatewayIP; printf(" Default Gateway : %s \n", ::inet_ntoa(in)); u_char *p = g_ucLocalMac; printf(" MAC Address : X-X-X-X-X-X \n", p[0], p[1], p[2], p[3], p[4], p[5]); printf(" \n \n "); return TRUE; }

    调用自定义函数GetGlobalData之后,程序运行结果如图2.1所示。

    要取得LAN中其他主机的MAC地址,最简单的方法是使用SendARP函数向目标主机发送ARP请求封包。这个函数返回指定的目的IP地址对应的物理地址,即MAC地址。第9章再详细讨论ARP,以及SendARP函数的用法。

    最新回复(0)