Unix/Linux编程:Unix domain socket

UNIX domain socket用于同一主机系统上的相互通信

UNIX domain socket 地址:struct sockaddr_un

在Unix domain中,socket地址以路径名来表示,domain特定的socket地址结构的定义如下所示:

struct sockaddr_un{
	sa_family_t sun_family;   // always AF_UNIX
	char sun_path[128];  
};

sockaddr_un 结构中字段的 sun_前缀与 Sun Microsystems 没有任何关系,它是根据 socket unix 而来的。

为将一个Unix domain socket绑定到一个地址上,需要初始化一个sockaddr_un结构,然后将指定这个结构的一个指针作为addr参数bind()并将addrlen指定为这个结构的大小。如下所示:

	const char *SV_SOCK_PATH = "/tmp/mysock";
	int sfd;
	struct sockaddr_un addr;

	 // 绑定一个 UNIX domain socket 
 	 sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1){
        perror("socket");
        exit(EXIT_FAILURE);
    }
    
     memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
    {
        printf("bind");
        exit(EXIT_FAILURE);
    }

当用来绑定 UNIX domain socket 时,bind()会在文件系统中创建一个条目(因此作为socket路径名的一部分目录需要可访问和可读写)。文件的所有权将根据常规的文件创建规则来确定。这个文件会被标记为一个socket。当在这个路径名上stat()时,它会在stat结构的st_mode字段中的文件类型部分返回值S_IFSOCK。当使用ls –l列出时,UNIX domain socket 在第一列将会显示类型 s,而 ls –F 则会在 socket 路径名后面附加上一个等号(=)。

  • 尽管Unix domain socket是通过路径名来标识的,但在这些socket上发生的IO无限对底层设备进行操作。
  • 将 UNIX domain socket 绑定到/tmp 目录下的一个路径名上并不是一个安全的设计,在诸如/tmp 此类公共可写的目录中创建文件可能会导致各种各样的安全问题。例如在/tmp 中创建一个名字与应用程序 socket 的路径名一样的路径名之
    后就能够完成一个简单的拒绝服务攻击了。现实世界中的应用程序应该将 UNIX domain socket bind()到一个采取了恰当的安全保护措施的目录中的绝对路径名上

有关绑定一个 UNIX domain socket 方面还需要注意以下几点

  • 无法将一个socket绑定到一个既有路径上(bind()会失败并返回 EADDRINUSE 错误)。
  • 通常将一个socket绑定到一个绝对路径上,这样这个socket就会位于文件系统中的一个固定地址处。当然,也可以使用一个相对路径名,但这种做法并不常见,因为它要求想要 connect()这个 socket 的应用程序知道执行 bind()的应用程序的当前工作目录。
  • 一个socket只能绑定到一个路径名上,相应的,一个路径名只能被一个socket绑定
  • 无法使用open()打开一个socket
  • 当不再需要一个socket时可以使用unlink()(或 remove())删除其路径名条目(通常也应该这样做)

UNIX domain 中的流 socket

下面是一个简单的 UNIX domain 流 socket 服务器 (这个服务器是一个简单的迭代式服务器——服务器在处理下一个客户端之前一次只处理一个客户端)

// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define BACKLOG 5
#define SV_SOCK_PATH "/tmp/us_xfr"
#define BUF_SIZE 100
int main(int argc, char *argv[])
{
    struct sockaddr_un addr;
    int sfd, cfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    // 创建一个 socket。
    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1){
        perror("socket");
        exit(EXIT_FAILURE);
    }


    if (strlen(SV_SOCK_PATH) > sizeof(addr.sun_path) - 1){
        printf("Server socket path too long: %s", SV_SOCK_PATH);
        exit(EXIT_FAILURE);
    }

    // 删除所有与路径名一致的既有文件,这样才能将 socket 绑定到这个路径名上
    if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT){
        printf("remove-%s", SV_SOCK_PATH);
        exit(EXIT_FAILURE);
    }


    //为服务器 socket 构建一个地址结构
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    //将 socket 绑定到该地址上
    if (bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
    {
        printf("bind");
        exit(EXIT_FAILURE);
    }

    // 将这个 socket 标记为监听 socket。
    if (listen(sfd, BACKLOG) == -1){
        printf("listen");
        exit(EXIT_FAILURE);
    }

    // 执行一个无限循环来处理进入的客户端请求。每次循环迭代执行下列任务。
    for (;;) {          /* Handle client connections iteratively */

        /* Accept a connection. The connection is returned on a new
           socket, 'cfd'; the listening socket ('sfd') remains open
           and can be used to accept further connections. */
        // 接受一个连接,为该连接获取一个新 socket cfd。
        cfd = accept(sfd, NULL, NULL);
        if (cfd == -1)
        {
            printf("accept");
            exit(EXIT_FAILURE);
        }

        /* Transfer data from connected socket to stdout until EOF */
        //从已连接的 socket 中读取所有数据并将这些数据写入到标准输出中。
        while ((numRead = read(cfd, buf, BUF_SIZE)) > 0)

            if (write(STDOUT_FILENO, buf, numRead) != numRead)
            {
                printf("partial/failed write");
                exit(EXIT_FAILURE);
            }

        if (numRead == -1)
        {
            printf("read");
            exit(EXIT_FAILURE);
        }

        // 关闭已连接的 socket cfd
        if (close(cfd) == -1)
        {
            printf("close");
            exit(EXIT_FAILURE);
        }
    }
}

下面是一个简单的 UNIX domain 流 socket 客户端

// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#define BACKLOG 5
#define SV_SOCK_PATH "/tmp/us_xfr"
#define BUF_SIZE 100
int
main(int argc, char *argv[])
{
    struct sockaddr_un addr;
    int sfd;
    ssize_t numRead;
    char buf[BUF_SIZE];

    // 创建一个 socket。
    sfd = socket(AF_UNIX, SOCK_STREAM, 0);      /* Create client socket */
    if (sfd == -1){
        perror("socket");
        exit(EXIT_FAILURE);
    }

    /* Construct server address, and make the connection */
    // 为服务器 socket 构建一个地址结构并连接到位于该地址处的 socket。
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sfd, (struct sockaddr *) &addr,
                sizeof(struct sockaddr_un)) == -1)
    {
        perror("connect");
        exit(EXIT_FAILURE);
    }

    /* Copy stdin to socket */
    //执行一个循环将其标准输入复制到 socket 连接上。当遇到标准输入中的文件结尾时客
    // 户端就终止,其结果是客户端 socket 将会被关闭并且服务器在从连接的另一端的
    // socket 中读取数据时会看到文件结束。
    while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0)
        if (write(sfd, buf, numRead) != numRead)
        {
            printf("partial/failed write");
            exit(EXIT_FAILURE);
        }

    if (numRead == -1)
    {
        printf("read");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);         /* Closes our socket; server sees EOF */
}

使用效果:首先在后台运行服务器。
在这里插入图片描述
然后创建一个客户端用作输入的测试文件并运行客户端。
在这里插入图片描述
此刻子进程已经结束了。现在终止服务器并检查服务器的输出是否与客户端的输入匹配。
在这里插入图片描述
diff 命令没有产生任何输出,表示输入和输出文件是一致的。

注意在服务器终止之后,socket 路径名会继续存在。这就是为何服务器在调用 bind()之前使用 remove()删除 socket 路径名的所有既有实例。

UNIX domain 中的数据报 socket

一般来说,使用数据报socket的通信是不可靠的,但这个论断适用于通过网络传输的数据报。对于Unix domin socket来讲,数据报的传输是在内核中发送的,并且也是可靠的,所有的消息都会按照顺序被传递而且也不会发生重复情况

下面是一个简单的 UNIX domain 数据报服务器 程序

// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>

#define BUF_SIZE 10             /* Maximum size of messages exchanged
                                   between client and server */

#define SV_SOCK_PATH "/tmp/ud_ucase"

