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 程序简单分析
- 首先通过
socketpair
函数创建了一对已经连接的socket,不同进程可以通过这对
socket进行通信,建立后的socket如下图所示
user process
+------------------------+
| |
| |
| fd[0] fd[1] |
+------------------------+
^ ^
| |
| |
V V
+------+ +------+
|socket|<------>|socket|
+------+ +------+
socketpair
调用成功后,会通过fds
参数返回两个文件描述符,后面通过这两个文件描
述符进行通信。
- 通过
fork
函数创建了一个子进程 - 父进程中利用已经创建好的socket,向
fd[1]
中写入字符串:hi! - 子进程利用已经创建好的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来说,这些参数取值比较确定。
domain
传AF_UNIX
或者AF_LOCAL
代表unix socket
type
可以是SOCK_STREAM
SOCK_DGRAM
或者SOCK_SEQPACKET
这几种不同类型是通
用的类型,对unix socket来说其实区别不大,例如unix socket下都是同一台机器,
SOCK_DGRAM
类型的socket也是可靠。
另一方面,一些接口层面的区别仍然是存在的,比如对于SOCK_STREAM
类型的socket一般
的通信过程是:
- 创建一个socket
- 绑定一个地址
- 调用
listen
告诉内核已经准备好接受连接请求 - 调用
accept
建立一个连接 - 双方通信
对于SOCK_DGRAM
类型的socket来说,是没有3和4这两步的
protocol
固定传0
- 返回值:成功返回0, 失败返回-1
socketpair
与pipe
的作用和用法都十分相似,主要区别是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_family
和sun_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;
}