Libevent前篇之异步IO

如果一个IO调用不是在调用它之后立即返回,而是在它完成了所有操作或者超时之后才返回,那么它就是一个典型的同步IO。比如,客户端通过调用connect()函数向服务端发起TCP连接请求时,只有当双方完成了三次握手或者超时之后,connect()函数才会返回。

1.阻塞调用

在初识socket编程时,我们编写的第一个回射客户/服务器模型就是一个阻塞调用的例子(详见http://dulishu.top/linux-socket02/),其中客户端代码如下:

其中,所有的网络调用都是阻塞调用。比如,connect()函数在建立连接之后才会返回(不报错、不超时的情况下);write()函数在将输出数据刷新到内核的写缓冲区之后才会返回;read()函数在收到数据之后或TCP连接关闭之后才会返回。

阻塞IO编程和非阻塞IO编程没有绝对的好坏之分。如果你的应用程序在那个时候(阻塞时)没有其他的事情要做,那么阻塞IO会是一个不错的选择;但是,如果在那个时候,你还有其他的请求或者其他的任务需要去做,那么阻塞IO就不适合你了。比如,处理多连接时:

其中,如果fd[1]有可读事件产生,而fd[0]没有可读事件。那么,fd[0]将一直阻塞,进而导致fd[1]无法及时处理它的可读事件。

针对上述问题,你可以选择使用多进程或多线程的方案来解决。比如,为每个连接创建一个进程或线程。使用多进程的方式实现多客户连接的示例可以参考这篇文章:

Linux网络编程之socket编程(三)

2.fcntl

然而,进程的创建(甚至是线程的创建)有的时候也是很昂贵的。使用fcntl()函数可以使你的socket不再阻塞:
fcntl(fd, F_SETFL, O_NONBLOCK);

使用fcntl使socket不再阻塞之后,当你再次通过该socket进行read(或其他网络调用函数)调用时,要么它立刻读取到数据并返回;要么立即返回一个错误码(比如EAGAIN),表示暂时无法操作。

此时,上面的代码片段可以稍作修改:

3.select

再深入考虑上面的代码片段。虽然,我们使每个socket都不再阻塞,解决了1中提及的问题。然而,又带来了新的问题:如果所有的socket都没有可读事件,循环将一直进行,浪费CPU的时间片;不管某个或某些连接是否产生了可读事件,你都将会为每一个连接执行一个内核调用。

所以,一个比较理想的方案是:让内核在某个或某些socket产生了可读事件的时候,再去通知用户;同时,也让内核明确的告诉用户,是哪个或哪些socket产生了这些事件。这样一来,不仅减少了循环所占用的CPU时间,还减少了内核调用的次数。

对此,比较古老的一种做法是使用select()函数。该函数提供了三个套接字集合,分别用于读、写、异常事件。在调用select()函数之后,它会一直等待直到其中一个套接字集合中的套接字产生了感兴趣事件,然后修改集合,等待用户处理。

在初识socket编程时,我们也编写了一个基于select()的回射客户/服务器模型,见如下代码片段(详见:http://dulishu.top/linux-socket-13/、http://dulishu.top/socket-select/):

4.select()替代品

虽然select()函数解决了3中提及的问题。但是,它也带来了一些其他的问题。

从上面的代码片段可以观察出:每当产生感兴趣事件之后,用户都需要去遍历一遍所有的socket,socket数量越多,遍历的次数越多,耗时就越久。这也是为什么在上面的代码中,我们将maxi尽可能压缩小的原因。

所以,当socket数量较大时,select()的性能也很堪忧呀。

对此,不同的操作系统提供了不同的select()替代品。主要有:poll(), epoll(), kqueue(), evports, /dev/poll等等。不幸的是,这些select()替代品没有一个通用的标准,比如,linux的epolll(), BSDs的kqeue(),Solaris的evports等,进而导致移植性低的问题。

5.libevent

对于不同平台之间的select()替换品没有一个统一标准,导致移植性低的问题。libevent提供了解决方案,它将这些select()替代品封装起来,提供了一致的接口供用户使用,实现了跨平台。

初识libevent,我们先实现一个简单的示例,来直观的感受一下libevent的魅力:客户端发送一条消息,服务端接收消息后将消息中的小写字母转换为大写字母,并发送给客户端,客户端再打印输出。

(1)服务端echosrv.c

(2)客户端echocli.c

客户端未使用Libevent库,编写比较简单:

(3)编译、运行

#服务端

#客户端

6.其他

bufferevent的相关接口,在代码中没有体现,而是使用了一个自定义的fd_state结构体来管理读写缓冲区。对于bufferevent的内容,后续介绍。

参考:
http://www.wangafu.net/~nickm/libevent-book/01_intro.html

《Libevent前篇之异步IO》有2个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注