在Linux系統中,有很多進程間通信方式,套接字(Socket)就是其中的一種。但傳統的套接字的用法都是基於TCP/IP協議棧的,需要指定IP地址。如果不同主機上的兩個進程進行通信,當然這樣做沒什么問題。但是,如果只需要在一台機器上的兩個不同進程間通信,還要用到IP地址就有點大材小用了。
其實很多人並不一定知道,對於套接字來說,還存在一種叫做Unix域套接字的類別,專門用來解決這個問題。其API的掉用方法基本上和普通TCP/IP的套接字一樣,只是有些許差別。
因此,再正式介紹之前,先來復習一下套接字從創建,到傳輸數據,再到最后關閉的所有過程:

上邊的圖代表面向流的套接字,對於TCP/IP套接字來說,代表TCP協議;下邊的圖代表面向數據包的套接字,對於TCP/IP套接字來說,代表UDP協議。接下來,我們將特別針對普通TCP/IP套接字和Unix域套接字的不同,對這些API做一些解釋。
1)socket()
最開始肯定還是要創建一個套接字:
- int socket (int domain, int type, int protocol);
API定義是一樣的,不過這里的第一個參數,也就是域一定要設置成AF_UNIX或AF_LOCAL,而不是普通TCP/IP套接字的AF_INET。第二個參數表示套接字的類型,分為流套接字(SOCK_STREAM)和數據包套接字(SOCK_DGRAM)。不同於普通的AF_INET的Socket,由於都是在本機通過內核通信,所以SOCK_STREAM和SOCK_DGRAM都是可靠的,不會丟包也不會出現發送包的次序和接收包的次序不一致的問題。它們的區別僅僅是,SOCK_STREAM無論發送多大的數據都不會被截斷,而對於SOCK_DGRAM來說,如果發送的數據超過了一個報文的最大長度,則數據會被截斷。而最后一個參數,表示協議,對於Unix域套接字來說,其一定是被設置成0。因此,一般通過下面的方式創建一個Unix域套接字:
- int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); // 流式Unix域套接字
- int sockfd = socket(AF_UNIX,SOCK_DGRAM, 0); // 數據包式套接字
2)bind()
對於流式套接字的服務器端來說,在用socket()函數獲得了新創建套接字的文件描述符之后,還要將其綁定到一個地址上去:
- int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在Unix域套接字中,套接字的地址是以sockaddr_un結構體來表示的,其結構如下:
- struct sockaddr_un {
- sa_family_t sun_family;
- char sun_path[108];
- }
結構體中的第一個字段必須要設置成“AF_UNIX”。而第二個字段,表示的是一個路徑名。因此,要將一個Unix域套接字綁定到一個本地地址上,需要創建並初始化一個sockaddr_un結構體,並將指向這個結構體的指針作為addr參數(需要類型轉換)傳入bind()函數,並將addrlen參數設置成這個結構體的實際大小。
這里還要特別提一下這個路徑名,其實還要分為兩種,一種是普通路徑名,另一種是抽象路徑名。
首先來說說普通路徑名,這個很好理解,就是一個基本的Linux文件路徑,其必須要以NULL('\0')結尾。在綁定一個Unix域套接字時,會在文件系統中的相應位置上創建一個文件,且這個文件的類型被標記為“Socket”,因此這個文件無法用open()函數打開。當不再需要這個Unix域套接字時,可以使用remove()函數或者unlink()函數將這個對應的文件刪除。如果在文件系統中,已經有了一個文件和指定的路徑名相同,則綁定會失敗(返回錯誤EADDRINUSE)。所以,一個套接字只能綁定到一個路徑上,同樣的,一個路徑也只能被一個套接字綁定。
接下來看看什么叫抽象路徑名,這其實是Linux特有的一個特性,它允許將一個Unix域套接字綁定到一個名字上,且不會在文件系統中創建這個名字的文件。如果要創建一個抽象名字空間的綁定,必須要將sun_path字段的第一個字節設置成NULL('\0'),而且和普通的文件系統名字空間不同的是,系統會用sun_path除第一個字節之后余下的所有字節當做抽象名字。也就是說在解析抽象路徑名時需要用到sun_path字段當中所有的字節,而不是像解析普通路徑名一樣,解析到第一個NULL就可以停止了。因為不會再在文件系統中創建文件了,所以對於抽象路徑名來說,就不需要擔心與文件系統中已存在的文件產生名字沖突的問題了,也不需要在使用完套接字之后刪除附帶產生的這個文件了,當套接字被關閉之后會自動刪除這個抽象名。
最后再提一下權限的問題,因為要在文件系統中創建相應的文件,對於普通路徑名來說,掉用bind()函數的進程必須要有路徑名中目錄部分的可寫和可訪問權限。還有,在默認情況下,在調用bind()函數時,會給所有者、組和其他用戶賦予所有的權限(即777),如果想改變這個行為,可以在bind()之后再修改創建的文件的權限和屬性。
3)listen()
對於流式套接字的服務器端來說,listen()函數在TCP/IP套接字和Unix域套接字中調用方式是一樣的,沒有區別:
- int listen(int sockfd, int backlog);
4)accept()
對於流式套接字的服務器端來說,在調用bind()綁定完本地路徑之后,還需要接收客戶端的請求,這是通過調用accept()函數來實現的:
- int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
與普通的TCP/IP套接字不同,Unix域套接字不存在客戶端地址的問題(都在一台機器上),因此這里的addr和addrlen參數都要設置成NULL。在這里,不同進程像這個服務器端進程發送的流數據是在內核里面區分的,並綁定到了accept()創建的套接字中了。而數據包套接字就沒有這種對應關系,所以還是要在代碼中區分出來,后面會介紹。
5)connect()
對於流式套接字的客戶端來說,在用socket()函數獲得了新創建套接字的文件描述符之后,就可以調用connect()函數連接服務器端了:
- int connect(int sockfd, struct sockaddr *addr,int addrlen);
這個函數在TCP/IP套接字中和在Unix域套接字中調用方式基本相同,只不過和bind()函數一樣,地址addr必須是以sockaddr_un結構體來表示。
6)read()和write()
對於流式套接字的服務器端來說,read()和write函數在TCP/IP套接字和Unix域套接字中調用方式是一樣的,沒有區別:
- ssize_t read(int sockfd, void *buf, size_t length);
- ssize_t write(int sockfd, const void *buf, size_t length);
7)recvfrom()和sendto()
對於數據包事套接字來說,在服務器端recvfrom()用來接收客戶端發送的請求,而在客戶端這個函數用來接收服務器端發送過來的響應:
- int recvfrom(int sockfd, void *buf, int length, unsigned int flags, struct sockaddr *addr, int *addrlen);
同時,在客戶端sendto()用來向服務器端發送請求數據,而服務器端用這個函數來向客戶端發送響應數據:
- int sendto (int sockfd, const void *buf, int length, unsigned int flags, const struct sockaddr *addr, int addrlen);
前面也提到了,對於數據包套接字來說,服務器端在發送響應數據時是需要知道客戶端到底是哪個的,從而后面可以將相應的響應數據發送給正確的客戶端。而客戶端也需要知道到底是向哪個服務器端發送數據,或者說接收到的響應數據到底來自哪個服務器端(當然,如果只保證和一個服務器端通信就沒有這個問題)。
但是,按照普通的包套接字創建和連接的流程,只是在服務器端掉用bind()函數綁定了一個地址,而客戶端並沒有地址。這在流式套接字中沒有問題,內核已經在服務器端調用accept()函數接收一個客戶端連接時創建了一個新的套接字,從而將一一對應關系綁定到了這個新的套接字上了。所以,對於包套接字來說,在客戶端還需要再掉用bind()函數綁定一次,人為的創建一個客戶端地址,且這個客戶端路徑名地址顯然不能和服務器端的路徑名相同。
剩下的就都和普通的TCP/IP套接字相同了,只不過地址addr必須是以sockaddr_un結構體來表示罷了。