16.1 引言
本章將考察不同計算機(通過網絡連接)上的進程相互通信的機制:網絡進程間通信(network IPC)。
套接字網絡進程間通信接口,進程用該接口能夠和其他進程通信,無論他們是在同一臺計算機上還是在不同的計算機上。
16.2 套接字描述符
套接字是通信端點的抽象。正如使用文件描述符訪問文件,應用程序用套接字描述符訪問套接字。套接字描述符在unix系統中被當作是一種文件描述符。
因此能夠操作文件的函數也能作用於套接字。
使用socket函數創建一個套接字。
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
- 參數domain(域)確定通信的特性,每個域都有自己表示地址的格式,而表示各個域的常數都以AF_開頭。意指地址族(address family)。
- 參數type確定套接字的類型,進一步確定通信特徵。
- 參數protocol通常是0,表示爲給定的域和套接字選擇默認協議。
套接字通信是雙向的。可以採用shutdown函數來禁止一個套接字的io。
int shutdown(int sockfd,int how);
- 參數how可以是SHUT_RD(關閉讀)、SHUT_WR(關閉寫)、SHUT_RDWR(關閉讀寫)。
- 參數socked是套接字描述符。
既然能夠關閉(close)套接字,爲何還使用shutdown呢?
- 首先,只有最後一個活動引用關閉時,close才釋放網絡端點。這意味着,如果複製一個套接字(如採用dup),要直到關閉了最後一個引用它的文件描述符纔會釋放這個套接字(意思是,如果一個套接字描述符被多個進程引用,只有最後一個進程關閉引用,纔會釋放該套接字)。
- 而shutdown允許一個套接字處於不活動狀態,和引用它的文件描述符的數目無關。
- 其次,有時可以很方便地關閉套接字雙向傳輸中的一個方向。
16.3 尋址
上一節學習瞭如何創建和銷燬一個套接字。在學習用套接字做一些有意義的事情之前,需要知道如何標識一個目標通信進程。進程標識由兩部分組成:
- 計算機網絡地址:用於標識網絡上我們想與之通信的計算機。
- 端口號:計算機上用端口號表示服務,用於標識特定的進程。
16.3.1 字節序
同一臺計算機上的進程通信時,一般不用考慮字節序。字節序是一個處理器架構特性,用於指示像整形這樣的大數據類型在內存中字節是如何排序的。
對於一個數據,最高有效字節(Most significant Byre,MSB)總是在左邊,LSB總是在右邊。例如0x04030201,不管字節序如何,MSB總是04,LSB總是01。
而在不同的字節序架構上,存儲順序不同。
大端(big-endian),內存字節增長是從MSB開始的。
小端(little-endian),內存內存字節增長從LSB開始的。
下圖總結了4種平臺的字節序。
網絡協議指定了字節序,因此異構計算機系統能夠交換協議信息而不會被字節序所混淆。TCP/IP協議棧使用了大端字節序。
對於TCP/IP應用程序,有4個用來在處理器字節序和網絡字節序之間實施轉換的函數。
#include <arpa/inet.h>
uint32 htonl(uint32 hostint32);
uint16 htons(uint16 hostint16);
uint32 ntohl(uint32 hostint32);
uint16 ntohs(uint16 hostint16);
h表示“主機”字節序,n表示“網絡字節序”。l表示32位整數,s表示16位幀數。
16.3.2 地址格式
一個地址標識一個特定通信域的套接字端點,地址格式與這個特定的通信域相關。爲使不同格式地址能夠傳入到套接字函數,地址會被轉換成一個通用的地址結構sockaddr:
struct sockaddr{
sa_family_t sa_family;
char sa_data[];
...
//可以自由填充
};
IPv4因特網域(AF_INET)中,套接字地址用結構sockaddr_in
表示,IPv6網域用sockaddr_in6
表示,根據SUS的要求,每個實現都可以自由添加更多字段。
在linux中,sockaddr_in
定義如下
struct sockaddr_in{
sa_family_t sin_family; //網域
in_port_t sin_port; //端口號
struct in6_addr sin6_addr; //IPv4地址
unsigned char sin_zero[8]; //爲了讓sockaddr與sockaddr_in兩個數據結構保持大小相同而保留的空字節
};
sockaddr_in
和sockaddr_in6
最終都會轉換成sockaddr6
結構輸入到套接字例程中。
例如:
struct sockaddr_in mysock;
mysock.sin_family = AF_INET; //TCP地址結構
mysock.sin_port = htons(3490); //字節順序轉換函數(後面我會介紹的)
mysock.sin_addr.s_addr = inet_addr("166.111.160.10");//設置IP地址
bzero(&(mysock.sin_zero),8);//設置sin_zero爲8位保留字節
//如果mysock.sin_addr.s_addr = INADDR_ANY,則不指定IP地址(用於server程序)
有時候需要打印出能被人理解而不是計算機所理解的地址格式。inet_addr和inet_ntoa函數,用於二進制地址格式與點分十進制字符表示(a.b.c.d)之間的相互轉換。但這兩個函數僅適用於IPv4地址和IPv6地址。
#include <arpa/inet.h>
const char *inet_ntop(int domain,const void *restrict addr,char *restrict str,socklen_t size);
int inet_pton(int domain,const char *restrict str,void *restrict afddr);
16.3.3 地址查詢
理想情況下,應用程序不需要了解一個套接字地址的內部結構。
這裏引入一些函數來查詢地址信息。這些信息存放在靜態文件(如/etc/hosts和/etc/services)也可以由名字服務管理,如域名系統(DNS)或者(NIS),無論這些信號在何處,都可以用相同的函數訪問它們。
函數gethostent,可以找到給定計算機系統的主機信息。
#include <netdb.h>
struct hostent *gethostent(void);
void sethostent(int stayopen);
vid endhostent(void);
服務是由地址的端口號部分表示的。每個服務由一個唯一的衆所周知的端口號來支持。可以使用函數getservbyname將一個服務映射到一個端口號,可以使用getservbyport將一個端口號映射到一個服務名,使用函數getservent順序掃描服務數據庫。
…
…
…
16.3.4 將套接字與地址關聯
將一個客戶端的套接字關聯上一個地址不是必須的,因爲可以讓系統選一個默認的地址。
而對於服務器,需要一個衆所周知的地址。最簡單的方法就是服務器保留一個地址並且註冊在/etc/services或者某個名字服務器中。
是用bind函數來關聯地址和套接字。
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t len);
對於使用的地址有一下限制。
- 地址必須有效,不能和其他機器上的地址衝突。
- 地址必須和創建套接字時的地址所支持的格式相匹配。
- 地址中的端口號必須不小於1024,除非該進程具有相應的特權(即超級用戶)。
- 一般只能將套接字端點綁定到一個給定的地址上,儘管有些協議允許多重綁定。 -
getsockname函數來發現綁定到套接字上的地址。
int getsockname(int sockfd,struct sockaddr *restrict addr,socklen_t *restrict alenp);
- sockfd爲socket描述符。
- addr爲一個地址指針,得到地址後存在這裏。
- socklen_t 爲地址長度,指定了addr的大小。
如果套接字已經和對等方鏈接,可以使用getpeername函數來找到對方的地址。
int getpeername(int sockfd,struct sockaddr *restrict addr,socklen_t *restrict alenp);
16.4 建立連接
如果需要處理一個面向連接的網絡服務(SOCK_STREAM和SOCK_SEQPACKET),那麼在開始交貨數據以前,需要在請求服務的進程套接字(客戶端)和提供服務的套接字(服務器)之間建立一個連接。
使用connect函數來建立連接(從客戶端鏈接到服務器,服務器端不使用)。
int connect(int sockfd,const struct sockaddr *addr,socklen_t len);
- sockfd 爲本地套接字描述符。
- addr爲需要鏈接的服務器地址。
- len爲地址長度。
嘗試連接時可能會出錯,因此應用程序必須能處理connect返回的錯誤。
以下函數採用了指數補償(exponential backoff)算法。如果調用connect失敗,進程會休眠一小段時間,然後進入下次循環再次嘗試。直到最大延時爲2分鐘左右。
#define MAXSLEEP 120
int connect_retry(int sockfd,const struct sockaddr *addr,socklen_t len)
{
int numsec,fd;
/*
try to connect with exponential backoff
*/
for(numsec=1;numsec<=MAXSLEEP;numsec<<=1)
{
if((fd=socket(domain,type,protocol))<0)
return (-1);
if(connect(fd,addr,alen)==0)
{
return(fd);
}
close (fd); //不成功就關閉socket
if(numsec<=MAXSLEEP/2)
sleep(numsec);
}
return (-1);
}
如果套接字描述符處於非阻塞模式,那麼鏈接不能馬上建立,connect會返回-1並且將errno設置爲特殊的錯誤碼EINPROGRESS.應用程序可以使用poll或者select來判斷文件描述符何時可寫,如果可寫,連接完成。
connect函數還可以用於無連接的網絡服務(SOCK_DGRAM)。
如果用SOCK_DGRAM套接字調用connect,傳輸報文的目的地址會設置成connect調用中所指定的地址,這樣每次傳送報文時就不需要再提供地址。另外,只能接收來自指定地址的報文。
服務器調用listen函數來宣告它願意接受連接請求。
int listen(int sockfd,int backlog);
backlog參數指定了該進程所要入隊的未完成鏈接請求數量。系統限定不大於128.一旦隊列滿,系統就會拒絕多餘的鏈接請求,所以backlog的值應該基於服務器期望負載和處理量來選擇。
一旦服務器調用了listen,所用的套接字就能接收鏈接請求。使用accept函數獲得連接請求並建立連接。
int accept(int sockfd,struct sockaddr *restrict addr,aoklen_t *restrict len);
- 返回值爲一個新的套接字描述符,該描述符連接到了調用connect的客戶端。(一旦accept接收到就說明已經connect了)。
- sockfd並沒有改變,任然保持可用狀態並接收其他連接請求。
- addr爲客戶端的地址;
- len爲客戶端地址長度。
如果沒有連接請求在等待,accept會阻塞直到一個請求到來。如果sockfd處於非阻塞模式,accept返回-1,並設errno爲EAGAIN或EWOULDBLOCK.
服務器可用調用poll或select來等待一個請求的到來。在這種情況下,一個帶有等待連接請求的套接字會以可讀的方式出現。
16.5 數據傳輸
既然一個套接字端點表示爲一個文件描述符,那麼只有建立連接,就可以使用read或write來通過套接字通信。
但是read和write函數功能有限,有6個專門用於傳遞的函數。3個用於發送,3個用於接收。
#include <sys/socket.h>
ssize_t send(int sockfd,const void *buf,size_t nbytes,int flags);
ssize_t sendto(int sockfd,const void *buf,size_t nbytes,int flags,const struct sockaddr *destaddr,socklen_t destlen);
ssize_t sendmsg(int sockfd,const struct msghdr,int flags);
#include <sys/socket.h>
ssize_t recv(int sockfd,void *buf,size_t nbytes,int flags);
ssize_t recvfrom(int sockfd,void *restrict buf,size_t len,int flags,struct sockaddr *restrict addr,socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);
16.6 套接字選項
套接字機制提供了兩個套接字接口來控制套接字行爲。一個接口用來設置選項,另一個接口可以查詢選項狀態。可以獲取或設置以下3種選項。
- 通用選項,工作在所有套接字類型上。
- 在套接字層次管理的選項,每個協議獨有。
- 特定於某個協議的選項,每個協議獨有。
#include <sys/socket.h>
int setsockopt(int sockfd,int level,int option,const void *val,socklen_t *restrinct lenp);
參數level表示了選項應用的協議。
#include <sys/socket.h>
int getsockopt(int sockfd,int level,int option,void *restrict val,socklen_t *restrinct lenp);
參數lenp是一個整數指針,在調用getsockopt之前,設置該整數爲複製選項緩衝區的長度。如果選項實際長度大於此值,則選項會被截斷。如果實際長度小於此值,那麼返回時將此值更新此值爲實際長度。
16.7 帶外數據
帶外數據(out-of-band data)是一些通信協議所支持的可選功能,與普通數據相比,它允許更高優先級的數據傳輸。帶外數據先行傳輸,即使傳輸隊列已經有數據。TCP支持帶外數據,但是UDP不支持。
TCP將帶外數據稱爲緊急數據(urgent data)。TCP僅支持一個字節的緊急數據,但是允許緊急數據在普通數據傳遞機制數據流之外傳輸。爲了產生緊急數據,可以在3個send函數中的任意一個裏指定MSG_OOB標誌。如果帶MSG_OOB標誌發送的字節數超過一個時,最後一個字節將被視爲緊急數據字節。
在緊急數據被接收時,會發送SIGURE信號。
TCP支持緊急標記(urgent mark)的概念,即在普通數據流中緊急數據所在的位置。如果採用套接字選項SO_OOBINLINE,那麼可以在普通數據中接收緊急數據。
#inclide <sys/socket.h>
int sockatmark(int sockfd);
當一個要讀取的字節在緊急標誌處時,sockatmark返回1.
16.8 非阻塞和異步IO
通常,recv函數沒有數據可用時會阻塞等待。同樣地,當套接字輸出隊列沒有足夠空間來發送消息時,send函數會阻塞。
在套接字非阻塞模式下,行爲會改變。在這種情況下,這些函數不會阻塞而是會失敗,將errno設置爲EWOULDBLOCK或者EAGAIN.在這種情況下,可以使用poll或select來判斷能否接受或者傳送數據。
16.9 小結
本章考察了IPC機制,這些機制允許進程與不同計算機上的以及同一計算機上的其他進程通信。
- 討論了套接字端點如何命名,在鏈接服務器時,如何發現所有的地址。
- 各處了採用無連接的(即基於數據包的)套接字和面向連接的套接字的客戶端和服務器實例。
- 簡要討論了異步和非阻塞的套接字IO,以及用於管理套接字選項的接口。