Visual C++ 2017网络编程实战
上QQ阅读APP看书,第一时间看更新

5.3 socket地址

一个套接字代表通信的一端,每端都有一个套接字地址,这个socket地址包含了IP地址和端口信息。有了IP地址,就能从网络中识别对方主机,有了端口就能识别对方主机上的进程。

socket地址可以分为通用socket地址和专用socket地址。前者会出现在一些socket api函数中(比如bind函数、connect函数等),这个通用地址原来想用来表示大多数网络地址,但现在有点不方便使用了,因此现在很多网络协议都定义自己的专用网络地址,专用网络地址主要是为了方便使用而提出来的,两者通常可以相互转换。

5.3.1 通用socket地址

通用socket地址就是一个结构体,名字是sockaddr,定义在ws2def.h中,该结构体如下:

    // Structure used to store most addresses.
    typedef struct sockaddr {
    #if (_WIN32_WINNT < 0x0600)
        u_short sa_family;
    #else
        ADDRESS_FAMILY sa_family;           // Address family.
    #endif //(_WIN32_WINNT < 0x0600)
        CHAR sa_data[14];                  // Up to 14 bytes of direct address.
    } SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;

其中,sa_family是一个无符号短整型(u_short)或枚举ADDRESS_FAMILY类型的变量,用来存放地址簇(或协议簇)类型,常用取值如下:

·PF_UNIX:UNIX本地域协议簇。

·PF_INET:IPv4协议簇。

·PF_INET6:IPv6协议簇。

·AF_UNIX:UNIX本地域地址簇。

·AF_INET:IPv4地址簇。

·AF_INET6:IPv6地址簇。

sa_data用来存放具体的地址数据,即IP地址数据和端口数据。

由于sa_data只有14个字节,随着时代的发展,一些新的协议提出来了,比如IPv6,它的地址长度不够14字节了。不同协议簇的具体地址长度见表5-1。

表5-1

sa_data太小了,容纳不下了,咋办?Windows定义了新的通用的地址存储结构:

    typedef struct sockaddr_storage {
        ADDRESS_FAMILY ss_family;      // address family
        CHAR __ss_pad1[_SS_PAD1SIZE];  // 6 byte pad, this is to make
                                   //   implementation specific pad up to
                                   //   alignment field that follows explicit
                                   //   in the data structure
        __int64 __ss_align;           // Field to force desired structure
        CHAR __ss_pad2[_SS_PAD2SIZE];  // 112 byte pad to achieve desired size;
                                   //   _SS_MAXSIZE value minus size of
                                   //   ss_family, __ss_pad1, and
                                   //   __ss_align fields is 112
    } SOCKADDR_STORAGE_LH, *PSOCKADDR_STORAGE_LH, FAR *LPSOCKADDR_STORAGE_LH;

这个结构体存储的地址就大了,而且是内存对齐的,我们可以看到有__ss_align。

5.3.2 专用socket地址

上面两个通用地址结构把IP地址、端口等数据一股脑放到一个char数组中,使得使用起来特不方便。为此,Windows为不同的协议簇定义了不同的socket地址结构体,这些不同的socket地址被称为专用socket地址。比如,IPv4有自己专用的socket地址,IPv6有自己专用的socket地址。

IPv4的socket地址定义了下面的结构体:

    typedef struct sockaddr_in {
    #if(_WIN32_WINNT < 0x0600)
       short   sin_family;
    #else //(_WIN32_WINNT < 0x0600)
       ADDRESS_FAMILY sin_family; //地址簇,取AF_INET
    #endif //(_WIN32_WINNT < 0x0600)
       USHORT sin_port; //端口号,用网络字节序表示
       IN_ADDR sin_addr;        //IPv4地址结构,用网络字节序表示
       CHAR sin_zero[8];
    } SOCKADDR_IN, *PSOCKADDR_IN;

