浅谈Unix域套接字

浅谈 Unix 域套接字

引言

Linux 中有许多进行 进程间通信 的方法。今天博主向大家介绍一种常用的进程间通信的方法 ——Unix 域套接字

简介

Unix 域套接字 是一种在本机的进程间进行通信的一种方法。虽然 Unix 域套接字的接口与 TCP 和 UDP 套接字 的接口十分相似,但是 Unix 域套接字只能用于同一台机器的进程间通信,不能让两个位于不同机器的进程进行通信。正由于这个特性,Unix 域套接字可以可靠地在两个进程间复制数据,不用像 TCP 一样采用一些诸如 * 添加网络报头 计算检验和 产生顺序号 * 等一系列保证数据完整性的操作。因此,在同一台机器上进行进程间通信时,Unix 域套接字的效率往往比 TCP 套接字的效率要高。

因为 Unix 域套接字的效率比较高,一些程序经常用 Unix 套接字代替 TCP 套接字。例如当 MySQL 的服务器进程和客户端进程在同一台机器上时,可以用 Unix 域套接字代替 TCP 套接字。

Unix 域套接字地址结构

在使用 TCP 套接字和 UDP 套接字时,我们需要用 struct sockaddr_inIPv4)定义套接字的地址结构,与之相似,Unix 域套接字使用 struct sockaddr_un 定义套接字的地址结构。struct sockaddr_un 的定义如下(* 位于头文件 sys/un.h 中 *):

1
2
3
4
5
struct sockaddr_un
{
sa_family_t sun_family;
char sun_path[108];
};

在使用 Internet 域套接字进行编程时,需要将 struct sockaddr_insin_family 成员设置为 AF_INETIPv4)。与之类似,在使用 Unix 域套接字时,需要将 sun_family 设置为 AF_UNIXAF_LOCAL(* 这两个宏的作用完全相同,都表示 UNIX 域 *)。struct sockaddr_un 的第二个成员 sun_path 表示 socket 的地址。在 Unix 域中,socket 的地址用路径名表示。例如,可以将 sun_path 设置为 /tmp/unixsock。由于路径名是一个字符串,所以 sun_path 必须能够容纳字符串的字符和结尾的 '\0'。需要注意的是,标准并没有规定 sun_path 的大小,在某些平台中,sun_path 的大小可能是 104、92 等值。所以如果需要保证可移植性,在编码时应该使用 sun_path 的最小值。

创建 Unix 域套接字

Unix 域套接字使用 socket 函数创建,与 Internet 域套接字一样,Unix 域套接字也有流套接字和数据报套接字两种:

1
2
int unix_sock_fd1 = socket(AF_UNIX, SOCK_STREAM, 0); // Unix 域中的流 socket
int unix_sock_fd2 = socket(AF_UNIX, SOCK_DGRAM, 0); // Unix 域中的数据包 socket

稍后将介绍这两种套接字的用法和区别。

绑定 Unix 域套接字

使用 bind 函数可以将一个 Unix 套接字绑定到一个地址上。绑定 Unix 域套接字时,bind 会在指定的路径名处创建一个表示 Unix 域套接字的文件。Unix 域套接字与路径名是一一对应关系,即一个 Unix 域套接字只能绑定到一个路径名上,一个路径名也只能被一个套接字绑定。一般要把 Unix 域套接字绑定到一个 绝对路径 上,例如:

1
2
3
4
5
6
7
8
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "/tmp/sockaddr");
int unix_sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (bind(unix_sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
fprintf(stderr, "bind error\n");
}

Unix 域套接字被绑定后,可以使用 getsockname 获取套接字绑定的路径名:

1
2
3
4
struct sockaddr_un addr2;
socklen_t len = sizeof(addr2);
getsockname(unix_sock_fd, (struct sockaddr *)&addr2, &len);
printf("%s\n", addr2.sun_path);

当一个 Unix 域套接字不再使用时,应当调用 unlink 将其删除。

Unix 域中的流 socket

Unix 域中的流套接字与 TCP 流套接字的用法十分相似。在服务器端,我们首先创建一个 Unix 域流套接字,将其绑定到一个路径上,然后调用 listen 监听客户端连接,调用 accept 接受客户端的连接。在客户端,在创建一个 Unix 域流套接字之后,可以使用 connect 尝试连接指定的服务器套接字。以下是一个使用 Unix 域流套接字实现的 echo 服务器和客户端的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// 服务器
#include <assert.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define BACKLOG 5
#define MSG_MAX_LENGTH 100

void echo(int client_fd);
void readLine(int fd, char *buf);

void signalHandler(int signo) // NOLINT
{
unlink(UNIX_SOCKET_PATH); // NOLINT
exit(EXIT_SUCCESS); // NOLINT
}

int main(void)
{
if (signal(SIGINT, signalHandler) == SIG_ERR) // NOLINT
{
fprintf(stderr, "signal error\n");
return -1;
}

int listen_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (listen_fd < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}

struct sockaddr_un unix_socket_addr;
memset(&unix_socket_addr, 0, sizeof(unix_socket_addr));
unix_socket_addr.sun_family = AF_LOCAL;
strcpy(unix_socket_addr.sun_path, UNIX_SOCKET_PATH);
if (bind(listen_fd, (struct sockaddr *)&unix_socket_addr, sizeof(unix_socket_addr))
< 0)
{
fprintf(stderr, "bind error\n");
return -1;
}

if (listen(listen_fd, BACKLOG) < 0)
{
fprintf(stderr, "listen error\n");
return -1;
}

for (;;)
{
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0)
{
fprintf(stderr, "accept error\n");
return -1;
}

switch (fork())
{
case -1:
{
fprintf(stderr, "fork error\n");
return -1;
}
case 0:
{
echo(client_fd);
break;
}
default:
{
break;
}
}
}

return 0;
}

