3. IP address、結構與資料轉換

這裡是好玩的地方,我們要開始談程式碼了。

不過,我們一開始要討論的程式碼會比較少!

耶!因為我想要先講點 IP address(位址)與 port(連接埠),這樣才會有點感覺;接著我們會討論 socket API 如何儲存與控制 IP address 和其它資料。

3.1. IPv4 與 IPv6

在 Ben Kenobi 還是叫 Obi Wan Kenobi 的那段過去的美好時光,有個很棒的 network routing system(網路路由系統),稱為 Internet Protocol Version 4(網際網路協定第四版),又稱為 IPv4。它的位址是由四個 bytes 組成(亦稱為四個"octets"),而格式是由句點與數字組成,像是這樣:192.0.2.111。

你或許曾經看過。

實際上,在撰寫本文時,幾乎整個 Internet(網際網路)的每個網站都還是使用 IPv4。

每個人跟 Obi Wan 都很開心,一切都是如此美好,直到某個名為 Vint Cerf 的人提出質疑,警告所有人 IPv4 address 即將耗盡。

Vint Cerf [10] 除了提出即將到來的 IPv4 危機警告,他本身還是有名的 Internet 之父,所以我真的沒資格能評論他的判斷。

你說的是耗盡 address 嗎?會發生什麼事呢?其實我的意思是,32-bit 的 IPv4 address 有幾十億個 IP address,我們真的有幾十億台的電腦在用嗎?

是的。在一開始大家也是認為這樣就夠用了,因為當時只有一些電腦,而且每個人認為幾十億是不可能用完的大數目,還很慷慨的分給某些大型組織幾百萬個 IP address 供他們自己使用[例如:Xerox、MIT、Ford、HP、IBM、GE、AT&T 及某個名為 Apple 的小公司,族繁不及備載]。

不過現實狀況是,如果不是有些變通的方法,我們早就用光 IPv4 位址了。

我們現在生活於每個人、每台電腦、每部計算機、每隻電話、每部停車計時收費器、以及每條小狗[為什麼不行?]都有一個 IP address 的年代,因此,IPv6 誕生了。

因為 Vint Cerf 可能是不朽的,[即使他的肉體終究應該會回歸自然,我也希望不要,不過他的精神或許已經以某種超智慧的 ELIZA [11] 程式存在於 Internet2 的核心],應該沒有人想要因為下一代網際網路協定又沒有足夠的位址,然後又聽到他說:"我要告訴你們一件事 ..."。

那你有什麼建議嗎?

我們需要更多的位址,我們需要不止兩倍以上的位址、不止幾十億倍、千兆倍以上,而是 79 乘以 百萬 乘以 十億 乘以 兆倍以上的可用位址!你們大家將會見識到的。

你說:"Beej,真的嗎?我還是有許多可以質疑這個大數字的理由。"

好的,32 bits 與 128 bits 的差異聽起來似乎不是很多;它只多了 96 個 bits 而已,不是嗎?不過請記得,我們所談的是等比級數;32 bits 表示個 40 億的數字[2 的 32 次方],而128 bits 表示的大約是 340 個兆兆兆的數字[2 的 128 次方],這相當於宇宙中的每顆星星都能擁有一百萬個 IPv4 Internets。

大家順便忘了 IPv4 的句號與數字的長相吧;現在我們有十六進制的表示法,每兩個 bytes 間以冒號分隔,類似這樣:

2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551。

這還不是全部呢!大部分的時候,你的 IP address 裡面會有很多個零,而你可以將它們壓縮到兩個冒號間,你也可以在每個 byte pair(位元組對)上保留零。例如,這些位址的配對是相等的:

2001:0db8:c9d2:0012:0000:0000:0000:0051
2001:db8:c9d2:12::51
2001:0db8:ab00:0000:0000:0000:0000:0000
2001:db8:ab00::
0000:0000:0000:0000:0000:0000:0000:0001
::1

[10] http://en.wikipedia.org/wiki/Vinton_Cerf
[11] http://en.wikipedia.org/wiki/ELIZA

