前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unix域协议学习小结

Unix域协议学习小结

原创
作者头像
chain
发布2018-06-12 23:29:47
2K4
发布2018-06-12 23:29:47
举报

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域字节流套接字示例

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

代码语言:txt
复制
#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);
  }
}

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

代码语言:txt
复制
#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域数据报套接字示例

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

代码语言:txt
复制
#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));
}

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

代码语言:txt
复制
#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函数创建两个单向管道,示例程序如下所示:

代码语言:txt
复制
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域套接字,函数描述如下所示:

代码语言:txt
复制
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是共享的,因此读的进程需要关闭写描述符,写的进程需要关闭读描述符

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

代码语言:txt
复制
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;
}

执行结果:

代码语言:txt
复制
Read result: SOCKET PAIR TEST.

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

代码语言:txt
复制
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;
}

执行结果:

代码语言:txt
复制
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

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Unix域协议
    • 概述
      • 使用方式
        • Unix域字节流套接字示例
          • Unix域数据报套接字示例
            • socketpair函数
            • 参考
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档