UNIX IPC工具使用總結 裏介紹了socket
是一種用於通信的IPC工具。它允許位於同一主機
或跨主機
上的應用程序之間交換數據。第一個被廣泛接受的socket API實現於1983年,出現在4.2BSD中,實際上這組API已經被移植到了所有UNIX實現以及大多數操作系統上。
概述
在一個典型的C/S場景中,應用程序使用socket進行通信的方式如下:
- 各個應用程序創建一個socket。socket是一個允許通信的“設備”,兩個應用程序都需要用到它。
- 服務器將自己的socket綁定到一個衆所周知的地址上,使得客戶端能夠定位到它的位置。
// 使用socket系統調用,能夠創建一個socket,它返回一個用來在後續系統調用中引用該socket的文件描述符
fd = socket(domain, type, protocol);
通信domain
socket存在於一個通信domain中,它確定:
- 識別出一個socket的方法,即,socket“地址”的格式。
- 通信範圍,即,在位於同一主機上的應用程序之間,還是位於跨主機的應用程序之間。
現代操作系統至少支持下列domain:
- UNIX(AF_UNIX)domain
允許在同一主機上的應用程序之間進行通信。
POSIX.1g使用
AF_LOCAL
作爲AF_UNIX
的同義詞,但是SUSv3並沒有使用這個名稱。
IPv4(AF_INET)domain
允許在使用IPv4
網絡連接起來的主機上的應用程序之間進行通信。IPv6(AF_INET6)domain
允許在使用IPv6
網絡連接起來的主機上的應用程序之間進行通信。
儘管IPv6被設計成了IPv4的接任者,但目前後一種協議仍然是使用最廣的協議。
總結
domain | 執行的通信 | 應用程序間的通信 | 地址格式 | 地址結構 |
---|---|---|---|---|
AF_UNIX | 內核中 | 同一主機 | 路徑名 | sockaddr_un |
AF_INET | IPv4 | IPv4連接起來的主機 | 32位IPv4地址 + 16位端口號 | sockaddr_in |
AF_INET6 | IPv6 | IPv6連接起來的主機 | 128位IPv6地址 + 16位端口號 | sockaddr_in6 |
socket類型
每個socket實現都至少提供了兩種socket
:流和數據報。這兩種socket類型在UNIX和Internet domain中都得到了支持。
屬性 | 流 | 數據報 |
---|---|---|
可靠的傳遞? | YES | NO |
消息邊界保留? | NO | YES |
面向連接? | YES | NO |
流socket(SOCK_STREAM)
流socket提供了一個可靠的雙向的字節流通信信道
。
可靠的
表示可以保證發送者傳輸的數據會完整無缺地到達接收應用程序(假設網絡連接和接收者都不會崩潰),或收到一個傳輸失敗的通知。雙向的
表示數據可以在兩個socket之間的任意方向上傳輸。字節流
表示與管道一樣不存在消息邊界的概念。
一個
流socket
類似於使用一對允許在兩個應用程序之間進行雙向通信的管道
,它們之間的差別在於socket(Internet domain)允在在網絡上進行通信。
數據報socket(SOCK_DGRAM)
數據報socket允許數據以被稱爲數據報的消息的形式進行交換。在數據報socket中,消息邊界
得到了保留,但是數據傳輸是不可靠的
,消息的到達可能是無序的,重複的,或者根本就無法達到。
數據報socket是更一般的無連接socket。與流socket不同,一個數據報socket在使用時無需與另一個socket連接。
注意:數據報socket可以與另一個socket連接,但其語義與連接的流socket是不同的。
對比
在Internet domain中:
類型 | 協議 | 名稱 |
---|---|---|
流socket | 傳輸控制協議TCP | TCP socket |
數據報socket | 數據報協議UDP | UDP socket |
socket系統調用
關鍵的socket系統調用包括:
系統調用 | 描述 |
---|---|
socket() | 創建一個新的socket |
bind() | 將一個socket綁定到一個地址上。通常,服務器需要使用這個調用來將其socket綁定到一個衆所周知的地址上,使得客戶端能夠定位到該socket上。 |
listen() | 允許一個流socket接收來自其他socket的接入連接。 |
accept() | 在一個監聽socket上接收來自一個對等應用程序的連接,並可選地返回對等socket的地址。 |
connet() | 建立與另一個socket之間的連接 |
在大多數Linux架構上(除了Alpha和IA-64),所有這些socket系統調用實際上被實現成了通過單個系統調用
socketcall()
進行多路複用的庫函數(最初的時候)。但是,上述函數都被稱爲系統調用,是因爲它們在最初的BSD實現,以及其他很多同時代的UNIX實現上,是被作爲系統調用實現的。
socket I/O可以使用傳統的read()
和write()
系統調用使用,或使用一組socket特有的系統調用,如,send()
,recv()
,sendto()
,recvfrom()
來完成。
注意:在默認情況下,這些系統調用在I/O操作無法被立即完成時會阻塞
。通過使用fcntl()
的F_SETFL
操作來啓用O_NONBLOCK
打開文件狀態標記可以執行非阻塞I/O。
流socket注意事項
流socket的運作與電話系統類似。具體流程可以參考TCP連接建立和終止及TCP狀態轉換 。在大多數使用流socket的應用程序中,服務器會執行被動打開,而客戶端會執行主動式打開。
#include <sys/socket.h>
// return file descriptor on success, or -1 on error
int socket(int domain, int type, int protocol);
// return 0 on success, or -1 on error
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 通用的地址結構(所有domain特定的地址結構模版),用途是,將各種domain特定的地址結構轉換成單個類型,以供socket系統調用中的各個參數適用
struct sockaddr {
sa_family_t sa_family; /* Address family (AF_* constant) */
char sa_data[14]; /* Socket address (size varies according to socket domain) */
}
// return 0 on success, or -1 on error
int listen(int sockfd, int backlog);
// return file descriptor on success, or -1 on error
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// return 0 on success, or -1 on error
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- socket()接口中,protocol參數通常爲0,在裸socket(SOCK_RAW)中,會將protocol指定爲IPPROTO_RAW。
從內核2.6.27開始,Linux爲type參數提供了第二種用途,即允許兩個非標準的標記與socket類型取OR。
除了將一個服務器的socket綁定到一個衆所周知的地址,對於一個Internet domain socket來講,服務器可以不調用bind()而直接調用listen(),這將會導致內核爲該socket選擇一個
臨時端口
。之後服務器可以使用getsockname()
來獲取socket的地址。無法在一個已連接的socket(即,已經成功執行connect()的socket,或由accept()返回的socket)上執行listen()。
理解
backlog參數
的用途。
客戶端可能會在服務器調用accept()之前調用connect(),這將會產生一個未決的連接。內核必須要記錄所有未決的連接請求的相關信息,這樣後續的accept()就能夠處理這些請求。backlog參數允許
限制
這種未決連接的數量。在這個限制內的連接請求會立即成功,之外的連接請求就會阻塞直到一個未決的連接被accept()接受,並從未決連接隊列刪除爲止。SUSv3規定實現應該通過在
<sys/socket.h>
中定義SOMAXCONN
常量來發布這個限制。在Linux上,這個常量的值被定義爲128
。但是,從2.4.25內核起,Linux允許在運行時通過/proc/sys/net/core/somaxconn
文件來調整這個限制。在最初的BSD socket實現中,backlog的上限是5,並且在較早的代碼中可以看到這個數值。但是,所有現代實現允許爲backlog指定更高的值,這對於使用TCP socket服務大量客戶的網絡服務器來講是由必要的。
- accept()通過文件描述符sockfd監聽流socket上接受一個接入連接,如果在調用accept()時不存在未決的連接,那麼調用就會阻塞直到有連接請求到達爲止。傳入accept()的剩餘參數會返回對端socket的地址。如果不關心對等socket的地址,可以將addr和addrlen分別指定爲NULL和0(後續可以通過
getpeername()
)來獲取對端的地址。
理解accept的關鍵:它會創建一個新socket,並且正是這個新socket會與執行connect()的對等socket進行連接。accept()返回的函數結果是已連接的socket的文件描述符。
從內核2.6.28開始,Linux支持一個新的非標準系統調用accept4()
。這個系統調用執行的任務與accept()相同,但是,支持一個額外的參數flags
,而這個參數可以用來改變系統調用的行爲。目前系統支持兩個標記:SOCK_CLOEXEC
和SOCK_NONBLOCK
。
- 如果connect()失敗並且希望重新進行連接,那麼SUSv3規定完成這個任務的可移植的方法是關閉這個socket,創建一個新socket,在該新socket上重新進行連接。
數據報socket注意事項
數據報socket的運作類似於郵政系統。
// return number of bytes received, 0 on EOF, or -1 on error
ssize_t recvfrom(int sockfd, void *buffer, size_t length, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
// return number of bytes sent, or -1 on error
ssize_t sendto(int sockfd, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
不管length的參數值是什麼,recvfrom()只會從一個數據報socket中讀取一條消息。如果消息的大小超過了length字節,那麼消息會被靜默地截斷爲length字節。
在Linux上可以使用sendto()發送長度爲0的數據報,但不是所有的UNIX實現都允許這樣做。
儘管數據報socket是
無連接的
,但在數據報socket上應用connect()仍然是有效的
。在數據報socket上調用connect()會導致內核記錄這個socket()的對等socket的地址。
當一個數據報socket已連接之後:
1. 數據報的發送可在socket上使用write()或send()來完成,與sendto()一樣,每個write()會發送一個獨立的數據報。
2. 在這個socket上只能讀取由對等socket發送的數據報。
- 注意,connect()的作用對數據報socket是
不對稱的
。通過再發起一個connect()可以修改一個已連接的數據報socket的對等socket。
總結
爲了一個數據報socket設置一個對等socket,這種做法的一個明顯優勢是在該socket上傳輸數據時可以使用更簡單的I/O系統調用,即,無需使用指定了dest_addr和addrlen參數的sendto(),而只需要使用write()即可。設置一個對等socket主要對那些需要單個對等socket發送多個數據報的應用程序是比較有用的。
在一些TCP/IP實踐中,將一個數據報socket連接到一個對等socket能夠帶來性能上的提升。