UNIX域套接字是单个主机上客户端与服务器通信的一种方式。允许同一台计算机上不同进程之间通过文件系统中的特殊文件(套接字文件)进行数据交换。

可以在同一台计算机上运行的两个进程之间传递打开的文件描述符。

与TCP套接字相比较,UNIX域套接字不涉及网络协议栈,因此传递速度更快,效率更高。

UNIX域套接字

UNIX域套接字就像是套接字和管道的混合

可以使用面向网络的域套接字接口socket函数或者使用socketpair函数来创建一对无命名的、相互连接的UNIX域套接字。

特点

高效性:UNIX域套接字由于不涉及网络协议栈,因此通信速度比任何基于TCP/IP的网络套接字速度都要快。

与TCP套接字相比,UNIX域套接字不涉及网络协议栈,仅仅复制数据,因此并不执行协议处理、添加或删除网络报文头、计算校验和、产生顺序号以及发送确认报文等。

可靠性:UNIX域套接字数据报服务是可靠的,既不会丢失信息也不会传递出错。

文件描述符传递:可以在同一台主机下的不同进程之间传递打开的文件描述符。

类型

字节流套接字(SOCK_STREAM:面向连接的、可靠的数据传输服务。

数据报套接字(SOCK_DGRAM:无连接的数据传输服务,数据以独立的数据包传输。与UDP相比是可靠的

应用场景

适用于同一台计算机的进程之间通信,需要高性能和低延迟的应用场景,比如服务器内部组件之间的通信。

匿名UNIX域套接字

使用socketpair函数可以创建一对匿名的、相互连接的UNIX域套接字。此时该UNIX套接字就等同于全双工管道,两边对读和写均开放。可称之为fd管道,区别于之前的半双工管道(匿名管道和FIFO)。

此时该匿名套接字仅对能够用于具有亲缘关系的进程之间。

要想在任意两个进程之间使用,类似于TCP套接字,使其他进程能够识别的原因在于绑定了唯一的地址或域名,同理也需要给UNIX域套接字绑定一个唯一的地址,其他进程可通过该地址与当前进程建立连接并进行通信。

socketpair函数说明如下:

#include <sys/socket.h>

int socketpair(int domain, int type, int protocol, int fd[2]);
// 返回值:成功返回0,失败返回-1并设置errno

前三个参数和socket函数的参数完全相同,但是domain一般取值为AF_LOCALtype取值可选为SOCK_STREAMSOCK_DGRAMprotocol取值为0表示采用默认协议。fd[2]用来返回创建的两个套接字描述符。UNIX域套接字

fd管道示例:

#include <sys/socket.h>

int fd_pipe(int fd[2])
{
    return socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
}

匿名管道、FIFO都是半双工的,全双工管道仅有socketpair域套接字

实例:借助UNIX域套接字轮询XSI消息队列

由于XSI消息队列是基于自有的数据类型的,因此无法将其与pollselect等I/O多路复用的接口串联起来使用。然而UNIX域套接字是基于文件描述符的,因此可以将UNIX域套接字作为一个桥。

当消息到达时监听消息队列的线程接收到消息之后,将其写入UNIX域套接字的一端,另一端采用pollselect监听UNIX域套接字是否有数据可读写

具体示例如下:

在该示例中,共创建了三个线程分别监听三个消息队列,当有消息到达时将消息写入到对应的UNIX域套接字的一端,主线程使用poll监听UNIX域套接字的另一端上是否有数据可读。

#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <unistd.h>

#define NQ 3        // 消息队列数
#define MAXMSZ 512  // 每条消息的最大字节数
#define KEY 0x123   // 消息队列的标识

// 将消息队列id和UNIX域套接字传递给线程
struct threadinfo
{
    int qid;
    int fd;
};

// 消息结构体
struct mymesg
{
    long mtype;
    char mtext[MAXMSZ];
};

// 消息接收线程
void* helper(void* arg)
{
    struct threadinfo* tip = (struct threadinfo*)arg;
    if (tip == NULL)
    {
        printf("invalid param\n");
        pthread_exit((void*)-1);
    }
    int n;
    struct mymesg m;
    for (;;)
    {
        memset(&m, 0x00, sizeof(m));
        // type==0,返回队列中第一个消息
        // type>0,返回队列中消息类型为type的第一个消息
        // type<0,返回队列中消息类型小于type觉得值的第一个消息,如果存在多个返回最小的。
        // m->mtype中存储的是返回的消息类型。
        if ((n = msgrcv(tip->qid, &m, MAXMSZ, 0, MSG_NOERROR)) < 0)
        {
            printf("msgrcv error\n");
            pthread_exit((void*)-1);
        }
        // 收到消息之后写入UNIX域套接字
        if (write(tip->fd, m.mtext, n) < 0)
        {
            perror("write");
            pthread_exit((void*)-1);
        }
    }
}

int main(int argc, char* argv[])
{
    int fd[2];                 // 用来接收域套接字
    int qid[NQ];               // 消息队列ID
    struct pollfd pfd[NQ];     // 每个消息队列至少对应一个
    struct threadinfo ti[NQ];  // 传递给线程的数据结构
    pthread_t tid[NQ];

    for (int i = 0; i < NQ; i++)
    {
        // 创建消息队列
        if ((qid[i] = msgget(KEY + i, IPC_CREAT | 0666)) < 0)
        {
            printf("msgget error\n");
            exit(-1);
        }
        printf("queue id %d is %d\n", i, qid[i]);

        // 每个线程创建一对数据报的UNIX域套接字
        if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fd) < 0)
        {
            perror("socketpair");
            exit(-1);
        }

        // 将UNIX域套接字设置为pollfd数组元素,主线程用来实现I/O多路复用
        pfd[i].fd = fd[0];
        pfd[i].events = POLLIN;

        // 将队列id和UNIX域套接字的另一端传递给消息接收线程
        ti[i].qid = qid[i];
        ti[i].fd = fd[1];

        if (pthread_create(&tid[i], NULL, helper, &ti[i]) != 0)
        {
            printf("pthread_create failed\n");
            exit(-1);
        }
    }

    for (;;)
    {
        // 超时时间为-1,表示永久等待
        // poll阻塞等待可读事件
        if (poll(pfd, NQ, -1) < 0)
        {
            perror("poll");
            exit(-1);
        }

        // 有数据需要接收,遍历检测哪个文件描述符中数据可读
        for (int i = 0; i < NQ; i++)
        {
            if (pfd[i].revents & POLLIN)
            {
                char buf[MAXMSZ] = {0};
                int n = 0;
                if ((n = read(pfd[i].fd, buf, sizeof(buf))) < 0)
                {
                    perror("read");
                    exit(-1);
                }
                buf[n] = '\0';
                printf("queue id %d message %s\n", qid[i], buf);
            }
        }
    }
    exit(0);
}

