Unix域套接字(Unix Domain Socket)
2021-03-06 21:33:39 阿炯

Unix domain socket 或者 IPC(inter-process communication:进程间通信) socket 是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。socket 原本是为网络通讯设计的,但后来在其框架上发展出一种 IPC 机制,这就是 UNIX domain socket。与管道相比,它既可以使用字节流,又可以使用数据队列,而管道通信则只能使用字节流,它用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程;这是因为IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。Unix domain sockets的接口和Internet socket很像,但它不使用网络底层协议来通信。它的工作模式是全双工的,API 接口语义丰富,相比其它 IPC 机制有明显的优越性,目前已成为使用最广泛的 IPC 机制,比如 X Window 服务器和 GUI 程序之间就是通过 UNIX domain socket 通讯的。

Unix domain socket 的功能是POSIX操作系统里的一种组件。它使用系统文件的地址来作为自己的身份,可以被系统进程引用,所以两个进程可以同时打开一个Unix domain sockets来进行通信。不过这种通信方式是发生在系统内核里而不会在网络里传播。

Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,是IPC的方法之一,特定于*nix平台。使用unix domain socket有几个好处:
1)在同一主机上,unix domain socket比一般的tcp socket快上一倍,性能因素这是一个主要原因
2)unix domain socket可以在同一主机的不同进程之间传递文件描述符
3)较新的unix domain socket实现把客户的ID和组ID提供给服务器,可以让服务器作安全检查
4)数据的传输不需要经过网络协议栈,不需要打包拆包等操作,只是数据的拷贝过程;其分为SOCK_STREAM(流套接字)和SOCK_DGRAM(数据包套接字),由于是在本机通过内核通信,不会丢包也不会出现发送包的次序和接收包的次序不一致的问题


工作流程介绍

如果熟悉Socket的话,Unix domain socket也是同样的方式,区别如下:
它不需要IP和Port,而是通过一个文件名来表示
domain 为 AF_UNIX
使用sockaddr_un表示

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

服务端:socket -> bind -> listen -> accet -> recv/send -> close
客户端:socket -> connect -> recv/send -> close

函数介绍

开始创建socket

 int socket(int domain, int type, int protocol)
 int socket(int family, int type, int protocol);
 domain(域) : AF_UNIX
 type : SOCK_STREAM/SOCK_DGRAM :
 protocol : 0

SOCK_STREAM(流) : 提供有序,可靠的双向连接字节流。 可以支持带外数据传输机制,无论多大的数据都不会截断。
SOCK_DGRAM(数据报):支持数据报(固定最大长度的无连接,不可靠的消息),数据报超过最大长度,会被截断。

使用 UNIX domain socket 的过程和网络 socket 十分相似,也要先调用 socket() 创建一个 socket 文件描述符。
family 指定为 AF_UNIX,使用 AF_UNIX 会在系统上创建一个 socket 文件,不同进程通过读写这个文件来实现通信。
type 可以选择 SOCK_DGRAM 或 SOCK_STREAM。SOCK_STREAM 意味着会提供按顺序的、可靠、双向、面向连接的比特流。SOCK_DGRAM 意味着会提供定长的、不可靠、无连接的通信。
protocol 参数指定为 0 即可。

UNIX domain socket 与网络 socket 编程最明显的不同在于地址格式不同,用结构体 sockaddr_un 表示,网络编程的 socket 地址是 IP 地址加端口号,而 UNIX domain socket 的地址是一个 socket 类型的文件在文件系统中的路径,这个 socket 文件由 bind() 调用创建,如果调用 bind() 时该文件已存在,则 bind() 错误返回。因此,一般在调用 bind() 前会检查 socket 文件是否存在,如果存在就删除掉。

网络 socket 编程类似,在 bind 之后要 listen,表示通过 bind 的地址(也就是 socket 文件)提供服务。接下来必须用 accept() 函数初始化连接。accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。与网络 socket 编程不同的是,UNIX domain socket 客户端一般要显式调用 bind 函数,而不依赖系统自动分配的地址。客户端 bind 一个自己指定的 socket 文件名的好处是,该文件名可以包含客户端的 pid 等信息以便服务器区分不同的客户端。

获取到socket文件描述符之后,还要将其绑定一个文件上。

 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd : 传入sock的文件描述符
addr : 用sockaddr_un表示
addrlen : 结构体长度
struct sockaddr_un {
    sa_family_t sun_family; /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX];   /* pathname */
};

监听客户端的连接

int listen(int sockfd, int backlog);
sockfd : 文件描述符
backlog : 连接队列的长度

接受客户端的连接

int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
由于不存在客户端地址的问题,因此这里的addr和addrlen参数可以设置为NULL。

参考来源:


Linux 进程必知必会