int main(int argc, char *argv[])
{
    struct sockaddr_un svaddr, claddr;
    int sfd, j;
    ssize_t numBytes;
    socklen_t len;
    char buf[BUF_SIZE];

  // 先创建一个 socket
    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);       /* Create server socket */
    if (sfd == -1){
        perror("socket");
        exit(EXIT_FAILURE);
    }


    if (strlen(SV_SOCK_PATH) > sizeof(svaddr.sun_path) - 1){
         printf("Server socket path too long: %s", SV_SOCK_PATH);
         exit(EXIT_FAILURE);
     }

   //服务器先删除了与该地址匹配的路径名,以防出现这个路径名已经存在的情况
    if (remove(SV_SOCK_PATH) == -1 && errno != ENOENT){
         printf("remove-%s", SV_SOCK_PATH);
         exit(EXIT_FAILURE);
	}
	// 将其绑定到一个众所周知的地址上
    memset(&svaddr, 0, sizeof(struct sockaddr_un));
    svaddr.sun_family = AF_UNIX;
    strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *) &svaddr, sizeof(struct sockaddr_un)) == -1)
    {
        printf("bind");
        exit(EXIT_FAILURE);
    }

    /* Receive messages, convert to uppercase, and return to client */

    for (;;) {
        len = sizeof(struct sockaddr_un);
        numBytes = recvfrom(sfd, buf, BUF_SIZE, 0,
                            (struct sockaddr *) &claddr, &len);
        if (numBytes == -1) {
        	printf("recvfrom");
        	exit(EXIT_FAILURE);
    	}

        printf("Server received %ld bytes from %s\n", (long) numBytes,
                claddr.sun_path);
        /*FIXME: above: should use %zd here, and remove (long) cast */

        for (j = 0; j < numBytes; j++)
            buf[j] = toupper((unsigned char) buf[j]);

        if (sendto(sfd, buf, numBytes, 0, (struct sockaddr *) &claddr, len) !=
                numBytes)
           {
				 printf("sendto");
				 exit(EXIT_FAILURE);
			}
    }
}

下面是一个简单的 UNIX domain 数据报客户端

#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>