位址 ::1 是個 loopback(遶回)位址,它永遠只代表"我現在執行的這台電腦",在 IPv4 中,loopback 位址是 127.0.0.1。

最後,你可能會遇到 IPv6 與 IPv4 相容的模式。例如,如果你願意的話,你可以將 IPv4 address 192.0.2.33 以 IPv6 位址表示,可以使用如下的符號:"::ffff:192.0.2.33"。

我們所謂的自信,實際上,因為自信,所以 IPv6 的發明人很有把握的將兆來兆去的位址用在保留用途上,不過說實在的,我們有這麼多位址,誰能算清楚呢?

還剩下很多位址可以分配給星系中每個行星的每個男人、女人、小孩、小狗跟停車計時收費器。相信我,星系中的每個行星都有行車計時收費器。你明白這是真的。

3.1.1 Sub network (子網段)

為了結構化的理由,有時我們這樣宣告是很方便的:"IP address 的前段是 IP address 的 network(網段),而後面的部分是 host(主機)。"

例如:在 IPv4,你可能有 192.0.2.12,而我們可以說前面三個 bytes 是 network,而最後一個 byte 是 host。或者換個方式,我們能說 host 12 位在 network 192.0.2.0。[請參考我們如何將 host byte 清為零]。

接下來要講的是過時的資訊了!

真的嗎?

很久很久以前,有 subnets(子網路)的"class"(分級),在這裡,位址的第一個、前二個或前三個 bytes 都是屬於 network 的一部分。如果你很幸運可以擁有一個 byte 的 network,而另外三個 bytes 是 host 位址,那在你的網路上,你有價值 24 bits 的 host number[大約兩千四百萬個位址左右]。這是一個"Class A"(A 級)的網路;相對則是一個"Class C"(C 級)的網路,network 有三個 bytes、而 host 只有一個 byte[256 個 hosts,而且還要再扣掉兩個保留的位址]。

所以,如同你所看到的,只有一些 Class A 網路,一大堆的 Class C 網路,以及一些中等的 Class B 網路。

IP address 的網段部分由 netmask(網路遮罩)決定,你可以將 IP address 與 netmask 進行 AND 位元運算,就能得到 network 的值。Netmask 一般看起來像是 255.255.255.0[如:若你的 IP 是 192.0.2.12,那麼使用這個 netmask 時,你的 network 就會是 192.0.2.12 AND 255.255.255.0 所得到的值:192.0.2.0]。

無庸置疑的,這樣的分級對於 Internet 的最終需求而言並不夠細膩;我們已經相當以相當快的速度在消耗 Class C 網路,這是我們都知道一定會耗盡的 Class,所以不用費心去想了。補救的方式是,要能接受任意個 bits 的 netmask,而不單純是 8、16 或 24 個而已。所以你可以有個 255.255.255.252 的 netmask,這個 netmask 能切出一個 30 個 bits 的 network 及 2 個 bits 的 host,這個 network 最多有四台 hosts[注意,netmask 的格式永遠都是:前面是一連串的 1,然後,後面是一連串的 0]。

不過一大串的數字會有點不好用,比如像 255.192.0.0 這樣的 netmask。首要是人們無法直觀地知道有多少個 bits 的 1;其次是這樣真的很不嚴謹。因此,後來的新方法就好多了。你只需要將一個斜線放在 IP address 後面,接著後面跟著一個十進制的數字用以表示 network bits 的數目,類似這樣:192.0.2.12/30。

或者在 IPv6 中,類似這樣:2001:db8::/32 或 2001:db8:5413:4028::9db9/64。

3.1.2. Port Number(連接埠號碼)

如果你還記得我之前跟你說過的分層網路模型(Layered Network Model),它將網路層(IP)與主機到主機間的傳輸層[TCP 與 UDP]分開。

我們要加快腳步了。

除了 IP address 之外[IP 層],有另一個 TCP[stream socket]使用的位址,剛好 UDP[datagram socket]也是。它就是 port number,這是一個 16-bit 的數字,就像是連線的本地端位址一樣。

將 IP address 想成飯店的地址,而 port number 就是飯店的房間號碼。這是貼切的比喻;或許以後我會用汽車工業來比喻。

