通过UNIX域套接字传送文件描述符

UNIX域套接字用于在同一台机器上运行的进程之间的通信。UNIX域套接字提供流和数据包两种接口。UNIX域套接字是套接字和管道之间的混合物。为了创建一对非命名的、相互连接的UNIX域套接字,可以使用socketpair函数。

#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sockfd[2]);

pipe创建的管道,第一描述符的写端和第二描述符的读端都被关闭,socketpair创建的则是全双工UNIX域套接字。

使用面向网络的域套接字接口,可以创建命名UNIX域套接字,使无关进程之间也可以用UNIX域套接字进行通信。UNIX域套接字的地址由sockaddr_un结构表示:

#include <sys/un.h>
struct sockaddr_un {
	sa_family_t	sun_family; //AF_UNIX
	char		sun_path[108]; //pathname
}

UNIX域套接字没有端口号,依靠唯一的路径名来标识。

UNIX域套接字可以用来传送文件描述符。描述符传递不是简单的传送一个int类型的描述符的值,而是在接收进程中创建一个新的描述符,这个描述符与发送进程的描述符指向内核文件表中的相同项。
当发送进程将描述符传送给接收进程后,通常它关闭该描述符。被发送者关闭的描述符并不真正关闭文件或设备,因为描述符在接收进程里仍视为打开的,即使接收者还没有明确地收到这个描述符。

传送文件描述符典型的应用场景就是进程池模式的网络服务程序:
1.控制进程负责监视接收网络连接请求,并将对应描述字通过描述符传递功能通知工作进程。
2.工作进程从网络连接读取和处理,并发回处理结果。
《APUE》提到的一个应用场景是:服务器可以是设置用户id程序,于是使其具有客户进程没有的附加权限。

流式UNIX域套接字传送文件描述符

下面是流式UNXI域套接字传送文件描述符的例子:
struct msghdr这个东西比较坑,sendmsg的时候就是把每个数据依次发送而已,recvmsg必须定义好msghdr每个字段的长度才能正确接收数据。

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <list>
#include <vector>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
using namespace std;

int serv_listen(const char *name);
int serv_accept(int listenfd, uid_t *uidptr);
int cli_conn(const char *name);

int send_fd(int fd, int fd_to_send, const char *strmsg);
int recv_fd(int fd);

int main(int argc, char *argv[])
{
	//server without parameter
	if(argc == 1)
	{
		int lfd = serv_listen("un.domain");
		cout << "listen fd is " << lfd << endl;
		if(lfd < 0) return 0;
		uid_t uid;
		int fd = serv_accept(lfd, &uid);
		cout << "accept fd is " << fd << endl;
		if(fd < 0) return 0;
		cout << "client uid is " << uid << endl;
		while(true)
		{
			char buf[128];
			ssize_t n = read(fd, buf, sizeof(buf)-1);
			if(n <= 0) { close(fd); break; }
			buf[n] = 0;
			puts(buf);

			int open_fd = open("test.txt", O_RDONLY);
			cout << "open_fd is " << open_fd << endl;
			if(open_fd < 0) break;
			cout << "send_fd " << send_fd(fd, open_fd, "abcd") << " chars\n";
			close(open_fd);
			sleep(600);
		}
	}
	//client with some parameters
	else
	{
		int fd = cli_conn("un.domain");
		cout << "connect fd is " << fd << endl;
		if(fd < 0) return 0;
		for(int counter = 1; ;counter++)
		{
			char buf[16];
			sprintf(buf, "\n>>>%04d", counter);
			//对端关闭了socket再write会产生SIGPIPE,默认退出程序
			write(fd, buf, strlen(buf));
			puts("\n>>>");
			int trans_fd = recv_fd(fd);
			cout << "trans_fd is " << trans_fd << endl;
			if(trans_fd >= 0)
			{
				char bufs[64];
				int nbufs = read(trans_fd, bufs, 16);
				if(nbufs > 0)
				{
					bufs[nbufs] = 0;
					cout << "read data " << bufs << endl;
				}
			}
			sleep(2);
			//shutdown(fd, SHUT_WR);
			//close(fd);
		}
	}
	return 0;
}

