Unix域协议学习小结

Unix域协议

概述

Unix域协议不是一个真正意义上的协议族,只是一个利用socket api在单个主机上进行进程间通信的方法。它不需要走传统网络协议栈,也就不需要计算校验和、维护序列号以及应答等操作。

Unix域提供两种套接字:字节流套接字(类似TCP)以及数据报套接字(类似UDP)。

根据《Unix网络编程卷1》,选择Unix域套接字有以下三点理由:

  • 尽管使用的API类似于网络套接字,但是所有的通信几乎都是发生在操作系统内核层面,往往比在同一个主机上使用TCP通信快一倍
  • Unix域套接字可以在同一主机的不同进程间传递描述符
  • Unix域套接字可以把客户的凭证(用户ID以及组ID)提供给服务器,从而能够提供额外的安全检查措施

使用方式

Unix域套接字对比网络套接字,在适用方式上主要有以下几点不同:

1、地址

Unix域套接字使用sockaddr_un表示。网络套接字地址则是IP+Port,Unix域套接字地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind调用创建。

2、客户端显示调用bind

客户端使用Unix域套接字一般都需要显示调用bind函数,而不像网络socket一样依赖系统自动分配的地址。套接字bind的文件名可以包含客户端的pid,这样服务器就可以区分不同的客户端。

Unix域字节流套接字示例

服务端示例程序如下所示:

#define UNIXSTR_PATH  "/tmp/srv_sock"
#define LISTENQ 5
#define ERR_EXIT(msg)       \
  do {                      \
    perror(msg);            \
    exit(EXIT_FAILURE);     \
  } while (0)

void srv_echo(int conn) {
  char recvbuf[1024];
  int ret;
  while (1) {

    memset(recvbuf, 0, sizeof(recvbuf));
    ret = read(conn, recvbuf, sizeof(recvbuf));
    if (ret == -1) {
      if (ret == EINTR)
        continue;

      ERR_EXIT("read error");
    }

    else if (ret == 0) {
      printf("client close\n");
      break;
    }

    fputs(recvbuf, stdout);
    write(conn, recvbuf, strlen(recvbuf));
  }
  close (conn);
}

