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

引言

前一章中講了經典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 - 固定長度的、無鏈接的、不可靠的報文傳輸dom

  3. SOCKET_RAW - IP協議的數據報接口異步

參數protocol一般是0,表示爲給定的域和類型選擇默認協議,通常狀況下,都只支持單協議,當域和套接字支持多協議的時候,可使用protocol參數給定一個特定協議,每一個系統都有本身實現的協議,在蘋果系統下,能夠經過查看/etc/protocols文件來查詢具體的協議。
其中,用的最多的就是TCP和UDP協議,也就是SOCK_DGRAMSOCK_STREAM,當使用數據報的時候,不須要鏈接創建,所以數據報是一種面向無鏈接的服務,而字節流會要求在交換數據以前創建鏈接,因此這是面向鏈接的服務。
對於一個用完的套接字,可使用shutdown函數禁止IOsocket

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的範疇了。而屬於網絡通訊的基本原理。this

套接字和地址綁定

可能有一些朋友已經學過有關於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操做不會阻塞的時候發送信號,從而進程獲得了阻塞非阻塞的狀況。

相關文章
相關標籤/搜索