UNP 学习笔记——非阻塞式 IO

栏目: IT技术 · 发布时间: 6年前

内容简介:UNP 学习笔记——非阻塞式 IO

为什么要使用非阻塞式 IO?

  • 对于边缘触发通知模式,非阻塞式 IO 可以最大程度的执行 IO 操作
  • 对于水平触发模式,即使文件描述符已经就绪,如果我们在单个 readwrite 调用中写入足够大的数据,调用仍然可能会被阻塞
  • 在非常罕见的情况下,水平触发模式会错误的通知我们文件描述符已经就绪了
  • 如果多个进程或线程处理文件描述符,那么很可能在通知和调用 IO 之间,文件描述符的状态发生了改变

1、readwrite 如何使用非阻塞?

  • 利用 fcntl 函数将 fd 设置为 O_NONBLOCK
  • 利用 select 等 IO 复用方法等待 socket 就绪
  • 调用 readwrite 等非阻塞函数后,注意 EWOULDBLCOK 错误,虽然很少会发生
  • 处理 readwrite 等非阻塞函数返回 0 的情况,一般为 FIN 结束标志
  • 处理返回的数据

我们维护着两个缓冲区:to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据。

其中 toiptr 指针指向从标准输入读入的数据可以存放的下一个字节,tooptr 指向下一个必须写到套接字的字节。有(toiptr-tooptr)个字节需要写到套接字。可从标准输入读入的字节数是(&to[MAXLINE]-toiptr)。一旦 tooptr 移动到 toiptr,这两个指针就一起恢复到缓冲区开始处。

UNP 学习笔记——非阻塞式 IO

