Sirius
Sirius

目录

Unix domain Socket使用详解

一、基本概念

unix操作系统中一切都可以看做是文件,包括程序运行的一些信息。

Unix domain Socket可以简称为UDS,不同程序间的数据可以在操作系统层,借助于文件系统来进行数据交换。

对于程序本身来说,只需要读取和写入共享的socket文件即可,也就是说不同的程序之间通过socket文件来进行数据交互。

Unix domain Socket也可以分为Stream Socket和Datagram Socket

我们最多看到Unix domain socket的地方可能就是docker了

作为一种容器技术,docker需要和实体机进行快速的数据传输和信息交换

一般情况下UDS的文件是以.socket结尾的,我们可以在/var/run目录下面使用下面的命令来查找

unix-listen:<filename>    groups=FD,SOCKET,NAMED,LISTEN,CHILD,RETRY,UNIX
unix-recvfrom:<filename>  groups=FD,SOCKET,NAMED,CHILD,RETRY,UNIX

这里我们要使用到unix-listen和unix-recvfrom这两个参数

unix-listen表示的是创建stream-based UDS服务

unix-recvfrom表示的是创建datagram-based UDS

socat unix-listen:/tmp/stream.sock,fork /dev/null&
socat unix-recvfrom:/tmp/datagram.sock,fork /dev/null&

这里我们使用/tmp/datagram.sock来表示这个socket信息。

其中fork参数表示程序在接收到程序包之后继续运行,如果不用fork,那么程序会自动退出

socat后面本来要接一个bi-address,这里我们使用/dev/null,表示丢弃掉所有的income信息

image.png

最前面的一位表示的是文件类型,s表示的就是socket文件

扩展一下,这个位置还可以有其他几种选项:p、d、l、s、c、b和-:

- p表示命名管道文件

- d表示目录文件

- l表示符号连接文件

- -表示普通文件

- s表示socket文件

- c表示字符设备文件

- b表示块设备文件

   -n, --numeric       don't resolve service names
   -l, --listening     display listening sockets
   -x, --unix          display only Unix domain sockets

这里我们需要使用到上面3个选项,x表示的是显示UDS,因为是监听,所以使用-l参数,最后我们希望看到具体的数字,而不是被解析成了服务名,所以这里使用-n参数。

ss -xln | grep tmp

u_str表示的是UDS stream socket,而u_dg表示的是UDS datagram socket

image.png

stat /tmp/stream.sock /tmp/datagram.sock

image.png

  -U, --unixsock             Use Unix domain sockets only
  -u, --udp                  Use UDP instead of default TCP
  -z                         Zero-I/O mode, report connection status only

-U表示连接的是一个unixsocket。-u表示是一个UDP连接。

默认情况下nc使用的是TCP连接,所以不需要额外的参数。

另外我们直接建立连接,并不发送任何数据,所以这里使用-z参数。

nc -U -z /tmp/stream.sock
nc -uU -z /tmp/datagram.sock

如果没有输出任何异常数据,说明连接成功了

image.png

二、socket API

int socket (int domain, int type, int protocol);

第一个参数,域一定要设置成AF_UNIX或AF_LOCAL。

第二个参数表示套接字的类型,分为流套接字(SOCK_STREAM)和数据包套接字(SOCK_DGRAM)。

第三个参数表示协议,表示按给定的域和套接字类型选择默认协议,对于Unix域套接字来说,其一般设置成0。

不同于普通的AF_INET的Socket,由于都是在本机通过内核通信,所以SOCK_STREAM和SOCK_DGRAM都是可靠的,不会丢包也不会出现发送包的次序和接收包的次序不一致的问题

它们的区别仅仅是,SOCK_STREAM无论发送多大的数据都不会被截断,而对于SOCK_DGRAM来说,如果发送的数据超过了一个报文的最大长度,则数据会被截断。

对于Unix域套接字来说,其最后一个协议参数一定是被设置成0。因此,一般通过下面的方式创建一个Unix域套接字:

int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);  // 流式Unix域套接字
int sockfd = socket(AF_UNIX,SOCK_DGRAM, 0);    // 数据包式套接字
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

在Unix域套接字中,套接字的地址是以sockaddr_un结构体来表示的

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

结构体中的第一个字段必须要设置成AF_UNIX。

第二个字段,表示的是一个路径名。因此,要将一个Unix域套接字绑定到一个本地地址上,需要创建并初始化一个sockaddr_un结构体,并将指向这个结构体的指针作为addr参数(需要类型转换)传入bind()函数,并将addrlen参数设置成这个结构体的实际大小。

image.png

最后再提一下权限的问题,因为要在文件系统中创建相应的文件,对于普通路径名来说,掉用bind()函数的进程必须要有路径名中目录部分的可写和可访问权限。还有,在默认情况下,在调用bind()函数时,会给所有者、组和其他用户赋予所有的权限(即777),如果想改变这个行为,可以在bind()之后再修改创建的文件的权限和属性。

int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, struct sockaddr *addr,int addrlen);
ssize_t read(int sockfd, void *buf, size_t length); 
ssize_t write(int sockfd, const void *buf, size_t length);

对于流式套接字的服务器端来说,listen()函数在TCP/IP套接字和Unix域套接字中调用方式是一样的,没有区别

与普通的TCP/IP套接字不同,Unix域套接字不存在客户端地址的问题(都在一台机器上),因此这里的addr和addrlen参数都要设置成NULL。

在这里,不同进程像这个服务器端进程发送的流数据是在内核里面区分的,并绑定到了accept()创建的套接字中了。

而数据包套接字就没有这种对应关系,所以还是要在代码中区分出来,后面会介绍。

在用socket()函数获得了新创建套接字的文件描述符之后,就可以调用connect()函数连接服务器端了,只不过和bind()函数一样,地址addr必须是以sockaddr_un结构体来表示