int serv_listen(const char *name)
{
	int fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if(fd < 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 = offsetof(sockaddr_un, sun_path) + strlen(un.sun_path);
	int rval, err;
	if(bind(fd, (sockaddr*)&un, len) < 0)
	{
		rval = -2;
		goto errout;
	}
	if(chmod(un.sun_path, S_IRWXU|S_IRWXO) < 0)
	{
		rval = -3;
		goto errout;
	}
	if(listen(fd, 10) < 0)
	{
		rval = -4;
		goto errout;
	}
	return fd;

errout:
	err = errno;
	close(fd);
	errno = err;
	return rval;
}

int serv_accept(int listenfd, uid_t *uidptr)
{
	struct sockaddr_un un;
	socklen_t len = sizeof(un);
	int clifd = accept(listenfd, (struct sockaddr *)&un, &len);
	if(clifd < 0) return -1;

	len -= offsetof(struct sockaddr_un, sun_path);
	un.sun_path[len] = 0;

	cout << "client path is " << un.sun_path << endl;
	struct stat buf;
	int rval, err;
	if(stat(un.sun_path, &buf) < 0)
	{
		rval = -2;
		goto errout;
	}
	if(uidptr) *uidptr = buf.st_uid;
	unlink(un.sun_path);
	return clifd;

errout:
	err = errno;
	close(clifd);
	errno = err;
	return rval;
}

int cli_conn(const char *name)
{
	int fd = socket(AF_UNIX, SOCK_STREAM, 0);
	if(fd < 0) return -1;
	int err, rval;
	socklen_t len2;

	struct sockaddr_un un;
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	sprintf(un.sun_path, "%05d", getpid());
	int len = offsetof(sockaddr_un, sun_path) + strlen(un.sun_path);
	unlink(un.sun_path);
	//和tcp一样, 不bind直接connect也是可以的
	if(bind(fd, (sockaddr*)&un, len) < 0)
	{
		rval = -2;
		goto errout;
	}

	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, name);
	len2 = offsetof(sockaddr_un, sun_path) + strlen(un.sun_path);
	if(connect(fd, (sockaddr*)&un, len2) < 0)
	{
		rval = -3;
		goto errout;
	}
	return fd;

errout:
	err = errno;
	close(fd);
	errno = err;
	return rval;
}

int send_fd(int fd, int fd_to_send, const char *strmsg)
{
	//strmsg + \0 + char
	struct iovec vec[2];
	char tmp[2];
	tmp[0] = 0;
	if(fd_to_send >= 0) tmp[1] = 1;
	else tmp[1] = 0;
	vec[0].iov_base = (void *)strmsg;
	vec[0].iov_len = std::min(strlen(strmsg), size_t(255));
	vec[1].iov_base = tmp;
	vec[1].iov_len = 2;
	struct msghdr msg;
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = vec;
	msg.msg_iovlen = 2;
	msg.msg_controllen = 0;
	//这里一定要从堆里分配空间
	//不能从栈上分配, 不然CMSG_DATA会把上个变量冲掉
	struct cmsghdr *cmp = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int)));
	//就不手动free了
	if(cmp && fd_to_send >= 0)
	{
		cmp->cmsg_level = SOL_SOCKET;
		cmp->cmsg_type = SCM_RIGHTS;
		cmp->cmsg_len = CMSG_LEN(sizeof(int));
		*(int *)CMSG_DATA(cmp) = fd_to_send;
		msg.msg_control = cmp;
		msg.msg_controllen = cmp->cmsg_len;
	}
	return sendmsg(fd, &msg, 0);
}

int recv_fd(int fd)
{
	char strmsg[256];
	memset(strmsg, 0, 256);
	struct iovec vec[1];
	vec[0].iov_base = strmsg;
	vec[0].iov_len = 255;
	struct msghdr msg;
	msg.msg_namelen = 0;
	msg.msg_iov = vec;
	msg.msg_iovlen = 1;
	struct cmsghdr *cmp = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int)));
	if(cmp == NULL)
	{
		cout << "malloc error\n";
		return -1;
	}
	msg.msg_control = cmp;
	msg.msg_controllen = CMSG_LEN(sizeof(int));

	int nr = recvmsg(fd, &msg, 0);
	if(nr < 0)
	{
		cout << "recvmsg error " << errno << endl;
		return -2;
	}
	else if(nr == 0)
	{
		puts("connection closed by server");
		return -3;
	}
	cout << "recv " << nr << " chars\n";
	cout << "strmsg is " << strmsg << endl;
	int len = strlen(strmsg);
	if(strmsg[len] == 0 && strmsg[len+1])
		return *(int *)CMSG_DATA(cmp);
	else
		return -4;
}

数据包式UNIX域套接字传送文件描述符

为了搞清楚msghdr中的数据是如何序列化的,控制信息是如何发送的,尝试去抓包,搜到有人说socat可以,于是把上面的例子改成数据包方式发送:

int bind_un(const char *name);
int send_fd(int fd, const char *dest, int fd_to_send, const char *strmsg);
int recv_fd(int fd);

