Socket编程实践(13) --UNIX域协议
UNIX域协议
UNIX域套接字与TCP相比, 在同一台主机上, UNIX域套接字更有效率, 几乎是TCP的两倍(由于UNIX域套接字不需要经过网络协议栈,不需要打包/拆包,计算校验和,维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程, 而且UNIX域协议机制本质上就是可靠的通讯, 而网络协议是为不可靠的通讯设计的).
UNIX域套接字可以在同一台主机上各进程之间传递文件描述符;
UNIX域套接字与传统套接字的区别是用路径名来表示协议族的描述;
UNIX域套接字也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX套接字也是可靠的,消息既不会丢失也不会顺序错乱。
使用UNIX域套接字的过程和网络socket十分相似, 也要先调用socket创建一个socket文件描述符, family指定为AF_UNIX, type可以选择SOCK_DGRAM/SOCK_STREAM;
UNIX域套接字地址结构:
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
基于UNIX域套接字的echo-server/client程序
/**Server端**/ void echoServer(int sockfd); int main() { signal(SIGCHLD, sigHandlerForSigChild); int listenfd = socket(AF_UNIX, SOCK_STREAM, 0); if (listenfd == -1) err_exit("socket error"); char pathname[] = "/tmp/test_for_unix"; unlink(pathname); struct sockaddr_un servAddr; servAddr.sun_family = AF_UNIX; strcpy(servAddr.sun_path, pathname); if (bind(listenfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1) err_exit("bind error"); if (listen(listenfd, 128) == -1) err_exit("listen error"); while (true) { int connfd = accept(listenfd, NULL, NULL); if (connfd == -1) err_exit("accept error"); pid_t pid = fork(); if (pid == -1) err_exit("fork error"); else if (pid > 0) close(connfd); else if (pid == 0) { close(listenfd); echoServer(connfd); close(connfd); exit(EXIT_SUCCESS); } } } void echoServer(int sockfd) { char buf[BUFSIZ]; while (true) { memset(buf, 0, sizeof(buf)); int recvBytes = read(sockfd, buf, sizeof(buf)); if (recvBytes < 0) { if (errno == EINTR) continue; else err_exit("read socket error"); } else if (recvBytes == 0) { cout << "client connect closed..." << endl; break; } cout << buf ; if (write(sockfd, buf, recvBytes) == -1) err_exit("write socket error"); } }
/**Client端代码**/ void echoClient(int sockfd); int main() { int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd == -1) err_exit("socket error"); char pathname[] = "/tmp/test_for_unix"; struct sockaddr_un servAddr; servAddr.sun_family = AF_UNIX; strcpy(servAddr.sun_path, pathname); if (connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1) err_exit("connect error"); echoClient(sockfd); } void echoClient(int sockfd) { char buf[BUFSIZ] = {0}; while (fgets(buf, sizeof(buf), stdin) != NULL) { if (write(sockfd, buf, strlen(buf)) == -1) err_exit("write socket error"); memset(buf, 0, sizeof(buf)); int recvBytes = read(sockfd, buf, sizeof(buf)); if (recvBytes == -1) { if (errno == EINTR) continue; else err_exit("read socket error"); } cout << buf ; memset(buf, 0, sizeof(buf)); } }
UNIX域套接字编程注意点
1.bind成功将会创建一个文件,权限为0777 & ~umask
2.sun_path最好用一个/tmp目录下的文件的绝对路径, 而且server端在指定该文件之前首先要unlink一下;
3.UNIX域协议支持流式套接口(需要处理粘包问题)与报式套接口(基于数据报)
4.UNIX域流式套接字connect发现监听队列满时,会立刻返回一个ECONNREFUSED,这和TCP不同,如果监听队列满,会忽略到来的SYN,这导致对方重传SYN。
传递文件描述符
socketpair
#include <sys/types.h> #include <sys/socket.h> int socketpair(int domain, int type, int protocol, int sv[2]);
创建一个全双工的流管道
参数:
domain: 协议家族, 可以使用AF_UNIX(AF_LOCAL)UNIX域协议, 而且在Linux上, 该函数也就只支持这一种协议;
type: 套接字类型, 可以使用SOCK_STREAM
protocol: 协议类型, 一般填充为0;
sv: 返回的套接字对;
socketpair 函数跟pipe 函数是类似: 只能在具有亲缘关系的进程间通信,但pipe 创建的匿名管道是半双工的,而socketpair 可以认为是创建一个全双工的管道。
可以使用socketpair 创建返回的套接字对进行父子进程通信, 如下例:
int main() { int sockfds[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1) err_exit("socketpair error"); pid_t pid = fork(); if (pid == -1) err_exit("fork error"); // 父进程, 只负责数据的打印 else if (pid > 0) { close(sockfds[1]); int iVal = 0; while (true) { cout << "value = " << iVal << endl; write(sockfds[0], &iVal, sizeof(iVal)); read(sockfds[0], &iVal, sizeof(iVal)); sleep(1); } } // 子进程, 只负责数据的更改(+1) else if (pid == 0) { close(sockfds[0]); int iVal = 0; while (read(sockfds[1], &iVal, sizeof(iVal)) > 0) { ++ iVal; write(sockfds[1], &iVal, sizeof(iVal)); } } }
sendmsg/recvmsg
#include <sys/types.h> #include <sys/socket.h> ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
它们与sendto/send 和 recvfrom/recv 函数类似,只不过可以传输更复杂的数据结构,不仅可以传输一般数据,还可以传输额外的数据,如文件描述符。
//msghdr结构体 struct msghdr { void *msg_name; /* optional address */ socklen_t msg_namelen; /* size of address */ struct iovec *msg_iov; /* scatter/gather array */ size_t msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* ancillary data, see below */ size_t msg_controllen; /* ancillary data buffer len */ int msg_flags; /* flags on received message */ }; struct iovec /* Scatter/gather array items */ { void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ };
msghdr结构体成员解释:
1)msg_name :即对等方的地址指针,不关心时设为NULL即可;
2)msg_namelen:地址长度,不关心时设置为0即可;
3)msg_iov:是结构体iovec 的指针, 指向需要发送的普通数据, 见下图。
成员iov_base 可以认为是传输正常数据时的buf;
成员iov_len 是buf 的大小;
4)msg_iovlen:当有n个iovec 结构体时,此值为n;
5)msg_control:是一个指向cmsghdr 结构体的指针(见下图), 当需要发送辅助数据(如控制信息/文件描述符)时, 需要设置该字段, 当发送正常数据时, 就不需要关心该字段, 并且msg_controllen可以置为0;
6)msg_controllen:cmsghdr 结构体可能不止一个(见下图):
7)flags: 不用关心;
//cmsghdr结构体 struct cmsghdr { socklen_t cmsg_len; /* data byte count, including header */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-specific type */ /* followed by unsigned char cmsg_data[]; */ };
为了对齐,可能存在一些填充字节(见下图),跟系统的实现有关,但我们不必关心,可以通过一些函数宏来获取相关的值,如下:
#include <sys/socket.h> struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh); //获取辅助数据的第一条消息 struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); //获取辅助数据的下一条信息 size_t CMSG_ALIGN(size_t length); size_t CMSG_SPACE(size_t length); size_t CMSG_LEN(size_t length); //length使用的是的(实际)数据的长度, 见下图(两条填充数据的中间部分) unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
进程间传递文件描述符
/**示例: 封装两个函数send_fd/recv_fd用于在进程间传递文件描述符**/ int send_fd(int sockfd, int sendfd) { // 填充 name 字段 struct msghdr msg; msg.msg_name = NULL; msg.msg_namelen = 0; // 填充 iov 字段 struct iovec iov; char sendchar = '\0'; iov.iov_base = &sendchar; iov.iov_len = 1; msg.msg_iov = &iov; msg.msg_iovlen = 1; // 填充 cmsg 字段 struct cmsghdr cmsg; cmsg.cmsg_len = CMSG_LEN(sizeof(int)); cmsg.cmsg_level = SOL_SOCKET; cmsg.cmsg_type = SCM_RIGHTS; *(int *)CMSG_DATA(&cmsg) = sendfd; msg.msg_control = &cmsg; msg.msg_controllen = CMSG_LEN(sizeof(int)); // 发送 if (sendmsg(sockfd, &msg, 0) == -1) return -1; return 0; }
int recv_fd(int sockfd) { // 填充 name 字段 struct msghdr msg; msg.msg_name = NULL; msg.msg_namelen = 0; // 填充 iov 字段 struct iovec iov; char recvchar; iov.iov_base = &recvchar; iov.iov_len = 1; msg.msg_iov = &iov; msg.msg_iovlen = 1; // 填充 cmsg 字段 struct cmsghdr cmsg; msg.msg_control = &cmsg; msg.msg_controllen = CMSG_LEN(sizeof(int)); // 接收 if (recvmsg(sockfd, &msg, 0) == -1) return -1; return *(int *)CMSG_DATA(&cmsg); }
int main() { int sockfds[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1) err_exit("socketpair error"); pid_t pid = fork(); if (pid == -1) err_exit("fork error"); // 子进程以只读方式打开文件, 将文件描述符发送给子进程 else if (pid == 0) { close(sockfds[1]); int fd = open("read.txt", O_RDONLY); if (fd == -1) err_exit("open error"); cout << "In child, fd = " << fd << endl; send_fd(sockfds[0], fd); } // 父进程从文件描述符中读取数据 else if (pid > 0) { close(sockfds[0]); int fd = recv_fd(sockfds[1]); if (fd == -1) err_exit("recv_fd error"); cout << "In parent, fd = " << fd << endl; char buf[BUFSIZ] = {0}; int readBytes = read(fd, buf, sizeof(buf)); if (readBytes == -1) err_exit("read fd error"); cout << buf; } }
分析:
我们知道,父进程在fork 之前打开的文件描述符,子进程是可以共享的,但是子进程打开的文件描述符,父进程是不能共享的,上述程序就是举例在子进程中打开了一个文件描述符,然后通过send_fd 函数将文件描述符传递给父进程,父进程可以通过recv_fd 函数接收到这个文件描述符。先建立一个文件read.txt 后输入几个字符,然后运行程序;
注意:
(1)只有UNIX域协议才能在本机进程间传递文件描述符;
(2)进程间传递文件描述符并不是传递文件描述符的值(其实send_fd/recv_fd的两个值也是不同的), 而是要在接收进程中创建一个新的文件描述符, 并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项.