UNIX域套接字被用来和同一机器上运行的进程通信。尽管因特网域套接字可以用作同样的目的,然而UNIX域套接字更高效。UNIX域套接字只拷贝
数据;它们没有要执行的协议处理,没有要增加或删除的网络头,没有要计算的校验和,没有要产生的序列号,没有要发送的确认信息。
UNIX域
套接字同时提供了流和数据报接口。尽管如此,UNIX域数据报服务是可靠的。消息不会被丢失也不会乱序。UNIX域套接字像在套接字和管道之间的过渡。你
可以使用面向网络的套接字接口来使用它们,或者你可以使用socketpair函数然创建一对没有名字的、连接的UNIX域套接字。
- #include <sys/socket.h>
- int socketpair(int domain, int type, int protocol, int sockfd[2]);
- 成功返回0,错误返回-1。
尽管接口是充分通用的,允许socketpair在任何域里使用,但是操作系统典型地只提供对UNIX域的支持。
下面的代码展示了前面17.2节里的s_pipe函数的基于套接字的版本。这个函数创建了一对已连接的UNIX域流套接字。
- #include <sys/socket.h>
- /*
- * Returns a full-duplex "stream" pipe (a UNIX domain socket)
- * with the two file descriptors returned in fd[0] and fd[1].
- */
- int
- s_pipe(int fd[2])
- {
- return(socketpair(AF_UNIX, SOCK_STREAM, 0, fd));
- }
一些基于BSD的系统使用UNIX域套接字来实现管道。但是当pipe被调用时,第一个描述符的写端和第二个描述符的读端都被关闭。为了得到一个全双工的管道,我们需要直接调用socketpair。
17.3.1 命名UNIX域套接字(Naming UNIX Domain Sockets)
尽管socketpair函数创建了和对方相连的套接字,但是这个套接字没有名字。这意味着它们不能被无关进程寻址。
在16.3.4节,我们学到了如何把一个地址绑定到一个因特网域套接字上。正如因特网域套接字一样,UNIX域套接字可以被命令并用来宣传服务。然而,UNIX域使用的地址格式和因特网域套接字不同。
回想下16.3节套接字地址格式在不同的实现上都会有所区别。一个UNIX域套接字的地址由一个sockaddr_un结构体表示。在Linux2.4.22和Solaris 9上,sockaddr_un结构全被定义在头文件里,如下:
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* pathname */
};
然而,在FreeBSD 5.2.1和Mac OS X10.3上,sockaddr_un结构体被定义为:
struct sockaddr_un {
unsigned char sun_len; /* length including null */
sa_family_t sun_family; /* AF_UNIX */
char sun_path[104]; /* pathname */
};
sockaddr_un的结构体sun_path成员包含一个路径名。当我们把一个地址绑定到一个UNIX域套接字时,系统用相同的名字创建一个类型为S_IFSOCK的文件。
这个文件的存在只做为对客户宣传套接字名的一种方式。这个文件不能被打开或被应用为通信的其它使用。
当我们尝试绑定到相同的地址时如果文件已经存在,bind请求将会失败。当我们关闭套接字时,这个文件会自动被删除,所以我们在程序退出时需要确保我们反链接了它。
下面的程序展示了绑定一个地址到一个UNIX域套接字的一个例子。
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <stddef.h>
- int
- main(void)
- {
- int fd, size;
- struct sockaddr_un un;
- un.sun_family = AF_UNIX;
- strcpy(un.sun_path, "foo.socket");
- if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
- printf("socket failed\n");
- exit(1);
- }
- size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
- if (bind(fd, (struct sockaddr *)&un, size) < 0) {
- printf("bind failed\n");
- exit(1);
- }
- printf("UNIX domain socket bound\n");
- exit(0);
- }
当我们运行这个程序时,bind请求成功,但是如果我们第二次运行这个程序时,我们得到一个错误,因为文件已经存在。直到我们删除这个文件,程序会再次成功。
$ ./a.out
UNIX domain socket bound
$ ls -l foo.socket
srwxrwxr-x 1 tommy tommy 0 2012-03-24 17:11 foo.socket
$ ./a.out
bind failed
$ rm foo.socket
$ ./a.out
UNIX domain socket bound
我们确定要绑定的地址的尺寸的方法是确定sun_path成员在sockaddr_un结构体里的偏移量并加上路径名的长度,不包含
终止空字符。因为在sockaddr_un结构体里在sun_path之前的成员会随着实现而改变,所以我们使用
里的
offsetof宏来计算sun_path成员从结构体开始的偏移量。如果你进入,你将看到类似于如下的定义:
#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER)
这个表达式得到一个整型,它是成员的起始地址,假定结构体起始于0。
17.3.2 唯一连接(Unique Connections)
一个服务器可以使用bind、listen和accept函数来安排唯一的到客户的UNIX域连接。客户使用connect来联系服务器;在连接请求被服务器接受后,在客户和服务器之间有唯一的连接。这种操作风格和我们在16.5节里演示的因特网域套接字相同。
下面的代码展示了serv_listen函数的UNIX域套接字版本。
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <errno.h>
- #include <stddef.h>
- #define QLEN 10
- /*
- * Create a server endpoint of a connection.
- * Return fd if all OK, <0 on error.
- */
- int
- serv_listen(const char *name)
- {
- int fd, len, err, rval;
- struct sockaddr_un un;
- /* creat a UNIX domain stream socket */
- if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
- return(-1);
- unlink(name); /* in case it already exists */
- /* fill in socket address structure */
- memset(&un, 0, sizeof(un));
- un.sun_family = AF_UNIX;
- strcpy(un.sun_path, name);
- len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
- /* bind the name to the descriptor */
- if (bind(fd, (struct sockaddr *)&un, len) < 0) {
- rval = -2;
- goto errout;
- }
- if (listen(fd, QLEN) < 0) { /* tell kernel we're a server */
- rval = -3;
- goto errout;
- }
- return(fd);
- errout:
- err = errno;
- close(fd);
- errno = err;
- return(rval);
- }
首先,我们通过调用socket创建单个UNIX域套接字。我们然后填充一个sockaddr_un结构体,把熟知的路径名赋
到这个套接字上。这个结构体是bind的参数。注意我们不需要设置在一些平台上出现的sun_len域,因为操作系统使用我们传给bind函数的地址长度
为我们设置了这个。
最后,我们调用listen(16.4节)来告诉内核进程将扮演一个服务器,等待客户的连接。当一个客户的一个连接请求到达时,服务器调用serv_accept函数(下面的代码)。
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <time.h>
- #include <errno.h>
- #include <fcntl.h>
- #include <stddef.h>
- #define STALE 30 /* client's name can't be older than this (sec) */
- /*
- * Wait for a client connection to arrive, and accept it.
- * We also obtain the client's user ID from the pathname
- * that it must bind before calling us.
- * Returns new fd if all OK, <0 on error
- */
- int
- serv_accept(int listenfd, uid_t *uidptr)
- {
- int clifd, len, err, rval;
- time_t staletime;
- struct sockaddr_un un;
- struct stat statbuf;
- len = sizeof(un);
- if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0)
- return(-1); /* often errno=EINTR, if signal caught */
- /* obtain the client's uid from its calling address */
- len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */
- un.sun_path[len] = 0; /* null terminate */
- if (stat(un.sun_path, &statbuf) < 0) {
- rval = -2;
- goto errout;
- }
- #ifdef S_ISSOCK /* not defined for SVR4 */
- if (S_ISSOCK(statbuf.st_mode) == 0) {
- rval = -3; /* not a socket */
- goto errout;
- }
- #endif
- if ((statbuf.st_mode & (S_IRWXG | S_IRWXO)) ||
- (statbuf.st_mode & S_IRWXU) != S_IRWXU) {
- rval = -4; /* is not rwx------ */
- goto errout;
- }
- staletime = time(NULL) - STALE;
- if (statbuf.st_atime < staletime ||
- statbuf.st_ctime < staletime ||
- statbuf.st_mtime < staletime) {
- rval = -5; /* i-node is too old */
- goto errout;
- }
- if (uidptr != NULL)
- *uidptr = statbuf.st_uid; /* return uid of caller */
- unlink(un.sun_path); /* we're done with pathname now */
- return(clifd);
- errout:
- err = errno;
- close(clifd);
- errno = err;
- return(rval);
- }
服务器阻塞在accept的调用里,等待一个客户调用cli_conn。当accept返回时,它的返回值是一个和客户连接的全新的描
述符。(这和connld模块在STREAMS子系统上做的事有些相似。)此外,客户赋到它套接字上的路径名(包含客户进程ID的那个名字)被由
accept返回,通过第二个参数(指向sockaddr_un结构体的指针)。我们使路径名空字符终止并调用stat。这让我们验证路径名确实是一个套
接字且和这个套接字相关的三个时间都不老于30秒。(回想6.10节time函数返回自Epoch至今的秒数。)
如果三个检查都成功,那么我们假定客户的标识(它的用效用户ID)是套接字的属主。尽管这个检查并不完美,但是它是在当前系统上我们能做的最好的了。(如果内核返回有效用户ID给accept,就像I_RECVFD ioctl命令做的那样,那就更好了。)
客户通过调用cli_conn函数来初始化到服务器的连接。如下面的代码所示。
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <errno.h>
- #include <fcntl.h>
- #include <stddef.h>
- #define CLI_PATH "/var/tmp" /* +5 for pid = 14 chars */
- #define CLI_PERM S_IRWXU /* rwx for user only */
- /*
- * Create a client endpoint and connect to a server.
- * Returns fd if all OK, <0 on error.
- */
- int
- cli_conn(const char *name)
- {
- int fd, len, err, rval;
- struct sockaddr_un un;
- /* create a UNIX domain stream socket */
- if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
- return(-1);
- /* fill socket address structure with our address */
- memset(&un, 0, sizeof(un));
- un.sun_family = AF_UNIX;
- sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
- len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
- unlink(un.sun_path); /* in case it already exists */
- if (bind(fd, (struct sockaddr *)&un, len) < 0) {
- rval = -2;
- goto errout;
- }
- if (chmod(un.sun_path, CLI_PERM) < 0) {
- rval = -3;
- goto errout;
- }
- /* fill socket address structure with server's address */
- memset(&un, 0, sizeof(un));
- un.sun_family = AF_UNIX;
- strcpy(un.sun_path, name);
- len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
- if (connect(fd, (struct sockaddr *)&un, len) < 0) {
- rval = -4;
- goto errout;
- }
- return(fd);
- errout:
- err = errno;
- close(fd);
- errno = err;
- return(rval);
- }
我们调用socket来创建一个UNIX域套接字的客户的那端。我们然后用客户相关的名字来填充sockaddr_un结构体。
我们不让系统选择一个默认的地址,因为服务器会不能把一个客户和另一个区分开。事实上,我们绑定自己的地址,一个当开发使用套接字的客户程序时通常不会采取的步骤。
我
们绑定的路径名的最后最后五个字符由客户的进程ID组成。我们调用unlink,只是防止路径名已经存在。我们然后调用bind来给客户的套接字赋一个名
字。这在文件系统里用和被绑定的路径路径名相同的名字创建一个套接字文件。我们调用chmod来关闭除了用户读、用户写和用户执行之外的所有权限。在
serv_accept里,服务器检查这些权限和套接字的用户ID来检查客户的身份。
我们然后必须填充另一个sockaddr_un结构体,这将是服务器的被熟知的路径名。最后,我们调用connect函数来初始化和服务器的连接。