天天看點

淺談Unix域套接字

文章目錄

    • 歡迎通路我的個人部落格
    • 引言
    • 簡介
    • Unix 域套接字位址結構
    • 建立 Unix 域套接字
    • 綁定 Unix 域套接字
    • Unix 域中的流 socket
    • Unix 域中的資料報 socket
    • Unix 域套接字的權限
    • 使用 socketpair 建立互聯的 socket 對
    • 使用 Unix 域套接字傳遞描述符
    • 參考資料

歡迎通路我的個人部落格

部落格

引言

在 Linux 中有許多進行

程序間通信

的方法。今天部落客向大家介紹一種常用的程序間通信的方法 ——

Unix 域套接字

簡介

Unix 域套接字

是一種在本機的程序間進行通信的一種方法。雖然 Unix 域套接字的接口與

TCP 和 UDP 套接字

的接口十分相似,但是 Unix 域套接字隻能用于同一台機器的程序間通信,不能讓兩個位于不同機器的程序進行通信。正由于這個特性,Unix 域套接字可以可靠地在兩個程序間複制資料,不用像 TCP 一樣采用一些諸如 * 添加網絡報頭 、 計算檢驗和 、 産生順序号 * 等一系列保證資料完整性的操作。是以,在同一台機器上進行程序間通信時,Unix 域套接字的效率往往比 TCP 套接字的效率要高。

因為 Unix 域套接字的效率比較高,一些程式經常用 Unix 套接字代替 TCP 套接字。例如當 MySQL 的伺服器程序和用戶端程序在同一台機器上時,可以用 Unix 域套接字代替 TCP 套接字。

Unix 域套接字位址結構

在使用 TCP 套接字和 UDP 套接字時,我們需要用

struct sockaddr_in

(IPv4)定義套接字的位址結構,與之相似,Unix 域套接字使用

struct sockaddr_un

定義套接字的位址結構。

struct sockaddr_un

的定義如下(* 位于頭檔案 sys/un.h 中 *):

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

在使用 Internet 域套接字進行程式設計時,需要将

struct sockaddr_in

sin_family

成員設定為

AF_INET

(IPv4)。與之類似,在使用 Unix 域套接字時,需要将 sun_family 設定為

AF_UNIX

AF_LOCAL

(* 這兩個宏的作用完全相同,都表示 UNIX 域 *)。

struct sockaddr_un

的第二個成員

sun_path

表示 socket 的位址。在 Unix 域中,socket 的位址用路徑名表示。例如,可以将 sun_path 設定為

/tmp/unixsock

。由于路徑名是一個字元串,是以 sun_path 必須能夠容納字元串的字元和結尾的

'\0'

。需要注意的是,标準并沒有規定 sun_path 的大小,在某些平台中,sun_path 的大小可能是 104、92 等值。是以如果需要保證可移植性,在編碼時應該使用 sun_path 的最小值。

建立 Unix 域套接字

Unix 域套接字使用

socket

函數建立,與 Internet 域套接字一樣,Unix 域套接字也有流套接字和資料報套接字兩種:

int unix_sock_fd1 = socket(AF_UNIX, SOCK_STREAM, 0); // Unix 域中的流 socket
int unix_sock_fd2 = socket(AF_UNIX, SOCK_DGRAM, 0); // Unix 域中的資料包 socket
           

稍後将介紹這兩種套接字的用法和差別。

綁定 Unix 域套接字

使用

bind

函數可以将一個 Unix 套接字綁定到一個位址上。綁定 Unix 域套接字時,bind 會在指定的路徑名處建立一個表示 Unix 域套接字的檔案。Unix 域套接字與路徑名是一一對應關系,即一個 Unix 域套接字隻能綁定到一個路徑名上,一個路徑名也隻能被一個套接字綁定。一般要把 Unix 域套接字綁定到一個

絕對路徑

上,例如:

struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "/tmp/sockaddr");
int unix_sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (bind(unix_sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
    fprintf(stderr, "bind error\n");
}
           

Unix 域套接字被綁定後,可以使用 getsockname 擷取套接字綁定的路徑名:

struct sockaddr_un addr2;
socklen_t          len = sizeof(addr2);
getsockname(unix_sock_fd, (struct sockaddr *)&addr2, &len);
printf("%s\n", addr2.sun_path);
           

當一個 Unix 域套接字不再使用時,應當調用

unlink

将其删除。

Unix 域中的流 socket

Unix 域中的流套接字與 TCP 流套接字的用法十分相似。在伺服器端,我們首先建立一個 Unix 域流套接字,将其綁定到一個路徑上,然後調用

listen

監聽用戶端連接配接,調用

accept

接受用戶端的連接配接。在用戶端,在建立一個 Unix 域流套接字之後,可以使用

connect

嘗試連接配接指定的伺服器套接字。以下是一個使用 Unix 域流套接字實作的 echo 伺服器和用戶端的例子:

// 伺服器
#include <assert.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define BACKLOG          5
#define MSG_MAX_LENGTH   100

void echo(int client_fd);
void readLine(int fd, char *buf);

void signalHandler(int signo)    // NOLINT
{
    unlink(UNIX_SOCKET_PATH);    // NOLINT
    exit(EXIT_SUCCESS);          // NOLINT
}

int main(void)
{
    if (signal(SIGINT, signalHandler) == SIG_ERR)    // NOLINT
    {
        fprintf(stderr, "signal error\n");
        return -1;
    }

    int listen_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if (listen_fd < 0)
    {
        fprintf(stderr, "socket error\n");
        return -1;
    }

    struct sockaddr_un unix_socket_addr;
    memset(&unix_socket_addr, 0, sizeof(unix_socket_addr));
    unix_socket_addr.sun_family = AF_LOCAL;
    strcpy(unix_socket_addr.sun_path, UNIX_SOCKET_PATH);
    if (bind(listen_fd, (struct sockaddr *)&unix_socket_addr, sizeof(unix_socket_addr))
        < 0)
    {
        fprintf(stderr, "bind error\n");
        return -1;
    }

    if (listen(listen_fd, BACKLOG) < 0)
    {
        fprintf(stderr, "listen error\n");
        return -1;
    }

    for (;;)
    {
        int client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd < 0)
        {
            fprintf(stderr, "accept error\n");
            return -1;
        }

        switch (fork())
        {
            case -1:
            {
                fprintf(stderr, "fork error\n");
                return -1;
            }
            case 0:
            {
                echo(client_fd);
                break;
            }
            default:
            {
                break;
            }
        }
    }

    return 0;
}

void echo(int client_fd)
{
    char buf[MSG_MAX_LENGTH + 1] = {0};
    for (;;)
    {
        readLine(client_fd, buf);
        int msg_len = (int)strlen(buf);
        if (write(client_fd, buf, msg_len) != msg_len)
        {
            fprintf(stderr, "write error\n");
            exit(EXIT_FAILURE);    // NOLINT
        }
    }
}

void readLine(int fd, char *buf)
{
    int i = 0;
    for (; i < MSG_MAX_LENGTH; i++)
    {
        switch (read(fd, buf + i, 1))
        {
            case 1:
            {
                break;
            }
            case 0:
            {
                exit(EXIT_FAILURE);    // NOLINT
                break;
            }
            case -1:
            {
                fprintf(stderr, "read error\n");
                exit(EXIT_FAILURE);    // NOLINT
                break;
            }
            default:
            {
                assert(0);
            }
        }

        if (buf[i] == '\n')
        {
            i++;
            break;
        }
    }

    buf[i] = '\0';
}
           
// 用戶端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH   100

