目录
一、概述
二、使用
三、实现
1、框架
2、Socket
3、定时器
4、线程池
5、读写缓冲区
6、日志
7、对象池
四、测试
五、遇到的问题mark
六、后续
一、概述
kikilib网络库是轻量,高性能,纯c++11,更符合OOP语言特点且易于使用的一个Linux服务器网络库。并发模型使用的是Reactor模型+非阻塞IO,坚持One Loop One Thread,基于反馈的负载均衡策略派发新连接。
什么是面向对象使用的网络库呢?
之前我们使用的网络库一般都是写一个回调函数,然后set_callback进网络库中,并配合context上下文指针使用。但是在c++中,为何不直接写一个类作为回调函数和上下文成员共同的载体呢,其中上下文的内容作为private成员(这也是符合语义的),回调函数作为类的成员函数,这样做也更符合C++这个OOP语言的特点,何乐而不为。基于这个想法,我写了这个网络库。
Github源码地址: https://github.com/YukangLiu/kikilib
老哥们顺便去给个star呗~
二、使用 这个网络库的使用非常简单,只需要实现一个EventService子类就可以了。以我的echo为例:
class EchoService : public kikilib::EventService
{
public:
EchoService(kikilib::Socket sock, kikilib::EventManager* evMgr)
: EventService(sock, evMgr)
{ };
~EchoService() {};
void handleReadEvent()
{
std::string str = readAll();
sendContent(std::move(str));
forceClose();
};
};
int main()
{
kikilib::EventMaster evMaster;
evMaster.init(4, 80);
evMaster.loop();
return 0;
}
从头到尾都不需要设置回调函数和对应一个连接的上下文指针。
使用这个网络库的核心是一个EventService类,提供了一些供用户使用的API,还有几个处理各种事件的虚函数。这个类可以理解成“为一个连接中的各种事件服务”的类,用户继承这个类,上下文记录信息作为该类的私有成员,实现要处理的事件的处理函数即可。
这里就会出现一个问题,网络库如何实例化用户的这个对象呢?有两种方法,一是使用模板,二是使用工厂。这里我选择了将两种方式融合,即用户只需要将自己实现的具体EventService子类放在EventMaster的模板中就可以使用了,而在网络库内部是用工厂生产对象的。这样做的原因如下:第一,如果让用户再去实例化一个工厂那使用起来太麻烦了,这个是主要原因。第二,那为何还要加入工厂呢,因为将生产对象这个动作剥离出来的话,可以让类的职责更加分明,未来需要加入EventService对象池的话可以嵌到工厂中。
具体的使用方法可以参看http和chatroom,分别是我用这个库实现的一个简单的静态网页服务器和一个聊天室(广播)服务器。
http的测试站点:http://www.liuyukang.com/
三、实现 1、框架
模型如下:
文章图片
大体上可以说是Reactor模型+非阻塞IO,One Loop One Thread,使用Round Robin派发新连接。实现这些主要依赖以下一些类:EventService,EventEpoller,EventManager,EventMaster。
类图如下:
文章图片
用户继承EventService实现UsrLmolEventService即可。框架主要是以下几个类:
(1)EventService
这个类如它的名字,是一个为事件服务的类,用户需要继承这个类,实现处理事件的方法。当一个连接到来时,网络库会实例化一个该类对象,为这个连接上的事件服务。
这个类还有一个重要身份,那就是API集合,它封装了供用户使用的网络库接口,以便处理连接上的各种事件。它提供自身socket的操作API,自身事件相关的操作API,定时器相关的操作API,线程池工具的操作API,socket缓冲区的读写操作API。
(2)EventEpoller
该类功能很简单,一个是监视epoll中是否有事件发生,一个是向epoll中添加、修改、删除监视的fd。值得注意的是,该类并不存储事件服务对象实体,也不维护任何事件对象实体的生命期,这个工作是EventManager做的。
这里的epoll用的LT模式,原因如下:
read事件到来时,若server的业务并不会每次readall并进行及时处理,那么,如果遭遇client疯狂发送巨大包体,ET模式必须每次将内容读进内存,而server不及时处理就会导致内容堆积,内存爆满,使用LT不会出现这个问题。
(3)EventManager
该类是事件服务对象实体的管理器,拥有一个EventEpoller和一个Timer定时器对象,提供插入事件,移除,修改事件的接口(对EventEpoller接口的封装),提供定时器的使用接口(对Timer接口的封装)。维护所有事件服务对象实体的生命期,维护定时器和EventEpoller的生命期。
该类主要就是在Loop函数中创建了一个线程,然后使用EventEpoller循环扫描其管理的事件,若有激活的对象,则首先会按照优先级放到不同队列中,然后根据事件优先级先后处理事件——根据事件类型调用其相关函数。处理完所有的事件,最后会销毁被要求销毁的事件服务对象实体。
(4)EventMaster
该类有三个职责:
一是循环监听端口,当有一个新连接到来时,首先使用用户实现的工厂实例化事件服务对象,然后调用其HandleConnectionEvent()函数,若该连接没有被关闭,则Round Robin将其插入到一个EventManager中,让该EventManager一直循环监视其事件。这样有一个好处,就是不会发生“惊群”现象,因为只在EventMaster这一个线程上进行了accept。这里还有一个问题,就是当使用的fd到达上限,即服务器无法接收新连接的时候,需要close掉那些新来的连接,显示地告诉客户端服务器不能再接收连接了。有两种做法:一是程序启动时会保留一个fd, 当fd分发满了时,就把这个保留的fd close掉然后给新的,然后立刻把新的close掉;二是设置fd上限(小于系统设置的上限值),超过了就把该连接close掉。本网络库采用的第二种方法,因为第一种存在竞态,测试结果不太好,故使用了第二种。每次来了一个新连接会先判断fd是不是大于上限值,大于了就close。
二是管理EventManager生命周期,负责EventManager的创建与销毁。
三是负责线程池工具实体的创建与销毁,这里还有另外一层含义,即线程池工具可以理解为是全局唯一的,因为它仅仅在EventMaster这个主线程中创建,并且不会再增加。
2、Socket
生命期的管理一直都是一个需要考虑的重点,以内存泄漏为例,当有高并发+ 5x24h运转的需求时,一点点的内存泄露很容易就会积累,所以在这个网络库中,处处强调每个类的生命应该由谁负责。
设计这个类的初衷也不例外,是为了管理fd的生命期,防止串话。
这个类中封装了一个fd,一个引用计数,一个ip字符串,一个端口号,发生拷贝构造和转移构造时会将引用计数+1,析构时会将引用计数-1,为0时会调用::close(fd)。
同时这个类封装了一些fd的操作,供用户使用,当然,用户更多时候还是应该调用EventService中更高层的封装。
3、定时器
定时器主要使用的linux的timerfd_create创建的时钟fd配合一棵红黑树实现。每个EventManager中都有一个定时器对象。没有用优先队列(小根堆)的原因是考虑到未来可能会有remove定时器任务的需求,这个需求用优先队列实现比较麻烦。
这里的红黑树(用的std::map