handy网络库是个按照Reactor模式设计的库,在github(https://github.com/yedf/handy)上有2.9k个点赞,说明大家对这个库的实现还是很认可的。笔者也反复读过这个库的代码。
这个库没有像muduo一样使用boost,而是完全用c++11的特性去实现了函数指针、多线程等网络库的必备,所以对于了解c++11也是一个好的入门指南。
我上个星期开始抽出了一点业余时间来体验了下这个库,主要是这个库自带的example例子,最简单的像echo之类的。
在跑例子的时候,我发现了handy的EventBase即事件分发器是单线程处理请求的,这就导致可能有些请求比较慢,就会阻塞后续的请求相应。其实标准的Reactor处理逻辑应该是主线程负责read接受客户端请求,然后把请求内容分发到工作线程池中去处理,这样能最大化利用服务器效率,不会阻塞请求。
void PollerKqueue::loop_once(int waitMs) {
//..
lastActive_ = kevent(fd_, NULL, 0, activeEvs_, kMaxEvents, &timeout);
while (--lastActive_ >= 0) {
//..
if (!(ke.flags & EV_EOF) || ch->readEnabled()) {
ch->handleRead();
}
}
}
上面的代码就是会先拉取事件,然后就处理read事件,在handleRead函数中会read套接字然后回调该channel的回调函数。
ch->handleRead()里会执行如下,简化如下:
void TcpConn::handleRead(const TcpConnPtr& con) {
//1、read socket
rd = readImp(channel_->fd(), input_.end(), input_.space());
//2、收完包后处理
if (readcb_ && input_.size()) {
readcb_(con);
}
}
这整个流程是在poller主线程去实现的,只要有一个请求耗时很久,后面的请求根本无法响应。
举个例子。修改自带的例子:Example/echo.cc,如果接受客户端请求字段是hello,就会sleep10秒,这就阻塞了dispatcher的分发能力。在阻塞的这段时间内,开启另一个终端去telnet连接服务,发送任何数据都不会有响应,因为主线程分发被阻塞了。
svr->onConnRead([](const TcpConnPtr& con) {
Buffer buf = con->getInput();
if (0 == strncmp(buf.data(), "hello", 5))
{
trace("some request block the loop");
sleep(10);
}
con->send(con->getInput());
});
那为什么不read完数据后分发到工作线程中处理呢。
void TcpConn::handleRead(const TcpConnPtr& con) {
//1、read socket
rd = readImp(channel_->fd(), input_.end(), input_.space());
//2、收完包后处理
if (readcb_ && input_.size()) {
//3、丢到线程池中处理
base_->thread_pool_.addTask([=]{
readcb_(con);
});
}
}
这样调整后,请求之间不会互相干扰,因为它们都是被分派给不同的线程去做了。当然如果所有的请求都要阻塞很久,那么开10个线程池就是杯水车薪了,但是总比单线程跑好得多了。
代码调整很小,因为handy实现的线程池已经很好了,我只是稍微调整了下而已。
网友评论