网络编程笔记(三)-I/O复用:select和poll函数

网络编程笔记(三)-I/O复用:select和poll函数

参考《UNIX网络编程》第 6 章,《TCP/IP 网络编程》 第 7、12 章。

I/O 模型

I/O 复用的场合:

  1. 当客户处理多个描述符时(一般是交互式输入和网络套接字),必须使用I/O复用。

  2. 当客户同时处理多个套接字时,这种情况很少出现。

  3. 如果一个 TCP 服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用 I/O 复用。

  4. 如果一个服务器既要处理 TCP,又要处理 UDP,一般就要使用 I/O 复用。

  5. 如果一个服务器要处理多个服务或多个协议,一般就要使用 I/O 复用。

UNIX 下可用的 5 种 I/O 模型:

  • 阻塞式 I/O;
  • 非阻塞式 I/O;
  • I/O 复用(select 和 poll);
  • 信号驱动式 I/O(SIGIO);
  • 异步 I/O(POSIX 的 aio_系列函数)。

输入操作的 2 个阶段:

  1. 等待数据准备好(一般是等待数据从网络中到达,到达时被数据被复制到内核中的缓冲区);
  2. 从内核向进程复制数据(把数据从内核缓冲区复制到应用进程缓冲区)。

image

前四种为同步 I/O,因为真正的 I/O 操作将阻塞进程。

image

select 函数

定义和功能

#include <sys/select.h>  
#include <sys/time.h>  

/* 
参数:
	maxfd:监视对象文件描述符数量(最大文件描述符+1)
	readset:是否存在待读取数据的文件描述符  
	writeset:是否可传输无阻塞数据的文件描述符  
	exceptset:是否发生异常的文件描述符  
	timeout:超时信息 (如果select发生了阻塞,那么就通过设置timeout防止这种情况)	
 */
// 返回值:若有就绪描述符则返回其数目,超时返回 0,失败返回 -1  
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset,
           const struct timeval * timeout)  
  1. 可读:是否存在套接字接收数据。
  2. 可写:无需阻塞传输数据的套接字有哪些。
  3. 异常:哪些套接字发生异常。

select 函数调用过程:

  1. 设置文件描述符,指定监视范围,设置超时。
  2. 调用 select 函数。
  3. 查看调用结果。

fd_set:设置文件描述符

fd_set 数据类型的操作以为单位进行。注册和更改值得操作由下列宏完成。

void FD_ZERO(fd_set *fdset);			// 将fd_set变量得所有位初始化为0
void FD_SET(int fd, fd_set *fdset);		// 注册:将文件描述符fd置1
void FD_CLR(int fd, fd_set *fdset);		// 清除:将文件描述符fd置0
void FD_ISSET(int fd, fd_set *fdset);	// 判断文件描述符fd是否被设置为1

image

maxfd 和中间 3 个描述符集参数

maxfd:指定待测试的描述符个数(待测试的最大描述符 +1,+1 是因为从 0 开始),描述符 0, 1, 2, ... 一直到 maxfd - 1 都将被测试。

如果我们对 readset、writeset、exceptset 中的某一个条件不感兴趣,就可以把它设为空指针。

描述符集参数是值-结果参数。原来为 1 的所有位均变为 0,但发生变化的文件描述符对应位除外——值仍为 1 的文件描述符发生了变化。select 返回后使用 FD_ISSET 宏来测试 fd_set 数据类型。因此,每次重新调用 select 函数时,都得再次把所有描述符集内所关心的位置为 1

struct timeval:超时时间

struct timeval{
    long tv_sec; 	// sec  
    long tv_usec;	// microsec
};

这个参数有三种选择:

  1. 永远等待下去:仅在有一个描述符准备好 I/O 时才返回。为此要将该参数设置为空。
  2. 等待一段固定时间(等待时间不超过这个固定时间)。为此要设置秒数和微秒数。
  3. 根本不等待:检查描述符后立即返回,这称为轮询(polling)。为此要设置秒数和微秒数均为 0。

描述符就绪条件

对于每一列满足任何一行,该列的表头就准备好了。

image

str_cli 的两次修改

