第5章 Linux網絡編程基礎API
探討Linux網絡編程基礎API與內核中TCP/IP協議族之間的關系,並未后續章節提供編程基礎。從3個方面討論Linux網絡API.
-
socket地址API。socket 最開始的含義是一個IP地址和端口對(ip, port)。它唯一地表示了使用TCP通信的一端。本書稱其為socket地址。
-
socket基礎API。socket的主要API都定義在 sys/socket.h 頭文件中,包括創建socket、命名socket、監聽socket、接受連接、發起連接、讀寫數據、 獲取地址信息、檢測帶外標記,以及讀取和設置socket選項。
-
網絡信息API。Linux 提供了一套網絡信息API,以實現主機名和IP地址之間的轉換,以及服務名稱和端口號之間的轉換。 這些API都定義在 netdb.h 頭文件中,我們將討論其中幾個主要的函數。
1.主機字節序和網絡字節序
大端字節序,也稱網絡字節序。網絡上傳輸的數據,都是網絡字節序的。
#include <stdio.h> void byteorder() { union MyUnion { short value; char union_bytes[sizeof(short)]; } test; test.value = 0x0102; if ((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2)) { printf("Big endian. \r\n");// **網絡字節序** ,大端對齊(高位在前面) } else if ((test.union_bytes[1] == 1) && (test.union_bytes[0] == 2)) { printf("little endian. \r\n");//主機字節序,小端對齊 } else { printf("unkonwn...\r\n"); } }
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 means "host to network long".主機字節序數據轉為網絡字節序數據。
2.通用socket地址
socket網絡編程接口中表示socket地址的結構體 sockaddr,其定義如下:
#include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14]; };
Linux定義了新的通用socket地址結構體,而且還是內存對齊的(__ss_aligin成員的作用)
#include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_aligin; char __ss_padding[128-sizeof(__ss_aligin)]; };
3.專用socket地址
Linux為各個協議族提供了專門的socket地址和結構
#include <sys/un.h> struct sockaddr_un { sa_family_t sin_family; //地址族:AF_UNIX char sun_path[108]; //文件路徑名 };
IPv4的專用socket地址結構體 sockaddr_in
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地址,要用網絡字節序表示 };
IPv6的專用socket地址結構體 sockaddr_in6
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地址(以及sockaddr storage)類型的變量 在實際使用時都要轉化為通用socket地址類型 sockaddr (強制轉換即可), 因為 所有socket編程接口使用的地址參數的類型都是sockaddr。
4.IP地址轉換函數
僅適用於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不可重入,非線程安全,該函數內部用一個靜態變量存儲轉化結果, 函數返回值指向該靜態內存。
同時適用於IPv4和IPv6地址
#include <arpa/inet.h> int inet_pton(int af, const char* src, void* src); 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函數進行相反的轉換,前3個參數的含義與inet_pton的參數相同,最后一個參數cnt 指定目標存儲單元大小。下面的2個宏可以幫助我們指定這個大小(分別用於IPv4和IPv6)
- inet_ntop成功時返回目標存儲單元的地址,失敗則返回NULL並設置errno。
#include <netinet/in.h> #define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
舉個IP地址轉換的例子
address.sin_port = htons(port);//little to big inet_pton(AF_INET, ip, &address.sin_addr); char dest[100] ; inet_ntop(AF_INET, &peerHost.sin_addr,dest,100);
5.創建socket
UNIX/Linux的一個哲學是:所有的東西都是文件。socket也不例外,它就是可讀、可寫、 可控制、可關閉的文件描述符。下面的socket系統調用可創建一個socket:
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); eg: int sock = socket(PF_INET, SOCK_STREAM, 0);
domain 參數告訴系統使用哪個底層協議族
type 參數指定服務器類型(SOCK_STREAM, SOCK_DGRAM)
protocol 參數是在前面兩個參數構成的協議集合下,再選擇一個具體協議。通常為0,使用默認協議。
- socket系統調用成功時返回一個socket文件描述符,失敗則返回-1,並設置errno。
6.命名socket
創建socket時,我們給它指定了地址族,但是並未指定使用該地址族中的哪個具體socket地址。 將一個socket與socket地址綁定成為給socket命名。
在服務器程序中,我們通常要命名socket,因為只有命名之后客戶端才能知道該如何連接它。 客戶端則通常不需要命名socket,而是采用匿名方式,即使用操作系統自動分配的socket地址。
命名socket的系統調用時 bind,其定義如下:
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
bind將my_addr所指的socket地址分配給未命名的sockfd文件描述符,addrlen參數支出該socket地址的長度。
- bind 成功時返回0,失敗則返回-1並設置errno。
7.監聽socket
socket被命名之后,還不能馬上接受客戶端連接,我們需要使用如下系統調用來創建一個監聽隊列以存放待處理的客戶端連接:
#include <sys/socket.h> int listen(int sockfd, int backlog);
socket參數指定被監聽的socket。
backlog參數提示內核監聽隊列的最大長度。
- listen成功時返回0,失敗則返回-1,並設置errno。
半連接狀態:SYN_RCVD 完全連接狀態:ESTABLISHED
8.接受連接
下面的系統調用從listen監聽隊列中接受一個連接:
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd參數是執行過listen系統調用的監聽socket。
addr參數用來獲取被接受連接的遠端socket地址,該socket地址的長度由addrlen參數指出。
accept成功時返回一個新的連接socket,該socket唯一地標識了被接受的這個連接, 服務器可以通過讀寫該socket來與被接受連接對應的客戶端通信。
- accept失敗時返回-1,並設置errno。
9.發起連接
如果說服務器通過listen調用來被動接受連接,那么客戶端需要通過如下系統調用來主動與服務器建立連接:
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
sockfd參數由socket系統調用返回一個socket
serv_addr參數是服務器監聽的socket地址
addlen參數則指定這個地址的長度
一旦成功建立連接,sockfd 就唯一表示了這個連接,客戶端就可以通過讀寫sockfd來與服務器通信。
- connect成功時返回0。失敗則返回-1,並設置errno。
10.關閉連接
關閉一個連接實際上就是關閉該連接對應的socket,這可以通過如下關閉普通文件描述符的系統調用來完成:
#include <unistd.h> int close(int fd);
fd參數是待關閉的socket。
不過,close系統調用並非總是立即關閉一個連接,而是將fd的引用計數減1.只有當fd的引用計數為0時,才真正關閉連接。 在多進程中,一次fork系統調用默認將是父進程中打開的socket的引用計數加1,因此必須在父進程和子進程中 都對該socket執行close調用才能真正將連接關閉。
無論如何都要立即終止連接(而不是將socket的引用計數減1),可以使用如下的shutdown系統調用:
#include <sys/socket.h> int shutdown(int sockfd, int howto);
sockfd參數是待關閉的socket。
howto參數決定了shutdown的行為。
- shutdown成功時返回0,失敗則返回-1,並設置errno。
11.TCP數據讀寫
對文件的讀寫操作read和write同樣適用於socket。但是socket編程接口提供了幾個專門用於socket數據讀寫的系統調用, 它們增加了對數據讀寫的控制。 其中適用於TCP流數據讀寫的系統調用是:
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv讀取sockfd上的數據, buf和len參數分別制定讀寫緩沖區的位置和大小, flags參數通常設置為0。 recv可能返回0,這意味着通信對方已經關閉連接了。 recv讀取到的數據可能小於期望的長度,因此可能需要多次調用recv,才能讀取到完整的數據。
- recv 成功時返回實際讀取到的數據的長度,出錯時返回-1,並設置errno。
send往sockfd上寫入數據, buf和len參數分別指定寫緩沖區的位置和大小。 flags參數為數據收發提供了額外的控制(MSG_MORE)
- send成功時返回寫入的數據長度,失敗則返回-1,並設置errno。
12.UDP數據讀寫
socket編程接口中用於UDP數據報讀寫的系統調用是:
#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen); ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t* addrlen);
recvfrom讀取sockfd上的數據,buf和len參數分別指定讀緩沖區的位置和大小。
UDP通信沒有連接的概念,每次讀取數據都需要獲取發送端的socket地址,即參數src_addr所指的內容,addrlen參數則指定該地址的長度。
13.通用數據讀寫函數
socket編程接口還提供了一對通用的數據讀寫系統調用。它們不僅能用於TCP流數據,也能用於UDP數據報:
#include <sys/socket.h> ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags); ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
sockfd參數指定被操作的目標socket。 msg參數是msghdr結構體類型的指針 falgs與前面recv,send的相同。
msghdr結構體定義如下:
struct msghdr { void* msg_name; //socket地址 socklen_t msg_namelen; //socket地址的長度 struct iovec* msg_iov; //****分散的內存塊 ***** int msg_iovlen; //分散內存塊的數量 void* msg_control; //指向輔助數據的起始位置 socklen_t msg_controllen; //輔助數據的大小 int msg_flags; //復制函數中的flags參數,並在調用過程中更新 };
msg_iov成員是iovec結構體類型的指針,iovec結構體定義如下:
struct iovec { void *iov_base;//內存塊起始地址 size_t iov_len;//這塊內存的長度 };
14.帶外標記
內核通知應用程序帶外數據到達的兩種常見方式是:I/O復用產生的異常事件和SIGURG信號。
#include <sys/socket.h> int sockatmark(int sockfd);
sockatmark判斷sockfd是否處於帶外標記,即下一個被讀取到的數據是否是帶外數據。
如果是,sockatmark返回1,此時我們就可以利用MSG_OOB標志的recv調用來接收帶外數據。 如果不是,則sockatmark返回0。
15.地址信息函數
在某些情況下,我們想知道一個連接socket的本端socket地址, 以及遠端的socket地址。 下面這2個函數正是用於解決這個問題:
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len); int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
getsockname獲取sockfd對應的本端socket地址,並將其存儲於address參數 指定的內存中,該socket地址的長度則存儲於address_len參數指向的變量中。 如果實際socket地址的長度大於address所指內存的大小, 那么該socket地址將被截斷。
- getsockname成功時返回0,失敗返回-1,並設置errno。
getpeername獲取sockfd對應的遠端socket地址, 其參數及返回值的含義與getsockname的參數及返回值相同。
16.socket選項
如果說fcntl系統調用是控制文件描述符屬性的通用POSIX方法, 那么下面兩個系統調用則是專門用來讀取和設置socket文件描述符屬性的方法:
#include <sys/socket.h> int getsockopt(int sockfd, int level, int option_name, void* option_value,socklen_t* restrict option_len); int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);
sockfd參數指定被操作的目標socket。 level參數指定要操作哪個協議的選項(即屬性),比如IPv4、IPv6、TCP等。 option_name參數則指定選項的名字。 option_value和option_len參數分別是被操作選項的值和長度。
- getsockopt和setsockopt這兩個函數成功時返回0,失敗時返回-1並設置errno。
對服務器而言,有部分socket選項要在監聽(listen)前針對監聽socket設置才有效。 對客戶端而言,這些socket選項則應在調用connect函數之前設置, 因為connect調用成功之后,TCP三次握手已完成。
17.SO_REUSEADDR選項
服務器程序可以通過設置socket選項SO_REUSEADDR來強制使用 被處於TIME_WAIT狀態的連接占用的socket地址。
//重用本地地址 const char* ip = argv[1]; int port = atoi( argv[2] ); int sock = socket( PF_INET, SOCK_STREAM, 0 ); assert( sock >= 0 ); int reuse = 1; setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) ); struct sockaddr_in address; bzero( &address, sizeof( address ) ); address.sin_family = AF_INET; inet_pton( AF_INET, ip, &address.sin_addr ); address.sin_port = htons( port ); int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) ); assert( ret != -1 );
此外,我們也可以通過修改內核參數 /proc/sys/net/ipv4/tcp_tw_recycle 來 快速回收被關閉的socket,從而使得TCP連接根本不進入 TIME_WAIT狀態, 進而允許應用程序立即重用本地的socket地址。
18.SO_RCVBUF和SO_SNDBUF選項
SO_RCVBUF和SO_SNDBUF選項分別表示TCP接收緩沖區和發送緩沖區的大小。 不過,當我們用setsockopt來設置TCP的接收緩沖區和發送緩沖區的大小時, 系統都會將其值加倍,並且不得小於某個最小值。
此外,我們可以直接修改內核參數 /proc/sys/net/ipv4/tcp_rmem 和 /proc/sys/net/ipv4/tcp_wmen 來強制 TCP接收緩沖區和發送緩沖區的帶下沒有最小值限制。
19.SO_RCVLOWAT和SO_SNDLOWAT選項
SO_RCVLOWAT和SO_SNDLOWAT選項分別表示TCP接收緩沖區 和發送緩沖區的低水位標記。 它們一般被I/O復用系統調用,用來判斷socket是否可讀或可寫。
默認情況下,TCP接收緩沖區的低水位標記和TCP發送緩沖區的低水位標記均為1字節。
20.SO_LINGER選項
SO_LINGER選項用於控制close系統調用在關閉TCP連接時的行為。 默認情況下,當我們使用close系統調用來關閉一個socket時, close將立即返回,TCP模塊負責把該socket對應的TCP發送緩沖區 中殘留的數據發送給對方。
#include <sys/socket.h> struct linger { int l_onoff;//開啟(非0)還是關閉(0)該選項 int l_linger;//滯留時間 };
21.gethostbyname和gethostbyaddr
gethostbyname 函數根據主機名稱獲取主機的完整信息, gethostbyaddr函數根據IP地址獲取主機的完整信息。 gethostbyname函數通常先在本地的 /etc/hsots配置的文件中查找主機, 如果沒有找到,再去訪問DNS服務器。
這兩個函數定義如下:
#include <netdb.h> struct hostent* gethostbyname(const char* name); struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
hostent結構體定義如下:
#include <netdb.h> struct hostent { char* h_name; //主機名 char** h_aliases; //主機別名列表,可能有多個 int h_addrtype; //地址類型(地址族) int h_length; //地址長度 char** h_addr_list;//按網絡字節序列出的主機IP地址列表 };
22.getservbyname和getservbyport
getservbyname函數根據名稱獲取某個服務的完整信息, getsrvbyport函數根據端口號獲取某個服務的完整信息。 他們實際上都是通過讀取 /etc/services 文件來獲取服務信息的。
#include <netdb.h> struct servent* getservbyname(const char* name, const char* proto); struct servent* getsrvbyport(int port, const char* proto);
name參數指定目標服務器的名字,port參數指定目標服務對應的端口號, proto參數指定服務類型。
結構體servent定義如下:
#include <netdb.h> struct servent { char* s_name; //服務名稱 char** s_aliases; //服務的別名列表,可能有多個 int s_port; //端口號 char* s_proto; //服務類型,通常是tcp或者udp };
23.getaddrinfo
getaddrinfo函數既能通過主機名獲取ip地址(內部使用gethostbyname)也能 通過服務名獲得端口號(內部使用getservbyname)。
#include <netdb.h> int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result)
hostname參數可以接收主機名,也可以接收字符串表示的IP地址(IPv4用點分十進制 字符串,IPv6用十六進制字符串)。 同樣,service參數可以接收服務名,也可以接收字符串表示的十進制端口號。 hints參數是應用程序給getaddrinfo的一個提示,一對getaddrinfo的輸出進行更精確的控制。 result參數指向一個鏈表,該鏈表用於存儲getaddrinfo反饋的結果。
- getaddrinfo成功返回0,失敗返回錯誤碼
getaddrinfo反饋的每一條結果都是addrinfo結構體類型的對象, 結構體addrinfo定義如下:
#include <netdb.h> struct addrinfo { int ai_flags; // int ai_family;//地址族 int ai_socktype;//服務類型,SOCK_STREAM 或 SOCK_DGRAM int ai_protocol;// socklent_t ai_addrlen;// socket地址 ai_addr的長度 char* ai_canonname;//主機的別名 struct sockaddr* ai_addr; //指向socket地址 struct addrinfo* ai_next; //指向下一個sockinfo結構的對象 }; //使用 getaddrinfo 函數 struct addrinfo hints; struct addrinfo* res; bzero(&hints, sizeof(hints)); hints.ai_socktype = SOCK_STREAM; getaddrinfo("ernest-laptop", "daytime", &hints, &res);
getaddrinfo將隱式地分配堆內存(可通過valgrind工具查看), 因為res指針原本沒有指向一塊合法內存的, 所以,getaddrinfo調用結束后,必須使用如下配對函數來釋放這塊內存:
#include <netdb.h> void freeaddrinfo(struct addrinfo* res);
24.getnameinfo
getnameinfo函數能通過socket地址同時獲得以字符串表示的主機名(內部使用gethostbyaddr函數)和服務名(內部使用getservbyport函數)。
#include <netdb.h> int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
getnameinfo將返回的主機名存儲在host參數指向的緩存中, 將服務名存儲在serv參數指向的緩存中, hostlen和servlen參數分別指定這兩塊緩存的長度。 flags參數控制getnameinfo的行為。
- getnameinfo成功返回0,失敗返回錯誤碼
25.錯誤碼
Linux下strerror函數能將數值錯誤碼errno轉換成易讀的字符串形式。 同樣,下面的函數可將表5-8(getaddrinfo和getnameinfo的錯誤碼)的錯誤碼轉換成其字符串形式:
#include <netdb.h> const char* gai_strerror(int error);
更多內容:請訪問https://github.com/liangzai90/MySocketLearning