int recvfrom(int sockfd, void *buf, int length, unsigned int flags, struct sockaddr *addr, int *addrlen);
int sendto (int sockfd, const void *buf, int length, unsigned int flags, const struct sockaddr *addr, int addrlen);

对于数据包套接字来说,在服务器端recvfrom()用来接收客户端发送的请求,而在客户端这个函数用来接收服务器端发送过来的响应

同时,在客户端sendto()用来向服务器端发送请求数据,而服务器端用这个函数来向客户端发送响应数据:

前面也提到了,对于数据包套接字来说,服务器端在发送响应数据时是需要知道客户端到底是哪个的,从而后面可以将相应的响应数据发送给正确的客户端。

客户端也需要知道到底是向哪个服务器端发送数据,或者说接收到的响应数据到底来自哪个服务器端(当然,如果只保证和一个服务器端通信就没有这个问题)。

但是,按照普通的包套接字创建和连接的流程,只是在服务器端掉用bind()函数绑定了一个地址,而客户端并没有地址。

这在流式套接字中没有问题,内核已经在服务器端调用accept()函数接收一个客户端连接时创建了一个新的套接字,从而将一一对应关系绑定到了这个新的套接字上了。

所以,对于包套接字来说,在客户端还需要再掉用bind()函数绑定一次,人为的创建一个客户端地址,且这个客户端路径名地址显然不能和服务器端的路径名相同

剩下的就都和普通的TCP/IP套接字相同了,只不过地址addr必须是以sockaddr_un结构体来表示罢了。

三、 Unix域套接字类型梳理

对于UNIX域套接字来说,支持以下几种套接字类型:

类型 描述
SOCK_DGRAM 长度固定、无连接的不可靠的报文传递
SOCK_STREAM 有序、可靠、双向的面向连接的字节流
SOCK_SEQPACKET 长度固定、有序、可靠的面向连接的报文传递
SOCK_RAW 原始套接字
struct sockaddr_un {
   sa_family_t sun_family; /* AF_UNIX */
   char sun_path[108]; /* Pathname */
};

image.png

UNIX域socket可以是:

1、文件名套接字

文件名套接字创建时会在指定的文件路径下创建socket文件,该socket文件创建后,可通过chmod或chown修改。

文件名套接字可以支持SOCK_DGRAM、SOCK_STREAM和SOCK_SEQPACKET类型。

SOCK_DGRAM、SOCK_STREAM用法类似,不过SOCK_DGRAM服务器端无法向客户端回复数据,

SOCK_SEQPACKET具有两者共同属性(面向连接)

使用bind()将UNIX域套接字绑定到文件系统路径名。它的长度等于sizeof(sa_family_t) + strlen(sun_path) + 1。该地址结构具有较好的扩展性。

文件命名的socket在使用完后,需要unlink该套接字,否则可能会导致绑定套接字失败的问题。

2、无名套接字

没有用bind()绑定文件路径。socketpair()创建的是未命名的。当一个未命名的地址返回时,它的长度是sizeof(sa_family_t)。

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

创建一对未命名的连接的socket,返回值sv[0]和sv[1]是被引用与新socket的文件描述符。

成功时返回0; 失败时返回-1,errno被设置,sv[]不会被修改。无名套接字优点类似与管道的用法。

int fd[2];
ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fd);

使用read-write进行通信

3、抽象套接字

抽象套接字sun_path[0]=’\0’,文件系统下无文件名。套接字会随着文件描述符关闭而自动关闭

获取一个抽象socket的地址时,其长度为sizeof(sa_family_t)+2,抽象套接字随着所有引用套接字的引用关闭而自动消失。

server_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);

memset(&saddr, 0, sizeof(saddr));
saddr.sun_family = AF_UNIX;
saddr.sun_path[0] = '\0';

bind(server_fd, (struct sockaddr*)&saddr, sizeof(saddr));
listen(server_fd, 1);
client_fd = accept(server_fd, NULL, NULL);

**SOCK_DGRAM:**用于保留消息边界的面向数据报(无连接)的套接字(在大多数UNIX实现中,UNIX域数据报套接字总是可靠的,并且不会对数据报重新排序);

**SOCK_STREAM:**用于面向流(或面向连接)的套接字。

**SOCK_SEQPACKET:**对于面向连接的序列数据包套接字,将保留消息边界,并按照发送的顺序传递消息。

- UNIX DGRAM、STREAM套接字对比AF_INET的差异

都在本机通过内核通信,均是可靠的,不会丢包也不会出现发包次序、接收包次序不一致的问题

- UNIX DGRAM、STREAM套接字,差异点是:

SOCK_STREAM不论多大数据都不会被截断;SOCK_DGRAM发送数据超过了最大长度后会被截断

- UNIX DGRAM、SEQPACKET的主要区别:

使用SOCK_DGRAM类型,不需要创建连接(例如connect到服务器),只需将数据包发送到服务器套接字,接口传输消息。

但如果**服务器需要回复消息时(服务端不知道客户端地址信息,没有创建对应的socket文件****),**客户端也需创建自己的unix套接字,让服务器知道这个套接字,然后服务器方可向它回复消息(强行按照网络数据报类型的流程处理,执行会报错误:Transport endpoint is not connected)这对应用而言并不友好,适用于仅需客户端发送消息给服务端,无需服务器回复的场景。

如果需要客户端、服务端交互通信:使用面向连接的方法,SOCK_SEQPACKET就是首选

总结,UNIX域套接字主要用于同一主机上进程间的通信,在许多应用中都被用到。

SOCK_SEQPACKET类型的文件名套接字用法是比较常见的,例如可以通过socket通信方式查询进程的一些信息。