你說想要有一台電腦能處理收到的電子郵件與網頁服務-你要如何在一台只有一個IP位址的電腦上分辨呢?

好,Internet 上不同的服務都有已知的(well-known)port numbers。你可以在 Big IANA Port 清單 [12] 中找到,或者若你在 Unix 系統上,你可以參考檔案 /etc/services。HTTP(網站)是 port 80、telnet 是 port 23、SMTP 是 port 25,而 DOOM 遊戲 [13] 使用 port 666 等,諸如此類。Port 1024 以下通常是有特地用途的,而且要有作業系統管理員權限才能使用。

摁,這就是 port number 的介紹。

3.2 Byte Order(位元組順序)

長久以來都有兩種 byte orderings,不過後來才知道,根本是 LP 比雞腿。

我開玩笑的,不過其中一個真的比另一個好 :-)

這真的不太好說明,所以我只會瞎扯:你的電腦可能背著在用相反的順序來儲存 bytes。

我知道!沒有人跟你說。

Byte Order 其實就是,在 Internet 世界中的每個人一般都已經同意的,如果你想要用兩個 bytes 的十六進制數字來表示,比如說 b34f,你可以將它以 b34f 的順序儲存。很合理,而 Wilford Brimley [14] 會跟你說,這麼做是對的。這個數字是先儲存比較大的那一邊(big end),所以稱為 Big-Endian。

毫無疑問地,世界上的電腦那麼多,像 Intel 或 Intel 相容處理器就是將 bytes 反過來儲存,所以 b34f 存在記憶體中的順序就是 4fb3,這樣的儲存方式稱為 Little-Endian。

不過,等等。我還沒解釋名詞!照理說,Big-Endian 又稱為 Network Byte Order,因為這個順序與我們網路類型順序一樣。

你的電腦會以 Host Byte Order 儲存數字,如果是 Intel 80x86,Host Byte Order 是 Little-Endian;若是 Motorola 68k,則 Host Byte Order 是 Big-Endian;若是 PowerPC,Host Byte Order 就是 … 恩,這要看你的 PowerPC 而定。

大多數當你在打造封包或填寫資料結構時,你需要確認你的兩個數字跟四個數字都是 Network Byte Order。只是如果你不知道本機的 Host Byte Order,那該怎麼做呢?

好消息是你只需要假設 Host Byte Order 不正確,然後每次都透過一個函式將值設定為 Network Byte Order。如果有必要,該函式會進行魔法的轉換,而這個方式可以讓你的程式碼能方便的移植到不同 endian 的機器上。

你可以轉換兩種型別的數值:short[兩個 bytes]與 long[四個 bytes]。這些函式也可以用在 unsigned 變數。比如說,你想要將 short 從 Host Byte Order 轉換為 Network Byte Order,用"h"代表"host",用"n"代表"network",而"s"代表"short",所以是:h-to-n-s,或者htons()[讀做:"Host to Network Short"]。

這真是太簡單了…

你可以用任何你想要的方式來組合"n"、"h"、"s"與"l",不過別用太蠢的組合,比如:沒有這樣的函式 stolh()["Short to Long Host"],沒有這種東西,不過有:

htons() host to network short
htonl() host to network long
ntohs() network to host short
ntohl() network to host long

基本上,你需要在送出以前將數值轉換為 Network Byte Order,並在收到之後將數值轉回 Host Byte Order。

抱歉,我不知道 64-bit 的改變,如果你想要做浮點數的話,可以參考第 7-4 節。

[12] http://www.iana.org/assignments/port-numbers
[13] http://en.wikipedia.org/wiki/Doom_(video_game)
[14] http://en.wikipedia.org/wiki/Wilford_Brimley

如果我沒特別強調的話,本文中的數值預設是視為 Host Byte Order。

3.3. 資料結構

很好,終於講到這裡了,該是談談程式設計的時間了。在本節,我會介紹 socket 介面的各種資料型別,因為它們有些會不太好理解。

首先是最簡單的:socket descriptor,型別如下:

int

