欢迎访问我的个人博客
https://vincillau.github.io/
在 Linux 中有许多进行 进程间通信
的方法。今天博主向大家介绍一种常用的进程间通信的方法 ——Unix 域套接字
。
Unix 域套接字
是一种在本机的进程间进行通信的一种方法。虽然 Unix 域套接字的接口与 TCP 和 UDP 套接字
的接口十分相似,但是 Unix 域套接字只能用于同一台机器的进程间通信,不能让两个位于不同机器的进程进行通信。正由于这个特性,Unix 域套接字可以可靠地在两个进程间复制数据,不用像 TCP 一样采用一些诸如 * 添加网络报头 、 计算检验和 、 产生顺序号 * 等一系列保证数据完整性的操作。因此,在同一台机器上进行进程间通信时,Unix 域套接字的效率往往比 TCP 套接字的效率要高。
因为 Unix 域套接字的效率比较高,一些程序经常用 Unix 套接字代替 TCP 套接字。例如当 MySQL 的服务器进程和客户端进程在同一台机器上时,可以用 Unix 域套接字代替 TCP 套接字。
在使用 TCP 套接字和 UDP 套接字时,我们需要用 struct sockaddr_in
(IPv4)定义套接字的地址结构,与之相似,Unix 域套接字使用 struct sockaddr_un
定义套接字的地址结构。struct sockaddr_un
的定义如下(* 位于头文件 sys/un.h 中 *):
struct sockaddr_un
{
sa_family_t sun_family;
char sun_path[108];
};
在使用 Internet 域套接字进行编程时,需要将 struct sockaddr_in
的 sin_family
成员设置为 AF_INET
(IPv4)。与之类似,在使用 Unix 域套接字时,需要将 sun_family 设置为 AF_UNIX
或 AF_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 域套接字使用 socket
函数创建,与 Internet 域套接字一样,Unix 域套接字也有流套接字和数据报套接字两种:
int unix_sock_fd1 = socket(AF_UNIX, SOCK_STREAM, 0); // Unix 域中的流 socket
int unix_sock_fd2 = socket(AF_UNIX, SOCK_DGRAM, 0); // Unix 域中的数据包 socket
稍后将介绍这两种套接字的用法和区别。
使用 bind
函数可以将一个 Unix 套接字绑定到一个地址上。绑定 Unix 域套接字时,bind 会在指定的路径名处创建一个表示 Unix 域套接字的文件。Unix 域套接字与路径名是一一对应关系,即一个 Unix 域套接字只能绑定到一个路径名上,一个路径名也只能被一个套接字绑定。一般要把 Unix 域套接字绑定到一个 绝对路径
上,例如:
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 获取套接字绑定的路径名:
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 域中的流套接字与 TCP 流套接字的用法十分相似。在服务器端,我们首先创建一个 Unix 域流套接字,将其绑定到一个路径上,然后调用 listen
监听客户端连接,调用 accept
接受客户端的连接。在客户端,在创建一个 Unix 域流套接字之后,可以使用 connect
尝试连接指定的服务器套接字。以下是一个使用 Unix 域流套接字实现的 echo 服务器和客户端的例子:
// 服务器
#include
#include
#include
#include
#include
#include
#include
#include
#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';
}
// 客户端
#include
#include
#include
#include
#include
#include
#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 域数据报套接字
与 UDP 套接字
类似,可以通过 Unix 域数据报套接字在进程间发送具有边界的数据报。但由于 Unix 域数据报套接字是在本机上进行通信,所以 Unix 域数据报套接字的数据传递是可靠的,不会像 UDP 套接字那样发生丢包的问题。Unix 域数据报套接字的接口与 UDP 也十分相似。在服务器端,通常先创建一个 Unix 域数据报套接字,然后将其绑定到一个路径上。然后调用 recvfrom
接收客户端发送来的数据,调用 sendto
向客户端发送数据。对于客户端,通常是先创建一个 Unix 域数据报套接字,将这个套接字绑定到一个路径上,然后调用 sendto
发送数据,调用 recvfrom
接收客户端发来的数据。以下是使用 Unix 域数据报套接字实现的 echo 服务器和客户端:
// 服务器
#include
#include
#include
#include
#include
#include
#include
#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;
}
// 客户端
#include
#include
#include
#include
#include
#include
#include
#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;
}
当程序调用 bind
时,会在文件系统中的指定路径处创建一个与套接字对应的文件。我们可以通过控制该文件的权限来控制进程对这个套接字的访问。当进程想要连接一个 Unix 域流套接字或通过一个 Unix 域数据报套接字发送数据包时,需要拥有对该套接字的 写权限
以及对 socket 路径名的所有目录的 执行权限
。在调用 bind
时,会自动赋予用户、组和其他用户的所有权限。如果想要修改这一行为,可以在调用 bind
之前调用 umask
禁用掉某些权限。
有时我们需要在同一个进程中创建一对相互连接的 Unix 域 socket(* 与管道类似 *),这可以通过 socket
、bind
、listen
、accept
和 connect
等调用实现。而 socketpair
提供了一个简单方便的方法来创建一对互联的 socket。socketpair
创建的一对 socket 是 全双工
的。socketpair 的函数原型如下:
#include
int socketpair(int domain, int type, int protocol, int socketfd[2]);
socketpair 的前三个参数与 socket
函数的含义相同。由于 socketpair 只能用于 Unix 域套接字,所以 domain
参数必须是 AF_UNIX
或 AF_LOCAL
。type
参数可以是 SOCK_DGRAM
或 SOCK_STREAM
,分别创建一对数据报 socket 或流 socket。protocol
参数必须是 0。socketfd
用于返回创建的两个套接字文件描述符。
通常,在调用 socketpair 创建一对套接字后会调用 fork 创建子进程,这样父进程和子进程就可以通过这一对套接字进行进程间通信了。
Unix 域套接字的一个 “特色功能” 就是在进程间 传递描述符
。描述符可以通过 Unix 域套接字在没有亲缘关系的进程之间传递。描述符是一种 辅助数据
,可以通过 sendmsg
发送,通过 recvmsg
接收。这里的 描述符
可以是 open
、pipe
、mkfifo
、socket
、accept
等函数打开的描述符。以下是一个子进程向父进程传递描述符的例子:
#include
#include
#include
#include
#include
#include
#include
#include
#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;
}