windows網路編程第二版 第三章 Internet Protocol 讀書筆記

來源:互聯網
上載者:User
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。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.