采用数据报SOCK_DGRAM套接字的原因在于可以保证每次收到一条消息,不用考虑数据的边界问题

通过上面的方式可以在XSI消息队列中使用pollselect等I/O复用技术,虽然要为每一条消息队列增加一个线程的开销。

下面是测试接收消息功能的示例程序,该程序需要传入两个参数,分别是消息队列id和消息内容:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>

#define MAXMSZ 512

struct mymesg
{
    long mtype;
    char mtext[MAXMSZ];
};

int main(int argc, char* argv[])
{
    key_t key;
    long qid;
    size_t nbytes;
    struct mymesg m;

    if (argc != 3)
    {
        printf("usage: sendmsg KEY message\n");
        exit(-1);
    }

    // 获取消息队列id
    key = strtol(argv[1], NULL, 0);
    if ((qid = msgget(key, 0)) < 0)
    {
        printf("can't open queue key %s\n", argv[1]);
        exit(-1);
    }

    // 生成消息
    memset(&m, 0x00, sizeof(m));
    strncpy(m.mtext, argv[2], MAXMSZ - 1);
    nbytes = strlen(m.mtext);
    // 消息类型为1,由于接收端设置为按顺序接收,因此这里没有意义
    m.mtype = 1;

    // 发送消息
    if (msgsnd(qid, &m, nbytes, 0) < 0)
    {
        printf("msgsnd error\n");
        exit(-1);
    }
    exit(0);
}

执行结果如下:

ubuntu24:~$ ./sockpair_demo1& # 后台运行接收进程
[1] 2353
ubuntu24:~$ queue id 0 is 0 # 创建了三个消息队列
queue id 1 is 1
queue id 2 is 2
ubuntu24:~$
ubuntu24:~$ ./sockpair_demo1_test 0x123 hello1 # 对每个消息队列发送消息
queue id 0 message hello1 # 接收线程收到的消息
ubuntu24:~$ ./sockpair_demo1_test 0x124 hello2
queue id 1 message hello2
ubuntu24:~$ ./sockpair_demo1_test 0x125 hello3
queue id 2 message hello3
ubuntu24:~$

命名UNIX域套接字

上面提到使用socketpair函数可以创建一对匿名的、相互连接的UNIX域套接字,但是这种套接字仅能用于有关系的进程之间,比如fork产生的父子进程,这是因为有关系的进程之间可以共享内存空间,进而共享UNIX域套接字

那怎么才能在不相关的两个进程之间通过UNIX域套接字进行通信呢?

参考TCP套接字,通过将套接字绑定在IP地址上,然后其他进程通过IP地址(域名最终要解析为IP地址)与当前进程进行通信。同理UNIX域套接字也可以绑定在其特定地址上,不过该地址与TCP套接字的格式不同

UNIX域套接字地址格式如下:

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

该地址格式中包含一个sun_path路径名字段(等同于TCP套接字的IP地址+端口号),当把一个地址和UNIX域套接字绑定在一起时,系统会用该路径名创建一个S_IFSOCK类型的文件

该文件用于通知其他进程该套接字名称。当该文件已存在时,绑定请求会失败。当关闭套接字时,并不会删除该文件,因此在应用进程退出之前需要手动删除。

实例:创建UNIX域套接字地址并进行绑定

在实例中,通过socket函数创建了一个UNIX域套接字(上面提到过创建UNIX域套接字的方式有两种,一种是标准的socket函数,另一种是socketpair函数),同时也创建了一个UNIX域套接字地址,然后将两者进行绑定,生成了sun_path指定的文件。

#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    int fd;
    struct sockaddr_un un;
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, "foo.socket"); // 在当前目录下创建该文件

    // 创建数据流UNIX域套接字
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");
        return -1;
    }

    // int size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
    // offsetof宏返回成员相对于结构体开头的偏移
    if (bind(fd, (struct sockaddr*)&un, sizeof(struct sockaddr_un)) < 0)
    {
        perror("bind");
        return -1;
    }

    printf("UNIX domain socket bound\n");
    return 0;
}

从下面的结果来看,已经生成了UNIX域地址指定的文件,其他进程可通过该文件与当前UNIX域套接字进行通信。

$ ls -l foo.socket
srwxrwxr-x 1 xxx xxx 0 Aug 18 15:36 foo.socket

当未删除该文件的情况下,第二次运行就会出错,出错原因一般为Address already in use