IPC (本地进程间通信) 方式:
- 本地套接字
- 管道
- 共享消息队列
从例子开始
通过 netstat 命令查看 Linux 系统内的本地套接字状况:
$ sudo netstat -a -p -A unix
本地套接字概述
本地套接字一般也叫做 UNIX 域套接字,最新的规范已经改叫本地套接字。
本地套接字是一种特殊类型的套接字,和 TCP/UDP 套接字不同。TCP/UDP 即使在本地地址通信,也要走系统网络协议栈,而本地套接字,严格意义上说提供了一种单主机跨进程间调用的手段,减少了协议栈实现的复杂度,效率比 TCP/UDP 套接字都要高许多。类似的 IPC 机制还有 UNIX 管道、共享内存和 RPC 调用等。
本地字节流套接字
字节流类型的本地套接字服务器端例子
#include "lib/common.h"
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: unixstreamserver <local_path>");
}
int listenfd, connfd;
socklen_t clilen;
struct sockaddr_un cliaddr, servaddr;
listenfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (listenfd < 0) {
error(1, errno, "socket create failed");
}
char *local_path = argv[1];
unlink(local_path);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, local_path);
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
error(1, errno, "bind failed");
}
if (listen(listenfd, LISTENQ) < 0) {
error(1, errno, "listen failed");
}
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
error(1, errno, "accept failed"); /* back to for() */
else
error(1, errno, "accept failed");
}
char buf[BUFFER_SIZE];
while (1) {
bzero(buf, sizeof(buf));
if (read(connfd, buf, BUFFER_SIZE) == 0) {
printf("client quit");
break;
}
printf("Receive: %s", buf);
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", buf);
int nbytes = sizeof(send_line);
if (write(connfd, send_line, nbytes) != nbytes)
error(1, errno, "write error");
}
close(listenfd);
close(connfd);
exit(0);
}
- AF_UNIX 基本上可以认为和 AF_LOCAL 是等价的。
- unlink 操作: 把存在的文件删除掉,这样可以保持幂等性
- 关于本地文件路径,需要明确一点,它必须是“绝对路径”,这样的话,编写好的程序可以在任何目录里被启动和管理。如果是“相对路径”,为了保持同样的目的,这个程序的启动路径就必须固定,这样一来,对程序的管理反而是一个很大的负担。
- 这个本地文件,必须是一个“文件”,不能是一个“目录”。如果文件不存在,后面 bind 操作时会自动创建这个文件。
- 权限问题
客户端程序
#include "lib/common.h"
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: unixstreamclient <local_path>");
}
int sockfd;
struct sockaddr_un servaddr;
sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (sockfd < 0) {
error(1, errno, "create socket failed");
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, argv[1]);
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
error(1, errno, "connect failed");
}
char send_line[MAXLINE];
bzero(send_line, MAXLINE);
char recv_line[MAXLINE];
while (fgets(send_line, MAXLINE, stdin) != NULL) {
int nbytes = sizeof(send_line);
if (write(sockfd, send_line, nbytes) != nbytes)
error(1, errno, "write error");
if (read(sockfd, recv_line, MAXLINE) == 0)
error(1, errno, "server terminated prematurely");
fputs(recv_line, stdout);
}
exit(0);
}
- 20 行和 TCP 客户端一样,发起对目标套接字的 connect 调用,不过由于是本地套接字,并不会有三次握手。
只启动客户端
只启动客户端程序:
$ ./unixstreamclient /tmp/unixstream.sock
connect failed: No such file or directory (2)
服务器端监听在无权限的文件路径上
我们让服务器端程序的应用属主没有 /var/lib/ 目录的权限,然后试着启动一下这个服务器程序 :
$ ./unixstreamserver /var/lib/unixstream.sock
bind failed: Permission denied (13)
试一下 root 用户启动该程序:
sudo ./unixstreamserver /var/lib/unixstream.sock
(阻塞运行中)
我们看到 /var/lib 下创建了一个本地文件,大小为 0,而且文件的最后结尾有一个(=)号。其实这就是 bind 的时候自动创建出来的文件。
$ ls -al /var/lib/unixstream.sock
rwxr-xr-x 1 root root 0 Jul 15 12:41 /var/lib/unixstream.sock=
服务器 - 客户端应答
我们让服务器和客户端都正常启动,并且客户端依次发送字符:
$./unixstreamserver /tmp/unixstream.sock
Receive: g1
Receive: g2
Receive: g3
client quit
$./unixstreamclient /tmp/unixstream.sock
g1
Hi, g1
g2
Hi, g2
g3
Hi, g3
^C
本地数据报套接字
本地数据报服务器端程序
#include "lib/common.h"
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: unixdataserver <local_path>");
}
int socket_fd;
socket_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (socket_fd < 0) {
error(1, errno, "socket create failed");
}
struct sockaddr_un servaddr;
char *local_path = argv[1];
unlink(local_path);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_LOCAL;
strcpy(servaddr.sun_path, local_path);
if (bind(socket_fd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
error(1, errno, "bind failed");
}
char buf[BUFFER_SIZE];
struct sockaddr_un client_addr;
socklen_t client_len = sizeof(client_addr);
while (1) {
bzero(buf, sizeof(buf));
if (recvfrom(socket_fd, buf, BUFFER_SIZE, 0, (struct sockadd *) &client_addr, &client_len) == 0) {
printf("client quit");
break;
}
printf("Receive: %s \n", buf);
char send_line[MAXLINE];
bzero(send_line, MAXLINE);
sprintf(send_line, "Hi, %s", buf);
size_t nbytes = strlen(send_line);
printf("now sending: %s \n", send_line);
if (sendto(socket_fd, send_line, nbytes, 0, (struct sockadd *) &client_addr, client_len) != nbytes)
error(1, errno, "sendto error");
}
close(socket_fd);
exit(0);
}
- 21~23 行 bind 到本地地址之后,没有再调用 listen 和 accept,回忆一下,这其实和 UDP 的性质一样。
客户端程序
#include "lib/common.h"
int main(int argc, char **argv) {
if (argc != 2) {
error(1, 0, "usage: unixdataclient <local_path>");
}
int sockfd;
struct sockaddr_un client_addr, server_addr;
sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (sockfd < 0) {
error(1, errno, "create socket failed");
}
bzero(&client_addr, sizeof(client_addr)); /* bind an address for us */
client_addr.sun_family = AF_LOCAL;
strcpy(client_addr.sun_path, tmpnam(NULL));
if (bind(sockfd, (struct sockaddr *) &client_addr, sizeof(client_addr)) < 0) {
error(1, errno, "bind failed");
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sun_family = AF_LOCAL;
strcpy(server_addr.sun_path, argv[1]);
char send_line[MAXLINE];
bzero(send_line, MAXLINE);
char recv_line[MAXLINE];
while (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
size_t nbytes = strlen(send_line);
printf("now sending %s \n", send_line);
if (sendto(sockfd, send_line, nbytes, 0, (struct sockaddr *) &server_addr, sizeof(server_addr)) != nbytes)
error(1, errno, "sendto error");
int n = recvfrom(sockfd, recv_line, MAXLINE, 0, NULL, NULL);
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
exit(0);
}
- 16~22 行将本地套接字 bind 到本地一个路径上,然而 UDP 客户端程序是不需要这么做的。本地数据报套接字这么做的原因是,它需要指定一个本地路径,以便在服务器端回包时,可以正确地找到地址;而在 UDP 客户端程序里,数据是可以通过 UDP 包的本地地址和端口来匹配的。
服务器端和客户端通过数据报应答的场景
./unixdataserver /tmp/unixdata.sock
Receive: g1
now sending: Hi, g1
Receive: g2
now sending: Hi, g2
Receive: g3
now sending: Hi, g3
$ ./unixdataclient /tmp/unixdata.sock
g1
now sending g1
Hi, g1
g2
now sending g2
Hi, g2
g3
now sending g3
Hi, g3
^C
总结
- 本地套接字的编程接口和 IPv4、IPv6 套接字编程接口是一致的,可以支持字节流和数据报两种协议。
- 本地套接字的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报套接字实现。