初始的 str_cli 如下:

#include "unp.h"

void str_cli(FILE *fp, int sockfd) {
  char sendline[MAXLINE], recvline[MAXLINE];

  while (Fgets(sendline, MAXLINE, fp) != NULL) {
    Writen(sockfd, sendline, strlen(sendline));

    if (Readline(sockfd, recvline, MAXLINE) == 0)
      err_quit("str_cli: server terminated prematurely");

    Fputs(recvline, stdout);
  }
}

第一次修改

初始的 str_cli 问题是:当套接字发生某些事情时,客户可能阻塞与 fgets 调用。新版本改为阻塞于 select 调用——或是等待标准输入可读,或是等待套接字可读

image

  1. 如果对端 TCP 发送数据,那么该套接字变为可读,并且 read 返回一个大于 0 的值(即读入数据的字节数)
  2. 如果对端 TCP 发送一个 FIN(对端进程终止),那么该套接字变为可读,并且read 返回 0(EOF)
  3. 如果对端 TCP 发送一个 RST(对端主机崩溃并重新启动),那么该套接字变为可读,并且 read 返回 -1,errno 含有确切的错误码。
#include "../lib/unp.h"

void str_cli(FILE *fp, int sockfd) {
  int maxfdp1;
  fd_set rset;	// readset: 读描述符集
  char sendline[MAXLINE], recvline[MAXLINE];

  FD_ZERO(&rset);
  for (;;) {
    FD_SET(fileno(fp), &rset);	// 打开标准IO文件指针fp
    FD_SET(sockfd, &rset);		// 打开套接字描述符
    maxfdp1 = max(fileno(fp), sockfd) + 1;
    Select(maxfdp1, &rset, NULL, NULL, NULL);

    if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
      if (Readline(sockfd, recvline, MAXLINE) == 0) // EOF
        err_quit("str_cli: server terminated prematurely");
      Fputs(recvline, stdout);
    }

    if (FD_ISSET(fileno(fp), &rset)) {                  /* input is readable */
      if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* all done */
      Writen(sockfd, sendline, strlen(sendline));
    }
  }
}

这个版本的 str_cli 存在两个问题:

  1. 批量输入时,标准输入的 EOF 并不意味着同时也完成了从套接字的读入(套接字上还有数据在传送)。

image

  1. 混合使用 stdio 和 select 导致错误——select 并不知道 stdio 使用了缓冲区。fgets 返回一行写给服务器,随后 select 被调用以等待新的工作,而不管 stdio 缓冲区还有额外的输入待消费。

shutdown 函数

#include <sys/socket.h>

// 返回值:成功返回0,出错返回-1
int shutdown(int sockfd, int howto)

close 和 shutdown 对比:

  1. close 把描述符的引用计数减 1,仅在该计数变为 0 时才关闭套接字。 shutdown 可以不管引用计数,直接激发 TCP 的正常连接终止序列(四次握手)。
  2. close 终止读和写两个方向的数据传输。shutdown 可以告诉对方自己已经完成了数据发送,即使对端仍有数据要发送。shutdown 可以只关闭其中一个流,而非同时断开两个流。

image

shutdown 的行为依赖于 howto 参数:

  • SHUT_RD:断开输入流,关闭连接的读这一半——套接字中不再有数据可接收,进程不能再对这样的套接字调用任何读函数。可以把第二个参数指定为 SHUT_RD 防止环回复制。
    关闭SO_USELOOPBACK套接字选项也能防止回环
  • SHUT_WR:断开输出流:关闭连接的写这一半(半关闭,half-close)进程不能再对这样的套接字调用任何写函数。
  • SHUT_RDWR:同时断开 I/O 流,连接的读半部和写半部都关闭,等效于调用两次 shutdown。

第二次修改

  • 使用 select:服务器关闭它那一段的连接就会通知我们。
  • 使用 shutdown:废弃了以文本行为中心的代码,改而针对缓冲区操作。
#include "../lib/unp.h"