int main(int argc, char *argv[]) {
  int listenfd;
  if ((listenfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
    ERR_EXIT("server create socket error");
  }

  unlink(UNIXSTR_PATH);
  struct sockaddr_un srv_addr;
  memset(&srv_addr, 0, sizeof(srv_addr));
  srv_addr.sun_family = AF_UNIX;
  strcpy(srv_addr.sun_path, UNIXSTR_PATH);

  if(bind(listenfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
    ERR_EXIT("server bind error");
  }

  if(listen(listenfd, LISTENQ) < 0) {
    ERR_EXIT("server listen error");
  }

  int conn;
  pid_t child_pid;
  struct sockaddr_un cli_addr;
  socklen_t cli_len;
  while(1) {
    cli_len = sizeof(cli_addr);
    if((conn = accept(listenfd, (struct sockaddr *)&cli_addr, &cli_len)) < 0) {
      if (errno == EINTR) {
        continue;
      }
      else {
        ERR_EXIT("accept error");
      }
    }
    // 子进程
    if((child_pid = fork()) == 0) {
      close(listenfd);
      srv_echo(conn);
      exit(0);
    }
    // 父进程
    close(conn);
  }
}

客户端示例程序如下所示:

#define UNIXSTR_PATH  "/tmp/srv_sock"
#define ERR_EXIT(msg)       \
  do {                      \
    perror(msg);            \
    exit(EXIT_FAILURE);     \
  } while (0)


void cli_echo(int conn) {
  char sendbuf[1024] = { 0 };
  char recvbuf[1024] = { 0 };
  while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) {

    write(conn, sendbuf, strlen(sendbuf));
    read(conn, recvbuf, sizeof(recvbuf));
    fputs(recvbuf, stdout);
    memset(recvbuf, 0, sizeof(recvbuf));
    memset(sendbuf, 0, sizeof(sendbuf));
  }

  close(conn);
}

int main(int argc, char *argv[]) {
  int sockfd;
  if((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
    ERR_EXIT("client create socket error");
  }

  struct sockaddr_un svr_addr;
  memset(&svr_addr, 0, sizeof(svr_addr));
  svr_addr.sun_family = AF_UNIX;
  strcpy(svr_addr.sun_path, UNIXSTR_PATH);

  if(connect(sockfd, (struct sockaddr *)&svr_addr, sizeof(svr_addr)) < 0) {
    ERR_EXIT("client connect error");
  }

  cli_echo(sockfd);
}

Unix域套接字关联的路径名应该是一个绝对路径名,而不是一个相对路径名。

Connect系统调用中指定的路径名必须是一个当前绑定在某个打开的Unix域套接字上的路径名,而且套接字类型(字节流或数据报)必须要一致,以下三种条件都会出错:

  • 路径名已存在确不是套接字(通过ls -l命令查看对应的文件类型,例如srwxrwxr-x 1 xxx xxx 0 Mar 12 13:23 /tmp/srv_sock,其中s就表示套接字)
  • 套接字存在,但是没有与之关联打开的描述符
  • 套接字存在,有关联打开的描述符,但是connect的套接字类型和路径名关联的套接字类型不一致

如果connect调用发现这个舰艇套接字的队列已满,那么调用就会立即返回一个ECONNREFUSED错误(不同于TCP,如果TCP监听套接字的队列已满,TCP监听端就忽略新到达的SYN,client就会重新发送SYN)

Unix域数据报套接字示例

服务端示例程序如下所示:

#define UNIXSTR_PATH  "/tmp/srv_sock"
#define LISTENQ 5
#define ERR_EXIT(msg)       \
  do {                      \
    perror(msg);            \
    exit(EXIT_FAILURE);     \
  } while (0)

void srv_echo(int conn, struct sockaddr *cli_addr, socklen_t cli_len) {
  char recvbuf[1024];
  int n;
  socklen_t len;
  while (1) {
    memset(recvbuf, 0, sizeof(recvbuf));
    len = cli_len;
    n = recvfrom(conn, recvbuf, sizeof(recvbuf), 0, cli_addr, &len);
    recvbuf[n] = 0;
    fputs(recvbuf, stdout);
    sendto(conn, recvbuf, n, 0, cli_addr, len);
  }
}

int main(int argc, char *argv[]) {
  int sockfd;
  if ((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
    ERR_EXIT("server create socket error");
  }

  unlink(UNIXSTR_PATH);
  struct sockaddr_un srv_addr, cli_addr;
  memset(&srv_addr, 0, sizeof(srv_addr));
  srv_addr.sun_family = AF_UNIX;
  strcpy(srv_addr.sun_path, UNIXSTR_PATH);

  if(bind(sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0) {
    ERR_EXIT("server bind error");
  }

  srv_echo(sockfd, (struct sockaddr *)&cli_addr, sizeof(cli_addr));
}

客户端示例程序如下所示:

#define UNIXSTR_PATH  "/tmp/srv_sock"
#define ERR_EXIT(msg)       \
  do {                      \
    perror(msg);            \
    exit(EXIT_FAILURE);     \
  } while (0)


void cli_echo(int conn, struct sockaddr *svr_addr, socklen_t svr_len) {
  char sendbuf[1024] = {0};
  char recvbuf[1024] = {0};
  int n;
  while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) {
    sendto(conn, sendbuf, strlen(sendbuf), 0, svr_addr, svr_len);
    n = recvfrom(conn, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
    recvbuf[n] = 0;
    fputs(recvbuf, stdout);
    memset(recvbuf, 0, sizeof(recvbuf));
    memset(sendbuf, 0, sizeof(sendbuf));
  }
  close(conn);
}

int main(int argc, char *argv[]) {
  int sockfd;
  if((sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
    ERR_EXIT("client create socket error");
  }

  struct sockaddr_un cli_addr, svr_addr;

  memset(&cli_addr, 0, sizeof(cli_addr));
  cli_addr.sun_family = AF_UNIX;
  strcpy(cli_addr.sun_path, tmpnam(NULL));
  if(bind(sockfd, (struct sockaddr *)&cli_addr, sizeof(cli_addr)) < 0) {
    ERR_EXIT("client bind error");
  }

  memset(&svr_addr, 0, sizeof(svr_addr));
  svr_addr.sun_family = AF_UNIX;
  strcpy(svr_addr.sun_path, UNIXSTR_PATH);

  cli_echo(sockfd, (struct sockaddr *)&svr_addr, sizeof(svr_addr));
}

Unix域数据包协议要求客户端必须显示bind一个路径名到套接字,这样服务器才能够回射应答的路径名。这里使用tmpnam赋值一个唯一的路径名。

socketpair函数

Linux提供了pipe函数用来创建匿名管道进行父子进程通信。但是pipe函数创建的管道是半双工的(要么读、要么写,不能够同时在一个管道中进行读写)。但实际应用中,经常需要同时进行读写。常用的解决方案是使用pipe函数创建两个单向管道,示例程序如下所示:

int pipe_in[2], pipe_out[2];
pid_t pid;

pipe(&pipe_in);	                        // 创建父进程中用于读取数据的管道
pipe(&pipe_out);	                    // 创建父进程中用于写入数据的管道

if ((pid = fork()) == 0) {	            // 子进程
    close(pipe_in[0]);	                // 关闭父进程的读管道的子进程读端
    close(pipe_out[1]);	                // 关闭父进程的写管道的子进程写端
    ...                                 // 使用exec执行命令
} else {	                            // 父进程
    close(pipe_in[1]);	                // 关闭读管道的写端
    close(pipe_out[0]);	                // 关闭写管道的读端
    ...                                 // 向pipe_out[1]中写数据,并从pipe_in[0]中读结果
    close(pipe_out[1]);	                // 关闭写管道
    ...                                 // 读取pipe_in[0]中的剩余数据
    close(pipe_in[0]);	                // 关闭读管道
    ...                                 // 使用wait系列函数等待子进程退出并取得退出代码
}

上述示例代码的可读性以及可维护性比较差,根本原因就是pipe函数返回的一对描述符只能够从从第一个中读,第二个中写。

不过Linux中全双工socketpair函数可实现对两个描述符中的任何一个同时进行读写。该函数仅使用于Unix域套接字,函数描述如下所示:

int socketpair(int domain, int type, int protocol, int sockfd[2]);

其中domain只支持AF_UNIX/AF_LOCAL,protocol参数必须是0,type参数既可以是SOCK_STREAM,也可以是SOCK_DGRAM。该函数创建的两个套接字都是无名socket,在Linux中,完全可以把这一对socket当成pipe返回的描述符一样使用。

使用方式:

  • sockfd0和sockfd1每个套接字都可用于读写。例如可以往sockfd0中写,从sockfd1中读;或是向sockfd1中写,从sockfd0中读。但是如果向一个套接字(sockfd1)中写入,再从该套接字总读取,就会阻塞,只能够在另一个套接字(sockfd0)中读取
  • 读写可以位于同一个进程,也可以位于不同的进程,如父子进程。如果是父子进程,由于sockfd是共享的,因此读的进程需要关闭写描述符,写的进程需要关闭读描述符

读写操作位于同一个进程示例代码如下所示:

const char* str = "SOCKET PAIR TEST";

int main(int argc, char* argv[]) {
  char buf[128] = { 0 };
  int socket_pair[2];
  pid_t pid;

  if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {
    printf("Error, socketpair create failed, errno(%d): %s\n", errno,
           strerror(errno));
    return EXIT_FAILURE;
  }

  int size = write(socket_pair[0], str, strlen(str));
  // 读取成功
  read(socket_pair[1], buf, size);
  // 阻塞
  // read(socket_pair[0], buf, size);
  printf("Read result: %s\n", buf);
  return EXIT_SUCCESS;
}

执行结果:

Read result: SOCKET PAIR TEST.

读写操作位于不同进程示例代码如下所示:

const char* str = "SOCKET PAIR TEST";

int main(int argc, char* argv[]) {
  char buf[128] = { 0 };
  int socket_pair[2];
  pid_t pid;

  if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {
    printf("Error, socketpair create failed, errno(%d): %s\n", errno,
           strerror(errno));
    return EXIT_FAILURE;
  }

  pid = fork();
  if (pid < 0) {
    printf("Error, fork failed, errno(%d): %s\n", errno, strerror(errno));
    return EXIT_FAILURE;
  } else if (pid > 0) {
    // 关闭另外一个套接字
    close(socket_pair[1]);
    int val = 0;
    while (1) {
      sleep(1);
      ++val;
      printf("parent Sending data: %d\n", val);
      write(socket_pair[0], &val, sizeof(val));
      read(socket_pair[0], &val, sizeof(val));
      printf("parent Data received: %d\n", val);

    }

  } else if (pid == 0) {
    // 关闭另外一个套接字
    close(socket_pair[0]);
    int val;
    while (1) {
      read(socket_pair[1], &val, sizeof(val));
      printf("child Data received: %d\n", val);
      ++val;
      write(socket_pair[1], &val, sizeof(val));
      printf("child send received: %d\n", val);
    }
  }
  return EXIT_SUCCESS;
}

执行结果:

parent Sending data: 1
child Data received: 1
child send received: 2
parent Data received: 2
parent Sending data: 3
child Data received: 3
child send received: 4
parent Data received: 4
...

如果需要关闭子进程的输入同时通知子进程数据已经发送完毕,而随后从子进程的输出中读取数据直到遇到EOF,对于之前的pipe创建的单向管道来说不会存在任务问题;但是使用socketpair创建的双向管道时,如果不关闭管道就无法通知对端数据已经发送完毕,但是关闭了管道又无法送终读取结果数据。此时可以使用shutdown,来实现一个半关闭操作,通知对端进程不再发送数据,同时仍可以从该文件描述符中把剩余的数据接收完毕,最后再使用close关闭描述符。

参考

https://www.ibm.com/developerworks/cn/linux/l-pipebid/

http://blog.csdn.net/jnu_simba/article/details/9079359

http://www.itwendao.com/article/detail/170565.html

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏weixuqin 的专栏

win10 + Lubuntu 双系统安装

最近重装了系统,索性直接安装win10 + Lubuntu 双系统,便于在物理机下进行 Linux开发. 这里我选择的 Linux 发行版是 Lubuntu ....

74120
来自专栏散尽浮华

Mesos+Zookeeper+Marathon的Docker管理平台部署记录(1)

随着"互联网+"时代的业务增长、变化速度及大规模计算的需求,廉价的、高可扩展的分布式x86集群已成为标准解决方案,如Google已经在几千万台服务器上部署分布式...

37150
来自专栏张善友的专栏

IIS 7.0探索用于 Windows Vista 的 Web 服务器和更多内容

我经常听到 Microsoft 内部和外部的人将新的 IIS 7.0 Web 服务器称为 Microsoft 在过去几年中所进行的最重要的开发工作之一。考虑到 ...

25990
来自专栏运维技术迷

HP DL380服务器配置iLO2

由于BIOS开机密码忘记,所以对这台HP DL380 G6服务器进行BIOS清除操作,刚好看到iLO的选项,就顺手配置了一下,方便以后的管理。 首先,先说明一下...

649120
来自专栏魏艾斯博客www.vpsss.net

lnmp环境下如何手动备份网站文件和数据库

我们站长做个网站都是挺不容易的,从域名注册,掌握虚拟主机或者 VPS 的基本配置,到安全防护,搭建网站、图片处理、发布文章,SEO 等等,是样样精通,不过这里面...

458150
来自专栏散尽浮华

nginx反向代理tomcat访问时浏览器加载失败,出现 ERR_CONTENT_LENGTH_MISMATCH 问题

问题说明: 测试机上部署了一套业务环境,nginx反向代理tomcat,在访问时长时间处于加载中,十分缓慢! 通过浏览器调试(F12键->Console),发现...

363100
来自专栏云计算

开发者的福利--Cloud Foundry

要确保公司云资产的安全性,首先要应用基于虚拟网络独特性修改的可靠的数据安全实践。

83080
来自专栏linux驱动个人学习

Linux启动流程

启动第一步--加载BIOS  当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它。这是因为BIOS中包...

37550
来自专栏黑白安全

中间件漏洞与防护

中间件漏洞可以说是最容易被web管理员忽视的漏洞,原因很简单,因为这并不是应用程序代码上存在的漏洞,而是属于一种应用部署环境的配置不当或者使用不当造成的 我们...

32730
来自专栏L宝宝聊IT

kvm命令行安装

1K20

扫码关注云+社区

领取腾讯云代金券