Unix域协议不是一个真正意义上的协议族,只是一个利用socket api在单个主机上进行进程间通信的方法。它不需要走传统网络协议栈,也就不需要计算校验和、维护序列号以及应答等操作。
Unix域提供两种套接字:字节流套接字(类似TCP)以及数据报套接字(类似UDP)。
根据《Unix网络编程卷1》,选择Unix域套接字有以下三点理由:
Unix域套接字对比网络套接字,在适用方式上主要有以下几点不同:
1、地址
Unix域套接字使用sockaddr_un表示。网络套接字地址则是IP+Port,Unix域套接字地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind调用创建。
2、客户端显示调用bind
客户端使用Unix域套接字一般都需要显示调用bind函数,而不像网络socket一样依赖系统自动分配的地址。套接字bind的文件名可以包含客户端的pid,这样服务器就可以区分不同的客户端。
服务端示例程序如下所示:
#define UNIXSTR_PATH "/tmp/srv_sock"
#define LISTENQ 5
#define ERR_EXIT(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
void srv_echo(int conn) {
char recvbuf[1024];
int ret;
while (1) {
memset(recvbuf, 0, sizeof(recvbuf));
ret = read(conn, recvbuf, sizeof(recvbuf));
if (ret == -1) {
if (ret == EINTR)
continue;
ERR_EXIT("read error");
}
else if (ret == 0) {
printf("client close\n");
break;
}
fputs(recvbuf, stdout);
write(conn, recvbuf, strlen(recvbuf));
}
close (conn);
}
int main(int argc, char *argv[]) {
int listenfd;
if ((listenfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
ERR_EXIT("server create socket error");
}
unlink(UNIXSTR_PATH);
struct sockaddr_un srv_addr;
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sun_family = AF_UNIX;
strcpy(srv_addr.sun_path, UNIXSTR_PATH);
if(bind(listenfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
ERR_EXIT("server bind error");
}
if(listen(listenfd, LISTENQ) < 0) {
ERR_EXIT("server listen error");
}
int conn;
pid_t child_pid;
struct sockaddr_un cli_addr;
socklen_t cli_len;
while(1) {
cli_len = sizeof(cli_addr);
if((conn = accept(listenfd, (struct sockaddr *)&cli_addr, &cli_len)) < 0) {
if (errno == EINTR) {
continue;
}
else {
ERR_EXIT("accept error");
}
}
// 子进程
if((child_pid = fork()) == 0) {
close(listenfd);
srv_echo(conn);
exit(0);
}
// 父进程
close(conn);
}
}
客户端示例程序如下所示:
#define UNIXSTR_PATH "/tmp/srv_sock"
#define ERR_EXIT(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
void cli_echo(int conn) {
char sendbuf[1024] = { 0 };
char recvbuf[1024] = { 0 };
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) {
write(conn, sendbuf, strlen(sendbuf));
read(conn, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(recvbuf, 0, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(conn);
}
int main(int argc, char *argv[]) {
int sockfd;
if((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
ERR_EXIT("client create socket error");
}
struct sockaddr_un svr_addr;
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sun_family = AF_UNIX;
strcpy(svr_addr.sun_path, UNIXSTR_PATH);
if(connect(sockfd, (struct sockaddr *)&svr_addr, sizeof(svr_addr)) < 0) {
ERR_EXIT("client connect error");
}
cli_echo(sockfd);
}
Unix域套接字关联的路径名应该是一个绝对路径名,而不是一个相对路径名。
Connect系统调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接字上的路径名,而且套接字类型(字节流或数据报)必须要一致,以下三种条件都会出错:
srwxrwxr-x 1 xxx xxx 0 Mar 12 13:23 /tmp/srv_sock
,其中s就表示套接字)如果connect调用发现这个舰艇套接字的队列已满,那么调用就会立即返回一个ECONNREFUSED错误(不同于TCP,如果TCP监听套接字的队列已满,TCP监听端就忽略新到达的SYN,client就会重新发送SYN)
服务端示例程序如下所示:
#define UNIXSTR_PATH "/tmp/srv_sock"
#define LISTENQ 5
#define ERR_EXIT(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
void srv_echo(int conn, struct sockaddr *cli_addr, socklen_t cli_len) {
char recvbuf[1024];
int n;
socklen_t len;
while (1) {
memset(recvbuf, 0, sizeof(recvbuf));
len = cli_len;
n = recvfrom(conn, recvbuf, sizeof(recvbuf), 0, cli_addr, &len);
recvbuf[n] = 0;
fputs(recvbuf, stdout);
sendto(conn, recvbuf, n, 0, cli_addr, len);
}
}
int main(int argc, char *argv[]) {
int sockfd;
if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
ERR_EXIT("server create socket error");
}
unlink(UNIXSTR_PATH);
struct sockaddr_un srv_addr, cli_addr;
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sun_family = AF_UNIX;
strcpy(srv_addr.sun_path, UNIXSTR_PATH);
if(bind(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
ERR_EXIT("server bind error");
}
srv_echo(sockfd, (struct sockaddr *)&cli_addr, sizeof(cli_addr));
}
客户端示例程序如下所示:
#define UNIXSTR_PATH "/tmp/srv_sock"
#define ERR_EXIT(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
void cli_echo(int conn, struct sockaddr *svr_addr, socklen_t svr_len) {
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
int n;
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) {
sendto(conn, sendbuf, strlen(sendbuf), 0, svr_addr, svr_len);
n = recvfrom(conn, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
recvbuf[n] = 0;
fputs(recvbuf, stdout);
memset(recvbuf, 0, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(conn);
}
int main(int argc, char *argv[]) {
int sockfd;
if((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
ERR_EXIT("client create socket error");
}
struct sockaddr_un cli_addr, svr_addr;
memset(&cli_addr, 0, sizeof(cli_addr));
cli_addr.sun_family = AF_UNIX;
strcpy(cli_addr.sun_path, tmpnam(NULL));
if(bind(sockfd, (struct sockaddr *)&cli_addr, sizeof(cli_addr)) < 0) {
ERR_EXIT("client bind error");
}
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sun_family = AF_UNIX;
strcpy(svr_addr.sun_path, UNIXSTR_PATH);
cli_echo(sockfd, (struct sockaddr *)&svr_addr, sizeof(svr_addr));
}
Unix域数据包协议要求客户端必须显示bind一个路径名到套接字,这样服务器才能够回射应答的路径名。这里使用tmpnam赋值一个唯一的路径名。
Linux提供了pipe函数用来创建匿名管道进行父子进程通信。但是pipe函数创建的管道是半双工的(要么读、要么写,不能够同时在一个管道中进行读写)。但实际应用中,经常需要同时进行读写。常用的解决方案是使用pipe函数创建两个单向管道,示例程序如下所示:
int pipe_in[2], pipe_out[2];
pid_t pid;
pipe(&pipe_in); // 创建父进程中用于读取数据的管道
pipe(&pipe_out); // 创建父进程中用于写入数据的管道
if ((pid = fork()) == 0) { // 子进程
close(pipe_in[0]); // 关闭父进程的读管道的子进程读端
close(pipe_out[1]); // 关闭父进程的写管道的子进程写端
... // 使用exec执行命令
} else { // 父进程
close(pipe_in[1]); // 关闭读管道的写端
close(pipe_out[0]); // 关闭写管道的读端
... // 向pipe_out[1]中写数据,并从pipe_in[0]中读结果
close(pipe_out[1]); // 关闭写管道
... // 读取pipe_in[0]中的剩余数据
close(pipe_in[0]); // 关闭读管道
... // 使用wait系列函数等待子进程退出并取得退出代码
}
上述示例代码的可读性以及可维护性比较差,根本原因就是pipe函数返回的一对描述符只能够从从第一个中读,第二个中写。
不过Linux中全双工socketpair函数可实现对两个描述符中的任何一个同时进行读写。该函数仅使用于Unix域套接字,函数描述如下所示:
int socketpair(int domain, int type, int protocol, int sockfd[2]);
其中domain只支持AF_UNIX/AF_LOCAL,protocol参数必须是0,type参数既可以是SOCK_STREAM,也可以是SOCK_DGRAM。该函数创建的两个套接字都是无名socket,在Linux中,完全可以把这一对socket当成pipe返回的描述符一样使用。
使用方式:
读写操作位于同一个进程示例代码如下所示:
const char* str = "SOCKET PAIR TEST";
int main(int argc, char* argv[]) {
char buf[128] = { 0 };
int socket_pair[2];
pid_t pid;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {
printf("Error, socketpair create failed, errno(%d): %s\n", errno,
strerror(errno));
return EXIT_FAILURE;
}
int size = write(socket_pair[0], str, strlen(str));
// 读取成功
read(socket_pair[1], buf, size);
// 阻塞
// read(socket_pair[0], buf, size);
printf("Read result: %s\n", buf);
return EXIT_SUCCESS;
}
执行结果:
Read result: SOCKET PAIR TEST.
读写操作位于不同进程示例代码如下所示:
const char* str = "SOCKET PAIR TEST";
int main(int argc, char* argv[]) {
char buf[128] = { 0 };
int socket_pair[2];
pid_t pid;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {
printf("Error, socketpair create failed, errno(%d): %s\n", errno,
strerror(errno));
return EXIT_FAILURE;
}
pid = fork();
if (pid < 0) {
printf("Error, fork failed, errno(%d): %s\n", errno, strerror(errno));
return EXIT_FAILURE;
} else if (pid > 0) {
// 关闭另外一个套接字
close(socket_pair[1]);
int val = 0;
while (1) {
sleep(1);
++val;
printf("parent Sending data: %d\n", val);
write(socket_pair[0], &val, sizeof(val));
read(socket_pair[0], &val, sizeof(val));
printf("parent Data received: %d\n", val);
}
} else if (pid == 0) {
// 关闭另外一个套接字
close(socket_pair[0]);
int val;
while (1) {
read(socket_pair[1], &val, sizeof(val));
printf("child Data received: %d\n", val);
++val;
write(socket_pair[1], &val, sizeof(val));
printf("child send received: %d\n", val);
}
}
return EXIT_SUCCESS;
}
执行结果:
parent Sending data: 1
child Data received: 1
child send received: 2
parent Data received: 2
parent Sending data: 3
child Data received: 3
child send received: 4
parent Data received: 4
...
如果需要关闭子进程的输入同时通知子进程数据已经发送完毕,而随后从子进程的输出中读取数据直到遇到EOF,对于之前的pipe创建的单向管道来说不会存在任务问题;但是使用socketpair创建的双向管道时,如果不关闭管道就无法通知对端数据已经发送完毕,但是关闭了管道又无法送终读取结果数据。此时可以使用shutdown,来实现一个半关闭操作,通知对端进程不再发送数据,同时仍可以从该文件描述符中把剩余的数据接收完毕,最后再使用close关闭描述符。
https://www.ibm.com/developerworks/cn/linux/l-pipebid/
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。