1.1. 網(wǎng)絡(luò)字節(jié)序
我們已經(jīng)知道,內(nèi)存中的多字節(jié)數(shù)據(jù)相對(duì)于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對(duì)于文件中的偏移地址也有大端小端之分。網(wǎng)絡(luò)數(shù)據(jù)流同樣有大端小端之分,那么如何定義網(wǎng)絡(luò)數(shù)據(jù)流的地址呢?發(fā)送主機(jī)通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機(jī)把從網(wǎng)絡(luò)上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存,因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址應(yīng)這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。
TCP/IP協(xié)議規(guī)定,網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié)。例如上一節(jié)的UDP段格式,地址0-1是16位的源端口號(hào),如果這個(gè)端口號(hào)是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發(fā)0x03,再發(fā)0xe8,這16位在發(fā)送主機(jī)的緩沖區(qū)中也應(yīng)該是低地址存0x03,高地址存0xe8。但是,如果發(fā)送主機(jī)是小端字節(jié)序的,這16位被解釋成0xe803,而不是1000。因此,發(fā)送主機(jī)把1000填到發(fā)送緩沖區(qū)之前需要做字節(jié)序的轉(zhuǎn)換。同樣地,接收主機(jī)如果是小端字節(jié)序的,接到16位的源端口號(hào)也要做字節(jié)序的轉(zhuǎn)換。如果主機(jī)是大端字節(jié)序的,發(fā)送和接收都不需要做轉(zhuǎn)換。同理,32位的IP地址也要考慮網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的問題。
為使網(wǎng)絡(luò)程序具有可移植性,使同樣的C代碼在大端和小端計(jì)算機(jī)上編譯后都能正常運(yùn)行,可以調(diào)用以下庫(kù)函數(shù)做網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的轉(zhuǎn)換。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
這些函數(shù)名很好記,h表示host,n表示network,l表示32位長(zhǎng)整數(shù),s表示16位短整數(shù)。例如htonl表示將32位的長(zhǎng)整數(shù)從主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序,例如將IP地址轉(zhuǎn)換后準(zhǔn)備發(fā)送。如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回,如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。
socket API是一層抽象的網(wǎng)絡(luò)編程接口,適用于各種底層網(wǎng)絡(luò)協(xié)議,如IPv4、IPv6,以及后面要講的UNIX Domain Socket。然而,各種網(wǎng)絡(luò)協(xié)議的地址格式并不相同,如下圖所示:
IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結(jié)構(gòu)體表示,包括16位端口號(hào)和32位IP地址,IPv6地址用sockaddr_in6結(jié)構(gòu)體表示,包括16位端口號(hào)、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定義在sys/un.h中,用sockaddr_un結(jié)構(gòu)體表示。各種socket地址結(jié)構(gòu)體的開頭都是相同的,前16位表示整個(gè)結(jié)構(gòu)體的長(zhǎng)度(并不是所有UNIX的實(shí)現(xiàn)都有長(zhǎng)度字段,如Linux就沒有),后16位表示地址類型。IPv4、IPv6和UNIX Domain Socket的地址類型分別定義為常數(shù)AF_INET、AF_INET6、AF_UNIX。這樣,只要取得某種sockaddr結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的sockaddr結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容。因此,socket API可以接受各種類型的sockaddr結(jié)構(gòu)體指針做參數(shù),例如bind、accept、connect等函數(shù),這些函數(shù)的參數(shù)應(yīng)該設(shè)計(jì)成void *類型以便接受各種類型的指針,但是sock API的實(shí)現(xiàn)早于ANSI C標(biāo)準(zhǔn)化,那時(shí)還沒有void *類型,因此這些函數(shù)的參數(shù)都用struct sockaddr *類型表示,在傳遞參數(shù)之前要強(qiáng)制類型轉(zhuǎn)換一下,例如:
struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
本節(jié)只介紹基于IPv4的socket網(wǎng)絡(luò)編程,sockaddr_in中的成員struct in_addr sin_addr表示32位的IP地址。但是我們通常用點(diǎn)分十進(jìn)制的字符串表示IP地址,以下函數(shù)可以在字符串表示和in_addr表示之間轉(zhuǎn)換。
字符串轉(zhuǎn)in_addr的函數(shù):
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
in_addr轉(zhuǎn)字符串的函數(shù):
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr,因此函數(shù)接口是void *addrptr。