UNP_15_Unix域协议

Posted on Mar 15, 2020

15.1 概述

  Unix 域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方法。所用 API 就是不同主机通信的 API,Unix 域协议也可以看作 IPC 方法之一。
  Unix 域协议提供两类套接字:字节流套接字(类似 TCP)和数据报套接字(类似 UDP)
使用 Unix 域协议的原因:

  1. 同主机使用 Unix 域协议性能比普通 TCP 套接字快出一倍
  2. Unix 域套接字可用于同一主机上的不同进程之间传递描述符
  3. Unix 域套接字新的实现把客户的凭证提供给服务器,这样可以提供额外的安全措施检查

15.2 unix 域套接字地址结构

Unix 域套接字地址结构如下

struct sockaddr_un{
    sa_family_t sun_family;  // AF_LOCAL
    char sun_path[104];
}

sun_path 必须使用空字符结尾,为指定地址,就是 sun_path[0] = 0 的地址,等价于 IPv4 的 INADDR_ANY 常值

Unix 域套接字的 bind 调用

下面的程序创建了一个 Unix 域套接字,并 bind 一个路径名,在调用 getsockname 输出绑定的路径名

int main(int argc, char **argv)
{
	int					sockfd;
	socklen_t			len;
	struct sockaddr_un	addr1, addr2;

	if (argc != 2)
		err_quit("usage: unixbind <pathname>");

	sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
    // 如果文件系统中已经存在这个路径名,bind 会调用失败,所以先使用 unlink 删除这个路径名
	unlink(argv[1]);		/* OK if this fails */
    // 设置绑定的地址
	bzero(&addr1, sizeof(addr1));
	addr1.sun_family = AF_LOCAL;   // AF_LOCAL
	strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path)-1);
	Bind(sockfd, (SA *) &addr1, SUN_LEN(&addr1));

	len = sizeof(addr2);
	Getsockname(sockfd, (SA *) &addr2, &len);
	printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);
	
	exit(0);
}

15.3 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 存储返回

  使用 SOCK_STREAM 创建的结果称为 stream pipe,它和调用 pipe 创建的普通 Unix pipe 类似,差别在于 stream pipe 是全双工的,即两个描述符都是即可读又可写。

15.4 套接字函数

  1. 由 bind 创建的路径名默认访问权限应为 0777 并根据当前 umask 值进行修正
  2. 与 Unix 域套接字关联的路径名应该是一个绝对路径名,因为运行时的目录是从当前目录开始的,会造成不可预测的结果
  3. 在 connect 调用中指定的路径必须是一个绑定在某个打开的 Unix 域套接字上的路径名,而且它们的套接字类型(数据报或字节流)也必须一致
  4. 调用 connect 连接一个 Unix 域套接字涉及的权限测试等同于调用 open 以只写方式访问相应的路径名
  5. unix 域字节流套接字类似于 TCP 套接字:他们都为进程提供一个无记录边界的字节流接口
  6. 对于某个 Unix 域字节流套接字的 connect 发现监听队列满了,会直接返回错误。但是通常的 TCP 服务器在监视队列满了会忽略这个 SYN,会导致客户端重发消息
  7. unix 域数据报套接字类似于 UDP 套接字:他们都为进程提供一个保留记录边界的不可靠的数据报服务
  8. 在一个未绑定的 Unix 域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,这一点不同于 UDP 套接字。类似的,对于某个 Unix 域数据报套接字的 connect 调用不会给本套接字捆绑一个路径名,这一点不同于 TCP 和 UDP

15.5 unix 域字节流客户/服务器程序

使用 Unix 域字节流套接字重写回射服务器

int main(int argc, char **argv)
{
	int					listenfd, connfd;
	pid_t				childpid;
	socklen_t			clilen;
    // 两个套接字的地址结构均是 sockaddr_un 类型
	struct sockaddr_un	cliaddr, servaddr;
	void				sig_chld(int);
    // 创建 Unix 域字节流套接字描述符
	listenfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
    // 关联本机一个路径,并使用 unlink 先删除这个路径
	unlink(UNIXSTR_PATH);
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sun_family = AF_LOCAL;
	strcpy(servaddr.sun_path, UNIXSTR_PATH);

	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	Signal(SIGCHLD, sig_chld);

	for ( ; ; ) {
		clilen = sizeof(cliaddr);
		if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
			if (errno == EINTR)
				continue;		/* back to for() */
			else
				err_sys("accept error");
		}

		if ( (childpid = Fork()) == 0) {	/* child process */
			Close(listenfd);	/* close listening socket */
			str_echo(connfd);	/* process request */
			exit(0);
		}
		Close(connfd);			/* parent closes connected socket */
	}
}

