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 删除。

发表于

我来说两句

4 条评论
登录 后参与评论

相关文章

来自专栏Java成长之路

解决session共享问题方式调研

为了提高服务器性能,最近公司项目采用了分布式服务集群的部署方式。所谓集群,就是让一组计算机服务器协同工作,解决大并发,大数据量瓶颈问题。项目使用nginx做负载...

671
来自专栏smy

document.ready 与 window.onload的区别

document的ready事件通常会比window的onload事件先发生,为什么呢? 因为document的ready是在浏览器加载解析并构建完doc文档模...

41713
来自专栏Java后端生活

JDBC(九)数据库连接池

①、普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码(...

494
来自专栏冷冷

【springboot】 spring session 分布式会话共享

前言 如上图,是一个非常传统的服务端拓扑结构,一个web请求,经过负载均衡的转发,到不同的服务器处理。那么来自同一用户的请求将有可能被负载分发到不同的实例中去,...

2159
来自专栏Web 开发

PushPlugin-为iOS的Hybird App提供APNS服务

APNS是iOS生态下面的推送机制。其原理是APP启动的时候,向苹果注册,并获得一个唯一token,然后不论app是否继续在运行,都可以通过调用苹果的APNS服...

880
来自专栏北京马哥教育

Nginx 用得好,这个知识点最重要!

1636
来自专栏idealclover的填坑日常

Gradle Failed to open zip file. 解决方法

这两天在弄安卓相关,也就免不了与android-studio打交道. 在编译项目的过程中遇到了以下错误:

702
来自专栏散尽浮华

针对负载均衡集群中的session解决方案的总结

在日常运维工作中,当给Web站点使用负载均衡之后,必须面临的一个重要问题就是Session的处理办法,无论是PHP、Python、Ruby还是Java语言环境,...

20311
来自专栏Jerry的SAP技术分享

Chrome开发者工具关于网络请求的一个隐藏技能

这个隐藏技能的背景是,最近出于学习目的,我写了一个百度贴吧的网络爬虫,专门爬取一些指定主题的贴吧帖子。

661
来自专栏Laoqi's Linux运维专列

负载均衡集群中的session解决方案

1904

扫码关注云+社区