文档章节

网络编程学习——非阻塞式I/O

thanatos_y
 thanatos_y
发布于 2016/04/20 18:19
字数 2343
阅读 23
收藏 0

1 概述

  套接字的默认状态是阻塞的,这意味着当发出一个不能立即完成的套接字调用时,其进程将会投入睡眠,等待相应操作完成,可能阻塞的套接字调用可分为以下四类。

  1. 输入操作,包括read、readv、recv、recvfrom和recvmsg共五个函数。对于非阻塞的套接字,如果输入操作不能被满足(对于TCP套接字即至少有一个字节的数据可读,对于UDP套接字即有一个完整的数据报可读),相应调用将立即返回一个EWOULDBLOCK错误。

  2. 输出操作,包括write、writev、send、sendto和sendmsg共5个函数。对于一个TCP套接字,内核将从应用进程的缓冲区到该套接字的发送缓冲区复制数据。对于阻塞的套接字,如果其发送缓冲区没有空间,进程将被投入睡眠,直到有空间为止。

    对于非阻塞的TCP套接字,如果其发送缓冲区中根本没有空间,输出函数调用将立即返回一个EWOULDBLOCK错误。如果其发送缓冲区中没有空间,返回值将是内核能够复制到该缓冲区中的字节数。这个字节数也称为不足计数(short count)。

    UDP套接字不存在真正的发送缓冲区,内核只是复制应用进程数据并把它沿协议栈向下传送,渐次冠以UDP首部和IP首部。因此对一个阻塞的UDP套接字(默认设置),输出函数调用将不会因与TCP套接字一样的原因而阻塞,不过可能因其他原因而阻塞。

  3. 接受外来连接,即accept函数。如果对一个阻塞的套接字调用accept函数,并且尚无新的连接到达,调用进程将被投入睡眠。

    如果对一个非阻塞的套接字调用accept函数,并且尚无新的连接到达,accept调用将立即返回一个EWOULDBLOCK错误。

  4. 发起外出连接,即用于TCP的connect函数。(connect函数同样可用于UDP,不过它不能使一个“真正”的连接建立起来,它只是使内核保存对端的IP地址和端口号)。

    对于一个非阻塞的TCP套接字调用connect,并且连接不能立即建立,那么连接的建立能照样发起(譬如送出TCP三路握手的第一个分组),不过会返回一个EINPROGRESS错误。

2 非阻塞读和写:str_cli函数(修订版)

  我们维护着两个缓冲区:to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据。图1-1展示了to缓冲区的组织和指向该缓冲区中的指针。

图1-1 容纳从标准输入到套接字的数据的缓冲区

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

  图1-2展示了fr缓冲区相应的组织。

图1-2 容纳从套接字到标准输出的数据的缓冲区

  下面给出了本函数的一部分。

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
      }
    }
  }
  
}

  下面给出本函数调用的gf_time函数。

char * gf_time( void )
{
  struct timeval tv;
  static char str[ 30 ];
  char *ptr;
  
  if( gettimeofday( &tv, NULL ) < 0 )
  {
    printf( " gettimeofday error\n " );
    exit( 1 );
  }  
  
  ptr = ctime( &tv.tv_sec );
  strcpy( str, &ptr[ 11 ] );
  // Fri Sep 13 00:00:00 1986\n\0
  // 0123456789012345678901234 5
  snprintf( str + 8, sizeof( str ) - 8, " .%06ld ", tv.tv_usec );
  
  return( str );
}

  gf_time函数返回一个含有当前时间的字符串,包括微秒,格式如下。

  12:34:56.123456

  这里特意采用与tcpdump的时间戳输出一致的格式。

 

 

 

 

 

 

© 著作权归作者所有

共有 人打赏支持
thanatos_y
粉丝 7
博文 112
码字总数 315059
作品 0
成都
程序员
私信 提问
我的网络开发之旅——socket编程

上一篇文章《TCP/IP协议分析》讲述了自己是如何和网络领域的开发扯上关系的。正如从招聘网站上抽出的几个关键词“TCP/IP, Socket, 多线程”可见,协议分析并不是网络开发的主流,通常我们所说...

yaocoder
2014/09/21
0
0
0-Linux 网络编程修炼指南——内功心法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/q1007729991/article/details/69091877 学习交流群: Linux 学习交流群 610441700 说明:本系列文章并不能取代...

--Allen--
2017/04/04
0
0
服务器模型——从单线程阻塞到多线程非阻塞(上)

前言的前言 服务器模型涉及到线程模式和IO模式,搞清楚这些就能针对各种场景有的放矢。该系列分成三部分: 单线程/多线程阻塞I/O模型 单线程非阻塞I/O模型 多线程非阻塞I/O模型,Reactor及其...

2017/12/21
0
0
同步异步 ——Nginx与Apache

同步异步,阻塞非阻塞 和nginx的IO模型 同步与异步 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)。所谓同步,就是在发出一个调用时,在没有得到...

wujunqi1996
06/28
0
0
I/O复用——几种I/O模型对比

在这篇文章中: I/O复用——几种I/O模型对比 I/O复用——几种I/O模型对比 之前在服务器进程终止中讨论的情形,TCP客户端同时要处理两个输入,一是标准输入,二是TCP套接口。而此时若是服务器...

jackieluo
12/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

KaliLinux常用服务配置教程DHCP服务工作流程

KaliLinux常用服务配置教程DHCP服务工作流程 DHCP服务工作流程如图1.1所示。 具体的工作流程如下所示: (1)DHCP客户端以广播的方式发出DHCP Discover报文。 (2)所有的DHCP服务器(DHCP ...

大学霸
刚刚
0
0
Spring Junit单元测试配置

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.2.RELEASE</version></dependency><dependency> <group......

TonyTaotao
5分钟前
0
0
线程安全与非线程安全-个人理解

类的某个方法是线程安全的, 说明这个方法在并发执行中,从开始执行到执行完毕,都是同步的, 比如:之前做的并发数据导出,并发的查询数据库, 但是在写入excel的时候,需要做一个同步,因为...

Java搬砖工程师
7分钟前
0
0
如何提升JavaScript的任务效率?学会后教给你同事

本文由云+社区发表 一、概述 JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增...

腾讯云加社区
7分钟前
0
0
Hadoop运行在Kubernetes平台实践

Hadoop与Kubernetes就好像江湖里的两大绝世高手,一个是成名已久的长者,至今仍然名声远扬,一个则是初出茅庐的青涩少年,骨骼惊奇,不走寻常路,一出手便惊诧了整个武林。Hadoop与Kubernete...

微笑向暖wx
8分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部