void str_cli(FILE *fp, int sockfd) {
  int maxfdp1, stdineof; // stdineof是一个初始化为0的新标志,只要该标志为0,每次在主循环我们总是select标准输入的可读性。
  fd_set rset;
  char buf[MAXLINE];
  int n;

  stdineof = 0;
  FD_ZERO(&rset);
  for (;;) {
    if (stdineof == 0) FD_SET(fileno(fp), &rset); // select标准输入的可读性
    FD_SET(sockfd, &rset);
    maxfdp1 = max(fileno(fp), sockfd) + 1;
    Select(maxfdp1, &rset, NULL, NULL, NULL);

    if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
      if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
        if (stdineof == 1)  // 在套接字读到EOF时,如果我们已在标准输入上遇到 EOF,那就是正常的终止
          return; /* normal termination */
        else  // 如果没有在标准输入上遇到 EOF,那就是服务器进程已经过早终止。
          err_quit("str_cli: server terminated prematurely");
      }

      Write(fileno(stdout), buf, n);
    }

    if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
      if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) {
        stdineof = 1; // 标准输入上碰到EOF,将新标志置为1
        Shutdown(sockfd, SHUT_WR); /* send FIN */
        FD_CLR(fileno(fp), &rset);
        continue;
      }

      Writen(sockfd, buf, n); // read和write:对缓冲区而不是文本行进行操作。
    }
  }
}

TCP 回射服务器程序(修订版)

服务器使用 select 的版本,避免了为每个客户创建一个新进程。

《TCP/IP网络编程》P203-P205

#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>

#define BUF_SIZE 100

void error_handling(char *message);

int main(int argc, char *argv[]) {
  int serv_sock, clnt_sock;
  char buf[BUF_SIZE];
  struct sockaddr_in serv_adr;
  struct sockaddr_in clnt_adr;
  socklen_t adr_sz;
  fd_set reads, cpy_reads;
  struct timeval timeout;
  int fd_max, str_len, fd_num, i;

  if (argc != 2) {
    printf("Usage : %s <port>\n", argv[0]);
    exit(1);
  }

  serv_sock = socket(PF_INET, SOCK_STREAM, 0);
  if (serv_sock == -1) {
    error_handling("socket error");
  }

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;
  serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_adr.sin_port = htons(atoi(argv[1]));

  if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
    error_handling("bind() error");
  }

  if (listen(serv_sock, 5) == -1) {
    error_handling("listen() error");
  }

  FD_ZERO(&reads);
  FD_SET(serv_sock, &reads);	// 初始时,监听套接字是描述集中唯一的非0项
  fd_max = serv_sock;	// 这里不加1,下面select时就要加1,因为从0到fd_max-1

  while (1) {
    cpy_reads = reads;
    timeout.tv_sec = 5;
    timeout.tv_usec = 5000;

    if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) break;
    if (fd_num == 0) continue;
    for (i = 0; i < fd_max + 1; i++) {
      if (FD_ISSET(i, &cpy_reads)) {
        if (i == serv_sock) {  // connection request
          adr_sz = sizeof(clnt_adr);
          clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
          FD_SET(clnt_sock, &reads);	// 注册与客户端连接的套接字文件描述符

          if (fd_max < clnt_sock) {
            fd_max = clnt_sock;
          }
          printf("connected client : %d \n", clnt_sock);
        } else {  // read message
          str_len = read(i, buf, BUF_SIZE);

          if (str_len == 0) {  // close
            FD_CLR(i, &reads);
            close(i);
            printf("closed client %d \n", i);
          } else {
            write(i, buf, str_len);  // echo
          }
        }
      }
    }
  }
  close(serv_sock);

  return 0;
}

void error_handling(char *message) {
  fputs(message, stderr);
  fputs("\n", stderr);
  exit(1);
}

《UNIX 网络编程》P138 - P142

/* include fig01 */
#include "../lib/unp.h"

