MiniRpc 连接层

一个后台应用基本上可以笼统的分为网络连接层,逻辑业务层和数据存储层三层。就像我在之前开篇里提到过的,MiniRpc没有做消息的持久化存储,所有基本上只有两层: 网络连接层和逻辑层。这一篇我就来记录下网络层中的一些技术选型和各种自己给自己挖的坑。

ReactorLoop

linux下做tcp高并发绕不开的IO模式-Reactor模式。在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

在代码中我直接实现了ReactorLoop的类封装。为了完成这样的工作,需要抽象化出关注的事件Event和具体的IO复用机制IOMUtiplex。

IOMutiplex

在linux下实现IO多路复用最常用的系统调用就是epoll了,至于其他在unix网络编程中提到的select由于设计之初没有考虑大量连接的情况,所以每次都要轮询所有连接,效率不高基本已经很少使用了(另外不得不提到的一点是网上一搜都说epoll比select高效是因为使用了mmap减少文件描述符的拷贝,这个很不靠谱啊,我读过源代码没有找到这方面的内容,不知道是从哪里开始以讹传讹的)。

在MiniRpc中,我一开始直接封装了epoll来做使用,后来想起来FreeBSD下类似的系统调用是kqueue,为了统一IO多路复用的机制,后面我重构成共同继承一个公共的基类IOMutiplex。由于我使用的是linux,所以没有做kqueue的封装,多添加了一个poll的类封装,当然,默认情况下使用的是epoll。

Event

Event类抽象出ReactorLoop关注的事件。实际基本流程是通过ReactorLoop注册到IOMutiplex上。同时实现基于回调机制,Event还要添加关注的EventHandler,一般是读或者写。

技术细节

大体的连接层框架就是上面的这些,基本上参考了libvent的设计(后来发现redis也是这么做的),下面我谈谈自己在实际完成的过程中遇到的要注意的地方。

线程安全

使用多线程模型还是类似于Redis的单线程模型是在开发的一开始就应该定好的。这一点上我几乎是到最后面才决定从单线程转多线程的,那么就面临一个问题,epoll的并发操作。

根据man page,当一个线程阻塞在epoll_wait调用上时,另一个线程并发的往其添加fd是线程安全的,但是并发的删除fd是未定义的行为。

MiniRpc中client端是用户线程向ReactorLoop添加新建立连接的fd,server端是多线程应用,为了实现线程安全,我最后面限定所有需要需要跨ReactorLoop的操作必须继承task类,在ReactorLoop中添加了一个vector,用来接收其他线程推送的task,在ReactorLoop线程从epoll_wait返回处理完活跃连接后,处理vector中存储的task事件,在这些task事件中添加或者删除关注的fd。

这样做是线程安全了,但是vector需要靠mutex保护,锁的竞争可能会影响性能,后面需要想想解决的方案。

Buffer

实现中Buffer类也是中途决定添加的,看来还是经验不足啊。对于非阻塞IO,MiniRpc必须提供Buffer来缓存message,这样才能做到异步调用。在实现上,Buffer类就是简单的包裹了vector并提供了需要的接口供其他类调用,vector数据连续,如果更加在乎性能的话,可以仿照libevent或者STL中的deque的设计,将buffer设计成即是链表有是连续的存储,提高性能。

非阻塞的connect

Reactor模式要求在处理io操作的时候耗时尽可能的少,如果connect还是像传统那样阻塞住的话,则至少需要一个RTT的时间。所以需要换成非阻塞的connect,用select看是否可读可写,最后用getsocketop看是否连接出错,通过以上判断后才是成功连接。

资源所有权

下一篇也会提到这个,因为实在太重要了,在这上面吃了不少的亏。

在一开始划分模块设计类的时候并没有考虑类的所有权关系,导致测试的后core dump不断,valgrind也检测出不少的内存的泄露错误。后来的解决办法就是尽可能的将new/delete替换成shared_ptr,这样做后内存泄露是没了,却又引发了其他的问题,具体问题下一篇在来介绍吧。

总结

一开始是没有网络连接层的设计,准备直接用libevent的,但是看完了它的大体框架后动了自己写一个的心思。现在看来,基本满足了后面rpc对网络连接层的需要,但肯定还是有很多不足,还需要多多完善。