其中,类型IN_ADDR在inaddr.h中定义如下:

    // IPv4 Internet address
    // This is an 'on-wire' format structure.
    typedef struct in_addr {
            union {
                    struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                    struct { USHORT s_w1,s_w2; } S_un_w;
                    ULONG S_addr;
            } S_un;
    #define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
    #define s_host  S_un.S_un_b.s_b2    // host on imp
    #define s_net   S_un.S_un_b.s_b1    // network
    #define s_imp   S_un.S_un_w.s_w2    // imp
    #define s_impno S_un.S_un_b.s_b4     // imp #
    #define s_lh    S_un.S_un_b.s_b3    // logical host
    } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

其中,成员字段S_un用来存放实际的IP地址数据,是一个32位的联合体(联合体字段S_un_b有4个无符号char型数据,因此取值32位;联合体字段S_un_w有两个USHORT型数据,因此取值32位;联合体字段S_addr是ULONG型数据,因此取值也是32位)。

下面再来看一下IPv6的socket地址专用结构体:

    typedef struct sockaddr_in6 {
        ADDRESS_FAMILY sin6_family; // AF_INET6.
        USHORT sin6_port;         // Transport level port number.
        ULONG  sin6_flowinfo;     // IPv6 flow information.
        IN6_ADDR sin6_addr;       // IPv6 address.
        union {
            ULONG sin6_scope_id;   // Set of interfaces for a scope.
            SCOPE_ID sin6_scope_struct;
        };
    } SOCKADDR_IN6_LH, *PSOCKADDR_IN6_LH, FAR *LPSOCKADDR_IN6_LH;

其中,类型IN6_ADDR在in6addr.h中定义如下:

    // IPv6 Internet address (RFC 2553)
    // This is an 'on-wire' format structure.
    //
    typedef struct in6_addr {
        union {
            UCHAR       Byte[16];
            USHORT      Word[8];
        } u;
    } IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR;

这些专用的socket地址结构体显然比通用的socket地址更清楚,它把各个信息用不同的字段来表示。需要注意的是,socket API函数使用的是通用地址结构,因此我们具体使用的时候,最终要把专用地址结构转换为通用地址结构,不过可以强制转换。

5.3.3 IP地址的转换

IP地址转换是指将点分十进制形式的字符串IP地址与二进制IP地址进行相互转换。比如,“192.168.1.100”就是一个点分十进制形式的字符串IP地址。IP地址转换可以通过inet_aton、inet_addr和inet_ntoa这3个函数完成,这3个地址转换函数都只能处理IPv4地址,而不能处理IPv6地址。使用这些函数需要包含头文件Winsock2.h,并加入库Ws2_32.lib。

函数inet_addr将点分十进制IP地址转换为二进制地址,它返回的结果是网络字节序,该函数声明如下:

    unsigned long inet_addr(  const char* cp);

其中,参数cp指向点分十进制形式的字符串IP地址,如“172.16.2.6”。如果函数成功返回二进制形式的IP地址,类型是32位无符号整型,失败则返回一个常值INADDR_NONE(32位均为1)。通常失败的情况是参数cp所指的字符串IP地址不合法,比如“300.1000.1.1”(超过255了)。宏INADDR_NONE在ws2def.h中定义如下:

    #define INADDR_NONE  0xffffffff

下面我们再看看将结构体in_addr类型的IP地址转换为点分字符串IP地址的函数inet_ntoa。注意,这里说的是结构体in_addr类型,即inet_ntoa函数的参数类型是struct in_addr,而不是inet_addr返回的结果unsigned long类型,函数inet_ntoa声明如下:

    char* FAR inet_ntoa(struct  in_addr  in);

其中,in存放struct in_addr类型的IP地址。如果函数成功就返回字符串指针,此指针指向转换后的点分十进制IP地址,如果失败就返回NULL。

如果想要把inet_addr的结果再通过函数inet_ntoa转换为字符串形式,怎么办呢?重要的工作就是要将inet_addr返回的unsigned long类型转换为struct in_addr类型,可以这样:

    struct  in_addr  ia;
    unsigned long dwIP = inet_addr("172.16.2.6");
    ia.s_addr = dwIP;
    printf("real_ip=%s\n", inet_ntoa(ia));

s_addr就是S_un.S_addr(S_un.S_addr是ULONG类型的字段),因此可以把dwIP直接赋值给ia.s_addr,然后把ia传入inet_ntoa中,具体可以看下例。

