• windows网络编程第二版 第三章 Internet Protocol 读书笔记


    1. 本章主要讲述IP方面的东西,解释了IPv4, IPv6。在后面的两个章节中,讲述了地址和名字的解析(Address and Name Resolution),以及如何书写一个IPv4, IPv6自适应的程序。

    2. 简单摘录一下IPv4一节的内容:

    (1) 可以拿来做私有地址的IP有:

    10.0.0.0?10.255.255.255 (10.0.0.0/8)
    172.16.0.0?172.31.255.255 (172.16.0.0/12)
    192.168.0.0?192.168.255.255 (192.168.0.0/16)

    在书写IP段的时候,经常有/16, /24这样的写法。这表示掩码,/16就表示前16各bit都是1,也就是255.255.0.0。

    (2) 如果想在程序中获得本机的网卡和IP地址的配置的话,要使用WSAIoctl函数,配合SIO_ADDRESS_LIST_QUERY命令。在第七章和第16章有介绍。

    (3) IP地址的配置有DHCP和手动配置两种,如果配置了DHCP,但是DHCP服务器无法reach的话,在超时后,系统会给网卡赋一个 169.254.0.0/16这个区段内的地址。这依据的是APIPA协议(Automatic Private IP Address)

    (4) IPv4 Management Protocols. IPv4协议还需要很多其他协议的支撑,最常见的三种协议是ARP, ICMP, IGMP. IGMP不太熟悉,介绍一下。IGMP(Internet Group Management Protocol)是用于多播的。当一台机器上的某个应用想要加入到一个多播group的时候,它就发出IGMP membership reports,这个消息会通知路由器,这样路由器就会将这个请求记录下来,当以后有多播信息发出的时候,路由器就会把多播的信息转发到这个多播 group中的每个成员了。第九章会详细讨论多播。


    3. IPv6。本节没有看。

    4. Address and Name Resolution. 本节主要介绍两个函数:getaddrinfo, getnameinfo。getaddrinfo函数主要用于将我们给定的IP地址/主机名、端口转换成一个SOCKADDR的结构,也就是本书中经常提 到的二进制的Addressing。getnameinfo和getaddrinfo正好相反,getnameinfo是给定一个SOCKADDR的实 例,然后生成IP地址/主机名和端口信息。

    这里需要解释一下为什么要用这两个函数,因为我们在第一章的时候已经看到,我们可以手动申 请一个sockaddr_in的结构,然后在里面填入IP地址/主机名和端口这些信息,然后传给connect,sendto,bind这些需要 Addressing的函数。理由有这么几个:

    (1) 使用这两个函数,可以自适应IPv4和IPv6,而以前用sockaddr_in是针对IPv4的,要支持IPv6,还需要另外再写代码。比如,用这两个 函数,当用户输入程序连接的主机名和端口的时候,由于我们不知道用户输入的主机名对应的IP是IPv4的,还是IPv6的,还是这台主机v4, v6的地址都有,所以以往我们的代码要自己来适应这种情况,用这两个函数,代码就可以自适应

    (2) 使用这两个函数不用关心主机次序,网络次序这些东西。也就是说,传统的函数比如inet_addr, gethostbyname这些函数都可以不用写了。

    (3) 用这两个函数,代码其实更好理解了。我们只需要传入IP地址/主机名,端口这些信息,然后用getaddrinfo,通过设定不同的hint,就可以得到 addrinfo这个结构,这个结构中的东西既可以拿来创建socket,调用bind,connect,sendto等。


    所以我们应该尽量用这两个函数来操作有关Addressing方面的事宜,以前用的inet_addr, gethostbyname, gethostbyaddr这样的代码都应该被重写,之所以在winsock中还保留了这些函数,是为了和旧代码兼容。

    5. OK,现在来看这两个函数。首先要申明,这两个函数定义在WS2TCPIP.H中,但是这仅仅是WINXP中是这样,在其他支持WINSOCK 2的windows系统中,要使用这两个函数,还需要在include WS2TCPIP.H的前面再include WSPIAPI.H(主要要include在WS2TCPIP.H的前面哦)。

    Code: Select all
    int getaddrinfo(
          const char FAR *nodename,
          const char FAR *servname,
          const struct addrinfo FAR *hints,
          struct addrinfo FAR *FAR *res
    );


    nodename -- 主机名或IP地址
    servname -- service name, 其实就是指定端口,或者填写ftp这样的字符串也可以。在Windows NT这样的系统中,在%WINDOWS%/system32/drivers/etc目录下有一个services文件,里面填写了端口和service 的对应关系。
    hints -- 一个指向addrinfo结构的指针。
    res -- 返回的结果数据,不过这个数据可能是个数组,根据hints中填写的内容不同,函数可能会返回多个SOCKADDR的数据。

    getaddrinfo执行成功返回0,返回不是0就是出错,此时的返回值就是出错码(不需要用WSAGetLastError)

    所以,关键就是hints的填法,addrinfo结构如下:

    Code: Select all
    struct addrinfo {
          int      ai_flags;
          int      ai_family;
          int      ai_socktype;
          int      ai_protocol;
          size_t      ai_addrlen;
          char      *ai_canonname;
          struct sockaddr *ai_addr;
          struct addrinfo *ai_next;
    };


    ai_flags -- 只能取下列三个值中的一个:AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST. AI_CANONNAME表示getaddrinfo函数中,nodename一项填写的是主机名,例如 www.microsoft.com;AI_NUMERICHOST表示getaddrinfo函数中,nodename填写的是一个IP地 址;AI_PASSIVE后面会讲,主要是给bind用的

    ai_family -- AF_INET, AF_INET6, AF_UNSPEC。如果填写AF_UNSPEC,则getaddrinfo可能会返回一个IPv4的SOCKADDR,或IPV6的SOCKADDR, 或者两者都返回(所以res是一个数组结构了),关键看主机是不是支持IPv6

    ai_socktype -- 填写socket type,比如SOCK_DGRAM, SOCK_STREAM。当getaddrinfo函数中servname一项填写的是一个服务的名字而不是端口数字的时候,根据这一项的不同,则返回不 同的端口,因为我们知道有些服务可以使用TCP,也可以使用UDP,他们的端口是不一样的

    ai_protocol -- 指定protocol,比如IPPROTO_TCP,一样的,当getaddrinfo中填写的servname是一个service的名字的时候起作用。

    ai_next -- 当返回多个addressing信息的时候,这是指向下一个addrinfo的指针。注意getaddrinfo函数的返回res也是一个指向addrinfo结构数组的指针

    ai_addr -- 返回的sockaddr信息

    如果我们在调用getaddrinfo的时候,hint没有设定,那么,getaddrinfo就认为是一个空的hint structure被传入,而且ai_family一项是设定成AF_UNSPEC的。

    下面来看代码例子:

    Code: Select all
    SOCKET            s;
    struct addrinfo      hints,
                *result;
    int            rc;

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_CANONNAME;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    rc = getaddrinfo("foobar", "5001", &hints, &result);
    if (rc != 0) {
          // unable to resolve the name
    }
    s = socket(result->ai_family, result->ai_socktype,
    result->ai_protocol);
    if (s == INVALID_SOCKET) {
          // socket API failed
    }
    rc = connect(s, result->ai_addr, result->ai_addrlen);
    if (rc == SOCKET_ERROR) {
          // connect API failed
    }
    freeaddrinfo(result);


    上面的代码中,几个注意点:

    (1) 上面的代码尝试连接foobar这台机器的5001端口。这里我们可以看到,我们不关心foobar这台机器是IPv4的还是IPv6的,这完全看网络中 foobar这个名字解析到什么机器上。如果我们只想连接IPv4的foobar这台机器的话,那么我们在设置hint这个结构的时候,就可以把 ai_family设成AF_INET。

    (2) 千万注意,由于getaddrinfo返回的result,是动态分配的,所以我们在用完之后一定要记得调用freeaddrinfo函数来释放。

    上面的例子中,我们尝试连接foobar这台机器,我们也可以尝试连接一个IP地址(可以是IPv4的,也可以是IPv6的):

    Code: Select all
    struct addrinfo      hints,
                     *result;
    int            rc;

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_NUMERICHOST;
    hints.ai_family = AI_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    rc = getaddrinfo("172.17.7.1", "5001", &hints, &result);
    if (rc != 0) {
          // invalid literal address
    }
    // Use the result
    freeaddrinfo(result);


    例子很简单。如果我们在调用getaddrinfo函数的时候没有给hint,那么我们可以在返回的结果数据中(也就是addrinfo结构)查看flags的值,根据这个值来判断返回的sockaddr结构中是主机名还是IP地址

    在 addrinfo的flags中,我们还有一个没有介绍,就是AI_PASSIVE,这个flag是用来取得bind函数所需要的信息的。对于IPv4来 说,bind需要的是INADDR_ANY(0.0.0.0),对于IPv6来说,bind需要的是IN6ADDR_ANY(::)。OK,所以我们在调 用getaddrinfo的时候,nodename设成NULL,servname设成我们需要绑定的端口或服务名,在hint中,要设定 ai_family,是IPv4的还是IPv6的,或者干脆设成AF_UNSPEC,此时getaddrinfo就会把两个版本的信息都返回出来。

    6. getnameinfo:
    Code: Select all
    int getnameinfo(
          const struct sockaddr FAR *sa,
          socklen_t salen,
          char FAR *host,
          DWORD hostlen,
          char FAR *serv,
          DWORD servlen,
          int flags
    );


    这个函数是给定sockaddr数据,返回hostname和servname。参数很好理解,sa是给定的sockaddr信息,host,serv就是hostname和端口,hostname是FQDN的。最后的一个flags,他的取值如下:

    NI_NOFQDN -- 返回的hostname不带域名
    NI_NUMERICHOST -- 返回IP地址而不是主机名
    NI_NAMEREQD -- 如果sockaddr不能解析出FQDN的hostname,则返回失败
    NI_NUMERICSERV -- 返回数字端口,而不是一个service name。注意,如果我们不指定这个flag时,如果端口无法被解析成一个service name,那么函数会返回错误WSANO_DATA
    NI_DGRAM -- 用来区分datagram service和stream services

    7. Simple Address Conversion.
    Code: Select all
    INT WSAStringToAddress(
          LPTSTR AddressString,
          INT AddressFamily,
          LPWSAPROTOCOL_INFO lpProtocolInfo,
          LPSOCKADDR lpAddress,
          LPINT lpAddressLength
    );

    INT WSAAddressToString(
          LPSOCKADDR lpsaAddress,
          DWORD dwAddressLength,
          LPWSAPROTOCOL_INFO lpProtocolInfo,
          LPTSTR lpszAddressString,
          LPDWORD lpdwAddressStringLength
    );


    这 两个函数是单纯用来做IP地址和Addressing信息转换的。也就是说,只能从一个IP地址+端口转换成一个sockaddr或者是反过来转换。比如 WSAStringToAddress能接受类似"192.168.0.1:1200"这样的字符串,然后转换成addressing数据。而 且,WSAStringToAddress也没有getaddrinfo函数那么聪明,他必须指定IP地址是IPv4 的还是IPv6的。

    8. 传统的IPv4的处理address和name的函数。

    inet_addr -- 把一个IPv4的地址转换成网络次序的32位long型数
    inet_ntoa -- 把一个long型的网络次序的数转换成一个IPv4地址

    gethostbyname, WSAAsyncGetHostByName, gethostbyaddr, WSAAsyncGetHostByAddr,这些函数具体看书中的描述吧,也可以看MSDN。 WSAAsyncGetXXX这样的函数挺有意思,是异步的,在调用这个函数的时候需要给定一个buffer(这个buffer中将来会被函数填入我们想 要的东西),此外还要给定一个hwnd和msg,这样当函数完成的时候,会给指定的hwnd窗口发送msg的消息,从而我们就可以处理了。


    9. Writing IP Version-Independent Programs.

    本 节就是对上一节的getaddrinfo,getnameinfo函数的一个代码示例。在代码中,还有一点没有说道的就是,由于使用了这两个函数,我们不 需要care IPv4,IPv6这些东西了,而且我们也无需自己手动申明一个SOCKADDR_IN, SOCKADDR_IN6这样的结构变量了,因为getaddrinfo这个函数返回的addrinfo结构中,就有sockaddr的变量,直接拿来用 就行了。如果我们一定要手动申明SOCKADDR类型的变量的话,那也不要用SOCKADDR_IN, SOCKADDR_IN6这样的结构,而是要用SOCKADDR_STORAGE这个结构,这个结构被设计成能和任何协议的SOCKADDR结构兼容,用 这个结构,能保证我们写出来的程序不绑定在特定的网络协议上。此外,在使用bind等函数的时候用到的地址常量,在winsock的头文件中都有常量定 义,不需要手动hardcode。这里书中写了一个IPv6的程序的例子,用到了上一节中WSAStringToAddress这个简易函数来示范书写 IP版本无关的程序:

    Code: Select all
    SOCKADDR_STORAGE            saDestination;
    SOCKET               s;
    int               addrlen,
                   rc;

    s = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
    if (s == INVALID_SOCKET) {
          // socket failed
    }
    addrlen = sizeof(saDestination);
    rc = WSAStringToAddress(
             "3ffe:2900:d005:f28d:250:8bff:fea0:92ed",
             AF_INET6,
             NULL,
             (SOCKADDR *)&saDestination,
             &addrlen
             );
    if (rc == SOCKET_ERROR) {
          // conversion failed
    }
    rc = connect(s, (SOCKADDR *)&saDestination, sizeof(saDestination));
    if (rc == SOCKET_ERROR) {
          // connect failed
    }


    程序不难理解,下面我们来看以前写过的TCP的程序,这次我们用getaddrinfo函数来把Client和Server端的程序都改写成IP版本无关的代码。首先来看Client端的代码:

    Code: Select all
    SOCKET             s;
    struct addrinfo hints,
                   *res=NULL
    char         *szRemoteAddress=NULL,
                   *szRemotePort=NULL;
    int         rc;

    // Parse the command line to obtain the remote server's
    // hostname or address along with the port number, which are contained
    // in szRemoteAddress and szRemotePort.
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    // first resolve assuming string is a string literal address
    rc = getaddrinfo(
             szRemoteAddress,
             szRemotePort,
               &hints,
               &res
             );
    if (rc == WSANO_DATA) {
          // Unable to resolve name - bail out
       }
    s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (s == INVALID_SOCKET) {
          // socket failed
    }
    rc = connect(s, res->ai_addr, res->ai_addrlen);
    if (rc == SOCKET_ERROR) {
          // connect failed
    }
    freeaddrinfo(res);


    在 上面的代码中,我们看到:如果这是一个完整的程序的话,那么我们可以从命令行中得到szRemoteAddress, szRemotePort这两个信息,而且我们根本不用管这两项是IPv4的还是IPv6的,只需要把hint中的family设成AF_UNSPEC, 然后去调用getaddrinfo即可。很方便。

    而且,如果我们在connect或sendto之前,需要bind的话,也很简单,前面 说过了,bind唯一需要的就是本机地址和端口的描述。我们只需要把前面一次调用getaddrinfo生成的addrinfo结构中的family, socket type, protocol这三项设到一个新的hint中去(不能手动设定哦,一定要用上一次getaddrinfo的返回值,手动设定的话又会牵涉到IPv4, IPv6这样的family设定了,用上次返回的信息,这就是根据我们的szRemoteAddress生成的正确family),同时把 hint.ai_flags设成AI_PASSIVE,然后再调用一次getaddrinfo,这一次调用getaddrinfo,将nodename设 成NULL,servname填上我们想要的端口,然后调用getaddrinfo,就能返回我们bind需要的Addressing信息了。


    Server端代码:

    Code: Select all
    SOCKET            slisten[16];
    char            *szPort="5150";
    struct addrinfo              hints,
                * res=NULL,
                * ptr=NULL;
    int              count=0,
                  rc;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;
    rc = getaddrinfo(NULL, szPort, &hints, &res);
    if (rc != 0) {
          // failed for some reason
    }
    ptr = res;
    while (ptr)
    {
          slisten[count] = socket(ptr->ai_family,
          ptr->ai_socktype, ptr->ai_protocol);
          if (slisten[count] == INVALID_SOCKET) {
             // socket failed
          }
          rc = bind(slisten[count], ptr->ai_addr, ptr->ai_addrlen);
          if (rc == SOCKET_ERROR) {
             // bind failed
          }
          rc = listen(slisten[count], 7);
          if (rc == SOCKET_ERROR) {
             // listen failed
          }
          count++;
          ptr = ptr->ai_next;
    }


    OK,上面的代码很好理解,我们看到Server不需要去connect别人,只需要自己 bind然后listen(TCP)。所以,我们设置了hint.ai_flags为 AI_PASSIVE,然后,由于hint.ai_family设成了AF_UNSPEC,所以getaddrinfo会返回IPv4和IPv6两种 Addressing信息。既然这样,我们索性就用循环,在getaddrinfo返回的两个address上都创建socket,都bind,都 listen。
  • 相关阅读:
    0209利用innobackupex进行简单数据库的备份
    0208如何利用federated配置远程的数据库和本地数据相互交互
    0208MySQL5.7之Group Replication
    解决问题的方法
    0123简单配置LNMP
    0120Keeplived实现自动切换Mysql服务
    0116MySql主从复制监控
    大数据导入EXCEL
    OSI结构和TCP/IP模型
    ORA-12154 TNS无法解析指定的连接标识符
  • 原文地址:https://www.cnblogs.com/super119/p/2011306.html
Copyright © 2020-2023  润新知