Unix domain socket 簡介

sparkdev發表於2018-01-27

Unix domain socket 又叫 IPC(inter-process communication 程式間通訊) socket,用於實現同一主機上的程式間通訊。socket 原本是為網路通訊設計的,但後來在 socket 的框架上發展出一種 IPC 機制,就是 UNIX domain socket。雖然網路 socket 也可用於同一臺主機的程式間通訊(通過 loopback 地址 127.0.0.1),但是 UNIX domain socket 用於 IPC 更有效率:不需要經過網路協議棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層資料從一個程式拷貝到另一個程式。這是因為,IPC 機制本質上是可靠的通訊,而網路協議是為不可靠的通訊設計的。
UNIX domain socket 是全雙工的,API 介面語義豐富,相比其它 IPC 機制有明顯的優越性,目前已成為使用最廣泛的 IPC 機制,比如 X Window 伺服器和 GUI 程式之間就是通過 UNIX domain socket 通訊的。
Unix domain socket 是 POSIX 標準中的一個元件,所以不要被名字迷惑,linux 系統也是支援它的。

下面通過一個簡單的 demo 來理解相關概念。程式分為伺服器端和客戶端兩部分,它們之間通過 unix domain socket 進行通訊。

伺服器端程式

下面是一個非常簡單的伺服器端程式,它從客戶端讀字元,然後將每個字元轉換為大寫並回送給客戶端:

#include <stdlib.h>  
#include <stdio.h>  
#include <stddef.h>  
#include <sys/socket.h>  
#include <sys/un.h>  
#include <errno.h>  
#include <string.h>  
#include <unistd.h>  
#include <ctype.h>   
 
#define MAXLINE 80  
 
char *socket_path = "server.socket";  
 