int main(int argc, char **argv) {
  int i, maxi, maxfd, listenfd, connfd, sockfd;
  int nready, client[FD_SETSIZE];	// client数组,记录客户套接字
  ssize_t n;
  fd_set rset, allset;
  char buf[MAXLINE];
  socklen_t clilen;
  struct sockaddr_in cliaddr, servaddr;

  listenfd = Socket(AF_INET, SOCK_STREAM, 0);

  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(SERV_PORT);

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

  Listen(listenfd, LISTENQ);

  maxfd = listenfd; /* initialize */
  maxi = -1;        /* index into client[] array */
  for (i = 0; i < FD_SETSIZE; i++)
    client[i] = -1; /* -1 indicates available entry */
  FD_ZERO(&allset);
  FD_SET(listenfd, &allset);
  /* end fig01 */

  /* include fig02 */
  for (;;) {
    rset = allset; /* structure assignment */
    nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);

    if (FD_ISSET(listenfd, &rset)) { /* new client connection */
      clilen = sizeof(cliaddr);
      connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);

      for (i = 0; i < FD_SETSIZE; i++)
        if (client[i] < 0) {
          client[i] = connfd; /* save descriptor */
          break;
        }
      if (i == FD_SETSIZE) err_quit("too many clients");

      FD_SET(connfd, &allset);            /* add new descriptor to set */
      if (connfd > maxfd) maxfd = connfd; /* for select */
      if (i > maxi) maxi = i;             /* max index in client[] array */

      if (--nready <= 0) continue; /* no more readable descriptors */
    }

    for (i = 0; i <= maxi; i++) { /* check all clients for data */
      if ((sockfd = client[i]) < 0) continue;
      if (FD_ISSET(sockfd, &rset)) {
        if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
          /*connection closed by client */
          Close(sockfd);
          FD_CLR(sockfd, &allset);
          client[i] = -1;
        } else
          Writen(sockfd, buf, n);

        if (--nready <= 0) break; /* no more readable descriptors */
      }
    }
  }
}
/* end fig02 */

pselect 函数

#include <sys/select.h>
#include<signal.h>
#include<time.h>

// 返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
int pselect(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *execptset, 
            const struct timespec *timeout, const sigset_t *sigmask);
  1. pselect 使用 timespec 结构,而不使用 timeval 结构.

    struct timespec {
    	time_t tv_sec; 	// seconds
    	long tv_nsec; 	// nanoseconds,指定纳秒而不是微秒
    };
    
  2. pselect 函数增加了第六个参数——一个指向信号掩码的指针。该参数允许先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置全局变量,然后调用 pselect,告诉它重新设置信号掩码。

poll 函数

poll 提供的功能与 select 类似,不过在处理流设备时,它能提供额外信息。

#include <poll.h>

// 返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
/* 
参数:
	fd: 第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
	nfds: 指定结构结构数组中的元素个数。
	timeout: 指定poll函数在返回前等待多长时间。
*/
int poll(struct pollfd *fd, nfds_t nfds, int timeout);

要测试的条件由 events 指定,poll 函数在相应的 revents 成员中返回该描述符的状态。

struct pollfd {
  int fd;          // 被监视的文件描述符
  short events;    // 请求的事件
  short revents;   // 实际发生的事件
};

events 和 revents 的一些常值:第一部分处理输入,第二部分处理输出,第三部分处理错误(只能在 revents 中设定)。poll 识别三类数据:普通(normal),优先级带(priority band),高优先级(high priority)。

image

timeout 值:

  • INFTIM:永远等待;
  • 0:立即返回,不阻塞进程;
  • >0:等待指定数目的毫秒数。

使用 poll 函数改写 TCP 回射服务器程序:《UNIX 网络编程》P146-P148

总结

  1. 5 种 I/O 模型,这里只讨论 I/O 复用模型。
  2. I/O 复用最常用的函数是 select。批量输入时,即使用户输入结束,仍然有可能有数据残留在服务器和客户间的管道上(2个方向),需要使用 shutdown 函数以利用其半关闭特性。
  3. 混合使用 stdio 和 read/write 有危险,需要针对缓冲区而不是文本行进行操作。
  4. poll 函数功能类似 select,但它能为流设备提供额外信息

参考资料

https://wuhlan3.gitee.io/wuhlan3/2021/08/04/UNIX网络编程(六)

IO多路复用之select总结

IO多路复用之poll总结

posted @ 2021-10-06 19:25  CoolGin  阅读(196)  评论(0编辑  收藏  举报