一、什麼是socket
socket這個詞可以表示很多概念:
在TCP/IP協議中,“IP地址+TCP或UDP端口號”唯一標識網絡通訊中的一個進程,“IP地址+端口號”就稱爲socket。
在TCP協議中,建立連接的兩個進程各自有一個socket來標識,那麼這兩個socket組成的socket pair就唯一標識一個連接。socket本身有“插座”的意思,因此用來描述網絡連接的一對一關係。
TCP/IP協議最早在BSD UNIX上實現,爲TCP/IP協議設計的應用層編程接口稱爲socketAPI。
一下主要介紹socket API 和 TCP協議的函數接口。
二、網絡字節序
我們已經知道,內存中的多字節數據相對於內存地址有 大端 和小端 之分,磁盤文件中的多字節數據相對於文件中的偏移地址也有大端小端之分。網絡數據流同樣有大端小端之分,那麼如何定義網絡數據流的地址呢?發送主機通常將發送緩衝區中的數據按內存地址從低到高的順序發出,接收主機把從網絡上接到的字節依次保存在接收緩衝區中,也是按內存地址從低到高的順序保存,因此,網絡數據流的地址應這樣規定:先發出的數據是低地址,後發出的數據是高地址。
TCP/IP協議規定,網絡數據流應採用大端字節序,即低地址高字節。例如四十五節的UDP段格式,地址0-1是16位的源端口號,如果這個端口號是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發0x03,再發0xe8,這16位在發送主機的緩衝區中也應該是低地址存0x03,高地址存0xe8。但是,如果發送主機是小端字節序的,這16位被解釋成0xe803,而不是1000。因此,發送主機把1000填到發送緩衝區之前需要做字節序的轉換。同樣地,接收主機如果是小端字節序的,接到16位的源端口號也要做字節序的轉換。如果主機是大端字節序的,發送和接收都不需要做轉換。同理,32位的IP地址也要考慮網絡字節序和主機字節序的問題。
爲使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯後都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。
#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位長整數
s 表示16位短整數。
如果主機是小端字節序,這些函數將參數做相應的大小端轉換然後返回,如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。
三、IP地址轉換函數
早期使用的是一下函數:
#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地址
不可重入函數
注意參數是struct in_addr
現在多使用的是:
#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 不僅可以轉換IPv4的 in_addr,還可以轉換IPv6的 in6_addr,因此函數接口是void *addrptr。
四、sockaddr數據結構
很多網絡編程函數誕生早於IPv4協議,那時候都使用的是sockaddr結構體,爲了向前兼容,現在sockaddr退化成了(void *)的作用,傳遞一個地址給函數,至於這個函數是sockaddr_in還是sockaddr_in6,由地址族確定,然後函數內部再強制類型轉化爲所需的地址類型。
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx 地址族,格式爲AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address 14位協議地址*/
};
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__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)];
};
/* Internet address. */
struct in_addr {
__be32 s_addr; // 32位地址
};
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 ip地址128位*/
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
struct in6_addr {
union {
__u8 u6_addr8[16]; // 可以使用16個8位的地址
__be16 u6_addr16[8]; // 8個16位地址
__be32 u6_addr32[4]; // 4個32位地址
} 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結構體表示,包括16位端口號和32位IP地址,IPv6地址用sockaddr_in6結構體表示,包括16位端口號、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定義在sys/un.h中,用sock-addr_un結構體表示。各種socket地址結構體的開頭都是相同的,前16位表示整個結構體的長度(並不是所有UNIX的實現都有長度字段,如Linux就沒有),後16位表示地址類型。IPv4、IPv6和Unix Domain Socket的地址類型分別定義爲常數AF_INET、AF_INET6、AF_UNIX。這樣,只要取得某種sockaddr結構體的首地址,不需要知道具體是哪種類型的sockaddr結構體,就可以根據地址類型字段確定結構體中的內容。因此,socket API可以接受各種類型的sockaddr結構體指針做參數,例如bind、accept、connect等函數,這些函數的參數應該設計成void 類型以便接受各種類型的指針,但是sock API的實現早於ANSI C標準化,那時還沒有void 類型,因此這些函數的參數都用struct sockaddr *類型表示,在傳遞參數之前要強制類型轉換一下,例如:
struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));