[單刷 APUE 系列] 第十六章——網路 IPC:套接字

weixin_33797791發表於2016-10-09

引言

前一章中講了經典Unix程式間通訊,但是對於不同計算機的不同程式通訊是無法使用這種技術的,所以就有了網路間新程式通訊的機制。而網路套接字解釋一種非常實用的技術。程式將套接字繫結在埠上,通過該介面向其他程式通訊,這一章實際上是很重要的一章。

套接字描述符

就如同檔案描述符,套接字也有描述符,在檔案系統中,套接字也被認為是一種檔案,所以套接字描述符在Unix系統中也能被當做是一種檔案描述符。

int socket(int domain, int type, int protocol);

PF_LOCAL        Host-internal protocols, formerly called PF_UNIX,
PF_UNIX         Host-internal protocols, deprecated, use PF_LOCAL,
PF_INET         Internet version 4 protocols,
PF_ROUTE        Internal Routing protocol,
PF_KEY          Internal key-management function,
PF_INET6        Internet version 6 protocols,
PF_SYSTEM       System domain,
PF_NDRV         Raw access to network device

The socket has the indicated type, which specifies the semantics of communication.  Currently defined types are:

SOCK_STREAM
SOCK_DGRAM
SOCK_RAW複製程式碼

domain引數指定通訊將會發生的域,將會選擇即將使用的協議族。我們可以看到上面有8種協議族,其中PF_LOCALPF_UNIX的別名,並且PF_UNIX已經廢棄了,然後就是IPv4/6的協議,和其他幾個協議。
引數type將會更進一步確定套接字的型別,其中有3種:

  1. SOCKET_STREAM - 有序的、可靠地、雙向的、面向連線的位元組流
  2. SOCKET_DGRAM - 固定長度的、無連線的、不可靠的報文傳輸
  3. SOCKET_RAW - IP協議的資料包介面

引數protocol通常是0,表示為給定的域和型別選擇預設協議,一般情況下,都只支援單協議,當域和套接字支援多協議的時候,可以使用protocol引數給定一個特定協議,每個系統都有自己實現的協議,在蘋果系統下,可以通過檢視/etc/protocols檔案來查詢具體的協議。
其中,用的最多的就是TCP和UDP協議,也就是SOCK_DGRAMSOCK_STREAM,當使用資料包的時候,不需要連線建立,因此資料包是一種面向無連線的服務,而位元組流會要求在交換資料之前建立連線,所以這是面向連線的服務。
對於一個用完的套接字,可以使用shutdown函式禁止IO

int shutdown(int socket, int how);

The shutdown() call causes all or part of a full-duplex connection on the socket associated with socket to be shut down.  If how is SHUT_RD, further receives will be disallowed.  If how is SHUT_WR, further sends will be disallowed.  If how is SHUT_RDWR, further sends and receives will be disallowed.複製程式碼

如果how引數為SHUT_RD,則無法從套接字讀取資料,如果how是SHUT_WR,則無法向套接字寫入資料,前面講過,套接字描述符基本上可以認為是檔案描述符,那為什麼我們不用close函式關閉呢?因為套接字作為類似檔案描述符這樣的資源,是可以被複制的,我們講過,檔案描述符實際上是引用一個核心維護的連結串列檔案項,當複製的時候實際上支付至了檔案描述符本身,如果使用close函式,則必須要等到所有關聯到這個套接字的套接字描述符全部關閉才能真正關閉,而使用shutdown函式則可以無視描述符,直接操作檔案項,很方便的就能關閉其中一個方向。

定址

大家對網路通訊應該也已經有過一些粗淺的瞭解了,對於一個端對端的通訊,最重要的一步就是尋找目標位置,我們知道,TCP/IP協議包含了網路層和傳輸層,其中網路層是IP協議,而傳輸層是TCP協議、UDP協議和ICMP協議,IP地址是標誌了一臺主機的位置,而port部分則是標誌了傳輸層目標位置,也就是說,port是傳輸層對網路的封裝。我們知道,TCP/IP協議實際上是一個非常抽象良好的分層架構,每一層只對上一層負責,而無需瞭解上層內容,同時也遮蔽了上層對下層的瞭解,所以,有一些東西是需要注意的,比如位元組序、地址格式、地址查詢,這裡不再對其講解,因為筆者認為這已經超出了Unix的範疇了。而屬於網路通訊的基本原理。

套接字和地址繫結

可能有一些朋友已經學過有關於socket程式設計的內容了,socket程式設計對於伺服器和客戶端是不一樣的,伺服器需要固定一個埠,然後一直偵聽埠,客戶端則不需要偵聽固定埠,只需要在進行聯絡的時候隨意分配一個即可,所以這一小節實際上應當是屬於服務端開發的內容。我們可以使用bind函式來將套接字和地址繫結在一起