就是一般的 int。

從這裡開始會有點不好理解,所以不用問太多,直接讀過就好。

我的第一個 StructTM -struct addrinfo,這個資料結構是最近的發明,用來準備之後要用的 socket 位址資料結構,也用在主機名稱(host name)及服務名稱(service name)的查詢。當我們之後開始實際應用時,才會開始覺得比較合理,現在只需要知道你在建立連線呼叫時會用到這個資料結構。

struct addrinfo {
    int ai_flags; // AI_PASSIVE, AI_CANONNAME 等。
    int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
    int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
    int ai_protocol; // 用 0 當作 "any"
    size_t ai_addrlen; // ai_addr 的大小,單位是 byte
    struct sockaddr *ai_addr; // struct sockaddr_in 或 _in6
    char *ai_canonname; // 典型的 hostname
    struct addrinfo *ai_next; // 鏈結串列、下個節點
};

你可以載入這個資料結構,然後呼叫 getaddrinfo()。它會傳回一個指標,這個指標指向一個新的鏈結串列,這個串列有一些資料結構,而資料結構的內容記載了你所需的東西。

你可以在 ai_family 欄位中設定強制使用 IPv4 或 IPv6,或者將它設定為 AF_UNSPEC,AF_UNSPEC 很酷,因為這樣你的程式就可以不用管 IP 的版本。

要注意的是,這是個鏈結串列:ai_next 是指向下一個成員(element),可能會有多個結果讓你選擇。我會直接用它提供的第一個結果,不過你可能會有不同的個人考量;先生!我不是萬事通。

你會在 struct addrinfo 中看到 ai_addr 欄位是一個指向 struct sockaddr 的指標。這是我們開始要了解 IP 位址結構中有哪些細節的地方。有時候,你需要的是呼叫 getaddrinfo() 幫你填好 struct addrinfo。然而,你必須查看這些資料結構,並將值取出,所以我在這邊會進行說明。

[還有,在發明 struct addrinfo 以前的程式碼都要手動填寫這些資料的每個欄位,所以你會看到很多 IPv4 的程式碼真的用很原始的方式去做這件事。你知道的,本文件在舊版也是這樣做]。

有些 structs 是 IPv4,而有些是 IPv6,有些兩者都是。我會特別註明清楚它們屬於哪一種。

總之,struct sockaddr 記錄了很多 sockets 類型的 socket 的位址資訊。

struct sockaddr {
    unsigned short sa_family; // address family, AF_xxx
    char sa_data[14]; // 14 bytes of protocol address
};

sa_family 可以是任何東西,不過在這份文件中我們會用到的是 AF_INET[IPv4]或 AF_INET6[IPv6]。sa_data 包含一個 socket 的目地位址與 port number。這樣很不方便,因為你不會想要手動的將位址封裝到 sa_data 裡。

為了處理 struct sockaddr,程式設計師建立了對等平行的資料結構:struct sockaddr_in["in"是代表"internet"]用在 IPv4。

而這有個重點:指向 struct sockaddr_in 的指標可以轉型(cast)為指向 struct sockaddr 的指標,反之亦然。所以即使 connect() 需要一個 struct sockaddr *,你也可以用 struct sockaddr_in,並在最後的時候對它做型別轉換!

// (IPv4 only--see struct sockaddr_in6 for IPv6)
struct sockaddr_in {
    short int sin_family; // Address family, AF_INET
    unsigned short int sin_port; // Port number
    struct in_addr sin_addr; // Internet address
    unsigned char sin_zero[8]; // 與 struct sockaddr 相同的大小
};

這個資料結構讓它很容易可以參考(reference)socket 位址的成員。要注意的是 sin_zero[這是用來將資料結構補足符合 struct sockaddr 的長度],應該要使用 memset()函式將 sin_zero 整個清為零。還有,sin_family 是對應到 struct sockaddr 中的 sa_family,並應該設定為"AF_INET"。最後,sin_port 必須是 Network Byte Order[利用htons()]。

讓我們再更深入點!你可以在 sruct in_addr 裡看到 sin_addr 欄位。

那是什麼?