int main(void)  
{  
    struct sockaddr_un serun, cliun;  
    socklen_t cliun_len;  
    int listenfd, connfd, size;  
    char buf[MAXLINE];  
    int i, n;  
 
    if ((listenfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {  
        perror("socket error");  
        exit(1);  
    }  
 
    memset(&serun, 0, sizeof(serun));  
    serun.sun_family = AF_UNIX;  
    strcpy(serun.sun_path, socket_path);  
    size = offsetof(struct sockaddr_un, sun_path) + strlen(serun.sun_path);  
    unlink(socket_path);  
    if (bind(listenfd, (struct sockaddr *)&serun, size) < 0) {  
        perror("bind error");  
        exit(1);  
    }  
    printf("UNIX domain socket bound\n");  
      
    if (listen(listenfd, 20) < 0) {  
        perror("listen error");  
        exit(1);          
    }  
    printf("Accepting connections ...\n");  
 
    while(1) {  
        cliun_len = sizeof(cliun);         
        if ((connfd = accept(listenfd, (struct sockaddr *)&cliun, &cliun_len)) < 0){  
            perror("accept error");  
            continue;  
        }  
          
        while(1) {  
            n = read(connfd, buf, sizeof(buf));  
            if (n < 0) {  
                perror("read error");  
                break;  
            } else if(n == 0) {  
                printf("EOF\n");  
                break;  
            }  
              
            printf("received: %s", buf);  
 
            for(i = 0; i < n; i++) {  
                buf[i] = toupper(buf[i]);  
            }  
            write(connfd, buf, n);  
        }  
        close(connfd);  
    }  
    close(listenfd);  
    return 0;  
} 

簡單介紹一下這段程式碼:

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

使用 UNIX domain socket 的過程和網路 socket 十分相似,也要先呼叫 socket() 建立一個 socket 檔案描述符.
family 指定為 AF_UNIX,使用 AF_UNIX 會在系統上建立一個 socket 檔案,不同程式通過讀寫這個檔案來實現通訊。
type 可以選擇 SOCK_DGRAM 或 SOCK_STREAM。SOCK_STREAM 意味著會提供按順序的、可靠、雙向、面向連線的位元流。SOCK_DGRAM 意味著會提供定長的、不可靠、無連線的通訊。
protocol 引數指定為 0 即可。
UNIX domain socket 與網路 socket 程式設計最明顯的不同在於地址格式不同,用結構體 sockaddr_un 表示,網路程式設計的 socket 地址是 IP 地址加埠號,而 UNIX domain socket 的地址是一個 socket 型別的檔案在檔案系統中的路徑,這個 socket 檔案由 bind() 呼叫建立,如果呼叫 bind() 時該檔案已存在,則 bind() 錯誤返回。因此,一般在呼叫 bind() 前會檢查 socket 檔案是否存在,如果存在就刪除掉。
網路 socket 程式設計類似,在 bind 之後要 listen,表示通過 bind 的地址(也就是 socket 檔案)提供服務。
接下來必須用 accept() 函式初始化連線。accept() 為每個連線創立新的套接字並從監聽佇列中移除這個連線。

客戶端程式

下面是客戶端程式,它接受使用者的輸入,並把字串傳送給伺服器,然後接收伺服器返回的字串並列印:

#include <stdlib.h>  
#include <stdio.h>  
#include <stddef.h>  
#include <sys/socket.h>  
#include <sys/un.h>  
#include <errno.h>  
#include <string.h>  
#include <unistd.h>  
 
#define MAXLINE 80  
 
char *client_path = "client.socket";  
char *server_path = "server.socket";  
 
int main() {  
    struct  sockaddr_un cliun, serun;  
    int len;  
    char buf[100];  
    int sockfd, n;  
 
    if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0){  
        perror("client socket error");  
        exit(1);  
    }  
      
    // 一般顯式呼叫bind函式,以便伺服器區分不同客戶端  
    memset(&cliun, 0, sizeof(cliun));  
    cliun.sun_family = AF_UNIX;  
    strcpy(cliun.sun_path, client_path);  
    len = offsetof(struct sockaddr_un, sun_path) + strlen(cliun.sun_path);  
    unlink(cliun.sun_path);  
    if (bind(sockfd, (struct sockaddr *)&cliun, len) < 0) {  
        perror("bind error");  
        exit(1);  
    }  
 
    memset(&serun, 0, sizeof(serun));  
    serun.sun_family = AF_UNIX;  
    strcpy(serun.sun_path, server_path);  
    len = offsetof(struct sockaddr_un, sun_path) + strlen(serun.sun_path);  
    if (connect(sockfd, (struct sockaddr *)&serun, len) < 0){  
        perror("connect error");  
        exit(1);  
    }  
 
    while(fgets(buf, MAXLINE, stdin) != NULL) {    
         write(sockfd, buf, strlen(buf));    
         n = read(sockfd, buf, MAXLINE);    
         if ( n < 0 ) {    
            printf("the other side has been closed.\n");    
         }else {    
            write(STDOUT_FILENO, buf, n);    
         }    
    }   
    close(sockfd);  
    return 0;  
}  

與網路 socket 程式設計不同的是,UNIX domain socket 客戶端一般要顯式呼叫 bind 函式,而不依賴系統自動分配的地址。客戶端 bind 一個自己指定的 socket 檔名的好處是,該檔名可以包含客戶端的 pid 等資訊以便伺服器區分不同的客戶端。

執行上面的程式

分別把伺服器端程式和客戶端程式儲存為 server.c 和 client.c 檔案,並編譯:

$ gcc server.c -o server
$ gcc client.c -o client

先啟動伺服器端程式,然後啟動客戶端程式輸入字串並回車:

還不錯,客戶端得到了伺服器端返回的大寫字串。接下來看看當前目錄下的檔案:

哈哈,多了兩個 socket 檔案。

總結

Unix domain socket 主要用於同一主機上的程式間通訊。與主機間的程式通訊不同,它不是通過 "IP地址 + TCP或UDP埠號" 的方式程式通訊,而是使用 socket 型別的檔案來完成通訊,因此在穩定性、可靠性以及效率方面的表現都很不錯。

參考:
UNIX Domain Socket IPC
[linux] unix domain socket 例子

相關文章