int bind(int socket, const struct sockaddr *address, socklen_t address_len);複製程式碼

對於socket程式設計,大家應該也有一些瞭解,比如非root許可權不能使用1024以內埠,一個程式只能使用一個埠,埠不能被多個程式使用。這裡的繫結規則就是和上面的差不多,

建立連線

對於客戶端來說,不需要固定使用一個埠,完全可以隨機分配,所以介面API也是不同的,一般使用connect函式來連線。

int connect(int socket, const struct sockaddr *address, socklen_t address_len);複製程式碼

原著關於這段的講解很煩,筆者這裡就直接講述自己的看法。socket引數是一個套接字,如果型別是SOCK_DGRAM,函式呼叫就會指定套接字關聯的對方的地址為address引數,並且在接收的時候只能接收此地址傳過來的資料。如果套接字是SOCK_STREAM型別,函式呼叫將會嘗試連線另一個套接字,另一個套接字通過address引數對應的地址連線,對於UDP資料包來說,可以多次呼叫這個函式用於改變對應地址,而TCP流則只能使用一次用於建立連線。
對於服務端程式來說,只需要呼叫listen命令偵聽套接字就行了。

int listen(int socket, int backlog);複製程式碼

原著上面關於backlog引數寫的非常迷,當然,可能是筆者看的是中文版的,所以翻譯很迷,實際上這個backlog引數是用來定義阻塞請求佇列的最大長度的,如果超出了這個範圍,就會有ECONNREFUSED提示。一旦伺服器呼叫listen,所用的套接字就能接收連線請求,使用accept函式獲得連線請求並建立連線。

int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);複製程式碼

我們可以看到,上面accept函式接收一個socket引數,一個address引數,一個address_len引數,其中,socket引數是一個已經建立的套接字描述符,並且使用bind函式將其繫結到了埠上,並且正在使用listen函式偵聽埠,accept函式取出請求佇列中的第一個請求,然後生成與socket引數相同屬性的一個套接字,並且為其分配一個新的檔案描述符。如果呼叫時候請求佇列沒有任何請求,並且套接字沒有被標記為非阻塞,則accept函式將會阻塞當前程式直到連線到來,而原始的socket引數套接字將會繼續偵聽埠。

資料傳輸

套接字屬於檔案描述符,那麼當套接字描述符存在的時候,就能使用read和write等檔案IO函式對其讀寫,這樣就能簡化操作。但是,如果想要做到更多的選項和操作,則必須使用socket庫提供的6個函式。

ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t sendmsg(int socket, const struct msghdr *message, int flags);
ssize_t sendto(int socket, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);

ssize_t recv(int socket, void *buffer, size_t length, int flags);
ssize_t recvmsg(int socket, struct msghdr *message, int flags);
ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);複製程式碼

我們可以看到,這六個函式是一一對應的,三個傳送函式,三個接收函式,我們先來觀察傳送函式的引數。send函式是一個通用的傳送函式,它能傳送任何的buffer資料,只需要開發者手動指定長度和flags傳送引數,而sendmsg則是使用msghdr結構體傳送資料,下面是msghdr的結構體內容

struct msghdr {
        void            *msg_name;      /* [XSI] optional address */
        socklen_t       msg_namelen;    /* [XSI] size of address */
        struct          iovec *msg_iov; /* [XSI] scatter/gather array */
        int             msg_iovlen;     /* [XSI] # elements in msg_iov */
        void            *msg_control;   /* [XSI] ancillary data, see below */
        socklen_t       msg_controllen; /* [XSI] ancillary data buffer len */
        int             msg_flags;      /* [XSI] flags on received message */
};複製程式碼

由於結構體能夠做到定長,所以也就不需要指定length引數,sendmsg實際上就是個send函式的變體。
通過對比send函式和write函式,我們發現,實際上send函式只是多了個flags引數,通過檢視蘋果系統Unix系統手冊,可以發現以下內容。

The flags parameter may include one or more of the following:

#define MSG_OOB        0x1  /* process out-of-band data */
#define MSG_DONTROUTE  0x4  /* bypass routing, use direct interface */

The flag MSG_OOB is used to send ``out-of-band'' data on sockets that support this notion (e.g.  SOCK_STREAM); the underlying protocol must also support ``out-of-band'' data.  MSG_DONTROUTE is usually used only by diagnostic or routing programs.複製程式碼

我們可以發現上面就列舉出了兩個常量,上面只寫了包括並不限於下面兩個值,所以我們來看看標頭檔案是怎麼定義的

