所谓基于流的管道实际上就是一种全双工管道,它必须在基于流的系统上才能实现,linux 默认对它是不支持的,而同样的逻辑,我们通常可以用基于 UNIX domain 的 socket 来实现,所以这里对它只作简单介绍。
关于流机制,我在 Unix环境高级编程学习笔记(九) 高级IO中曾经介绍过,知道可以在流首处加入处理模块,对于基于流的管道而言,管道的两端都是流首,所以可以在两端都加入处理模块,但同时,我们也只能在处理模块加入的一端删除它。
有时候,为了达到在不相关的进程间通信的目的,我们可以将这种管道的一端和某个实际的文件关联起来,也就相当于给了它一个名字,使用 fattach 函数:
int fattach(int filedes, const char *path);
使用 fdetach 函数可以解除关联:
int fdetach(const char *path);
在该函数被调用以后,已经获得管道访问权的用户将继续拥有该管道的访问权,而之后打开该文件的进程获得的是对文件本身的访问权。
在 Unix环境高级编程学习笔记(十一) 网络IPC:套接字中,我介绍了 socket 使用,关于 domain,一共可以有4种情况,今天,我们来看一下用于同一机器下不同进程间通信的 UNIX 域。UNIX 域同时支付流和数据报接口,不同于英特网域,这两种接口都是可靠的。
我们可以像使用普通的 socket 那样来使用它,在 UNIX 域的限定下,其地址格式由 sockaddr_un 数据结构来呈现。在Linux 2.4.22 以及 Solaris 9 上,sockaddr_un 的定义如下:
struct sockaddr_un {
sa_family_t sun_family;/* AF_UNIX */
char sun_path[108];/* pathname */
};
sun_path 成员指定了一个文件名,当将一个 UNIX domain socket 和该地址绑定在一起之后,系统将为我们创建一个 S_IFSOCK 类型的该文件。当该文件已经存在时,bind 函数将失败。当我们关闭该 socket 之后,文件不会自动被删除,需要我们手动 unlink 它。
需要注意以几点:
1. 在connect调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接口上的路径名。如果对于某个Unix域套接口的connect调用发现这个监听套接口的队列已满,调用就立即返回一个ECONNREFUSED错误。这一点不同于TCP,如果TCP监听套接口队列已满,TCP监听端就忽略新到达的SYN,而TCP连接发送端将数次发送SYN进行重试。
2. 在一个未绑定的Unix域套接口上发送数据报不会自动给这个套接口捆绑一个路径名,这一点与UDP套接口不同。所以在客户端,我们也必须显示地bind一个路径名到我们的套接口。
来看一个实际的例子。
服务端:
/*
*author: justaipanda
*create time:2012/09/05 21:51:27
*/
#include
#include
#include
#include
#define FILE_NAME "1.txt"
#define BUF_SIZE 1024
int serv_listen(const char* name) {
int fd;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return -1;
unlink(name);
struct sockaddr_un un;
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
int len = (int)((void*)&(un.sun_path) - (void*)&un)
+ strlen(un.sun_path);
if (bind(fd, (struct sockaddr*)&un, len) < 0) {
close(fd);
return -2;
}
if (listen(fd, 128) < 0) {
close(fd);
return -3;
}
return fd;
}
int serv_accept(int listenfd) {
int fd;
struct sockaddr_un un;
int len = sizeof(un);
if ((fd = accept(listenfd, (struct sockaddr*)&un, &len)) < 0)
return -1;
((char*)(&un))[len] = '\0';
unlink(un.sun_path);
return fd;
}
int main() {
int fd;
if ((fd = serv_listen(FILE_NAME)) < 0) {
printf("listen error!\n");
exit(fd);
}
while(1) {
int sclient = serv_accept(fd);
if (sclient < 0) {
printf("accept error!\n");
exit(sclient);
}
char buffer[BUF_SIZE];
ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);
if (len < 0) {
printf("recieve error!\n");
close(sclient);
continue;
}
buffer[len] = '\0';
printf("receive[%d]:%s\n", (int)len, buffer);
if (len > 0) {
if('q' == buffer[0]) {
printf("server over!\n");
exit(0);
}
char* buffer2 = "I'm a server!";
len = send(sclient, buffer2, strlen(buffer2), 0);
if (len < 0)
printf("send error!\n");
}
close(sclient);
}
return 0;
}
客户端:
/*
*author: justaipanda
*create time:2012/09/06 10:36:43
*/
#include
#include
#include
#include
#include
#define FILE_NAME "1.txt"
#define CLI_PATH "/var/tmp/"
#define BUF_SIZE 1024
int cli_conn(const char* name) {
int fd;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return -1;
struct sockaddr_un un;
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
unlink(un.sun_path);
int len = (int)((void*)&(un.sun_path) - (void*)&un)
+ strlen(un.sun_path);
if (bind(fd, (struct sockaddr*)&un, len) < 0) {
close(fd);
return -2;
}
if(chmod(un.sun_path, S_IRWXU) < 0) {
close(fd);
return -3;
}
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = (int)((void*)&(un.sun_path) - (void*)&un)
+ strlen(un.sun_path);
if (connect(fd, (struct sockaddr*)&un, len) < 0) {
close(fd);
return -4;
}
return fd;
}
int main() {
int fd;
if ((fd = cli_conn(FILE_NAME)) < 0) {
printf("connect error!\n");
exit(fd);
}
char buffer[BUF_SIZE];
printf("input:");
scanf("%s", buffer);
int len = send(fd, buffer, strlen(buffer), 0);
if (len < 0) {
printf("send error!\n");
exit(len);
}
len = recv(fd, buffer, BUF_SIZE, 0);
if (len < 0) {
printf("recieve error!\n");
exit(len);
}
buffer[len] = '\0';
printf("receive[%d]:%s\n", (int)len, buffer);
return 0;
}
对于关联的进程,我们也可以使用 socketpair 函数简化工作:
int socketpair(int domain, int type, int protocol, int sockfd[2]);
许多时候,如果可以在进程间传递文件描述符,这将极大的简化我们程序的设计。
所谓传送文件描述符实际上是指——让接收进程打开一个文件描述符,该描述符的值不一定和发送进程发送的文件描述符相同,但它们都指向同一文件表。
如果发送进程关闭了文件描述符,这并不会真的关闭文件或设备,因为系统认为,接收进程仍然打开着文件,即使此时,接收进程可能还未收到该文件描述符。
传送文件描述符一般可以有两种方式:基于流的管道和 UNIX domain socket,对于前者,这里不多下文笔,主要讲后者。利用 UNIX domain socket 传递文件描述符需要使用前面讲过的 sendmsg 和 recvmsg 函数(参见Unix环境高级编程学习笔记(十一) 网络IPC:套接字)。利用 msghdr 结构体中的 msg_constrol 成员传递描述符。该成员指向 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 the actual control message data */
};
为了发送文件描述符,将 cmsg_len 设置为 cmsghdr 结构的长度再加上一个整型(描述符)的长度,cmsg_level 字段设置为 SOL_SOCKET,cmsg_type 字段设置为 SCM_RIGHTS,用以指明我们在传送访问权限。
对于这个结构体的操作宏:
unsigned char *CMSG_DATA(struct cmsghdr *cp);
//Returns: pointer to data associated with cmsghdr structure
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);
//Returns: pointer to first cmsghdr structure associated with the msghdr structure, or NULL if none exists
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mp, struct cmsghdr *cp);
//Returns: pointer to next cmsghdr structure associated with the msghdr structure given the current cmsghdr structure, or NULL if we're at the last one
unsigned int CMSG_LEN(unsigned int nbytes);
//Returns: size to allocate for data object nbytes large
下面看一个使用 UNIX domain socket 传递文件描述符的例子,该例子是由前面的例子修改而来,其中 serv_listen,serv_accept,以及 cli_conn 函数和原来一样,就不再重复了。
服务端:
int send_fd(int fd, int fd_to_send) {
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
struct cmsghdr *cmptr = NULL;
char buffer[BUF_SIZE];
struct iovec iov;
if (fd_to_send >= 0) {
int cmsg_len = CMSG_LEN(sizeof(int));
cmptr = malloc(cmsg_len);
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
cmptr->cmsg_len = cmsg_len;
*(int*)CMSG_DATA(cmptr) = fd_to_send;
msg.msg_control = cmptr;
msg.msg_controllen = cmsg_len;
sprintf(buffer, "OK!");
}
else {
if (-1 == fd_to_send)
sprintf(buffer, "cannot open file!");
else
sprintf(buffer, "wrong command!");
msg.msg_control = NULL;
msg.msg_controllen = 0;
}
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
iov.iov_base = buffer;
iov.iov_len = strlen(buffer);
sendmsg(fd, &msg, 0);
if (cmptr)
free(cmptr);
return 0;
}
int main() {
int fd;
if ((fd = serv_listen(FILE_NAME)) < 0) {
printf("listen error!\n");
exit(fd);
}
while(1) {
int sclient = serv_accept(fd);
if (sclient < 0) {
printf("accept error!\n");
exit(sclient);
}
char buffer[BUF_SIZE];
ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);
if (len < 0) {
printf("recieve error!\n");
close(sclient);
continue;
}
buffer[len] = '\0';
printf("receive[%d]:%s\n", (int)len, buffer);
if (len > 0) {
if('q' == buffer[0]) {
printf("server over!\n");
exit(0);
}
else if ('f' == buffer[0]) {
int new_fd = open(buffer + 2, O_RDWR);
send_fd(sclient, new_fd);
}
else
send_fd(sclient, -2);
}
close(sclient);
}
return 0;
}
int recv_fd(int fd, char *buffer, size_t size) {
struct cmsghdr *cmptr;
int cmsg_len = CMSG_LEN(sizeof(int));
cmptr = malloc(cmsg_len);
struct iovec iov;
iov.iov_base = buffer;
iov.iov_len = size;
struct msghdr msg;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmptr;
msg.msg_controllen = cmsg_len;
int len = recvmsg(fd, &msg, 0);
if (len < 0) {
printf("receve message error!\n");
exit(0);
}
else if (len == 0) {
printf("connection closed by server!\n");
exit(0);
}
buffer[len] = '\0';
int cfd = -1;
if (cmptr->cmsg_type != 0)
cfd = *(int*)CMSG_DATA(cmptr);
free(cmptr);
return cfd;
}
int main() {
int fd;
if ((fd = cli_conn(FILE_NAME)) < 0) {
printf("connect error!\n");
exit(fd);
}
char buffer[BUF_SIZE];
printf("input:");
fgets(buffer, BUF_SIZE, stdin);
buffer[strlen(buffer) - 1] = '\0';
int len = send(fd, buffer, strlen(buffer), 0);
if (len < 0) {
printf("send error!\n");
exit(len);
}
int cfd = recv_fd(fd, buffer, BUF_SIZE);
printf("data:%s\n", buffer);
if (cfd >= 0) {
printf("received open file:%d\n", cfd);
}
return 0;
}