1. 程式人生 > >Linux下的socket程式設計實踐(六)Unix域協議和socketpair傳遞檔案描述符

Linux下的socket程式設計實踐(六)Unix域協議和socketpair傳遞檔案描述符

UNIX域協議並不是一個實際的協議族,而是在單個主機上執行客戶/伺服器通訊的一種方法,所用API與在不同主機上執行客戶/伺服器通訊所使用的API相同。UNIX域協議可以視為IPC方法之一,Unix域協議主要用在同一臺機子(僅能用於本地程序間的通訊)的不同程序之間傳遞套接字。為什麼不用TCP或者UDP套接字呢?

1)在同一臺主機上, UNIX域套接字更有效率, 幾乎是TCP的兩倍(由於UNIX域套接字不需要經過網路協議棧,不需要打包/拆包,計算校驗和,維護序號和應答等,只是將應用層資料從一個程序拷貝到另一個程序, 而且UNIX域協議機制本質上就是可靠的通訊, 而網路協議是為不可靠的通訊設計的)。

2)UNIX域套接字可以在同一臺主機上各程序之間傳遞檔案描述符。

3)UNIX域套接字較新的實現把客戶的憑證(使用者ID和組ID)提供給伺服器,從而能夠提供額外的安全檢查措施。

注意:UNIX域套接字也提供面向流和麵向資料包兩種API介面,類似於TCP和UDP,但是面向訊息的UNIX套接字也是可靠的,訊息既不會丟失也不會順序錯亂。Unix域協議表示協議地址的是路徑名,而不是Internet域的IP地址和埠號。

UNIX域套接字地址結構:

#define UNIX_PATH_MAX    108  
struct sockaddr_un  
{  
    sa_family_t sun_family;               /* AF_UNIX */  
    char        sun_path[UNIX_PATH_MAX];  /* pathname */  
};
至於通訊程式的話,和使用TCP的通訊並沒有很大的區別,下面給出基於UNIX域套接字的server/client程式原始碼
/**Server端**/  

int main()  
{   
    int listenfd = socket(AF_UNIX, SOCK_STREAM, 0);  //使用AF_UNIX 或者AF_LOCAL 
    if (listenfd == -1)  
        err_exit("socket error");  
  
    char pathname[] = "/tmp/test_for_unix";  
    unlink(pathname);//如果檔案系統中已存在該路徑名,bind將會失敗。為此我們先呼叫unlink刪除這個路徑名,以防止它已經存在 
    struct sockaddr_un servAddr;  
    servAddr.sun_family = AF_UNIX;  
    strcpy(servAddr.sun_path, pathname);  
    if (bind(listenfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)  
        err_exit("bind error");  
    if (listen(listenfd, SOMAXCONN) == -1)  
        err_exit("listen error");  
  
    while (1)  
    {  
        int connfd = accept(listenfd, NULL, NULL);  
        if (connfd == -1)  
        {
       	    if(connfd==EINTR)
       	    continue;
       	    err_exit("accept");
        }
    } 
	return 0; 
} 

/**Client端程式碼**/  

int main()  
{  
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);  
    if (sockfd == -1)  
        err_exit("socket error");  
  
    char pathname[] = "/tmp/test_for_unix";  
    struct sockaddr_un servAddr;  
    servAddr.sun_family = AF_UNIX;  
    strcpy(servAddr.sun_path, pathname);  
    if (connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)  
        err_exit("connect error");  
    return 0;
}  

UNIX域套接字程式設計注意點

 1.bind成功將會建立一個檔案,是一個套接字型別,使用ls -l可以檢視到是s開頭的檔案,許可權為0777 & ~umask

 2.sun_path最好用一個/tmp目錄下的檔案的絕對路徑,再次啟動的時候最好使用unlink刪除這個檔案,否則會提示地址正在使用。

 3.UNIX域協議支援流式套介面(需要處理粘包問題)與報式套介面(基於資料報)

 4.UNIX域流式套接字connect發現監聽佇列滿時,會立刻返回一個ECONNREFUSED,這和TCP不同,如果監聽佇列滿,會忽略到來的SYN,這導致對方重傳SYN

5.如果使用流式套接字的話,還是要處理粘包問題的。

傳遞檔案描述符

socketpair
#include <sys/types.h>  
#include <sys/socket.h>  
int socketpair(int domain, int type, int protocol, int sv[2]); 

建立一個全雙工的流管道

引數:

   domain: 協議家族, 可以使用AF_UNIX(AF_LOCAL)UNIX域協議, 而且在Linux上, 該函式也就只支援這一種協議;

   type: 套接字型別, 可以使用SOCK_STREAM

   protocol: 協議型別, 一般填充為0,表示核心自動選擇協議型別。

   sv: 返回套接字對sv[0],sv[1];