int main(void)
{
    int socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if (socket_fd < 0)
    {
        fprintf(stderr, "socker error\n");
        return -1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, UNIX_SOCKET_PATH);
    if (connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    {
        fprintf(stderr, "sonnect error\n");
        return -1;
    }

    char buf[MSG_MAX_LENGTH + 1] = {0};
    for (;;)
    {
        fgets(buf, MSG_MAX_LENGTH, stdin);
        int len = (int)strlen(buf);
        if (write(socket_fd, buf, len) != len)
        {
            fprintf(stderr, "write error\n");
            return -1;
        }
        if (read(socket_fd, buf, len) != len)
        {
            fprintf(stderr, "read error\n");
            return -1;
        }
        printf("%s", buf);
    }

    return 0;
}
           

Unix 域中的資料報 socket

Unix 域資料報套接字

UDP 套接字

類似,可以通過 Unix 域資料報套接字在程序間發送具有邊界的資料報。但由于 Unix 域資料報套接字是在本機上進行通信,是以 Unix 域資料報套接字的資料傳遞是可靠的,不會像 UDP 套接字那樣發生丢包的問題。Unix 域資料報套接字的接口與 UDP 也十分相似。在伺服器端,通常先建立一個 Unix 域資料報套接字,然後将其綁定到一個路徑上。然後調用

recvfrom

接收用戶端發送來的資料,調用

sendto

向用戶端發送資料。對于用戶端,通常是先建立一個 Unix 域資料報套接字,将這個套接字綁定到一個路徑上,然後調用

sendto

發送資料,調用

recvfrom

接收用戶端發來的資料。以下是使用 Unix 域資料報套接字實作的 echo 伺服器和用戶端:

// 伺服器
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH   100

void signalHandler(int signo)    // NOLINT
{
    unlink(UNIX_SOCKET_PATH);    // NOLINT
    exit(EXIT_SUCCESS);          // NOLINT
}

int main(void)
{
    if (signal(SIGINT, signalHandler) == SIG_ERR)    // NOLINT
    {
        fprintf(stderr, "signal error\n");
        return -1;
    }

    int listen_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
    if (listen_fd < 0)
    {
        fprintf(stderr, "socket error\n");
        return -1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, UNIX_SOCKET_PATH);
    if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
    {
        fprintf(stderr, "bind error\n");
        return -1;
    }

    char buf[MSG_MAX_LENGTH + 1] = {0};
    for (;;)
    {
        struct sockaddr_un client_addr;
        socklen_t          len     = sizeof(client_addr);
        int                msg_len = (int)recvfrom(listen_fd,
                                    buf,
                                    MSG_MAX_LENGTH,
                                    0,
                                    (struct sockaddr *)&client_addr,
                                    &len);
        if (msg_len < 0)
        {
            fprintf(stderr, "recvfrom error\n");
            return -1;
        }
        if (sendto(listen_fd, buf, msg_len, 0, (struct sockaddr *)&client_addr, len) < 0)
        {
            fprintf(stderr, "sendto error\n");
            return -1;
        }
    }

    return 0;
}
           
// 用戶端
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH       "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH         100
#define SOCKET_PATH_MAX_LENGTH 50

void signalHandler(int signo)    // NOLINT
{
    char socket_path[SOCKET_PATH_MAX_LENGTH] = {0};
    sprintf(socket_path, "/tmp/echo_unix_socket_%ld", (long)getpid());    // NOLINT
    unlink(socket_path);                                                  // NOLINT
    exit(EXIT_SUCCESS);                                                   // NOLINT
}

int main(void)
{
    if (signal(SIGINT, signalHandler) == SIG_ERR)    // NOLINT
    {
        fprintf(stderr, "signal error\n");
        return -1;
    }

    int socket_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
    if (socket_fd < 0)
    {
        fprintf(stderr, "socket error\n");
        return -1;
    }

    struct sockaddr_un server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_LOCAL;
    strcpy(server_addr.sun_path, UNIX_SOCKET_PATH);

    struct sockaddr_un client_addr;
    memset(&client_addr, 0, sizeof(client_addr));
    client_addr.sun_family = AF_LOCAL;
    sprintf(client_addr.sun_path, "/tmp/echo_unix_socket_%ld", (long)getpid());

    if (bind(socket_fd, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0)
    {
        fprintf(stderr, "bind error\n");
        return -1;
    }

    char buf[MSG_MAX_LENGTH + 1] = {0};
    for (;;)
    {
        fgets(buf, MSG_MAX_LENGTH, stdin);
        int msg_len = (int)strlen(buf);
        if (sendto(socket_fd,
                   buf,
                   msg_len,
                   0,
                   (struct sockaddr *)&server_addr,
                   sizeof(server_addr))
            < 0)
        {
            fprintf(stderr, "sendto error\n");    // NOLINT
            return -1;
        }
        if (recvfrom(socket_fd, buf, MSG_MAX_LENGTH, 0, NULL, NULL) < 0)
        {
            fprintf(stderr, "recvfrom error\n");
            return -1;
        }
        printf("%s", buf);
    }

    return 0;
}
           

Unix 域套接字的權限

當程式調用

bind

時,會在檔案系統中的指定路徑處建立一個與套接字對應的檔案。我們可以通過控制該檔案的權限來控制程序對這個套接字的通路。當程序想要連接配接一個 Unix 域流套接字或通過一個 Unix 域資料報套接字發送資料包時,需要擁有對該套接字的

寫權限

以及對 socket 路徑名的所有目錄的

執行權限

。在調用

bind

時,會自動賦予使用者、組和其他使用者的所有權限。如果想要修改這一行為,可以在調用

bind

之前調用

umask

禁用掉某些權限。

使用 socketpair 建立互聯的 socket 對

有時我們需要在同一個程序中建立一對互相連接配接的 Unix 域 socket(* 與管道類似 *),這可以通過

socket

bind

listen

accept

connect

等調用實作。而

socketpair

提供了一個簡單友善的方法來建立一對互聯的 socket。

socketpair

建立的一對 socket 是

全雙工

的。socketpair 的函數原型如下:

#include <sys/socket.h>

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

socketpair 的前三個參數與

socket

函數的含義相同。由于 socketpair 隻能用于 Unix 域套接字,是以

domain

參數必須是

AF_UNIX

AF_LOCAL

type

參數可以是

SOCK_DGRAM

SOCK_STREAM

,分别建立一對資料報 socket 或流 socket。

protocol

參數必須是 0。

socketfd

用于傳回建立的兩個套接字檔案描述符。

通常,在調用 socketpair 建立一對套接字後會調用 fork 建立子程序,這樣父程序和子程序就可以通過這一對套接字進行程序間通信了。

使用 Unix 域套接字傳遞描述符

Unix 域套接字的一個 “特色功能” 就是在程序間

傳遞描述符

。描述符可以通過 Unix 域套接字在沒有親緣關系的程序之間傳遞。描述符是一種

輔助資料

,可以通過

sendmsg

發送,通過

recvmsg

接收。這裡的

描述符

可以是

open

pipe

mkfifo

socket

accept

等函數打開的描述符。以下是一個子程序向父程序傳遞描述符的例子:

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

#define BUF_SIZE  1
#define TEXT_SIZE 12

void sendFd(int fd, int socket_fd);
int  recvFd(int socket_fd);

int main(void)
{
    int fd_pair[2] = {0};

    if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_pair) < 0)
    {
        fprintf(stderr, "socket error\n");
        return -1;
    }

    pid_t pid = fork();
    if (pid < 0)
    {
        fprintf(stderr, "fork error\n");
        return -1;
    }
    if (pid > 0)
    {
        close(fd_pair[1]);
        int  recv_fd             = recvFd(fd_pair[0]);
        char text[TEXT_SIZE + 1] = {0};
        if (read(recv_fd, text, TEXT_SIZE) != TEXT_SIZE)
        {
            fprintf(stderr, "read error\n");
            return -1;
        }
        printf("%s", text);
        if (waitpid(pid, NULL, 0) < 0)
        {
            fprintf(stderr, "waitpid error\n");
            return -1;
        }
        return 0;
    }

    close(fd_pair[0]);
    // ./hello.txt的内容為"hello world\n"
    int fd = open("./hello.txt", O_RDONLY);
    if (fd < 0)
    {
        fprintf(stderr, "open error\n");
        exit(EXIT_FAILURE);    // NOLINT
    }

    sendFd(fd, fd_pair[1]);

    return 0;
}

