文档章节

网络编程学习——I/O复用(二)

thanatos_y
 thanatos_y
发布于 2016/04/13 10:32
字数 3783
阅读 22
收藏 0
点赞 1
评论 0

  客户在0时刻发出请求,我们假设RTT为8个时间单位。其应答在时刻4发出并在时刻7接收到。我们还假设没有服务器处理时间而且请求大小与应答大小相同。

  既然一个分组从管道的一端发出到达管道的另一端存在延迟,而管道是全双工的,就本例而言,我们仅仅使用了管道容量的1/8,这种停等方式对于交互式输入合适的,然而由于我们的客户是从标准输入读并往标准输出写,在Unix的Shell环境下重定向标准输入和标准输出又是轻而易举之事,我们可以很容易地以批量方式运行客户。当我们把标准输入和标准输出重定向到文件来运行新的客户程序时,却发现输出文件总是小于输入文件(而对回射服务器而言,它们理应相等)。

  为了搞清楚到底发生了什么,我们应该意识到在批量方式下,客户能够以网络可以接受的最快速度持续发送请求,服务器以相同的速度处理它们并发回应答。这就导致时刻7时管道充满,如图1-10所示。

图1-10 填充客户与服务器之间的管道,批量方式

  为了搞清楚上面客户处理函数存在的问题,我们假设输入文件只有9行。最后一行在时刻8发出,如图1-10所示。写完这个请求后,我们并不能立即关闭连接,因为管道中还有其他的请求和应答。问题的起因在于我们对标准输入中的EOF的处理:客户端处理函数就此返回到main函数,而main函数随后终止。然而在批量方式下,标准输入中的EOF并不意味着我们同时完成了从套接字的读入:可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。

  我们需要的是一种关闭TCP连接其中一半的方法。也就是说,我们想给服务器发送一个FIN,告诉我们已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。这将由shutdown函数来完成。

  一般来说,为了提升性能而引入缓冲机制增加了网络应用程序的复杂性。混合使用stdio和select被认为是非常容易犯错的,在这样做时必须极其小心。

1.6 shutdown函数

  终止网络连接通常的方法是调用close函数,不过close有两个限制,却可以使用shutdown来避免。

 (1)close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。

 (2)close终止读和写两个方向的数据传送。既然TCP连接是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即使对端任由数据要发送给我们。这就是上面客户端处理函数遇到的批量输入时的情况。图1-11展示了这样的情况下典型的函数调用。

图1-11 调用shutdown关闭一半TCP连接

/* The following constants should be used for the second parameter of
   `shutdown'.  */
enum
{
  SHUT_RD = 0,        /* No more receptions.  */
#define SHUT_RD        SHUT_RD
  SHUT_WR,        /* No more transmissions.  */
#define SHUT_WR        SHUT_WR
  SHUT_RDWR        /* No more receptions or transmissions.  */
#define SHUT_RDWR    SHUT_RDWR
};

/* Shut down all or part of the connection open on socket FD.
   HOW determines what to shut down:
     SHUT_RD   = No more receptions;
     SHUT_WR   = No more transmissions;
     SHUT_RDWR = No more receptions or transmissions.
   Returns 0 on success, -1 for errors.  */
extern int shutdown (int __fd, int __how) __THROW;

  该函数的行为依赖于__how参数的值。

  SHUT_RD  关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数

       据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调

       用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。

  SHUT_WR  关闭连接的写这一半——对于TCP套接字,这称为半关闭(half-close)。当前留在套

       接字发送缓冲区中的数据将被发送掉,后跟TCP正常连接终止序列。我们已经说过,不管套

       接字描述符的引用计数是否等于0,这样的写半部关闭照样执行。进程不能再对这样的套接

       字调用任何写函数。

  SHUT_RDWR 连接的读半部和写半部都关闭——这与调用shutdown两次等效:第一次调用指定SHUT_RD,

       第二次调用指定SHUT_RW。

1.7 客户端处理函数(再修订版)

  下面给出了客户端处理函数的改进(且正确)版本。它使用了select和shutdown,其中前者只要服务器关闭它那一端的连接就会通知我们,后者允许我们正确地处理批量输入。这个版本还废弃了以文本行为中心的代码,该而针对缓冲区操作,从而消除了前面提到的复杂性问题。

  // 完成剩余部分的客户处理工作。