好,別太激動,不過它是其中一個最恐怖的 union:

// (僅限 IPv4 — Ipv6 請參考 struct in6_addr)
// Internet address (a structure for historical reasons)
struct in_addr {
    uint32_t s_addr; // that's a 32-bit int (4 bytes)
};

哇!好耶,它以前是 union,不過這個包袱現在似乎已經不見了。因此,若你已將 ina 宣告為 struct sockaddr_in 的型別時,那麼 ina.sin_addr.s_addr 會參考到 4-byte 的 IP address(以 Network Byte Order)。要注意的是,如果你的系統仍然在 struct in_addr 使用超恐怖的 union,你依然可以像我上面說的,精確地參考到 4-byte 的 IP address[這是因為 #define]。

那麼 IPv6 會怎樣呢?

IPv6 也有提供類似的 struct,比如:
// (IPv6 only--see struct sockaddr_in and struct in_addr for IPv4)
struct sockaddr_in6 {
    u_int16_t sin6_family; // address family, AF_INET6
    u_int16_t sin6_port; // port number, Network Byte Order
    u_int32_t sin6_flowinfo; // IPv6 flow information
    struct in6_addr sin6_addr; // IPv6 address
    u_int32_t sin6_scope_id; // Scope ID
};
struct in6_addr {
    unsigned char s6_addr[16]; // IPv6 address
};
要注意到 IPv6 協定有一個 IPv6 address 與一個 port number,就像 IPv4 協定有一個 IPv4 address 與 port number 一樣。

我現在還不會介紹 IPv6 的流量資訊,或是 Scope ID 欄位 … 這只是一份入門文件嘛 :-)

最後要強調的一點,這個簡單的 struct sockaddr_storage 是設計用來足夠儲存 IPv4 與 IPv6 structures 的 structure。[你看看,對於某些 calls,你有時無法事先知道它是否會使用 IPv4 或 IPv6 address 來填好你的 struct sockaddr。所以你用這個平行的 structure 來傳遞,它除了比較大以外,也很類似 struct sockaddr ,因而可以將它轉型為你所需的型別]。

struct sockaddr_storage {
    sa_family_t ss_family; // address family
    // all this is padding, implementation specific, ignore it:
    char __ss_pad1[_SS_PAD1SIZE];
    int64_t __ss_align;
    char __ss_pad2[_SS_PAD2SIZE];
};

重點是你可以在 ss_family 欄位看到位址家族(address family),檢查它是 AF_INET 或 AF_INET6(是 IPv4 或 IPv6)。之後如果你願意的話,你就可以將它轉型為 sockaddr_in 或 struct sockaddr_in6。

3.4. IP位址,Part II

還好你運氣不錯,有一堆函式讓你能夠控制 IP address,而不需要親自用 long 與 << 運算符來處理它們。

咱們說,你有一個 struct sockaddr_in ina,而且你有一個 "10.12.110.57" 或 "2001:db8:63b3:1::3490" 這樣的一個 IP address 要儲存。你想要使用 inet_pton() 函式將 IP address 轉換為數值與句號的符號,並依照你指定的 AF_INET 或 AF_INET6 來決定要儲存在 struct in_addr 或 struct in6_addr。["pton"的意思是"presentation to network",你可以稱之為"printable to network",如果這樣會比較好記的話]。

這樣的轉換可以用如下的方式:

struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6
inet_pton(AF_INET, "192.0.2.1", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6

[小記:原本的老方法是使用名為 inet_addr() 的函式或另一個名為 inet_aton() 的函式;這些都過時了,而且不適合在 IPv6 中使用]。

目前上述的程式碼片段還不是很可靠,因為沒有錯誤檢查。inet_pton() 在錯誤時會傳回 -1,而若位址被搞爛了,則會傳回 0。所以在使用之前要檢查,並確認結果是大於 0 的。

好了,現在你可以將 IP address 字串轉換為它們的二進位表示。

還有其它方法嗎?

如果你有一個 struct in_addr 且你想要以數字與句號印出來的話呢?

[呵呵,或者如果你想要以"十六進位與冒號"印出 struct in6_addr]。在這個例子中,你會想要使用 inet_ntop()函式["ntop"意謂"network to presentation"-如果有比較好記的話,你可以稱它為"network to prinable"],像是這樣:
// IPv4:
char ip4[INET_ADDRSTRLEN]; // 儲存 IPv4 字串的空間
struct sockaddr_in sa; // pretend this is loaded with something
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // 儲存 IPv6 字串的空間
struct sockaddr_in6 sa6; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);

