套接字概念
Socket本身有“插座”的意思,在Linux環(huán)境下,用于表示進(jìn)程間網(wǎng)絡(luò)通信的特殊文件類型。本質(zhì)為內(nèi)核借助緩沖區(qū)形成的偽文件。
既然是文件,那么理所當(dāng)然的,我們可以使用文件描述符引用套接字。與管道類似的,Linux系統(tǒng)將其封裝成文件的目的是為了統(tǒng)一接口,使得讀寫套接字和讀寫文件的操作一致。區(qū)別是管道主要應(yīng)用于本地進(jìn)程間通信,而套接字多應(yīng)用于網(wǎng)絡(luò)進(jìn)程間數(shù)據(jù)的傳遞。
套接字的內(nèi)核實現(xiàn)較為復(fù)雜,不宜在學(xué)習(xí)初期深入學(xué)習(xí)。
在TCP/IP協(xié)議中,“IP地址+TCP或UDP端口號”唯一標(biāo)識網(wǎng)絡(luò)通訊中的一個進(jìn)程。“IP地址+端口號”就對應(yīng)一個socket。欲建立連接的兩個進(jìn)程各自有一個socket來標(biāo)識,那么這兩個socket組成的socket pair就唯一標(biāo)識一個連接。因此可以用Socket來描述網(wǎng)絡(luò)連接的一對一關(guān)系。
套接字通信原理如下圖所示:
在網(wǎng)絡(luò)通信中,套接字一定是成對出現(xiàn)的。一端的發(fā)送緩沖區(qū)對應(yīng)對端的接收緩沖區(qū)。我們使用同一個文件描述符索發(fā)送緩沖區(qū)和接收緩沖區(qū)。
TCP/IP協(xié)議最早在BSD UNIX上實現(xiàn),為TCP/IP協(xié)議設(shè)計的應(yīng)用層編程接口稱為socket API。本章的主要內(nèi)容是socket API,主要介紹TCP協(xié)議的函數(shù)接口,最后介紹UDP協(xié)議和UNIX Domain Socket的函數(shù)接口。
網(wǎng)絡(luò)字節(jié)序
我們已經(jīng)知道,內(nèi)存中的多字節(jié)數(shù)據(jù)相對于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對于文件中的偏移地址也有大端小端之分。網(wǎng)絡(luò)數(shù)據(jù)流同樣有大端小端之分,那么如何定義網(wǎng)絡(luò)數(shù)據(jù)流的地址呢?發(fā)送主機通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機把從網(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位的源端口號,如果這個端口號是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發(fā)0x03,再發(fā)0xe8,這16位在發(fā)送主機的緩沖區(qū)中也應(yīng)該是低地址存0x03,高地址存0xe8。但是,如果發(fā)送主機是小端字節(jié)序的,這16位被解釋成0xe803,而不是1000。因此,發(fā)送主機把1000填到發(fā)送緩沖區(qū)之前需要做字節(jié)序的轉(zhuǎn)換。同樣地,接收主機如果是小端字節(jié)序的,接到16位的源端口號也要做字節(jié)序的轉(zhuǎn)換。如果主機是大端字節(jié)序的,發(fā)送和接收都不需要做轉(zhuǎn)換。同理,32位的IP地址也要考慮網(wǎng)絡(luò)字節(jié)序和主機字節(jié)序的問題。
為使網(wǎng)絡(luò)程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯后都能正常運行,可以調(diào)用以下庫函數(shù)做網(wǎng)絡(luò)字節(jié)序和主機字節(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);
h表示host,n表示network,l表示32位長整數(shù),s表示16位短整數(shù)。
如果主機是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回,如果主機是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動地返回。
?IP地址轉(zhuǎn)換函數(shù)
早期:
#include?<sys/socket.h>
#include?<netinet/in.h>
#include?<arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
只能處理IPv4的ip地址
注意參數(shù)是struct in_addr
現(xiàn)在:
#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 size);
支持IPv4和IPv6
其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr。
因此函數(shù)接口是void *addrptr。
sockaddr數(shù)據(jù)結(jié)構(gòu)
strcut sockaddr 很多網(wǎng)絡(luò)編程函數(shù)誕生早于IPv4協(xié)議,那時候都使用的是sockaddr結(jié)構(gòu)體,為了向前兼容,現(xiàn)在sockaddr退化成了(void *)的作用,傳遞一個地址給函數(shù),至于這個函數(shù)是sockaddr_in還是sockaddr_in6,由地址族確定,然后函數(shù)內(nèi)部再強制類型轉(zhuǎn)化為所需的地址類型。
sockaddr數(shù)據(jù)結(jié)構(gòu)
struct?sockaddr?{
??sa_family_t?sa_family; ? ??/* address family, AF_xxx */
??char?sa_data[14]; ? ? ?/* 14 bytes of protocol address */
};
使用 sudo grep -r "struct sockaddr_in {" ?/usr 命令可查看到struct sockaddr_in結(jié)構(gòu)體的定義。一般其默認(rèn)的存儲位置:/usr/include/linux/in.h 文件中。
struct?sockaddr_in {
? __kernel_sa_family_t sin_family; ? ? ??/* Address family */? ? 地址結(jié)構(gòu)類型
? __be16 sin_port; ? ? ? ? ? ? ??/* Port number */? ? 端口號
??struct?in_addr sin_addr; ? ? ? ? ?/* Internet address */? IP地址
??/* Pad to size of `struct sockaddr'. */
??unsigned?char?__pad[__SOCK_SIZE__ -?sizeof(short?int) -
??sizeof(unsigned?short?int) -?sizeof(struct?in_addr)];
};
struct?in_addr { ? ? ? ? ? ?/* Internet address. */
? __be32 s_addr;
};
struct?sockaddr_in6 {
??unsigned?short?int?sin6_family; ? ??/* AF_INET6 */
? __be16 sin6_port; ? ? ? ? ??/* Transport layer port # */
? __be32 sin6_flowinfo; ? ? ? ??/* IPv6 flow information */
??struct?in6_addr sin6_addr; ? ? ?/* IPv6 address */
? __u32 sin6_scope_id; ? ? ? ??/* scope id (new in RFC2553) */
};
struct?in6_addr {
??union?{
? ? __u8 u6_addr8[16];
? ? __be16 u6_addr16[8];
? ? __be32 u6_addr32[4];
? } in6_u;
??#define?s6_addr ? ? in6_u.u6_addr8
? #define?s6_addr16 ? in6_u.u6_addr16
? #define?s6_addr32 ? ? in6_u.u6_addr32
};
#define?UNIX_PATH_MAX 108
? struct sockaddr_un {
? __kernel_sa_family_t sun_family; ??/* AF_UNIX */
? char sun_path[UNIX_PATH_MAX]; ??/* pathname */
};
IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結(jié)構(gòu)體表示,包括16位端口號和32位IP地址,IPv6地址用sockaddr_in6結(jié)構(gòu)體表示,包括16位端口號、128位IP地址和一些控制字段。
UNIX Domain Socket的地址格式定義在sys/un.h中,用sock-addr_un結(jié)構(gòu)體表示。各種socket地址結(jié)構(gòu)體的開頭都是相同的,前16位表示整個結(jié)構(gòu)體的長度(并不是所有UNIX的實現(xiàn)都有長度字段,如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è)計成void *類型以便接受各種類型的指針,但是sock API的實現(xiàn)早于ANSI C標(biāo)準(zhǔn)化,那時還沒有void *類型,因此這些函數(shù)的參數(shù)都用struct sockaddr *類型表示,在傳遞參數(shù)之前要強制類型轉(zhuǎn)換一下,例如:
struct?sockaddr_in servaddr;
bind(listen_fd, (struct?sockaddr *)&servaddr,?sizeof(servaddr)); ? ?/* initialize servaddr */