赞
踩
转载请注明出处:http://blog.csdn.net/luotuo44/article/details/39344743
和之前的《Libevent工作流程探究》一样,这里也是用一个例子来探究bufferevent的工作流程。具体的例子可以参考《Libevent使用例子,从简单到复杂》,这里就不列出了。其实要做的例子也就是bufferevent_socket_new、bufferevent_setcb、bufferevent_enable这几个函数。
因为本文会用到《 Libevent工作流程探究 》中提到的说法,比如将一个event插入到event_base中。所以读者最好先读一下那篇博文。
bufferevent其实也就是在event_base的基础上再进行一层封装,其本质还是离不开event和event_base,从bufferevent的结构体就可以看到这一点。
bufferevent结构体中有两个event,分别用来监听同一个fd的可读事件和可写事件。因为不用一个event同时监听可读和可写呢?这是因为监听可写是困难的, 下面 会说到原因。读者也可以自问一下,自己之前有没有试过用最原始的event监听一个fd的可写。由于socket 是全双工的,所以在bufferevent结构体中,也有两个evbuffer成员,分别是读缓冲区和写缓冲区。 bufferevent结构体定义如下:
[cpp] view plain copy
如果看过Libevent的参考手册的话,应该还会知道bufferevent除了用于socket外,还可以用于socketpair 和 filter。如果用面向对象的思维,应从这个三个应用中抽出相同的部分作为父类,然后派生出三个子类。
Libevent虽然是用C语言写的,不过它还是提取出一些公共部分,然后定义一个bufferevent_private结构体,用于保存这些公共部分成员。从集合的角度来说,bufferevent_private应该是bufferevent的一个子集,即一部分。但在Libevent中,bufferevent确实bufferevent_private的一个成员。下面是bufferevent_private结构体。
[cpp] view plain copy
函数bufferevent_socket_new可以完成这个工作。
[cpp] view plain copy
留意函数里面的evbuffer_add_cb调用,后面会说到。
函数在最后面会冻结两个缓冲区。其实,虽然这里冻结了,但实际上Libevent在读数据或者写数据之前会解冻的读完或者写完数据后,又会马上冻结。这主要防止数据被意外修改。用户一般不会直接调用evbuffer_freeze或者evbuffer_unfreeze函数。一切的冻结和解冻操作都由Libevent内部完成。还有一点要注意,因为这里只是把写缓冲区的头部冻结了。所以还是可以往写缓冲区的尾部追加数据。同样,此时也是可以从读缓冲区读取数据。这个是必须的。因为在Libevent内部不解冻的时候,用户需要从读缓冲区中获取数据(这相当于从socket fd中读取数据),用户也需要把数据写到写缓冲区中(这相当于把数据写入到socket fd中)。
在bufferevent_socket_new函数里面会调用函数bufferevent_init_common完成公有部分的初始化。
[cpp] view plain copy
代码中可以看到,默认是enable EV_WRITE的。
函数bufferevent_setcb完成这个工作。该函数相当简单,也就是进行一些赋值操作。
[cpp] view plain copy
如果不想设置某个操作的回调函数,直接设置为NULL即可。
相信读者也知道,即使调用了bufferevent_socket_new和bufferevent_setcb,这个bufferevent还是不能工作,必须调用bufferevent_enable。为什么会这样的呢?
如果看过之前的那些博文,相信读者知道,一个event能够工作,不仅仅需要new出来,还要调用event_add函数,把这个event添加到event_base中。在本文前面的代码中,并没有看到event_add函数的调用。所以还需要调用一个函数,把event添加到event_base中。函数bufferevent_enable就是完成这个工作的。
[cpp] view plain copy
上面代码可以看到,最终会调用对应bufferevent类型的enable函数,对于socket bufferevent,其enable函数是be_socket_enable,代码如下:
[cpp] view plain copy
如果读者熟悉Libevent的超时事件,那么可以知道Libevent是在event_add函数里面确定一个event的超时的。上面代码也展示了这一点,如果读或者写event设置了超时(即其超时值不为0),那么就会作为参数传给event_add函数。如果读者不熟悉的Libevent的超时事件的话,可以参考《超时event的处理》。
用户可以调用函数bufferevent_set_timeouts,设置读或者写事件的超时。代码如下:
[cpp] view plain copy
从上面代码可以看到:用户不仅仅可以设置超时值,还可以修改超时值,也是通过这个函数进行修的。当然也是可以删除超时的,直接把超时参数设置成NULL即可。
至此,已经完成了bufferevent的初始化工作,只需调用event_base_dispatch函数,启动发动机就可以工作了。
现在来看一下,底层的socket fd接收数据后,bufferevent是怎么工作的。
在讲读事件之前,先来看一下水位问题,函数bufferevent_setwatermark可以设置读和写的水位。这里只讲解读事件的水位。
水位有两个:低水位和高水位。
低水位比较容易懂,就是当可读的数据量到达这个低水位后,才会调用用户设置的回调函数。比如用户想每次读取100字节,那么就可以把低水位设置为100。当可读数据的字节数小于100时,即使有数据都不会打扰用户(即不会调用用户设置的回调函数)。可读数据大于等于100字节后,才会调用用户的回调函数。
高水位是什么呢?其实,这和用户的回调函数没有关系。它的意义是:把读事件的evbuffer的数据量限制在高水位之下。比如,用户认为读缓冲区不能太大(太大的话,链表会很长)。那么用户就会设置读事件的高水位。当读缓冲区的数据量达到这个高水位后,即使socket fd还有数据没有读,也不会读进这个读缓冲区里面。一句话说,就是控制evbuffer的大小。
虽然控制了evbuffer的大小,但socket fd可能还有数据。有数据就会触发可读事件,但处理可读的时候,又会发现设置了高水位,不能读取数据evbuffer。socket fd的数据没有被读完,又触发……。这个貌似是一个死循环。实际上是不会出现这个死循环的,因为Libevent发现evbuffer的数据量到达高水位后,就会把可读事件给挂起来,让它不能再触发了。Libevent使用函数bufferevent_wm_suspend_read把监听读事件的event挂起来。下面看一下Libevent是怎么把一个event挂起来的。
[cpp] view plain copy
居然是直接删除这个监听读事件的event,真的是挂了!!!
看来不能随便设置高水位,因为它会暂停读。如果只想设置低水位而不想设置高水位,那么在调用bufferevent_setwatermark函数时,高水位的参数设为0即可。
那么什么时候取消挂起,让bufferevent可以继续读socket 数据呢?从高水位的意义来说,当然是当evbuffer里面的数据量小于高水位时,就能再次读取socket数据了。现在来看一下Libevent是怎么恢复读的。看一下设置水位的函数bufferevent_setwatermark吧,它进行了一些为高水位埋下了一个回调函数。对,就是evbuffer的回调函数。前一篇博文说到,当evbuffer里面的数据添加或者删除时,是会触发一些回调函数的。当用户移除evbuffer的一些数据量时,Libevent就会检查这个evbuffer的数据量是否小于高水位,如果小于的话,那么就恢复 读事件。
不说这么多了,上代码。
[cpp] view plain copy
这个函数,不仅仅为高水位设置回调函数,还会检查当前evbuffer的数据量是否超过了高水位。因为这个设置水位函数可能是在bufferevent工作一段时间后才添加的,所以evbuffer是有可能已经有数据的了,因此需要检查。如果超过了水位值,那么就需要挂起读。当然也存在另外一种可能:用户之前设置过了一个比较大的高水位,挂起了读。现在发现错了,就把高水位调低一点,此时就需要恢复读。
现在假设用户移除了一些evbuffer的数据,进而触发了evbuffer的回调函数,当然也就调用了函数bufferevent_inbuf_wm_cb。下面看一下这个函数是怎么恢复读的。
[cpp] view plain copy
因为用户可以手动为这个evbuffer添加数据,此时也会调用bufferevent_inbuf_wm_cb函数。此时就要检查evbuffer的数据量是否已经超过高水位了,而不能仅仅检查是否低于高水位。
高水位导致读的挂起和之后读的恢复,一切工作都是由Libevent内部完成的,用户不用做任何工作。
从前面的一系列博文可以知道,如果一个socket可读了,那么监听可读事件的event的回调函数就会被调用。这个回调函数是在bufferevent_socket_new函数中被Libevent内部设置的,设置为bufferevent_readcb函数,用户并不知情。
当socket有数据可读时,Libevent就会监听到,然后调用bufferevent_readcb函数处理。该函数会调用evbuffer_read函数,把数据从socket fd中读取到evbuffer中。然后再调用用户在bufferevent_setcb函数中设置的读事件回调函数。所以,当用户的读事件回调函数被调用时,数据已经在evbuffer中了,用户拿来就用,无需调用read这类会阻塞的函数。
下面看一下bufferevent_readcb函数的具体实现。
[cpp] view plain copy
细心的读者可能会发现:对用户的读事件回调函数的触发是边缘触发的。这也就要求,在回调函数中,用户应该尽可能地把evbuffer的所有数据都读出来。如果想等到下一次回调时再读,那么需要等到下一次socketfd接收到数据才会触发用户的回调函数。如果之后socket fd一直收不到任何数据,那么即使evbuffer还有数据,用户的回调函数也不会被调用了。
对一个可读事件进行监听是比较容易的,但对于一个可写事件进行监听则比较困难。为什么呢?因为可读监听是监听fd的读缓冲区是否有数据了,如果没有数据那么就一直等待。对于可写,首先要明白“什么是可写”,可写就是fd的写缓冲区(这个缓冲区在内核)还没满,可以往里面放数据。这就有一个问题,如果写缓冲区没有满,那么就一直是可写状态。如果一个event监听了可写事件,那么这个event就会一直被触发,因为一般情况下,如果不是发大量的数据这个写缓冲区是不会满的。
也就是说,不能监听可写事件。但我们确实要往fd中写数据,那怎么办?Libevent的做法是:当我们确实要写入数据时,才监听可写事件。也就是说我们调用bufferevent_write写入数据时,Libevent才会把监听可写事件的那个event注册到event_base中。当Libevent把数据都写入到fd的缓冲区后,Libevent又会把这个event从event_base中删除。比较烦琐。
bufferevent_writecb函数不仅仅要处理上面说到的那个问题,还要处理另外一个坑爹的问题。那就是:判断socket fd是不是已经连接上服务器了。这是因为这个socket fd是非阻塞的,所以它调用connect时,可能还没连接上就返回了。对于非阻塞socket fd,一般是通过判断这个socket是否可写,从而得知这个socket是否已经连接上服务器。如果可写,那么它就已经成功连接上服务器了。这个问题,这里先提一下,后面会详细讲。
同前面的监听可读一样,Libevent是在bufferevent_socket_new函数设置可写的回调函数,为bufferevent_writecb。
[cpp] view plain copy
上面代码的逻辑比较清晰,调用evbuffer_write_atmost函数把数据从evbuffer中写到evbuffer缓冲区中,此时要注意函数的返回值,因为可能写的时候发生错误。如果发生了错误,就要调用用户设置的event回调函数(网上也有人称其为错误处理函数)。
之后,还要判断evbuffer的数据是否已经全部写到socket 的缓冲区了。如果已经全部写了,那么就要把监听写事件的event从event_base的插入队列中删除。如果还没写完,那么就不能删除,因为还要继续监听可写事件,下次接着写。
现在来看一下,把监听写事件的event从event_base的插入队列中删除后,如果下次用户有数据要写的时候,怎么把这个event添加到event_base的插入队列。
用户一般是通过bufferevent_write函数把数据写入到evbuffer(写入evbuffer后,接着就会被写入socket,所以调用bufferevent_write就相当于把数据写入到socket。)。而这个bufferevent_write函数是直接调用evbuffer_add函数的。函数evbuffer_add没有调用什么可疑的函数,能够把监听可写的event添加到event_base中。唯一的可能就是那个回调函数。对就是evbuffer的回调函数。关于evbuffer的回调函数,可以参考这里。
[cpp] view plain copy
还记得本文前面的bufferevent_socket_new函数吗?该函数里面会有
[cpp] view plain copy
当bufferevent的写缓冲区output的数据发生变化时,函数bufferevent_socket_outbuf_cb就会被调用。现在马上飞到这个函数。
[cpp] view plain copy
这个函数首先进行一些判断,满足条件后就会把这个监听写事件的event添加到event_base中。其中event_pending函数就是判断这个bufev->ev_write是否已经被event_base删除了。关于event_pending,可以参考这里。
对于bufferevent_write,初次使用该函数的读者可能会有疑问:调用该函数后,参数data指向的内存空间能不能马上释放,还是要等到Libevent把data指向的数据都写到socket 缓存区才能删除?其实,从前一篇博文可以看到,evbuffer_add是直接复制一份用户要发送的数据到evbuffer缓存区的。所以,调用完bufferevent_write,就可以马上释放参数data指向的内存空间。
网上的关于Libevent的一些使用例子,包括我写的《 Libevent使用例子,从简单到复杂》,都是在主线程中调用bufferevent_write函数写入数据的。从上面的分析可以得知,是可以马上把监听可写事件的event添加到event_base中。如果是在次线程调用该函数写入数据呢?此时,主线程可能还睡眠在poll、epoll这类的多路IO复用函数上。这种情况下能不能及时唤醒主线程呢?其实是可以的,只要你的Libevent在一开始使用了线程功能。具体的分析过程可以参考《evthread_notify_base通知主线程》。上面代码中的be_socket_add会调用event_add,而在次线程调用event_add就会调用evthread_notify_base通知主线程。
用户可以在调用bufferevent_socket_new函数时,传一个-1作为socket的文件描述符,然后调用bufferevent_socket_connect函数连接服务器,无需自己写代码调用connect函数连接服务器。
bufferevent_socket_connect函数会调用socket函数申请一个套接字fd,然后把这个fd设置成非阻塞的(这就导致了一些坑爹的事情)。接着就connect服务器,因为该socket fd是非阻塞的,所以不会等待,而是马上返回,连接这工作交给内核来完成。所以,返回后这个socket还没有真正连接上服务器。那么什么时候连接上呢?内核又是怎么通知通知用户呢?
一般来说,当可以往socket fd写东西了,那就说明已经连接上了。也就是说这个socket fd变成可写状态,就连接上了。
所以,对于“非阻塞connect”比较流行的做法是:用select或者poll这类多路IO复用函数监听该socket的可写事件。当这个socket触发了可写事件,然后再对这个socket调用getsockopt函数,做进一步的判断。
Libevent也是这样实现的,下面来看一下bufferevent_socket_connect函数。
[cpp] view plain copy
这个函数比较多错误处理的代码,大致看一下就行了。有几个地方要注意,即使connect的时候被拒绝,或者已经连接上了,都会手动激活这个event。一个event即使没有加入event_base,也是可以手动激活的。具体原理参考这里。
无论是手动激活event,或者监听到这个event可写了,都是会调用bufferevent_writecb函数。现在再次看一下该函数,只看connect部分。
[cpp] view plain copy
可以看到无论是connect被拒绝、发生错误或者连接上了,都在这里做统一的处理。
如果已经连接上了,那么会调用用户设置event回调函数(网上也称之为错误处理函数),通知用户已经连接上了。并且,还会把监听可写事件的event从event_base中删除,其理由在前面已经说过了。
函数evutil_socket_finished_connecting会检查这个socket,从而得知这个socket是处于什么状态。在bufferevent_socket_connect函数中,出现的一些错误,比如被拒绝,也是能通过这个函数检查出来的。所以可以在这里做统一的处理。该函数的内部是使用。贴一下这个函数的代码吧。
[cpp] view plain copy
好长啊!终于写完了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。