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缓冲区的组织和指向该缓冲区中的指针。

rtthread recvfrom设置非阻塞_数据

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

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

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

rtthread recvfrom设置非阻塞_数据_02

图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的时间戳输出一致的格式。