socket最開(kāi)始的含義是一個(gè)IP地址和端口隊(duì)(ip,port)。它唯一地表示了使用TCP通信的一端。這就是socket地址。
主機(jī)字節(jié)序和網(wǎng)絡(luò)字節(jié)序
現(xiàn)在CPU的累加器一次都能裝載(至少)4字節(jié)(這里考慮32位機(jī)器,下同),即一個(gè)整數(shù)。那么這4個(gè)字節(jié)在內(nèi)存中排列的順序?qū)⒂绊懰焕奂悠餮b載成的整數(shù)的值。這就是字節(jié)序的問(wèn)題。
字節(jié)序分為大端字節(jié)序(big endian)和小端字節(jié)序(little endian)。大端字節(jié)序是指一個(gè)整數(shù)的高位字節(jié)(23 ~ 31 bit)存儲(chǔ)在內(nèi)存的地址處,低位字節(jié)(0~7 bit)存儲(chǔ)在內(nèi)存的高地址處。小端字節(jié)序則指整數(shù)的高位字節(jié)序存儲(chǔ)在內(nèi)存的高地址處,而低位字節(jié)序則存在在內(nèi)存的低地址處。
下面的代碼是檢查機(jī)器的字節(jié)序:
#include <stdio.h>
void byteorder()
{
union{
short value;
char union_bytes[sizeof(short)];
}test;
test.value = 0x0102;
if((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2)){
printf("big endiann");
}
else if((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1)){
printf("little endiann");
}
else{
printf("unknownn");
}
}
int main(int argc, char const *argv[])
{
byteorder();
return 0;
}
當(dāng)格式化的數(shù)據(jù)(比如32bit整型數(shù)和16bit短型數(shù))在兩臺(tái)使用不同字節(jié)序的主機(jī)之間傳遞時(shí),接收端必然錯(cuò)誤地解釋之。
解決問(wèn)題的方法是:發(fā)送端總是把要發(fā)送的數(shù)據(jù)轉(zhuǎn)化成大端字節(jié)序再發(fā)送,而接收端知道對(duì)方傳送過(guò)來(lái)的數(shù)據(jù)總是采用大端字節(jié)序,所以接收端可以根據(jù)自身采用的字節(jié)序決定是否對(duì)接收到的數(shù)據(jù)進(jìn)行轉(zhuǎn)換(小端機(jī)轉(zhuǎn)換,大端機(jī)不轉(zhuǎn)換)。因此大端字節(jié)序也稱為網(wǎng)絡(luò)字節(jié)序,它給所有接受數(shù)據(jù)的主機(jī)提供了一個(gè)正確解釋收到的格式化數(shù)據(jù)的保證。
需要指出的是,即使是同一臺(tái)機(jī)器上的兩個(gè)進(jìn)程(比如一個(gè)由C語(yǔ)言,另一個(gè)JAVA編寫)通信,也要考慮字節(jié)序的問(wèn)題(JAVA虛擬機(jī)采用大端字節(jié)序)。
linux提供了4個(gè)函數(shù)來(lái)完成主機(jī)字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間的轉(zhuǎn)換。
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
它們的含義很明確,比如htonl表示“host to network long",即將長(zhǎng)整型(32bit)的主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序數(shù)據(jù)。這四個(gè)函數(shù)中,長(zhǎng)整型函數(shù)通常用來(lái)轉(zhuǎn)換IP地址,短整型函數(shù)用來(lái)轉(zhuǎn)換端口號(hào)。(當(dāng)然不限于此。任何格式化的數(shù)據(jù)通過(guò)網(wǎng)絡(luò)傳輸時(shí),都應(yīng)該使用這些函數(shù)來(lái)轉(zhuǎn)化字節(jié)序)。
通用socket地址
socket網(wǎng)絡(luò)編程接口中表示socket地址的是結(jié)構(gòu)體sockaddr,其定義如下:
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
sa_family成員是地址族類型(sa_family_t)的變量。地址族類型通常與協(xié)議族類型對(duì)應(yīng)。
常見(jiàn)的協(xié)議族(protocol family,也稱domain)和對(duì)應(yīng)的地址族如下表:
宏P(guān)F_*和AF_*都定義在bits/socket.h頭文件中,且后者與前者有完全相同的值,所以二者通常混用。
sa_data成員用于存放socket地址值。但是不同的協(xié)議族的地址值具有不同的含義和長(zhǎng)度。如下表所示:
由此可以發(fā)現(xiàn),14字節(jié)的sa_data根本無(wú)法完全容納多數(shù)協(xié)議族的地址值。因此,Linux定義了下面這個(gè)新的通用socket地址結(jié)構(gòu)體:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128 - sizeof(__ss_align)];
};
這個(gè)結(jié)構(gòu)體不僅提供了足夠大的空間用于存放地址值,而且是內(nèi)存對(duì)齊的(這是__ss_align成員的作用)。
專用socket地址
上面這兩個(gè)通用socket地址結(jié)構(gòu)體顯然很不好用,比如設(shè)置與獲取IP地址和端口號(hào)就需要執(zhí)行煩瑣的位操作。所以Liunx為各個(gè)協(xié)議族提供了專門的socket地址結(jié)構(gòu)體。
UNIX本地域協(xié)議族使用如下專用socket地址結(jié)構(gòu)體:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; /*地址族: AF_UNIX*/
char sun_path[108]; /*文件路徑名*/
};
TCP/IP協(xié)議族有sockaddr_in和sockaddr_in6兩個(gè)專用socket地址結(jié)構(gòu)體,它們分別用于IPv4和IPv6:
struct sockaddr_in
{
sa_family_t sin_family; /*地址族:AF_INET*/
u_int16_t sin_port; /*端口號(hào),要用網(wǎng)絡(luò)字節(jié)序表示*/
struct in_addr sin_addr; /*IPv4地址結(jié)構(gòu)體*/
};
struct in_addr
{
u_int32_t s_addr; /*IPv4地址, 要用網(wǎng)絡(luò)字節(jié)序表示*/
};
struct sockaddr_in6
{
sa_family_t sin6_family; /*地址族:AF_INET6*/
u_int16_t sin6_port; /*端口號(hào),要用網(wǎng)絡(luò)字節(jié)序表示*/
u_int32_t sin6_flowinfo; /*流信息,應(yīng)設(shè)置為0*/
struct in6_addr sin6_addr; /*IPv6地址結(jié)構(gòu)體*/
u_int32_t sin6_scope_id; /*scope ID, 尚處于實(shí)驗(yàn)階段*/
};
struct in6_addr
{
unsigned char sa_addr[16]; /*IPv6地址, 要用網(wǎng)絡(luò)字節(jié)序表示*/
};
這兩個(gè)專用socket地址結(jié)構(gòu)體各字段的含義很明確。
所有專用socket地址(以及sockaddr_storage)類型的變量在實(shí)際使用時(shí)都需要轉(zhuǎn)化為通用socket地址類型sockaddr(強(qiáng)制轉(zhuǎn)化即可),因?yàn)樗衧ocket編程接口使用的地址參數(shù)的類型都是sockaddr。
IP地址轉(zhuǎn)換函數(shù)
通常,人們習(xí)慣用可讀性好的字符串來(lái)表示IP地址,比如用點(diǎn)分十進(jìn)制字符串表示IPv4地址,以及用十六進(jìn)制字符串表示IPv6地址。但編程中我們需要先把它們轉(zhuǎn)化為整數(shù)(二進(jìn)制數(shù))方能使用。而記錄日志則相反,我們要把整數(shù)表示的IP地址轉(zhuǎn)化為可讀的字符串。
下面3個(gè)函數(shù)可用于用點(diǎn)分十進(jìn)制字符串表示的字符串表示的IPv4地址和用網(wǎng)絡(luò)字節(jié)序整數(shù)表示的IPv4地址之間轉(zhuǎn)換:
#include <arpa/inet.h>
in_addr_t inet_addr(const char * strptr);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
inet_addr函數(shù)將用點(diǎn)分十進(jìn)制串表示的IPv4地址轉(zhuǎn)化為用網(wǎng)絡(luò)字節(jié)序整數(shù)表示的IPv4地址。失敗返回INADDR_NONE。
inet_aton函數(shù)完成和inet_addr同樣的功能,但是將轉(zhuǎn)化結(jié)果存儲(chǔ)與參數(shù)inp指向的地址結(jié)構(gòu)中。它成功返回1,失敗則返回0。
inet_ntoa函數(shù)將用網(wǎng)絡(luò)字節(jié)序整數(shù)表示的IPv4地址轉(zhuǎn)換為用點(diǎn)分十進(jìn)制字符串表示的IPv4地址。但需要注意的是:該函數(shù)內(nèi)部用一個(gè)靜態(tài)變量存儲(chǔ)轉(zhuǎn)化結(jié)果,函數(shù)的返回值指向該靜態(tài)內(nèi)存,因此inet_ntoa是不可重入的。
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
char ip1[] = "1.2.3.4";
char ip2[] = "10.194.71.60";
struct in_addr inAddr1;
struct in_addr inAddr2;
inet_aton(ip1, &inAddr1);
inet_aton(ip2, &inAddr2);
char *szValue1 = inet_ntoa(inAddr1);
char *szValue2 = inet_ntoa(inAddr2);
printf("address1: %sn", szValue1);
printf("address2: %sn", szValue2);
return 0;
}
不可重入的inet_ntoa函數(shù)實(shí)驗(yàn)結(jié)果
下面這對(duì)更新的函數(shù)也能完成和前面3和函數(shù)一樣的功能,并且它們使用適用于IPv4地址和IPv6地址:
#include <arpa/inet.h>
int inet_pton(int af, const char* src, void *dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
inet_pton函數(shù)將用于字符串表示的IP地址src(用點(diǎn)分十進(jìn)制字符串表示的IPv4地址或用十六進(jìn)制字符串表示的IPv6地址)轉(zhuǎn)換成用網(wǎng)絡(luò)字節(jié)序整數(shù)表示的IP地址,并把轉(zhuǎn)換結(jié)果存儲(chǔ)于dst指向的內(nèi)存中。其中,af參數(shù)指定地址族:
- AF_INET
- AF_INET6
inet_pton成功返回1,失敗則返回0并設(shè)置errno。
inet_ntop函數(shù)進(jìn)行相反的轉(zhuǎn)換,前三個(gè)參數(shù)的含義與inet_pton參數(shù)相同,最后一個(gè)cnt指定目標(biāo)存儲(chǔ)單元的大小。下面兩個(gè)宏能幫助我們指定這個(gè)大小(分別用于IPv4和IPv6):
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功時(shí)返回目標(biāo)存儲(chǔ)單元的地址,失敗返回NULL并設(shè)置errno。
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
char *ipv4 = "10.0.0.200";
char *ipv6 = "fe80::4bde:83d8:dbcf:72f3";
in_addr inAddr4;
in6_addr inAddr6;
inet_pton(AF_INET, ipv4, &inAddr4);
inet_pton(AF_INET6, ipv6, &inAddr6);
char addr1[INET_ADDRSTRLEN];
char addr2[INET6_ADDRSTRLEN];
if(addr1 == inet_ntop(AF_INET, (void *)&inAddr4, addr1, INET_ADDRSTRLEN)){
printf("truen");
}
printf("IPv4 addr: %sn", inet_ntop(AF_INET, (void *)&inAddr4, addr1, INET_ADDRSTRLEN));
printf("IPv4 addr: %sn", inet_ntop(AF_INET6, (void*)&inAddr6, addr2, INET6_ADDRSTRLEN));
return 0;
}