Unix网络编程(8):Unix域协议

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

1. 套接字地址结构

#include <sys/un.h>

struct sockaddr_un {
  sa_family_t sun_family;  // AF_LOCAL
  char sun_path[104];  // null-terminated pathname
};

        存放在 $sun_-path$ 数组中的路径名必须以空字符结尾。实现提供一个 $SUN_-LEN$ 宏以一个指向 $sockaddr_-un$ 结构的指针为参数并返回该结构的长度,包括路径名中的非空字节数。未指定地址通过空字符串作为路径名指示,等价于IPv4的 $INADDR_-ANY$ 常值或IPv6的 $IN6ADDR_-ANY_-INIT$ 常值。

#include "unp.h"

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);
  unlink(argv[1]);
  bzero(&addr1, sizeof(addr1));
  addr1.sun_family = AF_LOCAL;
  strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path) - 1);
  Bind(sockfd, (struct sockaddr *) &addr2, SUN_LEN(&addr1));

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

  exit(0);
}

        我们调用 $bind$ 捆绑到套接字上的路径名就是命令行参数,如果文件系统中已存在该路径名,$bind$ 将会失败,所以我们先调用 $unlink$ 删除这个路径名,防止它已存在。如果不存在,$unlink$ 会返回一个可以忽略的错误。

2. socketpair函数

#include <sys/socket.h>

// 成功返回非0值,出错返回-1
int socketpair(int family, int type, int protocol, int sockfd[2]);

        $socketpair$ 函数创建两个随后连接起来的套接字,仅适用于Unix域套接字。$family$ 参数必须为 $AF_-LOCAL$ ,$protocol$ 参数必须为 $0$ ,$type$ 参数既可以是 $SOCK_-STREAM$ ,也可以是 $SOCK_-DGRAM$ 。新创建的两个套接字作为 $sockfd[0]$ 和 $sockfd[1]$ 返回。指定 $type$ 参数为 $SOCK_-STREAM$ 调用 $socketpair$ 得到的结果称为流管道 ( $stream$ $pipe$ ),与调用 $pipe$ 创建的普通Unix管道类似,差别在于前者是全双工的,即两个描述符都是既可读又可写的。

3. 套接字函数

        当作用于Unix域套接字时,套接字函数中存在一些差异和限制:

  1. 由 $bind$ 创建的路径默认访问权限应该为 $0777$ ( 创建者用户、组用户和其他用户都可读、可写并可执行 ),并按照当前 $umask$ 值进行修正;
  2. Unix域套接字关联的路径名应该是一个绝对路径名,而不是一个相对路径名。避免使用后者的原因是它的解析依赖于调用者当前的工作目录;
  3. 在 $connect$ 调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接字上的路径名,而且它们的套接字类型 ( 字节流或数据报 ) 也必须一致;
  4. 调用 $connect$ 连接一个Unix域套接字涉及的权限测试等同于调用 $open$ 以只写方式访问相应的路径名;
  5. Unix域字节流套接字类似于TCP套接字,它们都为进程提供一个无记录边界的字节流接口;
  6. 如果对于某个Unix域字节流套接字的 $connect$ 调用发现这个监听套接字的队列已满,调用就立即返回一个 $ECONNREFUSED$ 错误。这一点不同于TCP,如果TCP监听套接字的队列已满,TCP监听端就忽略新到达的SYN,而连接端将进行重试;
  7. Unix域数据报套接字类似于UDP套接字,它们都提供一个保留记录边界的不可靠的数据报服务;
  8. 在一未绑定的Unix域套接字上发送数据报不会自动给这个套接字捆绑一个路径名,不同于UDP套接字,后者会给套接字捆绑一个临时端口。这一点意味着除非数据报发送端已经捆绑了一个路径名,否则接收端无法应答。类似的,对某个Unix域数据报套接字的 $connect$ 调用也不会给套接字捆绑一个路径名。

4. 描述符传递

        考虑从一个进程到另一个进程传递打开的描述符时,通常有:

        当前Unix系统提供了用于从一个进程向任意其他进程传递任意打开描述符的方法,两个进程无需亲缘关系。这种技术要求首先在这两个进程之间创建一个Unix域套接字,然后使用 $sendmsg$ 发送一个特殊消息。这个消息会由内核专门处理,将打开的描述符传递给目标进程。在两个进程之间传递描述符涉及的步骤如下:

  1. 创建一个字节流的或数据报的Unix域套接字;
  2. 发送进程通过调用返回描述符的任意Unix函数打开一个描述符,如 $open$ 、$pipe$ 、$mkfifo$ 、$socket$ 和 $accept$ 等,这些描述符可以是任意类型;
  3. 发送进程创建一个 $msghdr$ 结构,其中含有待传递的描述符。POSIX规定描述符作为辅助数据 ( $msg_-control$ 成员 ) 发送,不过较老的实现使用 $msg_-accrights$ 成员。发送进程调用 $sendmsg$ 发送该描述符;
  4. 接收进程调用 $recvmsg$ 接收描述符,接收到的描述符值可以不同于发送的描述符值。另外,在期待接收描述符的 $recvmsg$ 调用中应该避免使用 $MSG_-PEEK$ 标志,否则后果不可预料。
#include "unp.h"

