Unix Domain Socket

1. 什么是Unix Domain Socket

Socket(套接字)是操作系统定义的一套通信方式和实现通信的系统调用,比如最常用的互
联网上两台终端之间的通信。

Unix Domain Socket也是Socket的一种,专门用于同一台机器的不同进程之间进行通信

2. 最简单的用法

在详细介绍Unix Domain Socket各种用法的细节前,先看一个简单的例子

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <wait.h>

int
main(int argc, char *argv[])
{
    int fds[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0)
    {
        perror("fail to create socketpair");
        return 1;
    }

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fail to fork");
        return 1;
    }
    if (pid > 0)
    {
        /*parent process*/
        if (write(fds[1], "hi!", 3) == -1)
        {
            perror("fail to write");
            return 1;
        }
        /*wait for the child process terminatation*/
        wait(NULL);
    } else {
        /*child process*/
        char buf[4] = {0};
        if (read(fds[0], buf, 3) == -1)
        {
            perror("fail to read");
            return 1;
        }
        printf("receive msg:%s\n", buf);
    }
    
}

2.1 程序简单分析

  1. 首先通过socketpair函数创建了一对已经连接的socket,不同进程可以通过这对
    socket进行通信,建立后的socket如下图所示
   user process
+------------------------+
|                        |
|                        |
| fd[0]           fd[1]  |
+------------------------+
     ^              ^
     |              |
     |              |
     V              V
 +------+        +------+
 |socket|<------>|socket|
 +------+        +------+
 

socketpair调用成功后,会通过fds参数返回两个文件描述符,后面通过这两个文件描
述符进行通信。

  1. 通过fork函数创建了一个子进程
  2. 父进程中利用已经创建好的socket,向fd[1]中写入字符串:hi!
  3. 子进程利用已经创建好的socket,从fd[0]中读取字符串
    因此程序的输出结果是:receive msg:hi!

2.2 socketpair函数

#include <sys/socket.h>

int socketpair(int domain, int type, int protocol, int socketfd[2]);
  • 函数作用:创建一对没有名字的且已经连接的socket
    没有名字是相对于后面有名字的socket来说的,名字相当于网络通信里的地址,客户端可
    以通过名字与特定的服务端进行通信。已经连接的含义是在创建成功的情况下,可以直
    接通过这两个socketfd进行通信。

  • 参数
    除了最后一个参数,前面3个参数表示创建socket的类型,对于网络通信的socket来说类型
    很多,但对于unix socket来说,这些参数取值比较确定。
    domainAF_UNIX或者AF_LOCAL代表unix socket
    type 可以是SOCK_STREAM SOCK_DGRAM或者SOCK_SEQPACKET 这几种不同类型是通
    用的类型,对unix socket来说其实区别不大,例如unix socket下都是同一台机器,
    SOCK_DGRAM类型的socket也是可靠。
    另一方面,一些接口层面的区别仍然是存在的,比如对于SOCK_STREAM类型的socket一般
    的通信过程是:

  1. 创建一个socket
  2. 绑定一个地址
  3. 调用listen告诉内核已经准备好接受连接请求
  4. 调用accept建立一个连接
  5. 双方通信
    对于SOCK_DGRAM类型的socket来说,是没有3和4这两步的

protocol固定传0

  • 返回值:成功返回0, 失败返回-1

socketpairpipe的作用和用法都十分相似,主要区别是scokpair调用成功返回的
一对描述符是可以互相通信的,而pipe返回的一对描述符只能单方向通信,一个描述符
只能用于写,另一个描述符只能用于读

3. 有名字的UNIX Domain Sockets

前面提到socketpair创建的unix socket是没有名字的,名字相对于网络通信中的地址,
没有地址,其他不相关进程也就不能和他进行通信,那么有名字的unix socket如何创建
呢?

下面的例子是一个服务端/客户端通过unix socket通信的例子,通信内容非常简单,
客户端向服务端发送一段字符串,服务端接受后打印出来。代码里大部分的都是socket通
信的一些前期常规准备工作,这些工作的主要步骤是:创建socket,绑定地址,等待连接,
握手连接,连接成功进行通信

  • unix socket服务端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h>   //strcpy
#include <stddef.h> //offsetof
#include <unistd.h>