#define BUF_SIZE 10             /* Maximum size of messages exchanged between client and server */
#define SV_SOCK_PATH "/tmp/ud_ucase"
int
main(int argc, char *argv[])
{
    struct sockaddr_un svaddr, claddr;
    int sfd, j;
    size_t msgLen;
    ssize_t numBytes;
    char resp[BUF_SIZE];

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        printf("%s msg...\n", argv[0]);
        exit(1);
        }

    /* Create client socket; bind to unique pathname (based on PID) */

    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sfd == -1)
    {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    memset(&claddr, 0, sizeof(struct sockaddr_un));
    claddr.sun_family = AF_UNIX;
    snprintf(claddr.sun_path, sizeof(claddr.sun_path),
            "/tmp/ud_ucase_cl.%ld", (long) getpid());

    if (bind(sfd, (struct sockaddr *) &claddr, sizeof(struct sockaddr_un)) == -1)
    {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    /* Construct address of server */

    memset(&svaddr, 0, sizeof(struct sockaddr_un));
    svaddr.sun_family = AF_UNIX;
    strncpy(svaddr.sun_path, SV_SOCK_PATH, sizeof(svaddr.sun_path) - 1);

    /* Send messages to server; echo responses on stdout */

    for (j = 1; j < argc; j++) {
        msgLen = strlen(argv[j]);       /* May be longer than BUF_SIZE */
        if (sendto(sfd, argv[j], msgLen, 0, (struct sockaddr *) &svaddr,
                sizeof(struct sockaddr_un)) != msgLen)
         {
	        perror("sendto");
	        exit(EXIT_FAILURE);
    	}

        numBytes = recvfrom(sfd, resp, BUF_SIZE, 0, NULL, NULL);
        /* Or equivalently: numBytes = recv(sfd, resp, BUF_SIZE, 0);
                        or: numBytes = read(sfd, resp, BUF_SIZE); */
        if (numBytes == -1) {
	        perror("recvfrom");
	        exit(EXIT_FAILURE);
    	}
        printf("Response %d: %.*s\n", j, (int) numBytes, resp);
    }

    remove(claddr.sun_path);            /* Remove client socket pathname */
    exit(EXIT_SUCCESS);
}

下面演示了如何使用服务器和客户端程序。
在这里插入图片描述
从客户端程序的第二个调用(有意在 recvfrom()调用中指定了一个比消息更小的 length 值),可以看出消息会被静默地截断

UNIX domain socket 权限

socket文件的所有权和权限绝对了哪些进程能够与这个socket进行通信:

  • 要连接一个Unix domain流socket需要在该socket文件上拥有写权限
  • 要通过一个Unix domain数据报socket发送一个数据报需要在该 socket 文件上拥有写权限

此外,需要在存放 socket 路径名的所有目录上都拥有执行(搜索)权限。

在默认情况下,创建 socket(通过 bind())时会给所有者(用户)、组以及 other 用户赋予所有的权限。要改变这种行为可以在调用 bind()之前先调用 umask()来禁用不希望赋予的权限。

创建互联 socket 对:socketpair()

有时候让单个进程创建一对socket并将它们连接起来是比较有用的。这可以通过使用两个socket()调用和一个bind()调用以及对listen()、connect()、accept()(用于流 socket)的调用或对 connect()(用于数据报 socket)的调用来完成。socketpair()系统调用则为这个操作提供了一个快捷方式。具体请参见socketpair()

Linux抽象socket名空间

所谓的抽象路径名空间是Linux特有的一项特性,它允许将一个Unix domain socket绑定到一个名字上但不会在文件系统中创建该名字。这种做法具备几点优势:

  • 无需担心与现有文件系统的既有名字冲突
  • 没有必要再使用完socket之后删除socket路径名。当socket被关闭之后会自动删除这个抽象名
  • 无需为socket创建一个文件系统路径名了。这对于 chroot 环境以及在不具备文件系统上的写权限时是比较有用的

要创建一个抽象绑定就需要将sun_path字段的第一个字节指定为NULL字节(\0)。这样就能够将抽象socket名字与传统的Unix domain socket路径名区分开来,因为传统的名字是由一个或者多个非空字节以及终止NULL字节构成的字符串。sun_path字段的余下字节为socket定义了抽象名字。在解释这个名字时需要用到全部字节,而不是将其看成是一个以 null结尾的字符串。

下面演示了如何创建一个抽象 socket 绑定

// us_xfr.h
#include <string.h>
#include <zconf.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_un addr;
    char *str;

    memset(&addr, 0, sizeof(struct sockaddr_un));  /* Clear address structure */
    addr.sun_family = AF_UNIX;                     /* UNIX domain address */

    /* addr.sun_path[0] has already been set to 0 by memset() */

    str = "xyz";        /* Abstract name is "\0xyz" */
    strncpy(&addr.sun_path[1], str, strlen(str));

    // In early printings of the book, the above two lines were instead:
    //
    // strncpy(&addr.sun_path[1], "xyz", sizeof(addr.sun_path) - 2);
    //            /* Abstract name is "xyz" followed by null bytes */

    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1)
    {
		perror("socket");
		exit(EXIT_FAILURE);
	}

    if (bind(sockfd, (struct sockaddr *) &addr,
            sizeof(sa_family_t) + strlen(str) + 1) == -1)
    {
		perror("bind");
		exit(EXIT_FAILURE);
	}

    // In early printings of the book, the final part of the bind() call
    // above was instead:
    //        sizeof(struct sockaddr_un)) == -1)

    sleep(60);

    exit(EXIT_SUCCESS);
}

使用一个初始null字节来区分抽象socket名和传统的socket名会带来不同寻常的结果。假设变量name正好指向了一个长度为零的字符串并将一个 UNIX domain socket绑定到一个照下列方式初始化 sun_path 的名字上。

 strncpy(&addr.sun_path, str, strlen(addr.sun_path) - 1);

在 Linux 上,就会在无意中创建了一个抽象 socket 绑定。但这种代码可能并不是期望中的代码(即一个 bug)。在其他 UNIX 实现中,后续的 bind()调用会失败

总结

Unix domain socket允许位于同一主机上的应用程序之间进行通信。Unix domain支持流和数据报socket

Unix domain socket是通过文件系统中的一个路径来标识的。文件权限可以用来控制对Unix domain socket的访问

socketpair()系统调用创建一对相互连接的Unix domain socket。这样就无需调用多个系统调用来创建、绑定以及连接socket。一个socket对的使用方式通常与管道类似:一个进程创建socket对,然后创建一个其引用socket对的描述符的紫禁城,然后这两个进程就能够通过这个socket对进行通信了。

Linux 特有的抽象 socket 名空间允许将一个 UNIX domain socket 绑定到一个不存在于文件系统中的名字上

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值