//  char buf[ MAX_MESG_SIZE ];
  char sendbuff[ MAX_MESG_SIZE ];
  char recvbuf[ MAX_MESG_SIZE ];
  int nfds;  // 描述符总数
  int stdineof; // 这是一个初始化为0的新标志。只要该标志为0,每次在主循环中我们总是select标准输入的可读性。
  fd_set rset; // 描述符集
  int n;
  
  stdineof = 0; 
  FD_ZERO( &rset );
  for( ; ; )
  {
    if( stdineof == 0 )
      FD_SET( fileno( stdin ), &rset );
    FD_SET( sockfd, &rset );
    nfds = max( fileno( stdin ), sockfd ) + 1;
    select( nfds, &rset, NULL, NULL, NULL );
    
    // 当我们在套接字上读到EOF时,如果我们已在标准输入上遇到EOF,那就正常的终止,于是函数返回;
    // 但是如果我们在标准输入上没有遇到EOF,那么服务器进程已过早终止。我们改用recv和send对缓冲区而不是
    // 文本行进行操作,使得select能够如期地工作。
    if( FD_ISSET( sockfd, &rset ) ) // socket is readable
    {
      if( ( n = read( sockfd, recvbuf, MAX_MESG_SIZE ) ) == 0 )
      {
        if( stdineof == 1 )
          return 0;  // normal termination
        else
          cout << " mimiasd:server terminated prematurely " << endl;
          exit( 1 );
      }
      recvbuf[ n ] = 0;
      write( fileno( stdout ), recvbuf, n);
    }
    
    //当我们在标准输入上碰到EOF时,我们把新标志stdineof置为1,并把第二个参数指定为SHUT_WR来调用shutdown以发送FIN。
    // 这儿我们也改用read和write对缓冲区而不是文本行进行操作。
    if( FD_ISSET( fileno( stdin ), &rset ) ) // input is readable
    {
      if( ( n = read( fileno( stdin ), sendbuff, MAX_MESG_SIZE ) ) == 0 )
      {
        stdineof = 1;
        shutdown( sockfd, SHUT_WR ); // send FIN
        FD_CLR( fileno( stdin ), &rset );
        continue;
      }
      write( sockfd, sendbuff, strlen( sendbuff ) );
    }
  }

 

1.8 TCP回射服务器程序(修订版)

  把前面TCP回射服务器程序,把它重写成使用select来处理人一个客户的单进程程序,而不是为每个客户派生一个子进程。图1-12给出了第一个客户建立连接前服务器的状态。

图1-12 第一个客户建立连接前的服务器状态

  服务器有单个监听描述符,我们用一个圆点来表示。

  服务器只维护一个读描述符集,如图1-13所示。假设服务器是在前台启动的,那么描述符0、1和2将被设置为标准输入、标准输出和标准错误输出。可见监听套接字的第一个可用描述符是3。图1-13还展示了一个名为client的整型数组,它含有每个客户的已连接套接字描述符。该数组的所有元素都被初始化为-1。

图1-13 仅有一个监听套接字的TCP服务器的数据结构

  描述符集中唯一的非0项是表示监听套接字的项,因此select的第一个参数将为4。

  当第一个客户与服务器建立连接时,监听描述符变为可读,我们的服务器于是调用accept。在本例假设下,由accept返回的新的已连接描述符将是4。图1-14展示了从客户到服务器的连接。

图1-14 第一个客户建立连接后的TCP服务器

  从现在起,我们的服务器必须在其client数组中记住每个新的已连接描述符,并把它加到描述符集中去。图1-15展示了这样更新后的数据结构。

图1-15 第一个客户连接建立后的数据结构

  稍后,第二个客户与服务器建立连接,图1-16展示了这种情形。

图1-16 第二个客户建立连接后的数据结构

  新的已连接描述符(建设是5)必须被记住,从而给出如图1-17所示的数据结构。

图1-17 第二个客户连接建立后的数据结构

  我们接着假设第一个客户终止它的连接。该客户的TCP发送一个FIN,使得服务器中的描述符4变为可读。当服务器读这个已连接套接字时,read将返回0。我们于是关闭该套接字并相应地更新数据结构:把client[0]的值置为-1,把描述符集中描述符4的为置为0,如图1-18所示。注意,maxfd的值没有改变。

图1-18 第一个客户终止连接后的数据结构

  总之,当有客户到达时,我们在client数组中的第一个可用项(即值为-1的第一个项)中记录其已连接套接字的描述符。我们还把这个已连接描述符加到读描述符集中。变量maxi是client数组当前使用项的最大下标,而变量maxfd(加1之后)是select函数第一个参数的当前值。对于本服务器所能处理的最大客户数目的限制是以下两个值中的较小值:FD_SETSIZE和内核允许本进程打开的最大描述符数。

  下面给出了这个版本服务器程序的前半部分。

