非阻塞式 IO

非阻塞式 IO

套接字默认是阻塞的,分为以下4类:

  1. 输入操作:read、readv、recv、recvfrom、fecvmsg,对于面向流的TCP可使用自定义的readn函数或MSG_WAITALL标志指定等到某个固定数目的数据可读为止,没有数据可读时非阻塞IO立即返回一个EWOULDBLOCK错误
  2. 输出操作:write、writev、send、sendto、sendmsg,将应用进程缓冲区数据复制到内核缓冲区,对于TCP没有足够空间写数据时非阻塞输出函数立即返回一个 EWOULDBLOCK,UDP没有真正的内核发送缓冲区但可能因为其他原因阻塞
  3. 接收外来连接 accept,无连接时立即返回 EWOULDBLOCK
  4. 发起外出连接,即TCP的 connect 函数。调用非阻塞 connect 后在三次握手完成前返回一个 EINPROGRESS 错误,当客户端与服务端位于同一机器时可能会直接返回成功

System V 返回 EAGAIN 错误;Berkeley 返回 EWOULDBLOCK 错误。POSIX 规定这两个都可以,多数提供将二者定义为相同的值

UNP中用 "非阻塞IO+select" 处理客户端socket
#include "unp.h"

void str_cli(FILE *fp, int sockfd) {
  int maxfdp1, val, stdineof;
  ssize_t n, nwritten;
  fd_set rset, wset;
  char to[MAXLINE], fr[MAXLINE];
  char *toiptr, *tooptr, *friptr, *froptr;

  // 将套接字、输入输出IO设置为非阻塞
  val = Fcntl(sockfd, F_GETFL, 0);
  Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);
  val = Fcntl(STDIN_FILENO, F_GETFL, 0);
  Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);
  val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
  Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

  // 初始化缓冲区
  toiptr = tooptr = to;
  friptr = froptr = fr;
  stdineof = 0;

  maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
  for (;;) {
    FD_ZERO(&rset);
    FD_ZERO(&wset);
    // 输出缓冲区还有空间用于读取标准输入,且输入数据没读完
    if (stdineof == 0 && toiptr < &to[MAXLINE]) FD_SET(STDIN_FILENO, &rset);
    // 输入缓冲区还有空间用于读取套接字
    if (friptr < &fr[MAXLINE]) FD_SET(sockfd, &rset);
    // 有数据要发给服务器
    if (tooptr != toiptr) FD_SET(sockfd, &wset);
    // 有数据要输出到标准输出
    if (froptr != friptr) FD_SET(STDOUT_FILENO, &wset);

    Select(maxfdp1, &rset, &wset, NULL, NULL);

    // 从 stdin 读取
    if (FD_ISSET(STDIN_FILENO, &rset)) {
      if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0) {
        if (errno != EWOULDBLOCK) err_sys("read error on stdin");
      } else if (n == 0) { // 读取到 EOF
        stdineof = 1;
        if (tooptr == toiptr) Shutdown(sockfd, SHUT_WR);
      } else {
        toiptr += n;
        FD_SET(sockfd, &wset);
      }
    }
    // 从socket读
    if (FD_ISSET(sockfd, &rset)) {
      if ((n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0) {
        if (errno != EWOULDBLOCK) err_sys("read error on socket");
      } else if (n == 0) {
        if (stdineof)
          return;
        else
          err_quit("str_cli: server terminated prematurely");
      } else {
        friptr += n;
        FD_SET(STDOUT_FILENO, &wset);
      }
    }
    // 向 stdout 写
    if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0)) {
      if ((nwritten = write(STDOUT_FILENO, froptr, n)) < 0) {
        if (errno != EWOULDBLOCK) err_sys("write error to stdout");
      } else {
        froptr += nwritten;
        if (froptr == friptr) froptr = friptr = fr;
      }
    }
    // 向 socket 写
    if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0)) {
      if ((nwritten = write(sockfd, tooptr, n)) < 0) {
        if (errno != EWOULDBLOCK) err_sys("write error to socket");
      } else {
        tooptr += nwritten;
        if (tooptr == toiptr) {
          toiptr = tooptr = to;
          if (stdineof) Shutdown(sockfd, SHUT_WR);
        }
      }
    }
  }
}

以上代码效率很高,但是代码太过复杂,可使用fork简化代码

使用 fork 实现双进程分别处理读写
 #include "unp.h"
void str_cli(FILE *fp, int sockfd) {
  pid_t pid;
  char sendline[MAXLINE], recvline[MAXLINE];

  if ((pid = Fork()) == 0) {
    while (Readline(sockfd, recvline, MAXLINE) > 0) Fputs(recvline, stdout);
    kill(getppid(), SIGTERM); // 通知父进程不再有数据
    exit(0);
  }

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

  Shutdown(sockfd, SHUT_WR); // 父进程只负责向socket写,所以此处只能通过 shtdown 关闭写端
  pause(); // 等待子进程发送 SIGTERM 信号
  return;
}

不同版本性能:

非阻塞 connect

非阻塞connect返回 EINPROGRESS 后可使用select检测这个连接成功或失败

  1. 节约时间
  2. 同时建立多个连接
  3. 可给select指定时间限制

注意:

  1. 如果双方在一台主机,调用connect时连接立即建立
  2. 在调用select前可能正确建立连接,套接字处于可读写状态
  3. 连接成功建立时,描述符可写;出错时描述符可读可写

判断select返回套接字是否成功:

  1. 使用 getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0 检查套接字是否存在待处理错误
  2. 调用 getpeername,返回 ENOTCONN 表示失败返回,可通过 getsockopt 获取错误
  3. 以值未0的长度参数调用read,返回失败则connect失败,errno给出失败原因,如果连接成功建立则read返回0
  4. 再调用 connect 一次,如若返回 EISCONN 表示套接字已连接

一个阻塞的connect调用在等待三次握手时被中断,如捕获信号,这时应把它当作非阻塞IO调用select处理

非阻塞 accept

IO复用select 必须搭配非阻塞accept以防止“定时问题”

客户端使用connect建立连接后发送一个 RST 断开连接。
服务器的select调用返回,期间建立连接并加入队列,在接收RST后将连接移出队列。
使用accept接收连接时,因为连接已断开所以阻塞式accept会阻塞在这里,直到下一个客户端连接上来。

 

posted @ 2023-08-06 08:55  某某人8265  阅读(21)  评论(0编辑  收藏  举报