socketpair 函式跟pipe 函式是類似: 只能在具有親緣關係的程序間通訊,但pipe 建立的匿名管道是半雙工的,而socketpair 可以認為是建立一個全雙工的管道。

可以使用socketpair 建立返回的套接字對進行父子程序通訊, 如下例:

int main()  
{  
    int sockfds[2];  
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1)  
        err_exit("socketpair error");  
  
    pid_t pid = fork();  
    if (pid == -1)  
        err_exit("fork error");  
    // 父程序 
    else if (pid > 0)  
    {  
        close(sockfds[1]);  //和pipe類似,先關閉一端 
        int iVal = 0;  
        while (true)  
        {  
            cout << "value = " << iVal << endl;  
            write(sockfds[0], &iVal, sizeof(iVal)); //傳送給子程序 
            read(sockfds[0], &iVal, sizeof(iVal));  
            sleep(1);  
        }  
    }  
    // 子程序 
    else if (pid == 0)  
    {  
        close(sockfds[0]);  
        int iVal = 0;  
        while (read(sockfds[1], &iVal, sizeof(iVal)) > 0)  
        {  
            ++ iVal;  
            write(sockfds[1], &iVal, sizeof(iVal));  //傳送給父程序 
        }  
    }  
}  
sendmsg/recvmsg
#include <sys/types.h>  
#include <sys/socket.h>  
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);  
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);  
它們與sendto/send 和 recvfrom/recv 函式類似,只不過可以傳輸更復雜的資料結構,功能更強大,不僅可以傳輸一般資料,還可以傳輸額外的資料,如檔案描述符。
//msghdr結構體  
struct msghdr  
{  
    void         *msg_name;       /* optional address */  
    socklen_t     msg_namelen;    /* size of address */  
    struct iovec *msg_iov;        /* scatter/gather array */  
    size_t        msg_iovlen;     /* # elements in msg_iov */  
    void         *msg_control;    /* ancillary data, see below */  
    size_t        msg_controllen; /* ancillary data buffer len */  
    int           msg_flags;      /* flags on received message */  
};  
struct iovec                      /* Scatter/gather array items */  
{  
    void  *iov_base;              /* Starting address */  
    size_t iov_len;               /* Number of bytes to transfer */  
};  

msghdr結構體成員解釋:

   1)msg_name :即對等方的地址指標,不關心時設為NULL即可;

   2)msg_namelen:地址長度,不關心時設定為0即可;

   3)msg_iov:是結構體iovec 的指標, 指向需要傳送的普通資料, 見下圖。   

      成員iov_base 可以認為是傳輸正常資料時的buf;

      成員iov_len 是buf 的大小;

   4)msg_iovlen:當有n個iovec 結構體時,此值為n;

   5)msg_control:是一個指向cmsghdr 結構體的指標(見下圖), 當需要傳送輔助資料(如控制資訊/檔案描述符)時, 需要設定該欄位, 當傳送正常資料時, 就不需要關心該欄位, 並且msg_controllen可以置為0;

   6)msg_controllen:cmsghdr 結構體可能不止一個(見下圖):

   7)flags: 不用關心;



填充位元組是用來進行對齊的,4的整數倍,緩衝區的大小就是輔助資料的大小,為了對齊,可能存在一些填充位元組(見下圖),跟系統的實現有關,但我們不必關心,可以通過一些函式巨集來獲取相關的值,如下:

#include <sys/socket.h>  
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);   
//獲取輔助資料的第一條訊息  
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); //獲取輔助資料的下一條資訊  
size_t CMSG_ALIGN(size_t length);     
size_t CMSG_SPACE(size_t length);  
size_t CMSG_LEN(size_t length); //length使用的是的(實際)資料的長度, 見下圖(兩條填充資料的中間部分)  
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);  


程序間傳遞檔案描述符

結構體的填充是重點和難點:

/**示例: 封裝兩個函式send_fd/recv_fd用於在程序間傳遞檔案描述符**/  
int send_fd(int sockfd, int sendfd)  
{  
    // 填充 name 欄位  
    struct msghdr msg;  
    msg.msg_name = NULL;  
    msg.msg_namelen = 0;  
  
    // 填充 iov 欄位  
    struct iovec iov;  
    char sendchar = '\0';  
    iov.iov_base = &sendchar;  
    iov.iov_len = 1;  
    msg.msg_iov = &iov;  
    msg.msg_iovlen = 1;  
  
    // 填充 cmsg 欄位  
    struct cmsghdr cmsg;  
    cmsg.cmsg_len = CMSG_LEN(sizeof(int));  
    cmsg.cmsg_level = SOL_SOCKET;  
    cmsg.cmsg_type = SCM_RIGHTS;  
    *(int *)CMSG_DATA(&cmsg) = sendfd;  
    msg.msg_control = &cmsg;  
    msg.msg_controllen = CMSG_LEN(sizeof(int));  
  
    // 傳送  
    if (sendmsg(sockfd, &msg, 0) == -1)  
        return -1;  
    return 0;  
}  