【例5.1IP地址的字符串和二进制的互转

(1)打开VC2017,新建一个控制台工程test。

(2)在test.cpp中输入如下代码:

    #include "stdafx.h"

    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    #include <Winsock2.h>

    int main(int argc, const char * argv[])
    {
    struct in_addr ia;

    DWORD dwIP = inet_addr("172.16.2.6");
    ia.s_addr = dwIP;
    printf("ia.s_addr=0x%x\n", ia.s_addr);
    printf("real_ip=%s\n", inet_ntoa(ia));
    return 0;
    }

代码很简单,先把IP172.16.2.6通过函数inet_addr转换为二进制并保存于ia.s_addr中,然后以十六进制形式打印出来,接着通过函数inet_ntoa转换为点阵的字符串形式。

(3)在工程中加入Ws2_32.lib。

(4)保存工程并运行,运行结果如下:

    ia.s_addr=0x60210ac
    real_ip=172.16.2.6

5.3.4 获取套接字地址

一个套接字绑定了地址就可以通过函数来获取它的套接字地址了。套接字通信需要本地和远程两端建立套接字,这样获取套接字地址可以分为获取本地套接字地址和获取远程套接字地址。其中,获取本地套接字地址的函数是getsockname,这个函数在下面两种情况下可以获得本地套接字地址:

(1)本地套接字通过bind函数绑定了地址(bind函数在下一节会讲到)。

(2)本地套接字没有绑定地址,但通过connect函数和远程建立了连接,此时内核会分配一个地址给本地套接字。

getsockname函数声明如下:

    int getsockname(SOCKET s,struct sockaddr* name,int* namelen);

其中,参数s是套接字描述符;name为指向存放套接字地址的结构体指针;namelen是name所指结构体的大小。

【例5.2】绑定后获取本地套接字地址

(1)打开VC2017,新建一个控制台工程test。在test.cpp中输入如下代码:

    #include "stdafx.h"
    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    #include <Winsock2.h>

    int main()
    {
    int sfp;
    struct sockaddr_in s_add;
    unsigned short portnum = 10051;
    struct sockaddr_in serv = { 0 };
    char on = 1;

    int serv_len = sizeof(serv);

    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号

    err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
    if (err != 0) return 0;

    sfp = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sfp)
    {
         printf("socket fail ! \r\n");
         return -1;
    }
    printf("socket ok !\r\n");
    //马上获取
    printf("ip=%s,port=%d\r\n", inet_ntoa(serv.sin_addr), ntohs(serv.sin_port));

    setsockopt(sfp,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));//允许地址的立即重用
    memset(&s_add, 0,sizeof(struct sockaddr_in));
    s_add.sin_family = AF_INET;
    s_add.sin_addr.s_addr = inet_addr("192.168.0.2"); //这个IP地址必须是本机上有的
    s_add.sin_port = htons(portnum);
     //绑定
    if (-1 == bind(sfp, (struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
    {
         printf("bind fail:%d!\r\n", errno);
         return -1;
    }
    printf("bind ok !\r\n");
    getsockname(sfp, (struct sockaddr *)&serv, &serv_len); //获取本地套接字地址
    //打印套接字地址里的IP和端口值
    printf("ip=%s,port=%d\r\n", inet_ntoa(serv.sin_addr), ntohs(serv.sin_port));

    WSACleanup(); //释放套接字库
    return 0;
    }

在上述代码中,我们首先创建了套接字,马上获取它的地址信息,然后绑定了IP和端口号,再去获取套接字地址。

(2)保存工程并运行,运行结果如下:

    socket ok !
    ip=0.0.0.0,port=0
    bind ok !
    ip=192.168.0.2,port=10051

可以看到没有绑定IP和端口号前获取到的都是0,绑定后就可以正确获取到地址信息了。

需要注意的是,192.168.0.2必须是本机上存在的IP地址,如果随便乱设一个并不存在的IP地址,程序会返回错误。大家可以修改一个并不存在的IP(比如0.0.0.0)地址后编译运行,应该会出现下面的结果:

    socket ok !
    ip=0.0.0.0,port=0
    bind fail:99!