socket最開始的含義是一個IP地址和端口隊(ip,port)。它唯一地表示了使用TCP通信的一端。這就是socket地址。
主機字節序和網絡字節序
現在CPU的累加器一次都能裝載(至少)4字節(這里考慮32位機器,下同),即一個整數。那么這4個字節在內存中排列的順序將影響它被累加器裝載成的整數的值。這就是字節序的問題。
字節序分為大端字節序(big endian)和小端字節序(little endian)。大端字節序是指一個整數的高位字節(23 ~ 31 bit)存儲在內存的地址處,低位字節(0~7 bit)存儲在內存的高地址處。小端字節序則指整數的高位字節序存儲在內存的高地址處,而低位字節序則存在在內存的低地址處。
下面的代碼是檢查機器的字節序:
#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;
}
當格式化的數據(比如32bit整型數和16bit短型數)在兩臺使用不同字節序的主機之間傳遞時,接收端必然錯誤地解釋之。
解決問題的方法是:發送端總是把要發送的數據轉化成大端字節序再發送,而接收端知道對方傳送過來的數據總是采用大端字節序,所以接收端可以根據自身采用的字節序決定是否對接收到的數據進行轉換(小端機轉換,大端機不轉換)。因此大端字節序也稱為網絡字節序,它給所有接受數據的主機提供了一個正確解釋收到的格式化數據的保證。
需要指出的是,即使是同一臺機器上的兩個進程(比如一個由C語言,另一個JAVA編寫)通信,也要考慮字節序的問題(JAVA虛擬機采用大端字節序)。
linux提供了4個函數來完成主機字節序和網絡字節序之間的轉換。
#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",即將長整型(32bit)的主機字節序轉換為網絡字節序數據。這四個函數中,長整型函數通常用來轉換IP地址,短整型函數用來轉換端口號。(當然不限于此。任何格式化的數據通過網絡傳輸時,都應該使用這些函數來轉化字節序)。
通用socket地址
socket網絡編程接口中表示socket地址的是結構體sockaddr,其定義如下:
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
sa_family成員是地址族類型(sa_family_t)的變量。地址族類型通常與協議族類型對應。
常見的協議族(protocol family,也稱domain)和對應的地址族如下表:

宏PF_*和AF_*都定義在bits/socket.h頭文件中,且后者與前者有完全相同的值,所以二者通常混用。
sa_data成員用于存放socket地址值。但是不同的協議族的地址值具有不同的含義和長度。如下表所示:

由此可以發現,14字節的sa_data根本無法完全容納多數協議族的地址值。因此,Linux定義了下面這個新的通用socket地址結構體:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128 - sizeof(__ss_align)];
};
這個結構體不僅提供了足夠大的空間用于存放地址值,而且是內存對齊的(這是__ss_align成員的作用)。
專用socket地址
上面這兩個通用socket地址結構體顯然很不好用,比如設置與獲取IP地址和端口號就需要執行煩瑣的位操作。所以Liunx為各個協議族提供了專門的socket地址結構體。
UNIX本地域協議族使用如下專用socket地址結構體:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; /*地址族: AF_UNIX*/
char sun_path[108]; /*文件路徑名*/
};
TCP/IP協議族有sockaddr_in和sockaddr_in6兩個專用socket地址結構體,它們分別用于IPv4和IPv6:
struct sockaddr_in
{
sa_family_t sin_family; /*地址族:AF_INET*/
u_int16_t sin_port; /*端口號,要用網絡字節序表示*/
struct in_addr sin_addr; /*IPv4地址結構體*/
};
struct in_addr
{
u_int32_t s_addr; /*IPv4地址, 要用網絡字節序表示*/
};
struct sockaddr_in6
{
sa_family_t sin6_family; /*地址族:AF_INET6*/
u_int16_t sin6_port; /*端口號,要用網絡字節序表示*/
u_int32_t sin6_flowinfo; /*流信息,應設置為0*/
struct in6_addr sin6_addr; /*IPv6地址結構體*/
u_int32_t sin6_scope_id; /*scope ID, 尚處于實驗階段*/
};
struct in6_addr
{
unsigned char sa_addr[16]; /*IPv6地址, 要用網絡字節序表示*/
};
這兩個專用socket地址結構體各字段的含義很明確。
所有專用socket地址(以及sockaddr_storage)類型的變量在實際使用時都需要轉化為通用socket地址類型sockaddr(強制轉化即可),因為所有socket編程接口使用的地址參數的類型都是sockaddr。
IP地址轉換函數
通常,人們習慣用可讀性好的字符串來表示IP地址,比如用點分十進制字符串表示IPv4地址,以及用十六進制字符串表示IPv6地址。但編程中我們需要先把它們轉化為整數(二進制數)方能使用。而記錄日志則相反,我們要把整數表示的IP地址轉化為可讀的字符串。
下面3個函數可用于用點分十進制字符串表示的字符串表示的IPv4地址和用網絡字節序整數表示的IPv4地址之間轉換:
#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函數將用點分十進制串表示的IPv4地址轉化為用網絡字節序整數表示的IPv4地址。失敗返回INADDR_NONE。
inet_aton函數完成和inet_addr同樣的功能,但是將轉化結果存儲與參數inp指向的地址結構中。它成功返回1,失敗則返回0。
inet_ntoa函數將用網絡字節序整數表示的IPv4地址轉換為用點分十進制字符串表示的IPv4地址。但需要注意的是:該函數內部用一個靜態變量存儲轉化結果,函數的返回值指向該靜態內存,因此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函數實驗結果
下面這對更新的函數也能完成和前面3和函數一樣的功能,并且它們使用適用于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函數將用于字符串表示的IP地址src(用點分十進制字符串表示的IPv4地址或用十六進制字符串表示的IPv6地址)轉換成用網絡字節序整數表示的IP地址,并把轉換結果存儲于dst指向的內存中。其中,af參數指定地址族:
- AF_INET
- AF_INET6
inet_pton成功返回1,失敗則返回0并設置errno。
inet_ntop函數進行相反的轉換,前三個參數的含義與inet_pton參數相同,最后一個cnt指定目標存儲單元的大小。下面兩個宏能幫助我們指定這個大小(分別用于IPv4和IPv6):
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功時返回目標存儲單元的地址,失敗返回NULL并設置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;
}