void echo(int client_fd)
{
char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
readLine(client_fd, buf);
int msg_len = (int)strlen(buf);
if (write(client_fd, buf, msg_len) != msg_len)
{
fprintf(stderr, "write error\n");
exit(EXIT_FAILURE); // NOLINT
}
}
}

void readLine(int fd, char *buf)
{
int i = 0;
for (; i < MSG_MAX_LENGTH; i++)
{
switch (read(fd, buf + i, 1))
{
case 1:
{
break;
}
case 0:
{
exit(EXIT_FAILURE); // NOLINT
break;
}
case -1:
{
fprintf(stderr, "read error\n");
exit(EXIT_FAILURE); // NOLINT
break;
}
default:
{
assert(0);
}
}

if (buf[i] == '\n')
{
i++;
break;
}
}

buf[i] = '\0';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH 100

int main(void)
{
int socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (socket_fd < 0)
{
fprintf(stderr, "socker error\n");
return -1;
}

struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, UNIX_SOCKET_PATH);
if (connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
fprintf(stderr, "sonnect error\n");
return -1;
}

char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
fgets(buf, MSG_MAX_LENGTH, stdin);
int len = (int)strlen(buf);
if (write(socket_fd, buf, len) != len)
{
fprintf(stderr, "write error\n");
return -1;
}
if (read(socket_fd, buf, len) != len)
{
fprintf(stderr, "read error\n");
return -1;
}
printf("%s", buf);
}

return 0;
}

Unix 域中的数据报 socket

Unix 域数据报套接字UDP 套接字 类似,可以通过 Unix 域数据报套接字在进程间发送具有边界的数据报。但由于 Unix 域数据报套接字是在本机上进行通信,所以 Unix 域数据报套接字的数据传递是可靠的,不会像 UDP 套接字那样发生丢包的问题。Unix 域数据报套接字的接口与 UDP 也十分相似。在服务器端,通常先创建一个 Unix 域数据报套接字,然后将其绑定到一个路径上。然后调用 recvfrom 接收客户端发送来的数据,调用 sendto 向客户端发送数据。对于客户端,通常是先创建一个 Unix 域数据报套接字,将这个套接字绑定到一个路径上,然后调用 sendto 发送数据,调用 recvfrom 接收客户端发来的数据。以下是使用 Unix 域数据报套接字实现的 echo 服务器和客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// 服务器
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH 100

void signalHandler(int signo) // NOLINT
{
unlink(UNIX_SOCKET_PATH); // NOLINT
exit(EXIT_SUCCESS); // NOLINT
}

int main(void)
{
if (signal(SIGINT, signalHandler) == SIG_ERR) // NOLINT
{
fprintf(stderr, "signal error\n");
return -1;
}

int listen_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (listen_fd < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}

struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, UNIX_SOCKET_PATH);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
fprintf(stderr, "bind error\n");
return -1;
}

char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
struct sockaddr_un client_addr;
socklen_t len = sizeof(client_addr);
int msg_len = (int)recvfrom(listen_fd,
buf,
MSG_MAX_LENGTH,
0,
(struct sockaddr *)&client_addr,
&len);
if (msg_len < 0)
{
fprintf(stderr, "recvfrom error\n");
return -1;
}
if (sendto(listen_fd, buf, msg_len, 0, (struct sockaddr *)&client_addr, len) < 0)
{
fprintf(stderr, "sendto error\n");
return -1;
}
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 客户端
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH 100
#define SOCKET_PATH_MAX_LENGTH 50

void signalHandler(int signo) // NOLINT
{
char socket_path[SOCKET_PATH_MAX_LENGTH] = {0};
sprintf(socket_path, "/tmp/echo_unix_socket_%ld", (long)getpid()); // NOLINT
unlink(socket_path); // NOLINT
exit(EXIT_SUCCESS); // NOLINT
}

