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。
posted @ 2011-04-10 14:13  super119  阅读(742)  评论(0编辑  收藏  举报