原序libevent封装了底层的多路复用接口,让我们可以更方便的跨平台使用异步网络IO。同时libevent也实现了定时任务,我们不用自己实现,更加方便。libevent官方提供了libevent教程,libevent实例,libevent接口文档,写的不错。看完之后开始尝试用它来实现一个负责与物联网设备通信的接入程序,即一个普通的TCP/UDP服务器,负责接收连接请求、接收数据、发送数据、验证身份、转发设备请求,管理连接超时,实现一些简单的接口,当然还有其他功能懒得说了。这个程序与nginx非常相似。之前用epoll实现过很多类似的程序。最近看到很多libevent开发的程序,于是开始尝试使用。然后有一个问题。我尝试创建多个IO线程,但事情并不像我想象的那样,event_del()被阻塞了。libevent的事件操作只能在dispatch循环的同一个线程中执行,即在循环退出后,或者在回调函数中执行。经过一番调试,有了这个echo-serverexample,还有这个description,记录了我的调试过程。libevent入门libevent中有两个概念,event_base和event。event是一个事件,可以设置触发条件(可读/可写/超时/信号)以及触发条件后需要执行的函数。event相当于epoll中的epoll_event。事件循环是在event_base上执行的,它记录了所有事件的触发条件,在循环中检查条件,如果条件满足则调用event中指定的函数。event_base相当于epoll中epoll_create()创建的结构体。例如读取一个文件描述符fd,可以创建一个read事件,在事件回调函数中读取fd://事件回调函数voidevent_cb(evutil_socket_tfd,shortwhat,void*arg){//这里读取fd//...}void*arg=NULL;//创建event_basestructevent_base*ev_base=event_base_new();//创建事件,可以从一个event_baseevent*ev=event_new(ev_base,fd,EV_READ,event_cb,arg);//在循环中注册事件event_add(ev,NULL);//启动事件循环,如果满足事件的条件,调用事件回调函数event_base_dispatch(ev_base);有两种设计结构的方法可以达到我的目的。一种是一个线程监听事件,线程池处理事件;二是多个线程监听事件,事件触发后直接在本线程执行。我用的是第二个。单事件循环,多处理线程event_base操作只能和事件循环在同一个线程,为了在多线程中处理事件,第一个想法是在事件线程中创建一个event_base循环,回调函数将是事件处理交给线程池。事件循环中有一个超时事件A,这个超时事件的回调函数负责执行线程池发送的操作事件的代码。如果线程池中也需要操作事件,则将操作事件的代码发送给事件线程,并激活超时事件来执行这些代码。我在使用epoll的时候经常使用这种方法。一个线程监听事件,事件被触发后交给线程池处理。如果要添加或操作事件,则将操作函数发送到队列中,并由监听该事件的线程执行。这种方式需要在多个线程中进行通信,会造成一定的性能损失,但是在我的实际项目中,这些性能损失相对于业务消耗的性能来说是微不足道的。但是我这次开发的程序还是采用了另一种模式。多个事件循环创建多个事件线程,每个线程创建一个event_base事件循环,事件触发时直接在事件线程中执行。在实际项目中,事件触发后,不仅要进行IO操作,还有很多阻塞任务需要处理,最常见的就是请求数据库。数据库操作也可以写成异步IO放在事件循环中,但是为了方便,我把数据库操作放在线程池中运行,运行完成后把事件操作放在队列中,是一样的与前面的结构一样,由超时任务事件操作处理。代码说明代码已经上传,去掉了业务相关的逻辑,只实现了echo-server功能。bufferbuffer.cc和buffer.h实现了可变长度的读写缓冲区。Libevent有一个evbuffer结构。之所以要实现buffer,是因为业务程序中不仅用到了libevent,还有很多遗留的IO代码。为了统一IO之后的业务接口,还是使用了自己实现的buffer。libevent的实现比我的方便高效,不知道比我高在哪。我会考虑在未来使用它。Buffer本质上是一个缓存队列,数据从头部插入,从尾部取出。获取数据的顺序与插入数据的顺序相同。dispatcherDispatcher是对event_base事件循环的封装。event_base_loop()函数在这里运行,每个IO线程使用一个Dispather实例。队列post_callbacks_保存了其他线程通过post()方法对Dispatcher的操作。在post()函数中激活超时事件,在超时事件中将函数从队列中取出执行。代码中只显示了一个队列。实际上,由于业务需要,可能需要多个队列来满足任务优先级的要求。在我们的业务程序中,关闭连接、释放资源等操作被认为是低优先级的。我们建立一个单独的队列,等其他队列的内容处理完后再处理低优先级队列的任务。thread_pool一个简单的线程池实现。多个线程从任务队列中获取任务后循环执行。有多个任务队列来达到优先的目的。在我们的业务中,设备身份认证涉及到很多密码学和数据库操作,非常耗时。我们把这个操作的优先级设置得很低;相对来说,设备数据上传的优先级更高。这样可以保证现有服务不会被大量的高性能消费设备访问请求打断。监听器打开监听端口,注册事件,事件触发后调用accept函数接收新连接,接收到新连接后调用指定函数(handler)处理连接。监听连接设置了SO_REUSEPORT选项,这样多个线程可以同时监听一个端口。handler连接处理方法。侦听器接收到新连接后,实例化此类来处理新连接。handler的read事件接收到client发送过来的数据,放入read_buf,然后从read_buf写入write_buf(这里有点多余),然后注册write事件。在写事件中,将write_buf的内容发送给客户端。完成echo-server的功能。在实际程序中,经常需要查询某个客户端连接,并主动从服务端向客户端发送数据,因此我们将handler存储在一个哈希表中,并设置引用计数。但这在这个例子中不是必需的。主程序入口,做一些初始化工作。综上所述,搞这些花里胡哨的并不一定比新员工学习用Go开发业务程序一周更好。如果你确实需要一个与业务关系不是那么密切、更新不频繁、效率要求高的程序,可以试试这里介绍的方法。
