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

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 : %02X-%02X-%02X-%02X-%02X-%02X \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函数的用法。

时间: 2024-09-20 18:24:56

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

《Windows网络与通信程序设计(第3版)》——1.4 网络应用程序设计基础

1.4 网络应用程序设计基础 本节讲述网络应用程序设计的原则和网络程序开发环境的设置. 1.4.1 网络程序体系结构在创建网络应用程序之前,首先要决定应用程序的体系结构.应用程序体系结构(application architecture)由应用程序开发者设计,它指定了在各种各样的终端系统上,应用程序是如何组织的.本节介绍现有的主要体系结构:客户机/服务器体系结构.P2P体系结构和这两种结构的混合. 1.客户机/服务器体系结构在客户机/服务器体系结构中,有一个总是在运行的主机,称为服务器,它为来自

《Windows网络与通信程序设计(第3版)》——2.3 Winsock编程详解

2.3 Winsock编程详解 使用TCP创建网络应用程序稍微复杂一些,因为TCP是面向连接的协议,需要通信双方首先建立一个连接.本节先以建立简单的TCP客户端和服务器端应用程序为例,详细说明Winsock的编程流程,然后再介绍较为简单的UDP编程. 2.3.1 Winsock编程流程 使用Winsock编程的一般步骤是比较固定的,可以结合后面的例子程序来理解它们. 1.套接字的创建和关闭 使用套接字之前,必须调用socket函数创建一个套接字对象,此函数调用成功将返回套接字句柄. SOCKET

《Windows网络与通信程序设计(第3版)》——第2章 Winsock编程接口2.1 Winsock库

第2章 Winsock编程接口 Winsock是Windows下网络编程的标准接口,它允许两个或多个应用程序在相同机器上,或者是通过网络相互交流.Winsock是真正的协议无关的接口,本章主要讲述如何使用它来编写应用层的网络应用程序. 2.1 Winsock库 Winsock库有两个版本,Winsock1和Winsock2.现在开发网络应用程序都使用Winsock2,需要在程序中包含头文件winsock2.h,它包含了绝大部分socket函数和相关结构类型的声明和定义.同时要添加的还有到WS2_

《Windows网络与通信程序设计(第3版)》——1.2 计算机网络参考模型

1.2 计算机网络参考模型 了解网络的相关概念之后,本节将讨论计算机网络中主机之间是如何进行通信的,以及各种通信协议之间的关系等. 1.2.1 协议层次为了降低设计难度,大部分网络都以层(layer或level)的形式组织在一起,每一层都建立在它的下层之上,使用它的下层提供的服务,下层对它的上层隐藏了服务实现的细节.这种方法几乎应用于整个计算机科学领域,也可以称为信息隐藏.数据类型抽象.数据封装.面向对象编程等. 一个机器上的第n层和另一个机器的第n层交流,所使用的规则和协定合起来称为第n层协议

《Windows网络与通信程序设计(第3版)》——第1章 计算机网络基础1.1 网络的概念和网络的组成

第1章 计算机网络基础 本章详细讲述网络程序设计中要用到的计算机网络方面的基础知识,包括各种网络术语.网络硬件设备.网络拓扑结构.网络协议等. 1.1 网络的概念和网络的组成 网络是各种连在一起的可以相互通信的设备的集合.本书讲述的网络是最常见的,将数亿计算机连接到一起的Internet.下面通过讲述组成Internet的基本硬件和软件来进一步明确计算机网络的概念. Internet是世界范围内的计算机网络,它不仅连接了PC.存储和传输信息的服务器,还连接了PDA.电视.移动PC等.所有的这些设

《Windows网络与通信程序设计(第3版)》——2.4 网络对时程序实例

2.4 网络对时程序实例 网络对时也就是从Internet上获得准确的时间,以此来校对本地计算机时钟.通过这样一个实例程序,大家可以初步了解协议和Winsock函数的具体应用. ** 2.4.1 时间协议(Time Protocol)** Time Protocol (RFC-868)是一种非常简单的应用层协议.它返回一个未格式化的32位二进制数字,这个数字描述了从1900年1月1日午夜到现在的秒数.服务器在端口37监听时间协议请求,以TCP/IP或者UDP/IP格式返回响应.将服务器的返回值转

《Windows网络与通信程序设计(第3版)》——1.3 网络程序寻址方式

1.3 网络程序寻址方式 编写网络程序,必须要有一种机制来标识通信的双方.本节详细讨论Internet中各层的寻址方式,以及相关的寻址协议. 1.3.1 MAC地址网络通信的最边缘便是LAN了,我们先来看看在LAN中是如何寻址的. 1.MAC子层和MAC地址LAN主要使用广播通信.在其内部,许多主机连在相同的通信通道上,通信时的关键问题是当竞争存在时如何决定谁使用通道.解决此问题的协议属于链路层的子层,称为MAC(Medium Access Control,介质访问控制)子层.MAC子层在LAN

《OSPF网络设计解决方案(第2版)》一1.2 理解OSI参考模型的7层

1.2 理解OSI参考模型的7层 OSPF网络设计解决方案(第2版) OSI参考模型中的7个层面,可以被划分为两类:上层层面和下层层面.上层层面通常只关注应用,而下层层面主要处理数据传输.本节将分别介绍3个上层层面.4个下层层面,以及它们各自的功能. 1.2.1 上层层面 OSI参考模型的上层层面(upper layer)--5.6及7层--关注与应用相关的问题,它们通常只在软件程序中被参考实施.应用层位于最上层,且最靠近终端用户.如果用户或应用进程需要进行网络通信,那么它们将直接与包含通信组件

Windows网络工具包NETSH常见问题

NETSH是Windows网络工具包中最强大的工具之一.下面介绍NETSH在各种情况下的一些优秀用法,说明如何简化网络配置.管理和文件资料. 1:NETSH是什么? NETSH是Windows 2000和Windows Server 2003中最强大但不太为人所知的一个工具.它被默认安装,位于系统根目录的system32文件夹内 .Windows XP中同样含有NETSH工具. NETSH帮助你显示.修改.输入和输出系统网络参数的许多元素.它还能通过远程机器参数(-r)远程连接其它系统. 2:N