UNP 学习笔记——非阻塞式 IO

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

  // 使用fcntl把所有3个描述符都设置为非阻塞,包括连接到服务器的套接字、标准输入和标准输出
  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( STDIN_FILENO, F_SETFL, val | O_NONBLOCK );

  // 初始化指向两个缓冲区的指针,并把最大的描述符号加1,以用做select的第一个参数
  toiptr = tooptr = to; // initialize buffer pointers
  friptr = froptr = fr;
  stdineof = 0;

  maxfdp1 = max( max( STDIN_FILENO, STDOUT_FILENO ), sockfd ) + 1;

  //这个版本的主循环也是一个select调用后跟对所关注各个条件所进行的单独测试
  for( ; ; )
  {
    // 两个描述符集都先清零再打开最多2位。如果在标准输入上尚未读到EOF,而且在to缓冲区中有至少一个字节
    // 的可用空间,那就打开描述符集中对应标准输入的位。如果在fr缓冲区中至少一个字节的可用空间,那就打
    // 开描述符集中对应套接字的位。最后,如果在fr缓冲区中有要写到标准输出的数据,那就打开写描述符集中
    // 对应标准输出的位
    FD_ZERO( &rset );
    FD_ZERO( &wset );
    if( stdineof == 0 && toiptr < &to[ MAX_MESG_SIZE ] )
      FD_SET( STDIN_FILENO, &rset ); // read from stdin
    if( fripter < &fr[ MAX_MESG_SIZE ] )
      FD_SET( sockfd, &rset ); // read from socket
    if( tooptr != toiptr )
      FD_SET( sockfd, &wset ); // data to write to sockfd
    if( froptr != friptr )
      FD_SET( STDOUT_FILENO, &wset ); // data to write to stdout

    //  调用select,等待4个可能条件中任何一个变为真。我们没有为本select设置超时。
    select( maxfdp1, &rset, &wset, NULL, NULL );

    // 如果标准输入可读,那就调用read。指定的第三个参数是to缓冲区中的可用空间量
    if( FD_ISSET( STDIN_FILENO, &rset ) )
    {
      if( ( n = read( STDIN_FILENO, toiptr, &to[ MAX_MESG_SIZE ] - toiptr ) ) < 0 )
      {
        // 如果发生一个EWOULDBLOCK错误,我们就忽略它。通常情况下这种条件“不应该发生”,因为这种条件意味着,
        // select告知我们相应描述符可读,然而read该描述符却返回EWOULDBLOCK错误,不过我们无论如何还是处
        // 理这种条件。
        if( errno != EWOULDBLOCK )
        { 
          printf( " read error on stdin\n " );
          exit( 1 );
        }
      }
      // 如果read返回0,那么标准输入处理就此结束,我们还设置stdineof标志。如果在to缓冲区中不再有数据要发送
      // (即tooptr等于toiptr),那就调用shutdown发送FIN到服务器。如果在to缓冲区仍有数据要发送,FIN的发送
      // 就得推迟到缓冲区中数据已写到套接字之后。
      else if( n == 0 )
      {
        fprintf( stderr, " %s: EOF on stdin\n ", gf_time() );
        stdineof = 1; // all done with stdin
        if( tooptr == toiptr )
          shutdown( sockfd, SHUT_WR ); // send FIN
      }
      // 当read返回数据时,我们相应地增加toiptr。我们还打开写描述符集中与套接字对应的位,使得以后在本循环
      // 对应该位的测试为真,从而导致调用write写到套接字。
      else
      {
        fprintf( srderr, " %s: read %d bytes from stdin\n ", gf_time(), n );
        toiptr += n; // just read
        FD_SET( sockfd, &wset ); // try and write to sockfd below
      }
    }

    // 这段代码类似刚才讲解的处理标准输入可读条件的if语句。如果read返回EWOULDBLOCK错误,那么不做任何处理。
    // 如果遇到来自服务器的EOF,那么若我们已经在标准输入上遇到EOF则没有问题,否则来自服务器的EOF并非预期。
    // 如果read返回一些数据,我们就相应地增加friptr,并把写描述符集中与标准输出对应的位打开,以尝试在本函
    // 数第三部分中将这些数据写到标准输出
    if( FD_ISSET( sockfd, &rset ) )
    {
      if( ( n = read( sockfd, friptr, &fr[ MAX_MESG_SIZE ] - friptr ) ) < 0 )
      {
        if( errno != EWOULDBLOCK )
        { 
          printf( " read error on socket\n " );
          exit( 1 );
        }
      }
      else if( n == 0 )
      {
        fprintf( stderr, " %s: EOF on stdin\n ", gf_time() );
        if( stdineof )
          return 0; // normal termination
        else
        {
          printf( " str_cli: server terminated prematurely " );
          exit( 1 );
        }
      }
      else
      {
        fprintf( srderr, " %s: read %d bytes from socket\n ", gf_time(), n );
        toiptr += n; // just read
        FD_SET( sockfd, &wset ); // try and write to below
      }
    }

    // 如果标准输出可写而且要写的字节数大于0,那就调用write。如果返回EWOULDBLCOK错误。那么不做任何处理。
    // 注意这种条件完全可能发生,因为本函数第二部分末尾的代码在不清楚write是否会成功的前提下就打开了写描述
    // 符集中与标准输出对应的位
    if( FD_ISSET( STDOUT_FILENO, &wset ) && ( ( n = friptr - froptr ) > 0 ) )
    {
      if( ( nwritten = write( STDOUT_FILENO, froptr, n ) ) < 0 )
      {
        if( errno != EWOULDBLOCK )
        {
          printf( " write error to stdout\n " );
          exit( 1 );
        }
      }
      // 如果write成功,froptr就增加写处的字节数。如果输出指针(froptr)追上输入指针(friptr),这两个指针
      // 就同时恢复为指向缓冲区开始
      else
      {
        fprintf( stderr, " %s: wrote %d bytes to stdout\n ", gf_time(), nwritten );
        froptr += nwritten; // just written
        if( froptr == friptr )
          froptr = friptr = fr; // back to beginning of buffer
      }
    }

    // 这段代码类似刚才讲解的处理标准输出可写条件的if语句。唯一的差别是当输出指针追上输入指针时,不仅这两
    // 个指针同时恢复到缓冲区开始处,而且如果已经在标准输入上遇到EOF就要发送FIN到服务器
    if( FD_ISSET( sockfd, &wset ) && ( ( n = toiptr - tooptr ) > 0 ) )
    {
      if( ( nwritten = write( sockfd, tooptr, n ) ) < 0 )
      {
        if( errno != EWOULDBLOCK )
        {
          printf( " write error to socket\n " );
          exit( 1 );
        }
      }
      else
      {
        fprintf( stderr, " %s: wrote %d bytes to socket\n ", gf_time(), nwritten );
        tooptr += nwritten; // just written
        if( tooptr == toiptr )
          toiptr = tooptr = to; // back to beginning of buffer
          if( stdineof )
            shutdown( sockfd, SHUT_WR ); // send FIN
      }
    }
  }

}