使用 Unix 域字节流套接字重写回射客户端

int main(int argc, char **argv)
{
	int					sockfd;
    // Unix 域套接字地址结构
	struct sockaddr_un	servaddr;
    // 参数必须是 AF_LOCAL
	sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sun_family = AF_LOCAL;
    // 复制路径名,这里不需要使用 unlink
	strcpy(servaddr.sun_path, UNIXSTR_PATH);

	Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

	str_cli(stdin, sockfd);		/* do it all */

	exit(0);
}

15.6 unix 域数据报客户/服务器程序

使用 Unix 域数据报套接字重写回射服务器

int main(int argc, char **argv)
{
	int					sockfd;
    // 两个套接字的地址结构均是 sockaddr_un 类型
	struct sockaddr_un	servaddr, cliaddr;
    // 参数必须是 AF_LOCAL
	sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);
    // 关联本机一个路径,并使用 unlink 先删除这个路径
	unlink(UNIXDG_PATH);
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sun_family = AF_LOCAL;
	strcpy(servaddr.sun_path, UNIXDG_PATH);

	Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));

	dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
}

使用 Unix 域数据报套接字重写回射客户端

int main(int argc, char **argv)
{
	int					sockfd;
    // Unix 域套接字地址结构
	struct sockaddr_un	cliaddr, servaddr;
    // 参数必须是 AF_LOCAL
	sockfd = Socket(AF_LOCAL, SOCK_DGRAM, 0);
    // 和 UDP 不同的是,使用 Unix 域套接字,必须使用 bind 绑定路径名到套接字,这样服务器才有回射的路径名
    // 否则服务器会回射到路径名为空的数据,这样服务器调用 sendto 会报错
	bzero(&cliaddr, sizeof(cliaddr));		/* bind an address for us */
	cliaddr.sun_family = AF_LOCAL;
	strcpy(cliaddr.sun_path, tmpnam(NULL));

	Bind(sockfd, (SA *) &cliaddr, sizeof(cliaddr));

	bzero(&servaddr, sizeof(servaddr));	/* fill in server's address */
	servaddr.sun_family = AF_LOCAL;
	strcpy(servaddr.sun_path, UNIXDG_PATH);

	dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));

	exit(0);
}

15.7 描述符传递

  可以在两个进程之间创建一个 Unix 域套接字,使用 sendmsg 跨这个套接字发送一个特殊的消息。这个消息交给内核处理,会把打开的描述符从发送进程传递到接收进程,步骤如下:

  1. 创建一个 Unix 域套接字
  2. 发送进程通过调用返回描述符的任一 Unix 函数打开一个描述符,例如 open、pipe、mkfifo、socket、accept。可以在进程之间传递的描述符类型不限,包括各种描述符,不仅仅局限于文件描述符
  3. 发送进程创建一个 msghdr 结构,描述符使用辅助数据进行传递,所以只能使用 recvmsg 和 sendmsg 来处理。发送一个描述符会让它的引用计数加一
  4. 接收方调用 recvmsg 来接受这个描述符。会在进程中创建一个新的描述符,和发送端的指向相同的文件表项

UNP-15.7 小节提供了一个很好的描述符传递的例子,推荐仔细看看

15.8 接收发送者的凭证

  本章开头说过的用户凭证通过辅助数据进行传递,其结构一般根据不同系统有所不同,当客户端和服务器进行通讯时,服务器通常需要根据一定手段获取客户的身份,用来验证客户是否有权限请求相应的服务。

小结

  • Unix 域套接字是客户和服务器在同一主机上的 IPC 方法之一。和其余 IPC 方式相比,Unix 域套接字的优势在于其 API 和网络传递数据的 API 几乎相同。并且在同一主机的情况下,性能优于 TCP 类连接性能
  • 编写 Unix 连接时,应注意的点是必须 bind 一个路径名到 UDP 套接字的客户,以使 UDP 服务器有发送应答的目的地
  • 同一个主机上客户和服务器之间的描述符传递是一个非常有用的技术,它通过 Unix 域套接字发生。