int main()
{
  int i, maxi, maxfd, listenfd, sockfd, connectfd;
  int nready, client[ FD_SETSIZE ];
  ssize_t n;
  fd_set rset, allset;
  char buf[ MAX_MESG_SIZE ];
  socklen_t clilen;
  struct sockaddr_in cliaddr, servaddr;
  
  bzero( &servaddr, sizeof( servaddr ) );
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons( SERV_PORT );
  servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
  
  // 创建一个TCP套接字
  if( ( listenfd = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) 
  {
    printf( " socket error!\n " );
    return -1;
  }
  
  // 在待绑定到TCP套接字的网际套接字地址结构中填入通配地址(INADDR_ANY)和服务器的众所周知端口(SERV_PORT,
  // 这里定义为5566)。绑定通配地址是在告知系统:要是系统是多宿主机,我们将接受目的地址为任何本地接口的连接。
  // 我们选择TCP端口号应该比1023大(我们不需要一个保留端口),比5000大(以免与许多源自Berkeley的实现分配临
  // 时端口的范围冲突),比49152小(以免与临时端口号的“正确”范围冲突),而且不应该与任何已注册的端口冲突。
  // listen把该套接字转换成一个监听套接字。 
  if( ( bind( listenfd, ( struct sockaddr* ) &servaddr, sizeof(servaddr) ) ) < 0 ) 
  {
    printf( " bind error!\n " );
    return -1;
  }
  
  if( listen( listenfd, 5 ) < 0 )
  {
    printf( " listen error!\n " );
    return -1;
  }
  
  maxfd = listenfd; // initialize
  maxi = -1; // index into client[] array
  for( i = 0; i < FD_SETSIZE; i++ )
    client[ i ] = -1;  // -1 indicates available entry
  FD_ZERO( &allset );
  FD_SET( listenfd, &allset );
  
  signal( SIGCHLD, sig_chld );

  main函数的后半部分如下。

  for( ; ; )
  {
    rset = allset; // structure assignment
    nready = select( maxfd + 1, &rset, NULL, NULL, NULL );
    
    if( FD_ISSET( listenfd, &rset ) )
      {
        clilen = sizeof( cliaddr );
        connectfd = accept( listenfd, ( struct sockaddr* ) &cliaddr, &clilen );
        
        for( i = 0; i < FD_SETSIZE; i++ )
          if( client[ i ] < 0 )
          {
            client[ i ] = connectfd; // save descriptor
            break;
          }
        if( i == FD_SETSIZE )
        {
          printf( " too many clients " );
          exit( 1 );
        }
        
        FD_SET( connectfd, &allset ); // add new descriptor to set
        if( connectfd > maxfd )
          maxfd = connectfd; // for select
        if( i > maxi )
          maxi = i; // max index in client[] array
        if( --nready <= 0 )
          continue; // no more readable descriptors
      }
      
      for( i = 0; i <= maxi; i++ )  // check all clients for data
      {
        if( ( sockfd = client[ i ] ) < 0 )
        continue;
        if( FD_ISSET( sockfd, &rset ) )
        {
          if( ( n = read( sockfd, buf, MAX_MESG_SIZE ) ) == 0 )
          {
            // connection closed by client
            close( sockfd );
            FD_CLR( sockfd, &allset );
            client[ i ] = -1;
          }
          else
            writen( sockfd, buf, n );
            
          if( --nready <= 0 )
            break; // no more readable descriptors
        }
      }
  }

拒绝服务型攻击

  当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则可能导致服务器被挂起,拒绝为所有其他客户提供服务。这就是所谓的拒绝服务(denial of service)型攻击。它就是针对服务器做些动作,导致服务器不再能为其他合法客户提供服务。可能解决的办法包括:(a)使用非阻塞式I/O;(b)让每个客户单独的控制线程提供服务(例如创建一个子进程或一个线程来服务每个客户);(c)对I/O操作设置一个超时。

 

1.9 pselect函数

  pselect函数是POSIX发明的,如今许多Unix变种支持它。

#include <sys/select.h>
/* Same as above only that the TIMEOUT value is given with higher
   resolution and a sigmask which is been set temporarily.  This version
   should be used.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int pselect (int __nfds, fd_set *__restrict __readfds,
            fd_set *__restrict __writefds,
            fd_set *__restrict __exceptfds,
            const struct timespec *__restrict __timeout,
            const __sigset_t *__restrict __sigmask);

  pselect相对于通常的select有两个变化。

 (1)pselect使用timespec结构,而不使用timeval结构。timespec结构是POSIX的有一个发明。

/* POSIX.1b structure for a time value.  This is like a `struct timeval' but
   has nanoseconds instead of microseconds.  */
struct timespec
  {
    __time_t tv_sec;        /* Seconds.  */
    __syscall_slong_t tv_nsec;    /* Nanoseconds.  */
  };

  这两个结构的区别在于第二个成员:新结构的该成员tv_nsec指定纳秒数,而旧结构的该成员tv_usec指定微秒数。

 (2)pselect函数增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。

  关于第二点,看下面的例子。这个程序的SIGINT信号处理函数仅仅设置全局变量intr_flag并返回。如果我们的进程阻塞于select调用,那么信号处理函数的返回将导致select返回EINTR错误。然而调用select时,代码看起来大体如下:

if( intr_flag )
  handle_intr(); // handle the signal
if( ( nready = select( ... ) ) < 0 )
{
  if( errno == EINTR )
  {
    if( intr_flag )
      handle_intr();
  }
}

  问题是,在测intr_flag和调用select之间如果有信号发生,那么若select永远阻塞,该信号将丢失。有了pselect之后,我们可以下方式可靠地编写这个例子的代码:

sigset_t newmask, oldmask, zermask;

sigemptyset( &zeromask );
sigemptyset( &newmask );
sigaddset( &newmask, SIGINT );

sigpromask( SIG_BLOCK, &newmask, &oldmask ) // block SIGINT
if( intr_flag )
  handle_intr(); // handle the signal
if( ( nready = pselect( ..., &zeromask ) ) < 0 )
{
  if( errno == EINTR )
  {
    if( intr_flag )
      handle_intr();
  }
}

  在测intr_flag变量之前,我们阻塞SIGINT。当pselect被调用时,它先以空集(即zeromask)替代进程的信号掩码,再检查描述符,并可能进入睡眠。然而当pselect函数返回时,进程的信号掩码又被重置为调用pselect之前的值(即SIGINT被阻塞)。

 

 

 

 

 

© 著作权归作者所有

共有 人打赏支持
thanatos_y
粉丝 7
博文 90
码字总数 315059
作品 0
成都
程序员
0-Linux 网络编程学习笔记导航

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

q1007729991 ⋅ 2017/04/04 ⋅ 0

我的网络开发之旅——socket编程

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

yaocoder ⋅ 2014/09/21 ⋅ 0

Linux IO模型漫谈(5)- IO复用模型之select

首先需要了解的是select函数: select函数 #include #include int select (int maxfd , fdset *readset ,fdset writeset, fd_set exceptionset , const struct timeval * timeout); 返回:就绪......

晨曦之光 ⋅ 2012/06/07 ⋅ 0

gRPC学习笔记——(一)学习铺垫

(如有不全,烦请指出,后续不断跟进修正) 一、什么是RPC?为什么要学习RPC?有没有RPC的代替品? ——今后所有的学习笔记都将以此三问起头。关于编程,个人觉得由此三问,可助于编码人更加...

志明丶 ⋅ 2017/11/29 ⋅ 0

(转) 坚持完成这套学习手册,你就可以去 Google 面试了

C++ —— 不使用内建的数据类型。C++ —— 使用内建的数据类型,如使用 STL 的 std::list 来作为链表。Python —— 使用内建的数据类型(为了持续练习 Python),并编写一些测试去保证自己代...

wangxiaocvpr ⋅ 2016/10/12 ⋅ 0

libevent源码深度剖析

原文地址:http://blog.csdn.net/sparkliang/article/details/4957667 libevent源码深度剖析一 ——序幕 张亮 1 前言 Libevent是一个轻量级的开源高性能网络库,使用者众多,研究者更甚,相关...

晨曦之光 ⋅ 2012/03/09 ⋅ 0

Header First设计模式学习笔记——策略模式

我们先来看看问题 —— 现在我们需要实现一个模拟鸭子的游戏,游戏中会出现各种各样的鸭子,他们会有不同的飞行方式,同样有不同的鸣叫方式,同时我们要考虑到以后还可能出现更多的各种各样新...

梦回雪夜观花 ⋅ 2014/04/12 ⋅ 2

所看书籍记录

《程序员教程(第三版)》 《深入理解计算机系统》 《程序员的自我修养--链接、装载与库》(两遍) 《编译原理(龙书)》 《现代操作系统(第三版)》 《图解网络硬件》 《图解TCP/IP》 《数据...

thanatos_y ⋅ 2016/03/14 ⋅ 0

François Chollet 谈深度学习的局限性和未来 - 下篇

雷锋网 AI 科技评论按:本篇是 Keras 作者 François Chollet 撰写的一篇博客,文中作者结合自己丰富的开发经验分享一些自己对深度学习未来发展方向的洞见。另外本篇也是一个关于深度学习局限...

隔壁王大喵 ⋅ 04/23 ⋅ 0

程序员练级-关键提炼

启蒙入门 1.学习一门脚本语言,例如Python/Ruby 2.熟悉Unix/Linux Shell和常见的命令行 3. 学习Web基础(HTML/CSS/JS) + 服务器端技术 (LINUX + APACHE + MYSQL + PHP)      未来必然是W...

zhayefei ⋅ 2013/01/23 ⋅ 4

没有更多内容

加载失败,请刷新页面

加载更多

下一页

从零开始搭建Risc-v Rocket环境---(1)

为了搭建Rocke环境,我买了一个2T的移动硬盘,安装的ubuntu-16.04 LTS版。没有java8,gcc是5.4.0 joe@joe-Inspiron-7460:~$ java -version程序 'java' 已包含在下列软件包中: * default-...

whoisliang ⋅ 8分钟前 ⋅ 0

大数据学习路线(自己制定的,从零开始学习大数据)

大数据已经火了很久了,一直想了解它学习它结果没时间,过年后终于有时间了,了解了一些资料,结合我自己的情况,初步整理了一个学习路线,有问题的希望大神指点。 学习路线 Linux(shell,高并...

董黎明 ⋅ 14分钟前 ⋅ 0

systemd编写服务

一、开机启动 对于那些支持 Systemd 的软件,安装的时候,会自动在/usr/lib/systemd/system目录添加一个配置文件。 如果你想让该软件开机启动,就执行下面的命令(以httpd.service为例)。 ...

勇敢的飞石 ⋅ 16分钟前 ⋅ 0

mysql 基本sql

CREATE TABLE `BBB_build_info` ( `community_id` varchar(50) NOT NULL COMMENT '小区ID', `layer` int(11) NOT NULL COMMENT '地址层数', `id` int(11) NOT NULL COMMENT '地址id', `full_......

zaolonglei ⋅ 25分钟前 ⋅ 0

安装chrome的vue插件

参看文档:https://www.cnblogs.com/yulingjia/p/7904138.html

xiaoge2016 ⋅ 27分钟前 ⋅ 0

用SQL命令查看Mysql数据库大小

要想知道每个数据库的大小的话,步骤如下: 1、进入information_schema 数据库(存放了其他的数据库的信息) use information_schema; 2、查询所有数据的大小: select concat(round(sum(da...

源哥L ⋅ 50分钟前 ⋅ 0

两个小实验简单介绍@Scope("prototype")

实验一 首先有如下代码(其中@RestController的作用相当于@Controller+@Responsebody,可忽略) @RestController//@Scope("prototype")public class TestController { @RequestMap...

kalnkaya ⋅ 55分钟前 ⋅ 0

php-fpm的pool&php-fpm慢执行日志&open_basedir&php-fpm进程管理

12.21 php-fpm的pool pool是PHP-fpm的资源池,如果多个站点共用一个pool,则可能造成资源池中的资源耗尽,最终访问网站时出现502。 为了解决上述问题,我们可以配置多个pool,不同的站点使用...

影夜Linux ⋅ 今天 ⋅ 0

微服务 WildFly Swarm 管理

Expose Application Metrics and Information 要公开关于我们的微服务的有用信息,我们需要做的就是将监视器模块添加到我们的pom.xml中: 这将使在管理和监视功能得到实现。从监控角度来看,...

woshixin ⋅ 今天 ⋅ 0

java连接 mongo伪集群部署遇到的坑

部署mongo伪集群 #创建mongo数据存放文件地址mkdir -p /usr/local/config1/datamkdir -p /usr/local/config2/data mkdir -p /usr/local/config3/data mkdir -p /usr/local/config1/l......

努力爬坑人 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部