[單刷APUE系列]第十七章——高階程式間通訊

山河永寂發表於2019-05-10

引言

前面講述了各種程式間通訊和網路IPC的內容,除此以外,還有一種非常常用的IPC——UNIX域套接字。這種套接字實際上就是一種檔案,能夠讓本機的程式之間互相通訊。

Unix域套接字

Unix域套接字用於同一臺電腦上執行的程式通訊。雖然TCP/IP協議的套接字很方便,但是在某些情況下需要保證更高的通訊效率,所以Unix域套接字更加適合,因為它是通過核心轉發的資訊,而網路協議通訊需要通過網路卡傳送,可能效率更低些。
雖然不通過網路傳輸資料,但是Unix域套接字也有流和資料包兩種介面。Unix域套接字就像是套接字和管道的組合,在資料傳輸上很像管道,但是是以套接字的形式使用。

int socketpair(int domain, int type, int protocol, int socket_vector[2]);

socketpair函式建立一對未命名的已連線的套接字在指定的域(domain)中,並且以指定的型別(type),可選指定協議(protocol),描述符將存放在socket_vector陣列中。需要注意的是,這對套接字實際上是全雙工的,在很多Unix實現中,全雙工的管道之類的實際上就是通過Unix域套接字實現的。

命名Unix域套接字

前面的socketpair雖然很方便,但是它建立的是未命名的套接字,也就是說不同程式無法使用,在前面的網路套接字章節中講了套接字如何繫結一個地址和埠,但是我們也可以將其繫結到路徑上,使其成為一個檔案,這樣就能讓不同程式使用。

#include "include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>

int main(int argc, char *argv[])
{
    int fd, size;
    struct sockaddr_un un;
    
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, "foo.socket");
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
        err_sys("socket failed");
    size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
    if (bind(fd, (struct sockaddr *)&un, size) < 0)
        err_sys("bind failed");
    printf("UNIX domain socket bound
");
    exit();
}

上面的例程非常簡單,首先是建立一個Unix域地址,使用strcpy函式將地址複製到地址族變數,然後使用socket函式建立一個套接字,然後計算路徑成員在結構體中的偏移量加上路徑本身的長度,求出size變數,然後使用bind函式將地址族結構體和套接字繫結在一起。
當我們執行程式完畢,就能在當前目錄下看到一個foo.socket檔案,可能就有人要問了,為什麼程式結束這個檔案仍然存在,套接字不是檔案描述符嗎,不是在程式結束的時候就回收了嗎?實際上,這是搞混了檔案描述符和檔案的區別,檔案描述符只是0、1、2之類的數字,用於指向檔案表項,實際上檔案的開啟是由核心維護的。套接字也一樣,當我們建立套接字的時候,並且將其繫結到具體路徑,核心就會幫助我們建立一個S_IFSOCK型別的檔案,但是實際上這個檔案並沒有什麼用,它不會用於實際的寫入,不然這不就是和普通的檔案一樣了嗎,所以這個檔案純粹就是個flag,用於標記地址,就跟通常使用的xxx.pid檔案這種形式類似。

唯一連線

這小節沒什麼重要內容,除了三個封裝函式,其中有一些內容可能是有困惑的,這裡筆者將自己的理解講一下。首先,我們先需要知道各個平臺實際上實現是有差異的,例如sockaddr_un的結構體不同,在Linux和Solaris中,是如下所示:

struct sockaddr_un {
    sa_family_t sun_family;
    char sun_path[108];
};

而在FreeBSD和OSX系統中,是如下的:

struct sockaddr_un {
    unsigned char sun_len;
    sa_family_t sun_family;
    char sun_path[104];
};

先給出serv_listen函式

#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

#define QLEN    10

/*
 * Create a server endpoint of a connection.
 * Returns fd if all OK, <0 on error.
 */
int
serv_listen(const char *name)
{
    int                    fd, len, err, rval;
    struct sockaddr_un    un;

    if (strlen(name) >= sizeof(un.sun_path)) {
        errno = ENAMETOOLONG;
        return(-1);
    }

    /* create a UNIX domain stream socket */
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
        return(-2);

    unlink(name);    /* in case it already exists */

    /* fill in socket address structure */
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, name);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);

    /* bind the name to the descriptor */
    if (bind(fd, (struct sockaddr *)&un, len) < 0) {
        rval = -3;
        goto errout;
    }

    if (listen(fd, QLEN) < 0) {    /* tell kernel we`re a server */
        rval = -4;
        goto errout;
    }
    return(fd);

errout:
    err = errno;
    close(fd);
    errno = err;
    return(rval);
}

首先是建立一個Unix域套接字,如果檔案已經存在則刪除原先檔案,然後構造sockaddr_un結構體,然後使用bind函式將地址和套接字繫結,系統自動生成套接字檔案,然後使用listen函式偵聽套接字。這裡基本沒什麼需要講解的。
然後是serv_accept函式