int
main(int argc, char *argv[])
{
    /* 1. 创建socket */
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("fail to create socket");
        return 1;
    }

    struct sockaddr_un addr;
    addr.sun_family = AF_UNIX;
    /* 绑定的地址为当前目录的文件名为my_unix_domain的文件*/
    strcpy(addr.sun_path, "my_unix_domain");
    int addrLen = offsetof(struct sockaddr_un, sun_path) 
        + strlen(addr.sun_path) + 1;
    /* 2. 绑定地址*/
    if (bind(fd, (struct sockaddr*)&addr, addrLen) == -1)
    {
        perror("fail to bind");
        return 1;
    }
    /* 3. 监听客户端连接 */
    if (listen(fd, 10) == -1)
    {
        perror("listen");
        return 1;
    }

    struct sockaddr_un clientAddr;
    socklen_t clientLen = sizeof(clientAddr);
    /* 4. 和客户端建立连接 */
    int clientFD = accept(fd, (struct sockaddr *)&clientAddr, &clientLen);
    if (clientFD == -1)
    {
        perror("accept");
        return 1;
    }

    /* 5. 读取客户端发来的内容 */
    char buf[256] = {0};
    int n = read(clientFD, buf, 256);
    if (n < 0)
    {
        perror("read");
        return 1;
    }
    printf("receive msg:%s\n", buf);
    close(fd);
    close(clientFD);
    /* 6. 释放绑定的文件 */
    unlink("my_unix_domain");
}

客户端代码

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0)
    {
        perror("socket");
        exit(1);
    }
    struct sockaddr_un servAddr;
    servAddr.sun_family = AF_UNIX;
    strcpy(servAddr.sun_path, "my_unix_domain");
    int addrLen = offsetof(struct sockaddr_un, sun_path) 
        + strlen(servAddr.sun_path) + 1;
    if (connect(fd, (struct sockaddr *)&servAddr, addrLen) < 0)
    {
        perror("connect");
        exit(1);
    }
    printf("connect to server\n");
    char buf[256] = "hi~";
    int n = write(fd, buf, strlen(buf));
    if (n < 0)
    {
        perror("write");
        exit(1);
    }
    close(fd);
    return 0;
}

3.1 Unix Domain Socket地址格式

通常Unix Socket的地址有两种情况,一种是上面提到的无名字的unix domain socket,另
一种有明确地址的,且地址和磁盘文件对应,使用下面结构表示

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

sun_family字段固定为AF_UNIX,sun_path是一个磁盘文件的路径,以null字符结尾。
上面的例子中,绑定的地址是当前目录下的名字为my_unix_domain的文件,绑定成功后,
操作系统会在磁盘上创建对应的文件,如果其他socket的试图绑定这个地址,操作系统检
测到磁盘中已经存在对应文件,就会报错,提示地址已经被绑定。
socket关闭后,操作系统不会自动清楚这个文件,所以需要显式的调用unlink函数删除这
个文件。

在Linux系统中,Unix Domain Socket地址除了上面两种情况外,还有一种虚拟地址类型,
也使用struct sockadd_un结构表示,但sun_path字段不一样,和磁盘文件无关,且
sun_path字段的第一个字节是null('\0')。另外,socket关闭后,这种虚拟的地址也会
自动释放

3.2 地址长度的计算

上述代码中试用的计算方式是:

offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1

而不是:

sizeof(sa_family_t) + strlen(sun_path) + 1

这是因为有的系统实现中,在sun_familysun_path字段之间可能还有其他字段,因
次采用第一种方式,代码移植性更好

4 传递辅助数据(Ancillary Data)

Unix Domain Socket还支持传递辅助数据,如文件句柄和认证信息,下面的例子是传递了
句柄,可以看出传递时所用的数据结构很复杂,而且必须调用sendmsg函数

  • 服务端代码
#include <stdio.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h>   //strcpy
#include <stddef.h> //offsetof
#include <unistd.h>

int NUM_FD = 1;

