网络I/O模型发展前因后果

  将之前letflysite.com文章搬运过来。

  1. 阻塞型网络编程模型

  咱们第一次接触到的网络编程是不是从connetc()、bind()、listen()、accept()、recv()、send()接口开始的啊。一开始我们使用这些接口构建一个C/S模型。如下。

  上面这个模型是不是就可以进行通信了啊。但是,后面我们发现在服务端调用send()同时,线程无法响应任何的网络请求。因为,我们几乎所有io接口(包括socket接口)都是阻塞型的(所谓阻塞型是指系统调用io接口不返回调用结果则让当前线程不能执行其他操作,只有当该系统调用获得结果或报错才返回)。这给多客户端、多业务逻辑的网络编程带来了挑战。咱们此时就想到用多线程解决这个问题

  2.多线程服务端模型

  应对多客户端的网络应用,我们想到了多线程,也就是让每个连接都拥有独立的线程,这样任何一个连接的阻塞都不会影响其他的连接

  使用多进程还是多线程,具体情况具体分析。因进程的开销要远远大于线程,所以,如果需要同时为较多的客户端提供服务,则推荐多线程;如果单个服务执行需要消耗较多的CPU资源,则推荐多进程。通常,使用pthread_create()创建新线程fork()创建新进程。所以针对多客户端,我们一般采用多线程。如下。

  上图中,主线程持续等待客户端的连接请求,如有请求,则创建一个新线程,并在新线程中提供与前例同样的服务。这里有个问题,为什么socket可以accept()多次。我们要从accept()接口原型看起:

  int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

  输入参数s是从socket(),bind()和listen()中传输下来的句柄值,执行完bind()和listen()后,操作系统开始在指定端口处监听请求,如果有请求,则将该请求加入请求队列。然后调用accept()接口从请求队列中抽取第一个请求信息,创建一个新的socket返回句柄。新的socket句柄即是后续read()和recv()的输入参数。如果请求队列当前没有请求,则accept()将进入阻塞状态直到有请求进入队列。

  上述模型解决了多个客户端访问服务端的问题,但后面我们发现同时访问服务端的客户端个数达到1千个以上的时候,程序响应慢了很多,这带来的体验差。为什么呢?我们发现由于多线程或多进程由于创建和销毁严重占据系统资源,降低了系统对外界响应效率,而线程和进程有时会进入假死状态。这时我们使用“线程池”,“线程池”可以减少创建和销毁的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务,很好的降低系统开销。这种技术被广泛应用在大型组件上,如tomcat和数据库。但是我们实践发现,“线程池”必须考虑面临的响应规模,根据响应规模调整“池”的大小,而且只是在一定程度上缓解了IO频繁调用带来的系统资源占用,当请求大大超过上限,如10万同时请求,“池”对请求的响应没有比没有“池”效果好多少。有人此时想到了另一种方案非阻塞解决大规模的请求的问题。

  3.非阻塞的服务端模型

  对于大规模请求,有人想到了使用非阻塞方式。对于阻塞,非阻塞线程在被调用之后立即返回。使用如下的函数可以将某线程句柄设为非阻塞状态。

  fcntl(fd, F_SETFL, O_NONBLOCK):

  非阻塞的服务端模型,只用一个线程,但能够同时从多个连接中检测数据是否送达,并且接受数据。如下。


  在非阻塞状态下,recv()接口在被调用后立即返回,返回值代表了不同的含义。

  1. recv()返回值大于0,表示接受数据完毕,返回值即是接受到的字节数

  2. recv()返回0,表示连接已经正常断开

  3. recv()返回-1,且errno等于EAGAIN,表示recv操作还没执行完

  4. recv()返回-1,且errno不等于EAGAIN,表示recv操作遇到系统错误errno

  可以看到服务器线程通过循环调用recv()接口,在单个线程内实现对所有连接的数据接收工作。但我们发现此时CPU占用率很高,因为recv()的循环调用。recv()起到检测"操作是否完成"的作用,而实际操作系统提供了更为高效的检测“操作是否完成”作用的select接口,不必占用更多CPU资源。因此我们可以使用select()解决这个问题。

  4. 基于select()接口的事件驱动服务端模型

  大部分Unix/Linux都支持select函数,该函数用于探测多个文件句柄的状态变化。我们从select接口看起:

  FD_ZERO(int fd, fd_set *fds)

  FD_SET(int fd, fd_set* fds) 

  FD_ISSET(int fd, fd_set* fds) 

  FD_CLR(int fd, fd_set* fds) 

  int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

  上接口中,fd_set类型为按bit位标记句柄队列,例如要在某fd_set中标记一个值为16的句柄,则该fd_set的第16个bit位标记为1。具体的置位、验证使用FD_SET、FD_ISSET等宏实现。在select()函数中,readfds、writefds和exceptfds作为输入参数和输出参数。如果输入的readfds标记了16号句柄,则select()将检测16号句柄是否可读。在select()返回后,可以通过检查readfds是否标记16号句柄,来判断该“可读”事件是否发生,并且用户可以设置timeout时间。其中模型如下。


  跟之前recv()循环调用的效果一样,但占用CPU资源较少。select接口可以同时对多个句柄进行读状态、写状态和错误状态的探测。其中如何维护select()三个参数readfds、writefds和exceptfds最关键。作为输入参数,readfds应该标记所有的需求探测的“可读事件“的句柄,其中永远包括那个探测connect()的那个“母”句柄;同时,writefds和exceptfds标记所有需要探测的“可写事件”和“错误事件”的句柄(使用FD_SET()标记)。作为输出参数,readfds、writefds和exceptfds中保存了select()捕抓到的所有事件的句柄值。程序员需要检查所有的标记位(使用FD_ISSET()检查),以确定哪些句柄发生了事件。一个执行周期流程大致如下:当select()捕捉到了某“可读事件”的句柄值,服务端程序将进行recv()操作,并根据接收到的数据准备好待发送数据,并将对应的句柄值加入writefds,准备下一次的“可写事件”的select()探测。同样,如果select()捕捉到了某“可写事件”的句柄值,则服务端程序将进行send()操作,并准备好下一次的“可读事件”探测准备。该流程图如下。

  

  该模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。因此我们称其为“事件驱动模型”。但这个模型仍有问题。首先,select()接口并不是实现“事件驱动”的最好选择,因为当需要探测的句柄值较大时,select()接口将消耗大量时间去轮询各个句柄。因此,大部分操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue等。涂过需要实现更高效的服务端程序,类似epoll这样的接口更被推荐。其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。如下。


  庞大的执行体1将导致响应事件2的执行迟迟推移,在很大程度上降低了事件执行与探测的及时性。因此,诞生了很多高效的事件驱动库,可以解决上述的问题,例如libevent,还有作为libevent替代之的libev库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号(signal)等技术以支持异步响应。

  5.事件驱动库libev的服务端模型

  libev因良好性能,很多系统使用libev库。与第4点模型类似,libev同样需要循环探测事件是否产生。但libev的循环体用ev_loop结构来表达,并用ev_loop()来启动。该接口如下。

  void ev_loop( ev_loop* loop, int flags )

  libev支持八种事件类型,其中包括IO事件,一个IO事件用ev_io来表征,并用ev_io_init()函数来初始化:

  void ev_io_init(ev_io *io, callback, int fd, int events)

  初始化内容包括回调函数callback,被探测的句柄fd,需要探测的事件events,EV_READ表示“可读事件”,EV_WRITE表示“可写事件”。

  现在,用户需要做的仅仅是在合适的时候,将某些ev_io从ev_loop加入或剔除。一旦加入,下个循环即会检查ev_io所指定的事件是否发生;如果该事件被探测到,则ev_loop会自动执行ev_io回调函数callback();如果ev_io被注销,则不再检测对应事件。

  无论某ev_loop启动与否,都可以对其添加或删除一个或多个ev_io,添加删除的接口是ev_io_start()和ev_io_stop()。

  void ev_io_start( ev_loop *loop, ev_io* io ) 

  void ev_io_stop( EV_A_* )

  由此,我们可以容易得出如下的“一问一答”的服务器模型。


  由于没有考虑服务器端主动终止连接机制,所以各个连接可以维持任意时间,客户端可以自由选择退出时机。因事件循环 / 事件驱动接口,该模型具备其他模型不能提供的低资源占用、稳定性好和编写简单等特点。

参考资料


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值