void sendFd(int fd, int socket_fd)
{
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    struct iovec iov;

    union
    {
        struct cmsghdr cm;
        char           control[CMSG_SPACE(sizeof(int))];
    } control_un;
    struct cmsghdr *cmptr      = NULL;
    msg.msg_control            = control_un.control;
    msg.msg_controllen         = sizeof(control_un.control);
    cmptr                      = CMSG_FIRSTHDR(&msg);
    cmptr->cmsg_len            = CMSG_LEN(sizeof(int));
    cmptr->cmsg_level          = SOL_SOCKET;
    cmptr->cmsg_type           = SCM_RIGHTS;
    *((int *)CMSG_DATA(cmptr)) = fd;
    msg.msg_name               = NULL;
    msg.msg_namelen            = 0;
    char buf[BUF_SIZE]         = {0};
    iov.iov_base               = &buf;
    iov.iov_len                = BUF_SIZE;
    msg.msg_iov                = &iov;
    msg.msg_iovlen             = 1;

    if (sendmsg(socket_fd, &msg, 0) < 0)
    {
        fprintf(stderr, "sendmsg error\n");
        exit(EXIT_FAILURE);    // NOLINT
    }
}

int recvFd(int socket_fd)
{
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));

    char         buf[BUF_SIZE] = {0};
    struct iovec iov;
    iov.iov_base = buf;
    iov.iov_len  = BUF_SIZE;

    union
    {
        struct cmsghdr cm;
        char           control[CMSG_SPACE(sizeof(int))];
    } control_un;

    msg.msg_control       = control_un.control;
    msg.msg_controllen    = sizeof(control_un.control);
    msg.msg_name          = NULL;
    msg.msg_namelen       = 0;
    msg.msg_iov           = &iov;
    msg.msg_iovlen        = 1;
    struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg);

    if (recvmsg(socket_fd, &msg, 0) < 0)
    {
        fprintf(stderr, "recvmsg error\n");
        exit(EXIT_FAILURE);    // NOLINT
    }

    int fd = *((int *)CMSG_DATA(cmptr));
    return fd;
}
           

參考資料

  1. 《UNIX 網絡程式設計 卷 1 套接字聯網 API(第 3 版)》
  2. 《Linux/UNIX 系統程式設計手冊(下冊)》
  3. 進階程序間通信之 UNIX 域套接字 - ITtecman - 部落格園

繼續閱讀