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目录下面使用下面的命令来查找
1 使用socat来创建Unix Domain Sockets
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信息
最前面的一位表示的是文件类型,s表示的就是socket文件
扩展一下,这个位置还可以有其他几种选项:p、d、l、s、c、b和-:
- p表示命名管道文件
- d表示目录文件
- l表示符号连接文件
- -表示普通文件
- s表示socket文件
- c表示字符设备文件
- b表示块设备文件
2 使用ss命令来查看Unix domain Socket
-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
3 使用stat命令来查看socket文件的具体信息
stat /tmp/stream.sock /tmp/datagram.sock
4 使用nc连接到Unix domain Socket服务
-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
如果没有输出任何异常数据,说明连接成功了
二、socket API
1 创建Unix域套接字
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); // 数据包式套接字
2 绑定Unix域套接字
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参数设置成这个结构体的实际大小。
最后再提一下权限的问题,因为要在文件系统中创建相应的文件,对于普通路径名来说,掉用bind()函数的进程必须要有路径名中目录部分的可写和可访问权限。还有,在默认情况下,在调用bind()函数时,会给所有者、组和其他用户赋予所有的权限(即777),如果想改变这个行为,可以在bind()之后再修改创建的文件的权限和属性。
3 stream建立连接
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结构体来表示
4 datagram建立连接
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 | 原始套接字 |
0.1 3.1 不同的UNIX域套接字地址结构
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* Pathname */
};
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);
0.2 3.2 文件名套接字中——UNIX域套接字类型
**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通信方式查询进程的一些信息。