ssize_t read_fd(int fd, void *ptr, size_t bytes, int *recvfd) {
  struct msghdr msg;
  struct iovec iov[1];
  ssize_t n;

#ifdef HAVE_MSGHDR_MSG_CONTROL
  union {
    struct cmsghdr cm;
    char control[CMSG_SPACE(sizeof(int))];
  } control_un;
  struct cmsghdr *cmptr;

  msg.msg_control = control_un.control;
  msg.msg_controllen = sizeof(control_un.control);
#else
  int newfd;
  msg.msg_accrights = (caddr_t) &newfd;
  msg.msg_controllen = sizeof(control_un.control);
#endif

  msg.msg_name = NULL;
  msg.msg_namelen = 0;
  iov[0].iov_base = ptr;
  iov[0].iov_len = nbytes;
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;

  if ((n = recvmsg(fd, &msg, 0)) <= 0)
    return n;

#ifdef HAVE_MSGHDR_MSG_CONTROL
  if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL &&
    cmptr->cmsg_len == CMSG_LEN(sizeof(int))) {
    if (cmptr->cmsg_level != SOL_SOCKET)
      err_quit("control level != SOL_SOCKET");
    if (cmptr->cmsg_type != SCM_RIGHTS)
      err_quit("control type != SCM_RIGHTS");
    *recvfd = *((int *) CMSG_DATA(cmptr));
  } else
    *recvfd = -1;  // 描述符传递失败
#else
  if (msg.msg_accrightslen == sizeof(int))
    *recvfd = newfd;
  else
    *recvfd = -1;  // 描述符传递失败
#endif

  return n;
}

        通过流管道发送和接收描述符时,我们总是发送 $1$ 字节的数据,即便接收进程不对数据做任何处理。要是不这样做,接收进程将难以辨别 $read_-fd$ 的返回为 $0$ 意味着没有数据还是文件已结束。$msg_-control$ 缓冲区必须为 $cmsghdr$ 结构适当地对齐,单纯分配一个字符数组是不够的,这里声明了一个联合。确保对齐的另一个办法是调用 $malloc$ ,不过需要在函数返回前释放所分配的空间。

#include "unp.h"

ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) {
  struct msghdr msg;
  struct iovec iov[1];

#ifdef HAVE_MSGHDR_MSG_CONTROL
  union {
    struct cmsghdr cm;
    char control[CMSG_SPACE(sizeof(int))];
  } control_un;
  struct cmsghdr *cmptr;
  msg.msg_control = control_un.control;
  msg.msg_controllen = sizeof(control_un.control);
  cmptr = CMSG_FIRSTHDR(&msg);
  cmptr->cmsg_len = CMSG_LEN(sizeof(int));
  cmptr->cmsg_level = SOL_SOCKET;
  cmptr->cmsg_type = SCM_RIGHTS;
  *((int *) CMSG_DATA(cmptr)) = sendfd;
#else
  msg.msg_accrights = (caddr_t) &sendfd;
  msg.msg_accrightslen = sizeof(int);
#endif

  msg.msg_name = NULL;
  msg.msg_namelen = 0;
  iov[0].iov_base = ptr;
  iov[0].iov_len = nbytes;
  msg.msg_iov = iov;
  msg.msg_iovlen = 1;

  return(sendmsg(fd, &msg, 0));
}

5. 接收发送者凭证

        可通过Unix域套接字作为辅助数据传递的另一种数据是用户凭证 ( $user$ $credential$ ),作为辅助数据的凭证其具体封装方式和发送方式往往特定于操作系统。凭证传递仍然是一个尚未普及而且无统一规范的特性,但还是一个对Unix域协议的简单却重要的补充。

#include <sys/socket.h>

struct cmsgcred {
  pid_t cmcred_pid;  // 发送端进程的PID
  uid_t cmcred_uid;  // 发送端进程的真实UID
  uid_t cmcred_euid;  // 发送端进程的有效UID
  gid_t cmcred_gid;  // 发送端进程的真实GID
  short cmcred_ngroups;  // 组数量
  gid_t cmcred_groups[CMGROUP_MAX];  // 组
};

        $CMGROUP_-MAX$ 常值通常为 $16$ ,$cmcred_-ngroups$ 总是 $1$ ,而且 $cmcred_-groups$ 数组的第一个元素是有效组ID。当客户和服务器进行通信时,服务器通常需要以一定手段获取客户身份,以便验证客户是否有权限请求相应服务。凭证信息总是可以通过Unix域套接字在两个进程之间传递,然而发送进程发送它们时往往需要做特殊的封装处理,接收进程接收它们时也往往需要做特殊的接受处理。

#include "unp.h"

#define CONTROL_LEN (sizeof(struct cmsghdr) + sizeof(struct cmsgcred))

ssize_t read_cred(int fd, void *ptr, size_t nbytes, struct cmsgcred *cmsgcredptr) {
  struct msghdr msg;
  struct iovec iov[1];
  char control[CONTROL_LEN];
  int n;

  msg.msg_name = NULL;
  msg.msg_namelen = 0;
  iov[0].iov_base = ptr;
  iov[0].iov_len = nbytes;
  msg.msg_iov = iov;
  msg_msg_iovlen = 1;
  msg.msg_control = control;
  msg.msg_controllen = sizeof(control);
  msg.msg_flags = 0;

  if ((n = recvmsg(fd, &msg, 0)) < 0)
    return n;

  cmsgcredptr->cmcread_ngroups = 0;  // indicates no credentials returned
  if (cmsgcredptr && msg.msg_controllen > 0)  {
    struct cmsghdr *cmptr = (struct cmsghdr *) control;
    if (cmptr->cmsg_len < CONTROL_LEN)
      err_quit("control length = %d", cmptr->cmsg_len);
    if (cmptr->cmsg_level != SOL_SOCKET)
      err_quit("control level != SOL_SOCKET");
    if (cmptr->cmsg_type != SCM_CREDS)
      err_quit("control type != SCM_CREDS");
    memcpy(cmsgcredptr, CMSG_DATA(cmptr), sizeof(struct cmsgcred));
  }

  return n;
}

Unix网络编程(8):Unix域协议