引言
前面讲述了各种进程间通信和网络IPC的内容,除此以外,还有一种非常常用的IPC——UNIX域套接字。这种套接字实际上就是一种文件,能够让本机的进程之间互相通信。
Unix域套接字
Unix域套接字用于同一台电脑上运行的进程通信。虽然TCP/IP协议的套接字很方便,但是在某些情况下需要保证更高的通信效率,所以Unix域套接字更加适合,因为它是通过内核转发的信息,而网络协议通信需要通过网卡发送,可能效率更低些。
虽然不通过网络传输数据,但是Unix域套接字也有流和数据报两种接口。Unix域套接字就像是套接字和管道的组合,在数据传输上很像管道,但是是以套接字的形式使用。
int socketpair(int domain, int type, int protocol, int socket_vector[2]);
socketpair函数创建一对未命名的已连接的套接字在指定的域(domain)中,并且以指定的类型(type),可选指定协议(protocol),描述符将存放在socket_vector数组中。需要注意的是,这对套接字实际上是全双工的,在很多Unix实现中,全双工的管道之类的实际上就是通过Unix域套接字实现的。
命名Unix域套接字
前面的socketpair虽然很方便,但是它创建的是未命名的套接字,也就是说不同进程无法使用,在前面的网络套接字章节中讲了套接字如何绑定一个地址和端口,但是我们也可以将其绑定到路径上,使其成为一个文件,这样就能让不同进程使用。
#include "include/apue.h"
#include <sys/socket.h>
#include <sys/un.h>
int main(int argc, char *argv[])
{
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)
err_sys("socket failed");
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
if (bind(fd, (struct sockaddr *)&un, size) < 0)
err_sys("bind failed");
printf("UNIX domain socket bound\n");
exit();
}
上面的例程非常简单,首先是创建一个Unix域地址,使用strcpy函数将地址复制到地址族变量,然后使用socket函数创建一个套接字,然后计算路径成员在结构体中的偏移量加上路径本身的长度,求出size变量,然后使用bind函数将地址族结构体和套接字绑定在一起。
当我们运行程序完毕,就能在当前目录下看到一个foo.socket
文件,可能就有人要问了,为什么程序结束这个文件仍然存在,套接字不是文件描述符吗,不是在程序结束的时候就回收了吗?实际上,这是搞混了文件描述符和文件的区别,文件描述符只是0、1、2之类的数字,用于指向文件表项,实际上文件的打开是由内核维护的。套接字也一样,当我们创建套接字的时候,并且将其绑定到具体路径,内核就会帮助我们创建一个S_IFSOCK
类型的文件,但是实际上这个文件并没有什么用,它不会用于实际的写入,不然这不就是和普通的文件一样了吗,所以这个文件纯粹就是个flag,用于标记地址,就跟通常使用的xxx.pid
文件这种形式类似。
唯一连接
这小节没什么重要内容,除了三个封装函数,其中有一些内容可能是有困惑的,这里笔者将自己的理解讲一下。首先,我们先需要知道各个平台实际上实现是有差异的,例如sockaddr_un
的结构体不同,在Linux和Solaris中,是如下所示:
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[108];
};
而在FreeBSD和OSX系统中,是如下的:
struct sockaddr_un {
unsigned char sun_len;
sa_family_t sun_family;
char sun_path[104];
};
先给出serv_listen函数
#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define QLEN 10
/*
* Create a server endpoint of a connection.
* Returns fd if all OK, <0 on error.
*/
int
serv_listen(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
if (strlen(name) >= sizeof(un.sun_path)) {
errno = ENAMETOOLONG;
return(-1);
}
/* create a UNIX domain stream socket */
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return(-2);
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 = -3;
goto errout;
}
if (listen(fd, QLEN) < 0) { /* tell kernel we're a server */
rval = -4;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
errno = err;
return(rval);
}
首先是创建一个Unix域套接字,如果文件已经存在则删除原先文件,然后构造sockaddr_un结构体,然后使用bind函数将地址和套接字绑定,系统自动生成套接字文件,然后使用listen函数侦听套接字。这里基本没什么需要讲解的。
然后是serv_accept函数
#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <errno.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, err, rval;
socklen_t len;
time_t staletime;
struct sockaddr_un un;
struct stat statbuf;
char *name;
/* allocate enough space for longest name plus terminating null */
if ((name = malloc(sizeof(un.sun_path + 1))) == NULL)
return(-1);
len = sizeof(un);
if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
free(name);
return(-2); /* 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 */
memcpy(name, un.sun_path, len);
name[len] = 0; /* null terminate */
if (stat(name, &statbuf) < 0) {
rval = -3;
goto errout;
}
#ifdef S_ISSOCK /* not defined for SVR4 */
if (S_ISSOCK(statbuf.st_mode) == 0) {
rval = -4; /* not a socket */
goto errout;
}
#endif
if ((statbuf.st_mode & (S_IRWXG | S_IRWXO)) ||
(statbuf.st_mode & S_IRWXU) != S_IRWXU) {
rval = -5; /* is not rwx------ */
goto errout;
}
staletime = time(NULL) - STALE;
if (statbuf.st_atime < staletime ||
statbuf.st_ctime < staletime ||
statbuf.st_mtime < staletime) {
rval = -6; /* i-node is too old */
goto errout;
}
if (uidptr != NULL)
*uidptr = statbuf.st_uid; /* return uid of caller */
unlink(name); /* we're done with pathname now */
free(name);
return(clifd);
errout:
err = errno;
close(clifd);
free(name);
errno = err;
return(rval);
}
首先是使用accept函数阻塞等待客户进程连接。当accept返回的时候,返回的是新的套接字描述符,也就是存在连接的套接字,并且从第二个参数得到套接字的路径名,接着复制路径名,最后调用stat函数检查路径名。
其中len -= offsetof(struct sockaddr_un, sun_path);
可能有些人不是很明白,这里实际上用了点编码技巧,实际上就是结构体总长度减去sun_path成员的内存偏移量,最终就是sun_path的长度。还有,accept第二个餐宿实际上和bind是不一样的,因为这个套接字是已经连接的套接字,所以会包含客户进程ID的名字。
#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define CLI_PATH "/var/tmp/"
#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, sun;
int do_unlink = 0;
if (strlen(name) >= sizeof(un.sun_path)) {
errno = ENAMETOOLONG;
return(-1);
}
/* 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%05ld", CLI_PATH, (long)getpid());
printf("file is %s\n", un.sun_path);
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;
do_unlink = 1;
goto errout;
}
/* fill socket address structure with server's address */
memset(&sun, 0, sizeof(sun));
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
if (connect(fd, (struct sockaddr *)&sun, len) < 0) {
rval = -4;
do_unlink = 1;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
if (do_unlink)
unlink(un.sun_path);
errno = err;
return(rval);
}
首先是创建一个套接字,然后用客户端进程ID附加到路径上,形成自己的套接字路径,并且将这个套接字绑定在地址上,也就是系统会在生成客户端的socket文件,可能有人要奇怪了,为什么不直接使用connect而是要bind地址,因为如果不绑定地址,我们就无法区分连接是属于哪个客户端进程的,也就是类似于网络上客户端特意绑定一个端口连接服务端。
小结
最后还有三节,实际上都是属于实际操作的内容了,所以这里就不讲了。这篇文章就算是最终的系列结尾,因为最后章节是项目源码解析了。所以就不再讲述
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。