文档章节

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

thanatos_y
 thanatos_y
发布于 2016/04/20 18:19
字数 2343
阅读 20
收藏 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 网络编程学习笔记导航

学习交流群: Linux 学习交流群 610441700 说明:本系列文章并不能取代 《UNP》这本旷世之作,文章中难免有错误与不足之处,希望读者们遇到有疑问的地方可以加群互相交流,共同进步。写这一系...

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

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

2017/12/21
0
0
解读I/O多路复用技术

前言 当我们要编写一个echo服务器程序的时候,需要对用户从标准输入键入的交互命令做出响应。在这种情况下,服务器必须响应两个相互独立的I/O事件:1)网络客户端发起网络连接请求,2)用户在...

新栋BOOK
2017/11/19
0
0
Unix 网络 IO 模型: 同步异步, 傻傻分不清楚?

出处 阻塞 IO, 非阻塞 IO, 同步 IO, 异步 IO 这些术语相信有不少朋友都也不同程度的困惑吧? 我原来也是, 什么同步非阻塞 IO, 异步非阻塞 IO 的, 搞的头都大了. 后来仔细读了一遍 《UNIX 网络...

永顺
2017/11/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

创建第一个react项目

sudo npm i -g create-react-app@1.5.2 create-react-app react-app cd react-apprm -rf package-lock.jsonrm -rf node_modules #主要是为了避免报错npm installnpm start......

lilugirl
今天
3
0
在浏览器中进行深度学习:TensorFlow.js (八)生成对抗网络 (GAN)

Generative Adversarial Network 是深度学习中非常有趣的一种方法。GAN最早源自Ian Goodfellow的这篇论文。LeCun对GAN给出了极高的评价: “There are many interesting recent development...

naughty
今天
0
0
搬瓦工镜像站bwh1.net被DNS污染,国内打不开搬瓦工官网

今天下午(2018年10月17日),继搬瓦工主域名bandwagonhost.com被污染后,这个国内的镜像地址bwh1.net也被墙了。那么目前应该怎么访问搬瓦工官网呢? 消息来源:搬瓦工优惠网->搬瓦工镜像站b...

flyzy2005
今天
7
0
SpringBoot自动配置

本篇介绍下,如何通过springboot的自动配置,将公司项目内的依赖jar,不需要扫描路径,依赖jar的情况下,就能将jar内配置了@configuration注解的类,创建到IOC里面 介绍下开发环境 JDK版本1.8 spr...

贺小五
今天
5
0
命令行新建Maven多项目

参考地址 # DgroupId 可以理解为包名# DartifactId 可以理解为项目名mvn archetype:generate -DgroupId=cn.modfun -DartifactId=scaffold -DarchetypeArtifactId=maven-archetype-quickst......

阿白
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部