#define MSG_OOB         0x1             /* process out-of-band data */
#define MSG_PEEK        0x2             /* peek at incoming message */
#define MSG_DONTROUTE   0x4             /* send without using routing tables */
#define MSG_EOR         0x8             /* data completes record */
#define MSG_TRUNC       0x10            /* data discarded before delivery */
#define MSG_CTRUNC      0x20            /* control data lost before delivery */
#define MSG_WAITALL     0x40            /* wait for full request or error */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define MSG_DONTWAIT    0x80            /* this message should be nonblocking */
#define MSG_EOF         0x100           /* data completes connection */
#ifdef __APPLE__
#ifdef __APPLE_API_OBSOLETE
#define MSG_WAITSTREAM  0x200           /* wait up to full request.. may return partial */
#endif
#define MSG_FLUSH       0x400           /* Start of 'hold' seq; dump so_temp */
#define MSG_HOLD        0x800           /* Hold frag in so_temp */
#define MSG_SEND        0x1000          /* Send the packet in so_temp */
#define MSG_HAVEMORE    0x2000          /* Data ready to be read */
#define MSG_RCVMORE     0x4000          /* Data remains in current pkt */
#endif
#define MSG_NEEDSA      0x10000         /* Fail receive if socket address cannot be allocated */
#endif  /* (!_POSIX_C_SOURCE || _DARWIN_C_SOURCE) */複製程式碼

上面就是蘋果系統標頭檔案的定義,確實比Unix系統手冊上講的多了非常多,但是也能推測出來,具體flags的實現實際上根據系統不同是不同的。
sendto函式跟send函式基本一樣,除了sendto函式能在一個無連線的套接字上面向指定目標傳送資料。
recv函式族基本和send函式族一樣,所以這裡就不再繼續講解了,有興趣的可以自己查詢Unix系統手冊和原著。

套接字選項

為了能讓開發者基礎套接字的程式設計,系統也會提供set、get函式,我們知道,套接字實際上是網路抽象模型,它能工作在任何協議上,而有些特定協議具有一些特殊行為,所以,套接字程式設計API也具有其特殊性。

int getsockopt(int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict option_len);
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);複製程式碼

這兩個函式操作socket關聯的選項,前面說過,套接字能工作在很多協議上,而一些特殊協議會有一些特殊選項,所以需呀使用level引數指定操作的級別,如果選項是通用套接字選項,則level設定為SOL_SOCKET,否則,level設定為控制這個選項的協議的編號。比如TCP協議則是IPPIPPROTO_TCP,下面是option_name可用的值

SO_DEBUG        enables recording of debugging information
SO_REUSEADDR    enables local address reuse
SO_REUSEPORT    enables duplicate address and port bindings
SO_KEEPALIVE    enables keep connections alive
SO_DONTROUTE    enables routing bypass for outgoing messages
SO_LINGER       linger on close if data present
SO_BROADCAST    enables permission to transmit broadcast messages
SO_OOBINLINE    enables reception of out-of-band data in band
SO_SNDBUF       set buffer size for output
SO_RCVBUF       set buffer size for input
SO_SNDLOWAT     set minimum count for output
SO_RCVLOWAT     set minimum count for input
SO_SNDTIMEO     set timeout value for output
SO_RCVTIMEO     set timeout value for input
SO_TYPE         get the type of the socket (get only)
SO_ERROR        get and clear error on the socket (get only)
SO_NOSIGPIPE    do not generate SIGPIPE, instead return EPIPE
SO_NREAD        number of bytes to be read (get only)
SO_NWRITE       number of bytes written not yet sent by the protocol (get only)
SO_LINGER_SEC   linger on close if data present with timeout in seconds複製程式碼

option_value根據option_name的不同指向不同的資料型別。

帶外資料

帶外資料可能翻譯不準確,原文是out-of-band data,也就是超範圍資料,熟悉網路基礎的朋友應該知道,各層會對上層資料封裝,比如使用限定字元將資料限定範圍,然後前後加上頭尾,組成一個封包,某些通訊協議支援帶外資料,允許其作為更高優先順序傳輸,至於具體內容,可以看原著講解,因為這小節實際上並不是特別重要。

非阻塞和非同步IO

recv在沒有資料可用的情況下會阻塞等待,而套接字沒有足夠空間傳送的情況下send也會阻塞等待。如果在套接字建立的時候指定非阻塞,行為就會改變。這樣函式就不會阻塞而是會直接返回失敗,並且設定errno。我們也知道,套接字描述符和檔案描述符基本可以等價,那麼我們是不是可以使用select和poll這種函式來判斷檔案描述符是否已經準備完畢。
前面講到過SUS標準實際上包含了非同步IO的內容,但是套接字實際上也是有著自己的一套非同步IO模型,也就是基於訊號的非同步IO模型。模型非常簡單,就是當套接字IO操作不會阻塞的時候傳送訊號,從而程式得到了阻塞非阻塞的情況。

相關文章