#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <errno.h>

#define    STALE    30    /* client`s name can`t be older than this (sec) */

/*
 * Wait for a client connection to arrive, and accept it.
 * We also obtain the client`s user ID from the pathname
 * that it must bind before calling us.
 * Returns new fd if all OK, <0 on error
 */
int
serv_accept(int listenfd, uid_t *uidptr)
{
    int                    clifd, err, rval;
    socklen_t            len;
    time_t                staletime;
    struct sockaddr_un    un;
    struct stat            statbuf;
    char                *name;

    /* allocate enough space for longest name plus terminating null */
    if ((name = malloc(sizeof(un.sun_path + 1))) == NULL)
        return(-1);
    len = sizeof(un);
    if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
        free(name);
        return(-2);        /* often errno=EINTR, if signal caught */
    }

    /* obtain the client`s uid from its calling address */
    len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */
    memcpy(name, un.sun_path, len);
    name[len] = 0;            /* null terminate */
    if (stat(name, &statbuf) < 0) {
        rval = -3;
        goto errout;
    }

#ifdef    S_ISSOCK    /* not defined for SVR4 */
    if (S_ISSOCK(statbuf.st_mode) == 0) {
        rval = -4;        /* not a socket */
        goto errout;
    }
#endif

    if ((statbuf.st_mode & (S_IRWXG | S_IRWXO)) ||
        (statbuf.st_mode & S_IRWXU) != S_IRWXU) {
          rval = -5;    /* is not rwx------ */
          goto errout;
    }

    staletime = time(NULL) - STALE;
    if (statbuf.st_atime < staletime ||
        statbuf.st_ctime < staletime ||
        statbuf.st_mtime < staletime) {
          rval = -6;    /* i-node is too old */
          goto errout;
    }

    if (uidptr != NULL)
        *uidptr = statbuf.st_uid;    /* return uid of caller */
    unlink(name);        /* we`re done with pathname now */
    free(name);
    return(clifd);

errout:
    err = errno;
    close(clifd);
    free(name);
    errno = err;
    return(rval);
}

首先是使用accept函式阻塞等待客戶程式連線。當accept返回的時候,返回的是新的套接字描述符,也就是存在連線的套接字,並且從第二個引數得到套接字的路徑名,接著複製路徑名,最後呼叫stat函式檢查路徑名。
其中len -= offsetof(struct sockaddr_un, sun_path);可能有些人不是很明白,這裡實際上用了點編碼技巧,實際上就是結構體總長度減去sun_path成員的記憶體偏移量,最終就是sun_path的長度。還有,accept第二個餐宿實際上和bind是不一樣的,因為這個套接字是已經連線的套接字,所以會包含客戶程式ID的名字。

#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

#define    CLI_PATH    "/var/tmp/"
#define    CLI_PERM    S_IRWXU            /* rwx for user only */

/*
 * Create a client endpoint and connect to a server.
 * Returns fd if all OK, <0 on error.
 */
int
cli_conn(const char *name)
{
    int                    fd, len, err, rval;
    struct sockaddr_un    un, sun;
    int                    do_unlink = 0;

    if (strlen(name) >= sizeof(un.sun_path)) {
        errno = ENAMETOOLONG;
        return(-1);
    }

    /* create a UNIX domain stream socket */
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
        return(-1);

    /* fill socket address structure with our address */
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    sprintf(un.sun_path, "%s%05ld", CLI_PATH, (long)getpid());
printf("file is %s
", un.sun_path);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);

    unlink(un.sun_path);        /* in case it already exists */
    if (bind(fd, (struct sockaddr *)&un, len) < 0) {
        rval = -2;
        goto errout;
    }
    if (chmod(un.sun_path, CLI_PERM) < 0) {
        rval = -3;
        do_unlink = 1;
        goto errout;
    }

    /* fill socket address structure with server`s address */
    memset(&sun, 0, sizeof(sun));
    sun.sun_family = AF_UNIX;
    strcpy(sun.sun_path, name);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
    if (connect(fd, (struct sockaddr *)&sun, len) < 0) {
        rval = -4;
        do_unlink = 1;
        goto errout;
    }
    return(fd);

errout:
    err = errno;
    close(fd);
    if (do_unlink)
        unlink(un.sun_path);
    errno = err;
    return(rval);
}

首先是建立一個套接字,然後用客戶端程式ID附加到路徑上,形成自己的套接字路徑,並且將這個套接字繫結在地址上,也就是系統會在生成客戶端的socket檔案,可能有人要奇怪了,為什麼不直接使用connect而是要bind地址,因為如果不繫結地址,我們就無法區分連線是屬於哪個客戶端程式的,也就是類似於網路上客戶端特意繫結一個埠連線服務端。

小結

最後還有三節,實際上都是屬於實際操作的內容了,所以這裡就不講了。這篇文章就算是最終的系列結尾,因為最後章節是專案原始碼解析了。所以就不再講述

相關文章