當你呼叫它時,你會傳遞位址的型別[IPv4 或 IPv6],該位址是一個指向儲存結果的字串,與該字串的最大長度。[有兩個 macro(巨集)可以很方便地儲存你想儲存的最大 IPv4 或 IPv6 位址字串大小:INET_ADDRSTRLEN 與 INET6_ADDRSTRLEN]。

[另一個要再次注意的是以前的方法:以前做這類轉換的函式名為 inet_ntoa(),它已經過期了,而也在 IPv6 中也不適用]。

最後,這些函式只能用在數值的 IP address 上,它們不需要 DNS nameserver 來查詢主機名稱,如"www.example.com"。你可以使用 getaddrinfo() 來做這件事情,如同你稍後會看到的。

3.4.1 Private(或 disconnected)Network

很多地方都有防火牆(firewall),由它們的保護將網路隱藏於世界的其它地方。而有時,防火牆會用所謂的網路位址轉換(NAT,Network Address Translation)的方法,將"internal"(內部的)IP 位址轉換為"external"(外部的)[世界上的每個人都知道的]IP address。

你又開始緊張了嗎?"他又要扯到哪裡去了?"

好啦,放輕鬆,去買瓶汽水[或酒精]飲料,因為身為一個初學者,你還可以不要理會 NAT,因為它所做的事情對你而言是透明的。不過我想在你開始對所見的網路數量開始感到困惑以前,談談防火牆身後的網路。

比如,我家有一個防火牆,我有兩個 DSL 電信公司分配給我的靜態 IPv4 位址,而我家的網路有七部電腦要用。這有可能嗎?兩台電腦不能共用同一個 IP address 阿,不然資料就不知道該送去哪一台電腦了!

答案揭曉:它們不會共用同一個 IP address,它們是在一個配有兩千四百萬個 IP address 的 private network 上,全部都是我的。

好,都是我的,有這麼多位址可以讓大家用來上網,而這裡要講的就是為什麼:

如果我登入到一台遠端的電腦,它會說我從 192.0.2.33 登入,這是我的 ISP 提供給我的 public IP。不過若是我問我自己本地端的電腦,它的 IP address 是什麼時,他會說是 10.0.0.5。是誰轉換 IP 的呢?答對了,就是防火牆!它做了 NAT!

10.x.x.x 是其中一個少數保留的網路,只能用在完全無法連上 Internet 的網路[disconnected network],或是在防火牆後的網路。你可以使用哪個 private network 編號的細節是記在 RFC 1918 [15]中,不過一般而言,你較常見的是 10.x.x.x 及 192.168.x.x,這裡的 x 是指 0-255。較少見的是 172.y.x.x,這裡的 y 範圍在 16 與 31 之間。

在 NAT 防火牆後的網路可以不必用這些保留的網路,不過它們通常會用。

[真好玩!我的外部 IP 真的不是 192.0.2.33,192.0.2.x 網段保留用來虛構本文要用的 "真實"IP address,就像本文也是虛構的一樣 Wowzers!]

IPv6 也很合理的會有 private network。它們是以 fdxx: 開頭[或者未來可能是 fcXX:],如同 RFC 4193 [16]。NAT 與 IPv6 通常不會混用,然而[除非你在做 IPv6 到 IPv4 的 gateway,這就不在本文的討論範圍內了],理論上,你會有很多位址可以使用,所以根本不再需要使用 NAT。不過,如果你想要在不會遶送到外面的網路[封閉網路]上配置位址給你自己,就用 NAT 吧。

[15] http://tools.ietf.org/html/rfc1918
[16] http://tools.ietf.org/html/rfc4193