一、UNIX Domain Socket IPC
socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。UNIX域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIXDomain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
二、回射/客户服务器程序
通信的流程跟前面说过的tcp/udp 是类似的。下面直接来看服务器serv.c程序:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<string.h> #include<sys/un.h> #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) void echo_ser(int conn) { char recvbuf[1024]; int n; while (1) { memset(recvbuf, 0, sizeof(recvbuf)); n = read(conn, recvbuf, sizeof(recvbuf)); if (n == -1) { if (n == EINTR) continue; ERR_EXIT("read error"); } else if (n == 0) { printf("client close "); break; } fputs(recvbuf, stdout); write(conn, recvbuf, strlen(recvbuf)); } close(conn); } /* unix domain socket与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。*/ int main(void) { int listenfd; if ((listenfd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) ERR_EXIT("socket error"); unlink("/tmp/test socket"); //地址复用 struct sockaddr_un servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sun_family = AF_UNIX; strcpy(servaddr.sun_path, "/tmp/test socket"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind error"); if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen error"); int conn; pid_t pid; while (1) { conn = accept(listenfd, NULL, NULL); if (conn == -1) { if (conn == EINTR) continue; ERR_EXIT("accept error"); } pid = fork(); if (pid == -1) ERR_EXIT("fork error"); if (pid == 0) { close(listenfd); echo_ser(conn); exit(EXIT_SUCCESS); } close(conn); } return 0; }
客户端程序cli.c程序:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<string.h> #include<sys/un.h> #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) void echo_cli(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(void) { int sock; if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) ERR_EXIT("socket error"); struct sockaddr_un servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sun_family = AF_UNIX; strcpy(servaddr.sun_path, "/tmp/test socket"); if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect error"); echo_cli(sock); return 0; }server 使用fork 的形式来接受多个连接,server调用bind 会创建一个文件,如下所示:
huangcheng@ubuntu:/tmp$ ls -l test socket srwxr-xr-x 1 huangcheng huangcheng 0 2013-08-02 19:30 test socket即文件类型为s,表示SOCKET文件,与FIFO(命名管道)文件,类型为p,类似,都表示内核的一条通道,读写文件实际是在读写内核通道。程序中调用unlink 是为了在开始执行程序时删除以前创建的文件,以便在重启服务器时不会提示address in use。其他方面与以前说过的回射客户服务器程序没多大区别,不再赘述。
三、UNIX域套接字编程注意点
1、bind成功将会创建一个文件,权限为:0777 & ~umask
2、sun_path最好用一个绝对路径。
3、UNIX域协议支持流式套接口与报式套接口。
4、UNIX域流式套接字connect发现监听队列满时,会立刻返回一个ECONNREFUSED,这和TCP不同,如果监听队列满,会忽略到来的SYN,这导致对方重传SYN。
四、socketpair 函数
功能:创建一个全双工的流管道
int socketpair(int domain, int type, int protocol, int sv[2]);参数:
- domain: 协议家族
- type: 套接字类型
- protocol:协议类型
- sv:返回套接字对
返回值:成功返回0;失败返回-1
实际上socketpair 函数跟pipe 函数是类似的,也只能在同个主机上具有亲缘关系的进程间通信,但pipe 创建的匿名管道是半双工的,而socketpair 可以认为是创建一个全双工的管道。
可以使用socketpair 创建返回的套接字对进行父子进程通信:
#include<stdio.h> #include<sys/types.h> #include<sys/socket.h> #include<unistd.h> #include<stdlib.h> #include<errno.h> #include<arpa/inet.h> #include<netinet/in.h> #include<string.h> #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) int main(void) { int sockfds[2]; if (socketpair(PF_UNIX, SOCK_STREAM, 0, sockfds) < 0) ERR_EXIT("sockpair"); pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork"); if (pid > 0) { int val = 0; close(sockfds[1]); while (1) { ++val; printf(" sending data: %d ", val); write(sockfds[0], &val, sizeof(val)); read(sockfds[0], &val, sizeof(val)); printf("recv data : %d ", val); sleep(1); } } else if (pid == 0) { int val; close(sockfds[0]); while (1) { read(sockfds[1], &val, sizeof(val)); ++val; write(sockfds[1], &val, sizeof(val)); } } return 0; }输出如下:
huangcheng@ubuntu:~$ ./a.out sending data: 1 recv data : 2 sending data: 3 recv data : 4 sending data: 5 recv data : 6 sending data: 7 recv data : 8 sending data: 9 recv data : 10 sending data: 11 recv data : 12 sending data: 13 .............................即父进程持有sockfds[0] 套接字进行读写,而子进程持有sockfds[1] 套接字进行读写。