2、非阻塞 connect 的优点有哪些?

  • 我们可以在三路握手的同时做一些其它的处理. connect 操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网.在这段时间内我们可能有一些其他的处理想要执行;
  • 可以用这种技术同时建立多个连接.在Web浏览器中很普遍;
  • 由于我们使用select来等待连接的完成,因此我们可以给 select 设置一个时间限制,从而缩短 connect 的超时时间.在大多数实现中,connect 的超时时间在75秒到几分钟之间.有时候应用程序想要一个更短的超时时间,使用非阻塞 connect 就是一种方法;

3、非阻塞 connect 需要处理的问题有哪些?

  • 即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况;
  • 源自 Berkeley 的实现(和 Posix.1g )有两条与 select 和非阻塞 IO 相关的规则:
    • 当连接建立成功时,套接口描述符变成可写;
    • 当连接出错时,套接口描述符变成既可读又可写;

因此,仅从socket可读或可写无法判断socket连接的状态。

#include    "unp.h"

int
connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
    int             flags, n, error;
    socklen_t       len;
    fd_set          rset, wset;
    struct timeval  tval;

    flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    error = 0;
    if ( (n = connect(sockfd, saptr, salen)) < 0)//连接不能立即建立,已经发起的三次握手继续进行,同时返回-1,errno设置为EINPROGRESS
        if (errno != EINPROGRESS)
            return(-1);

    /* Do whatever we want while the connect is taking place. */

    if (n == 0)//返回成功,连接能够立即建立
        goto done;  /* connect completed immediately */

    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    wset = rset;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;

    if ( (n = select(sockfd+1, &rset, &wset, NULL,
                     nsec ? &tval : NULL)) == 0) {
        close(sockfd);      /* timeout */
        errno = ETIMEDOUT;
        return(-1);
    }

    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
        len = sizeof(error);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)//error返回0,表示在等待的时间内连接建立;如果非零,表示连接建立遇到了套接字错误而导致的套接字可读可写
            return(-1);         /* Solaris pending error */
    } else
        err_quit("select error: sockfd not set");

done:
    fcntl(sockfd, F_SETFL, flags);  /* restore file status flags */

    if (error) {
        close(sockfd);      /* just in case */
        errno = error;
        return(-1);
    }
    return(0);
}

非阻塞 accecpt 的优点?

struct lingerl_onoff 标志设为 1,l_linger 设为 0。这个时候,如果关闭 TCP 连接时,会先在 socket 上发送一个RST 包。这个时候会出现下面的问题:

  • select向服务器返回监听socket可读,但是服务器要在一段时间之后才能调用accept;
  • 在服务器从select返回和调用accept之前,收到从客户发送过来的RST;
  • 这个已经完成的连接被从队列中删除,我们假设没有其它已完成的连接存在;
  • 服务器调用accept,但是由于没有其它已完成的连接存在,因而服务器被阻塞了;

解决这个问题的办法是:

  • 如果使用select来获知何时有链接已就绪可以accept时,总是把监听socket设置为非阻塞模式,并且
  • 在后面的accept调用中忽略以下错误:EWOULDBLOCK(源自Berkeley的实现在客户放弃连接时出现的错误)、ECONNABORTED(Posix.1g的实现在客户放弃连接时出现的错误)、EPROTO(SVR4的实现在客户放弃连接时出现的错误)和EINTR(如果信号被捕获).

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Looking For a Challenge

Looking For a Challenge

the University of Warsaw / the University of Warsaw / 2012-11 / 0

LOOKING FOR PROGRAMMING CHALLENGES? Then this book is for you! Whether it's the feeling of great satisfaction from solving a complex problem set, or the frustration of being unable to solve a task,......一起来看看 《Looking For a Challenge》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具