int main(void)
{
if (signal(SIGINT, signalHandler) == SIG_ERR) // NOLINT
{
fprintf(stderr, "signal error\n");
return -1;
}

int socket_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (socket_fd < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}

struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_LOCAL;
strcpy(server_addr.sun_path, UNIX_SOCKET_PATH);

struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = AF_LOCAL;
sprintf(client_addr.sun_path, "/tmp/echo_unix_socket_%ld", (long)getpid());

if (bind(socket_fd, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0)
{
fprintf(stderr, "bind error\n");
return -1;
}

char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
fgets(buf, MSG_MAX_LENGTH, stdin);
int msg_len = (int)strlen(buf);
if (sendto(socket_fd,
buf,
msg_len,
0,
(struct sockaddr *)&server_addr,
sizeof(server_addr))
< 0)
{
fprintf(stderr, "sendto error\n"); // NOLINT
return -1;
}
if (recvfrom(socket_fd, buf, MSG_MAX_LENGTH, 0, NULL, NULL) < 0)
{
fprintf(stderr, "recvfrom error\n");
return -1;
}
printf("%s", buf);
}

return 0;
}

Unix 域套接字的权限

当程序调用 bind 时,会在文件系统中的指定路径处创建一个与套接字对应的文件。我们可以通过控制该文件的权限来控制进程对这个套接字的访问。当进程想要连接一个 Unix 域流套接字或通过一个 Unix 域数据报套接字发送数据包时,需要拥有对该套接字的 写权限 以及对 socket 路径名的所有目录的 执行权限。在调用 bind 时,会自动赋予用户、组和其他用户的所有权限。如果想要修改这一行为,可以在调用 bind 之前调用 umask 禁用掉某些权限。

使用 socketpair 创建互联的 socket 对

有时我们需要在同一个进程中创建一对相互连接的 Unix 域 socket(* 与管道类似 *),这可以通过 socketbindlistenacceptconnect 等调用实现。而 socketpair 提供了一个简单方便的方法来创建一对互联的 socket。socketpair 创建的一对 socket 是 全双工 的。socketpair 的函数原型如下:

1
2
3
#include <sys/socket.h>

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

socketpair 的前三个参数与 socket 函数的含义相同。由于 socketpair 只能用于 Unix 域套接字,所以 domain 参数必须是 AF_UNIXAF_LOCALtype 参数可以是 SOCK_DGRAMSOCK_STREAM,分别创建一对数据报 socket 或流 socket。protocol 参数必须是 0。socketfd 用于返回创建的两个套接字文件描述符。

通常,在调用 socketpair 创建一对套接字后会调用 fork 创建子进程,这样父进程和子进程就可以通过这一对套接字进行进程间通信了。

使用 Unix 域套接字传递描述符

Unix 域套接字的一个 “特色功能” 就是在进程间 传递描述符。描述符可以通过 Unix 域套接字在没有亲缘关系的进程之间传递。描述符是一种 辅助数据,可以通过 sendmsg 发送,通过 recvmsg 接收。这里的 描述符 可以是 openpipemkfifosocketaccept 等函数打开的描述符。以下是一个子进程向父进程传递描述符的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

#define BUF_SIZE 1
#define TEXT_SIZE 12

void sendFd(int fd, int socket_fd);
int recvFd(int socket_fd);

int main(void)
{
int fd_pair[2] = {0};

if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_pair) < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}

pid_t pid = fork();
if (pid < 0)
{
fprintf(stderr, "fork error\n");
return -1;
}
if (pid > 0)
{
close(fd_pair[1]);
int recv_fd = recvFd(fd_pair[0]);
char text[TEXT_SIZE + 1] = {0};
if (read(recv_fd, text, TEXT_SIZE) != TEXT_SIZE)
{
fprintf(stderr, "read error\n");
return -1;
}
printf("%s", text);
if (waitpid(pid, NULL, 0) < 0)
{
fprintf(stderr, "waitpid error\n");
return -1;
}
return 0;
}

close(fd_pair[0]);
// ./hello.txt的内容为"hello world\n"
int fd = open("./hello.txt", O_RDONLY);
if (fd < 0)
{
fprintf(stderr, "open error\n");
exit(EXIT_FAILURE); // NOLINT
}

sendFd(fd, fd_pair[1]);

return 0;
}

void sendFd(int fd, int socket_fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
struct iovec iov;

union
{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
struct cmsghdr *cmptr = NULL;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*((int *)CMSG_DATA(cmptr)) = fd;
msg.msg_name = NULL;
msg.msg_namelen = 0;
char buf[BUF_SIZE] = {0};
iov.iov_base = &buf;
iov.iov_len = BUF_SIZE;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

if (sendmsg(socket_fd, &msg, 0) < 0)
{
fprintf(stderr, "sendmsg error\n");
exit(EXIT_FAILURE); // NOLINT
}
}

int recvFd(int socket_fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));

char buf[BUF_SIZE] = {0};
struct iovec iov;
iov.iov_base = buf;
iov.iov_len = BUF_SIZE;

union
{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;

msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg);

if (recvmsg(socket_fd, &msg, 0) < 0)
{
fprintf(stderr, "recvmsg error\n");
exit(EXIT_FAILURE); // NOLINT
}

int fd = *((int *)CMSG_DATA(cmptr));
return fd;
}

参考资料

  1. 《UNIX 网络编程 卷 1 套接字联网 API(第 3 版)》
  2. 《Linux/UNIX 系统编程手册(下册)》
  3. 高级进程间通信之 UNIX 域套接字 - ITtecman - 博客园