int main(int argc, char *argv[])
{
	if(argc == 1)
	{
		int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
		cout << "client fd is " << fd << endl;
		if(fd < 0) return 0;
		int open_fd = open("test.txt", O_RDONLY);
		cout << "open_fd is " << open_fd << endl;
		cout << "send_fd " << send_fd(fd, "un.domain", open_fd, "abcd") << " chars\n";
		close(open_fd);
		sleep(600);
	}
	else
	{
		int fd = bind_un("un.domain");
		cout << "bind fd is " << fd << endl;
		if(fd < 0) return 0;
		for(int counter = 1; ;counter++)
		{
			int trans_fd = recv_fd(fd);
			cout << "trans_fd is " << trans_fd << endl;
			if(trans_fd >= 0)
			{
				char bufs[64];
				int nbufs = read(trans_fd, bufs, 16);
				if(nbufs > 0)
				{
					bufs[nbufs] = 0;
					cout << "read data " << bufs << endl;
				}
			}
			sleep(1);
		}
	}
	return 0;
}

int bind_un(const char *name)
{
	int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
	if(fd < 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 = offsetof(sockaddr_un, sun_path) + strlen(un.sun_path);
	int rval, err;
	if(bind(fd, (sockaddr*)&un, len) < 0)
	{
		rval = -2;
		goto errout;
	}
	return fd;

errout:
	err = errno;
	close(fd);
	errno = err;
	return rval;
}

int send_fd(int fd, const char *dest, int fd_to_send, const char *strmsg)
{
	struct msghdr msg;
	struct sockaddr_un un;
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, dest);
	msg.msg_name = (sockaddr *)&un;
	msg.msg_namelen = sizeof(un);
	struct iovec vec[2];
	char tmp[2];
	tmp[0] = 0;
	if(fd_to_send >= 0) tmp[1] = 1;
	else tmp[1] = 0;
	vec[0].iov_base = (void *)strmsg;
	vec[0].iov_len = strlen(strmsg);
	vec[1].iov_base = tmp;
	vec[1].iov_len = 2;
	msg.msg_iov = vec;
	msg.msg_iovlen = 2;
	msg.msg_controllen = 0;
	struct cmsghdr *cmp = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int)));
	if(cmp && fd_to_send >= 0)
	{
		cmp->cmsg_level = SOL_SOCKET;
		cmp->cmsg_type = SCM_RIGHTS;
		cmp->cmsg_len = CMSG_LEN(sizeof(int));
		*(int *)CMSG_DATA(cmp) = fd_to_send;
		msg.msg_control = cmp;
		msg.msg_controllen = cmp->cmsg_len;
	}
	return sendmsg(fd, &msg, 0);
}

int recv_fd(int fd)
{
	struct msghdr msg;
	char strmsg[256];
	memset(strmsg, 0, 256);
	struct iovec vec[1];
	vec[0].iov_base = strmsg;
	vec[0].iov_len = 255;
	struct cmsghdr *cmp = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int)));
	if(cmp == NULL)
	{
		cout << "malloc error\n";
		return -1;
	}
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = vec;
	msg.msg_iovlen = 1;
	msg.msg_control = cmp;
	msg.msg_controllen = CMSG_LEN(sizeof(int));
	msg.msg_flags = 0;

	int nr = recvmsg(fd, &msg, MSG_WAITALL);
	if(nr < 0)
	{
		cout << "recvmsg error " << errno << endl;
		return -2;
	}
	else if(nr == 0)
	{
		puts("connection closed by server");
		return -3;
	}
	puts(">>>");
	cout << "recv " << nr << " chars\n";
	cout << "strmsg is " << strmsg << endl;
	int len = strlen(strmsg);
	if(strmsg[len] == 0 && strmsg[len+1])
		return *(int *)CMSG_DATA(cmp);
	else
		return -4;
}

抓包结果:
socat -L 100 -x -v unix-recvfrom:un.domain,mode=777,reuseaddr,fork unix-sendto:../un.domain
socat
数据流向是:client --> socat-recv --> socat-send --> server
然而socat-recv把控制信息都给丢掉了。。。
然后就没有再通过其他方式尝试了。

在UNIX域套接字上发送凭证

《APUE》17.4.2 后半部分讲了"如何在UNIX域套接字上发送凭证",我实际测试了,发现sendmsg失败,查看了CMSG_NXTHDR的源码后发现在计算下一个cmsghdr的时候,会对齐地址的,所以实际占用空间会比两个cmsghdr大小之和大。
然而修正之后,sendmsg虽然成功了,但strace发现实际上并没有发送凭证,改为将文件描述符去掉,只发送凭证之后,虽然sendmsg端检测到发送了,但recvmsg仍然收不到。
不知为何。

参考:《APUE》17.3 17.4

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值