UNIX网络编程读书笔记:UNIX域协议
概述
UNIX域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法,所用API与在不同主机上执行客户/服务器通信所用的API(套接口API)相同。UNIX域协议可视为进程间通信(IPC)方法之一。
UNIX域提供两类套接口:字节流套接口(类似TCP)和数据报套接口(类似UDP)。
使用UNIX域套接口的理由有3个:
在源自Berkeley的实现中,UNIX域套接口往往比通信两端位于同一主机的TCP套接口快出一倍。
UNIX域套接口可用于在同一个主机上的不同进程间传递描述字。
UNIX域套接口较新的实现把客户的凭证(用户ID和组ID)提供给服务器,从而能够提供额外的安全检查措施。
UNIX域中用于标识客户和服务器的协议地址是普通文件系统中的路径名。这些路径名不是普通的UNIX文件:除非把它们和UNIX域套接口关联起来,否则无法读写这些文件。
UNIX域套接口地址结构
在头文件<sys/un.h>中定义了UNIX域套接口地址结构:
struct sockaddr_un { sa_family_t sun_family; /* AF_LOCAL */ char sun_path[104]; /* null-terminated pathname */ };
存放在sun_path数组中的路径名必须以空格字符结尾。
实现提供的SUN_LEN宏以一个指向sockaddr_un结构的指针为参数并返回该结构的长度,其中包括路径名中非空字节数。
未指定地址(通配地址),通过以空字符串作为路径名指示,也就是一个sun_path[0]值为0的地址结构。这是UNIX域中与IPv4的INADDR_ANY常值以及IPv6的IN6ADDR_ANY_INIT常值等价的一个地址。
POSIX把UNIX域协议重新命名为“本地IPC”,以消除它对于UNIX操作系统的依赖。历史性的AF_UNIX常值变为AF_LOCAL。尽管POSIX努力使它独立于操作系统,它的套接口地址结构仍然保留_un后缀。
实例:UNIX域套接口的bind调用
创建一个UNIX域套接口,往其上bind一个路径名,再调用getsockname输出这个绑定的路径名。
#include <sys/un.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> int main(int argc, char **argv) { int sockfd; socklen_t len; struct sockaddr_un addr1, addr2; if(argc != 2) { printf("usage: unixbind <pathname> "); exit(0); } sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); unlink(argv[1]); /* 如果文件系统中已存在该路径名,bind将会失败。为此我们先调用unlink删除这个路径名,以防止它已经存在。 */ bzero(&addr1, sizeof(addr1)); addr1.sun_family = AF_LOCAL; strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1); bind(sockfd, (struct sockaddr *)&addr1, SUN_LEN(&addr1)); len = sizeof(addr2); getsockname(sockfd, (struct sockaddr *)&addr2, &len); printf("bound name = %s, returned len = %d\n", addr2.sun_path, len); exit(0); }
运行结果如下:
socketpair函数
socketpair函数创建两个随后连接起来的套接口。本函数仅适用于UNIX域套接口。
#include <sys/socket.h> int socketpair(int family, int type, int protocol, int sockfd[2]); 返回:0——成功,-1——出错
family参数必须为AF_LOCAL;
protocol参数必须为0;
type参数可以是SOCK_STREAM,也可以是SOCK_DGRAM。
新创建的两个套接口描述字作为sockfd[0]和sockfd[1]返回。
本函数类似于UNIX的pipe函数:返回两个彼此连接的描述字。事实上,源自berkeley的实现通过执行与socketpair一样的内部操作给出pipe接口。
这样创建的两个套接口不曾命名;也就是说其中没有涉及隐式的bind调用。它与调用pipe创建的普通UNIX管道类似,差别在于流管道(socketpair创建的)是全双工的,即两个描述字都是既可读又可写。
POSIX不要求全双工管道。
套接口函数
当用于UNIX域套接口时,套接口函数中存在一些差异和限制:
由bind创建的路径名缺省访问权限应为0777(属主用户、组用户和其他用户都可读、可写并可执行),并按照当前umask值进行修正。
与UNIX域套接口关联的路径名应该是一个绝对路径名,而不是一个相对路径名。
在connect调用中指定的路径名必须是一个当前捆绑在某个打开的UNIX域套接口上的路径名,而且它们的套接口类型(字节流或数据报)也必须一致。
调用connect连接一个UNIX域套接口涉及的权限测试等同于调用open以只读方式访问相应的路径名。
UNIX域字节流套接口类似于TCP套接口:它们都为进程提供一个无记录边界的字节流接口。
如果对于某个UNIX域字节流套接口的connect调用发现这个监听套接口的队列已满,调用就立即返回一个ECONNREFUSED错误。这一点不同于TCP:如果TCP监听套接口的队列已满,TCP监听端就忽略新到达的SYN,而TCP连接发起端将数次发送SYN进行重试。
UNIX域数据报套接口类似UDP套接口:它们都提供一个保留记录边界的不可靠的数据报服务。
在一个未绑定的UNIX域套接口上发送数据报不会自动给这个套接口捆绑一个路径名,这一点不同于UDP套接口:在一个未绑定的UDP套接口上发送UDP数据报导致给这个套接口捆绑一个临时端口。这一点意味着除非数据报发送端已经捆绑一个路径名到它的套接口,否则数据报接收端无法发回应答数据报。类似地,对于某个UNIX域数据报套接口的connect调用不会给本套接口绑定一个路径名,这一点不同于TCP和UDP。
UNIX域字节流客户/服务器程序
/* unixstrserv01.c */ #include <sys/un.h> #include <errno.h> #include <sys/wait.h> #include <signal.h> #include <sys/socket.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <string.h> #define UNIXSTR_PATH "/tmp/unix.str" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_un cliaddr, servaddr; void sig_chld(int); daemonize("unixstrserver"); listenfd = socket(AF_LOCAL, SOCK_STREAM, 0); unlink(UNIXSTR_PATH); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXSTR_PATH); bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); listen(listenfd, 5); signal(SIGCHLD, sig_chld); for(;;) { clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) { if(errno == EINTR) continue; /* back to for() */ else { perror("accept"); exit(1); } } if((childpid = fork()) == 0) { close(listenfd); str_echo(connfd); exit(0); } close(connfd); } } void sig_chld(int signo) { pid_t pid; int stat; while((pid = waitpid(-1, &stat, WNOHANG)) > 0) { printf("child %d terminated\n", pid); } return; }
/* unixstrcli01.c */ #include <sys/un.h> #include <strings.h> #include <sys/un.h> #include <sys/socket.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #define UNIXSTR_PATH "/tmp/unix.str" int main(int argc, char **argv) { int sockfd; struct sockaddr_un servaddr; sockfd = socket(AF_LOCAL, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXSTR_PATH); connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); exit(0); }
其他相关使用到的函数参见:http://www.cnblogs.com/nufangrensheng/p/3587962.html 以及http://www.cnblogs.com/nufangrensheng/p/3544104.html。
UNIX域数据报客户/服务器程序
/* unixdgserv01.c */ #include <sys/un.h> #include <sys/socket.h> #define UNIXDG_PATH "/tmp/unix.dg" int main(int argc, char **argv) { int sockfd; struct sockaddr_un servaddr, cliaddr; sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0); unlink(UNIXDG_PATH); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXDG_PATH); bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); }
/* unixdgcli01.c */ #include <sys/un.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #define UNIXDG_PATH "/tmp/unix.dg" int main(int argc, char **argv) { int sockfd; struct sockaddr_un cliaddr, servaddr; sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0); bzero(&cliaddr, sizeof(cliaddr)); cliaddr.sun_family = AF_LOCAL; strcpy(cliaddr.sun_path, tmpnam(NULL)); bind(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); bzero(&servaddr, sizeof(servaddr)); servaddr.sun_family = AF_LOCAL; strcpy(servaddr.sun_path, UNIXDG_PATH); dg_cli(stdin, sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); exit(0); }
使用到的相关函数可参考:http://www.cnblogs.com/nufangrensheng/p/3592158.html。
注意与UDP客户不同的是,当使用UNIX域数据报协议时,我们必须显式bind一个路径名到我们的套接口,这样服务器才会有回送应答的路径名。
描述字传递
当考虑从一个进程到另一个进程传递打开的描述字时,我们通常会想到:
(1)fork调用返回后,子进程共享父进程的所有打开的描述字。
(2)exec调用执行之后,所有描述字通常保持打开状态不变。
在(1)中,进程先打开一个描述字,再调用fork,然后父进程关闭这个描述字,子进程则处理这个描述字。这样一个打开的描述字就从父进程传递到子进程。然而我们也可能想让子进程打开一个描述字并把它传递给父进程。
当前的UNIX系系统提供了用于从一个进程到任一其他进程传递任一打开的描述字的方法。也就是说,这两个进程之间无需存在亲缘关系。这种技术要求首先在这两个进程之间创建一个UNIX域套接口,然后使用sendmsg跨这个UNIX域套接口发送一个特殊消息。这个消息由内核处理,从而把打开的描述字从发送进程传递到接收进程。使用UNIX域套接口的描述字传递方法是最便于移植的编程技术。
在两个进程之间传递描述字涉及的步骤如下:
(1)创建一个字节流或数据报的UNIX域套接口。
如果目标是fork一个子进程,让子进程打开待传递的描述字,再把它传递回父进程,那么父进程可以预先调用socketpair创建一个可用于在父子进程之间交换描述字的流管道。
如果进程之间没有亲缘关系,那么服务器进程必须创建一个UNIX域字节流套接口,bind一个路径到该套接口,以允许客户进程connect到该套接口。客户然后可以向服务器发送一个打开某个描述字的请求,服务器再把该描述字通过UNIX域套接口传递回客户。客户和服务器之间也可以使用UNIX域数据报套接口,不过这么做缺乏优势,而且数据报存在被丢弃的可能性。
(2)发送进程通过调用返回描述字的任一UNIX函数打开一个描述字,这些函数的例子有:open、pipe、mkfifo、socket和accept。可以在进程之间传递的描述字不限类型,这就是我们称这种技术为“描述字传递”而不是“文件描述字传递”的原因。
(3)发送进程创建一个msghdr结构(http://www.cnblogs.com/nufangrensheng/p/3567376.html),其中含有待传递的描述字。POSIX规定描述字作为辅助数据(msghdr结构的msg_control成员)发送。发送进程调用sendmsg跨来自步骤(1)的UNIX域套接口发送该描述字。至此我们说这个描述字“在飞行中(in flight)”。即使发送进程在调用sendmsg之后但在接收进程调用recvmsg之前就关闭了该描述字,对于接收进程它仍然保持打开状态。发送一个描述字导致该描述字的引用计数加1.
(4)接收进程调用recvmsg在来自步骤(1)的UNIX域套接口上接收这个描述字。这个描述字在接收进程中的描述字号不同于它在发送进程中的描述子号是正常的。传递一个描述字并不是传递一个描述字号,而是涉及在接收进程中创建一个新的描述字,而这个描述字指引的内核中文件表项和发送进程中飞行前的那个描述字指引的相同。
客户和服务器之间必须存在某种应用协议,以便描述字的接收进程预先知道何时期待接收。另外,在期待接收描述字的recvmsg调用中应该避免使用MSG_PEEK标志,否则后果不可预料。