void
sendfd(int clientfd, int fd_to_send)
{
    struct msghdr msg = { 0 };
    struct cmsghdr *cmsg;
    int myfds[NUM_FD];  /* Contains the file descriptors to pass */
    myfds[0] = fd_to_send;
    char iobuf[2] = {'f'};
    iobuf[0] = 'e';
    struct iovec io = {
        .iov_base = iobuf,
        .iov_len = sizeof(iobuf)
    };
    union {         /* Ancillary data buffer, wrapped in a union
                       in order to ensure it is suitably aligned */
        char buf[CMSG_SPACE(sizeof(myfds))];
        struct cmsghdr align;
    } u;

    msg.msg_iov = &io;
    msg.msg_iovlen = 1;
    msg.msg_control = u.buf;
    msg.msg_controllen = sizeof(u.buf);
    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    cmsg->cmsg_len = CMSG_LEN(sizeof(int) * NUM_FD);
    memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));
    printf("server send msg_controllen is %d\n", msg.msg_controllen);
    int send_bytes = sendmsg(clientfd, &msg, 0);
    if (send_bytes == -1)
    {
        perror("sendmsg");
    } else {
        printf("send %d bytes\n", send_bytes);
    }
}

int
main(int argc, char *argv[])
{
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd == -1)
    {
        perror("fail to create socket");
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(struct sockaddr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "my_unix_domain");
    int addrLen = offsetof(struct sockaddr_un, sun_path) 
        + strlen(addr.sun_path) + 1;
    if (bind(fd, (struct sockaddr*)&addr, addrLen) == -1)
    {
        perror("fail to bind");
        return 1;
    }
    if (listen(fd, 10) == -1)
    {
        perror("listen");
        return 1;
    }

    struct sockaddr_un clientAddr;
    memset(&clientAddr, 0, sizeof(struct sockaddr_un));
    socklen_t clientLen = sizeof(clientAddr);
    int clientFD = accept(fd, (struct sockaddr *)&clientAddr, &clientLen);
    if (clientFD == -1)
    {
        perror("accept");
        return 1;
    } 
    char buf[256] = {0};
    int n = read(clientFD, buf, 256);
    if (n < 0)
    {
        perror("read");
        return 1;
    }
    printf("receive msg:%s\n", buf);
    int fd_to_send = open("a.txt", O_RDONLY);
    if (fd_to_send == -1 )
    {
        perror("open");
        return 1;
    }

    sendfd(clientFD, fd_to_send);
    close(fd);
    close(clientFD);
    unlink("my_unix_domain");
}

  • 客户端代码
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0)
    {
        perror("socket");
        exit(1);
    }

    struct sockaddr_un servAddr;
    servAddr.sun_family = AF_UNIX;
    strcpy(servAddr.sun_path, "my_unix_domain");
    int addrLen = offsetof(struct sockaddr_un, sun_path) 
        + strlen(servAddr.sun_path) + 1;
    if (connect(fd, (struct sockaddr *)&servAddr, addrLen) < 0)
    {
        perror("connect");
        exit(1);
    }
    printf("connect to server\n");
    char buf[256] = "hi~";
    int n = write(fd, buf, strlen(buf));
    if (n < 0)
    {
        perror("write");
        exit(1);
    }

    sleep(1);
    /* receive ancillary data*/
    struct msghdr msg = {0};
    char iobuf[1] = {'b'};
    struct iovec iov[1];
    iov[0].iov_base = iobuf;
    iov[0].iov_len = sizeof(iobuf);
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    int myfds[1];
    union {         /* Ancillary data buffer, wrapped in a union
                       in order to ensure it is suitably aligned */
        char buf[CMSG_SPACE(sizeof(myfds))];
        struct cmsghdr align;
    } u;
    msg.msg_control = u.buf;
    msg.msg_controllen = sizeof(u.buf);

    ssize_t receive_size = recvmsg(fd, &msg, 0);
    if ( receive_size == -1)
    {
        perror("recvmsg");
    } else {
        printf("client receive size is %d,  receive msg_iovlen is %d, msg_controllen length is %d\n", receive_size, msg.msg_iovlen, 
                msg.msg_controllen);
        printf("client buf is %c\n", iobuf[0]);
        struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
        if (cmsg == NULL) {
            perror("cmsg is null");
        } else {
            int receive_fd = *(int *)CMSG_DATA(cmsg);
            char buf[1024];
            if (read(receive_fd, buf, 1024) == -1)
            {
                perror("read");
            } else {
                printf("read from receive fd:%s\n", buf);
            }
            close(receive_fd);
        }
    }
    close(fd);
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容