int recv_fd(int sockfd)  
{  
    // 填充 name 欄位  
    struct msghdr msg;  
    msg.msg_name = NULL;  
    msg.msg_namelen = 0;  
  
    // 填充 iov 欄位  
    struct iovec iov;  
    char recvchar;  
    iov.iov_base = &recvchar;  
    iov.iov_len = 1;  
    msg.msg_iov = &iov;  
    msg.msg_iovlen = 1;  
  
    // 填充 cmsg 欄位  
    struct cmsghdr cmsg;  
    msg.msg_control = &cmsg;  
    msg.msg_controllen = CMSG_LEN(sizeof(int));  
  
    // 接收  
    if (recvmsg(sockfd, &msg, 0) == -1)  
        return -1;  
    return *(int *)CMSG_DATA(&cmsg);  
}  

來解釋一下send_fd 函式:
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &vec;
msg.msg_iovlen = 1; //主要目的不是傳遞資料,故只傳1個字元
msg.msg_flags = 0;
vec.iov_base = &sendchar;
vec.iov_len = sizeof(sendchar);
這幾行中需要注意的是我們現在的目的不是傳輸正常資料,而是為了傳遞檔案描述符,所以只定義一個1位元組的char,其餘參照前面對引數的解釋可以理解。
現在我們只有一個cmsghdr 結構體,把需要傳遞的檔案描述符send_fd 長度,也就是需要傳輸的額外資料大小,當作引數傳給CMSG_SPACE 巨集,可以得到整個結構體的大小,包括一些填充位元組,如上圖所示,也即
char cmsgbuf[CMSG_SPACE(sizeof(send_fd))];
也就可以進一步得出以下兩行:
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);

接著,需要填充cmsghdr 結構體,傳入msghdr 指標,CMSG_FIRSTHDR巨集可以得到首個cmsghdr 結構體的指標,即

p_cmsg = CMSG_FIRSTHDR(&msg);

然後使用指標來填充各欄位,如下:
p_cmsg->cmsg_level = SOL_SOCKET;
p_cmsg->cmsg_type = SCM_RIGHTS;
p_cmsg->cmsg_len = CMSG_LEN(sizeof(send_fd));

傳入send_fd 的大小,CMSG_LEN巨集可以得到cmsg_len 欄位的大小。
最後,傳入結構體指標 p_cmsg ,巨集CMSG_DATA 可以得到準備存放send_fd 的位置指標,將send_fd 放進去,如下:

p_fds = (int*)CMSG_DATA(p_cmsg);
*p_fds = send_fd; // 通過傳遞輔助資料的方式傳遞檔案描述符
recv_fd 函式就類似了,不再贅述。

int main()  
{  
    int sockfds[2];  
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1)  
        err_exit("socketpair error");  
  
    pid_t pid = fork();  
    if (pid == -1)  
        err_exit("fork error");  
    // 子程序以只讀方式開啟檔案, 將檔案描述符傳送給子程序  
    else if (pid ==  0)  
    {  
        close(sockfds[1]);  
        int fd = open("read.txt", O_RDONLY);  
        if (fd == -1)  
            err_exit("open error");  
        cout << "In child,  fd = " << fd << endl;  
        send_fd(sockfds[0], fd);  
    }  
    // 父程序從檔案描述符中讀取資料  
    else if (pid > 0)  
    {  
        close(sockfds[0]);  
        int fd = recv_fd(sockfds[1]);  
        if (fd == -1)  
            err_exit("recv_fd error");  
        cout << "In parent, fd = " << fd << endl;  
  
        char buf[BUFSIZ] = {0};  
        int readBytes = read(fd, buf, sizeof(buf));  
        if (readBytes == -1)  
            err_exit("read fd error");  
        cout << buf;  
    }  
}  

我們知道,父程序在fork 之前開啟的檔案描述符,子程序是可以共享的,但是子程序開啟的檔案描述符,父程序是不能共享的,上述程式就是舉例在子程序中打開了一個檔案描述符,然後通過send_fd 函式將檔案描述符傳遞給父程序,父程序可以通過recv_fd 函式接收到這個檔案描述符。先建立一個檔案read.txt 後輸入幾個字元,然後執行程式。

注意:

   (1)只有UNIX域協議才能在本機程序間傳遞檔案描述符;

   (2)程序間傳遞檔案描述符並不是傳遞檔案描述符的值(其實send_fd/recv_fd的兩個值也是不同的), 而是要在接收程序中建立一個新的檔案描述符, 並且該檔案描述符和傳送程序中被傳遞的檔案描述符指向核心中相同的檔案表項.