目录
1. Buffer
muduo的作者对Buffer解释得非常清楚:https://blog.csdn.net/Solstice/article/details/6329080
2. 断开空闲连接
为了减轻server的负载,除了限制最大连接数以外,还可以关闭空闲的连接。如果一个连接连续N秒内没收到消息,就认为此连接太闲,应该断开。
2.1 原理
连接超时用一个循环队列来处理。假如超时时间定为8秒,则队列由8个桶(bucket)组成。第一个桶放置下一秒将要timeout的连接,第二个桶放置下两秒将要timeout的连接 .....第八个桶放置最新创建的连接。此外,每秒钟定时地往队列里添加一个空桶,这样之前的桶就往前移动,直到掉出队列。
这种方式称为Time wheel (时间盘)。下面模拟一下Time wheel的两种工作过程。
2.1.1 自生自灭
1. 在某时刻,conn1建立,被放到队列的最后一个桶。
2. 时间流逝,conn1没有新的数据发来,tail指向新的桶,conn1所在的桶变成了倒数第二个桶。
3. 又过了几秒钟,不知不觉conn1所在的桶已经走到淘汰的边缘了。
4. 再等了一秒,conn1还是没有消息发来,宣告out.
2.1.2 临时续命
若在conn1被断开前收到数据,则被再次加到最后一个桶。
时间继续流逝,但至少conn1获得了重生一次的机会。
2.1.3 多个连接
1.假设在同一秒内好几个连接被创建了,它们会被放在同一个桶里,具有相同的寿命。
2.但后来,conn1有消息发来了,得以续命,conn2却默默无闻,于是它们的距离拉开了。
2.2 实现
算法的实现可以说是充分利用了智能指针的特性。
队列:boost::circular_buffer
桶:boost::unordered_set
桶里面装的东西:Entry (定义如下,Entry里包含了connection的week ptr)
struct Entry : public muduo::copyable
{
explicit Entry(const WeakTcpConnectionPtr& weakConn)
: weakConn_(weakConn)
{
}
~Entry()
{
muduo::net::TcpConnectionPtr conn = weakConn_.lock();
if (conn)
{
conn->shutdown(); // 如果conn还活着,就断开它
}
}
WeakTcpConnectionPtr weakConn_; // connection的弱引用
};
typedef boost::shared_ptr<Entry> EntryPtr;
typedef boost::weak_ptr<Entry> WeakEntryPtr;
typedef boost::unordered_set<EntryPtr> Bucket; // 桶
typedef boost::circular_buffer<Bucket> WeakConnectionList; // 队列
muduo::net::TcpServer server_;
WeakConnectionList connectionBuckets_;
下面讲述算法的过程:
2.2.1 新连接到达
void EchoServer::onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
EntryPtr entry(new Entry(conn));
connectionBuckets_.back().insert(entry);
dumpConnectionBuckets();
WeakEntryPtr weakEntry(entry);
conn->setContext(weakEntry);
}
else
{
assert(!conn->getContext().empty());
WeakEntryPtr weakEntry(boost::any_cast<WeakEntryPtr>(conn->getContext()));
LOG_DEBUG << "Entry use_count = " << weakEntry.use_count();
}
}
读者可能会有几个问题:
1.为什么要Entry要持有connection的弱引用?
因为Entry析构时要负责断开connection,另外,Entry不应该管理connection的生命周期,所以不用shared_ptr.
2. 为什么要把Entry的强引用(shared_ptr) 放到bucket里?
试想一下,当bucket被挤出队列时,bucket会析构,如果Entry 引用计数为0,就会析构,从断开connection,所以要用shared_ptr.
3. 为什么要把Entry的弱引用传给connection 持有?
持有是为了在新消息到来时更新bucket,不持shared_ptr是因为connection不应该增加Entry的引用计数,否则如果connection一直活着,Entry就无法析构。
综上,Entry 和 connection互相持有对方的弱引用,bucket持有Entry的强引用,当Entry引用计数为0时,Entry析构,连接断开。
2.2.2 定时器超时
谁能想到,超时函数就一行代码:
void EchoServer::onTimer()
{
connectionBuckets_.push_back(Bucket());
dumpConnectionBuckets(); // 只是打印所有connection的状态,可忽视
}
boost::circular_buffer 是一个定长的列表,添加一个bucket,所有其它已存在的bucket都会向前移一个位置,相当于时间盘的转动。
2.2.3 新消息到来
新消息的到来扮演着救火员的角色:
void EchoServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_INFO << conn->name() << " echo " << msg.size()
<< " bytes at " << time.toString();
conn->send(msg);
assert(!conn->getContext().empty());
WeakEntryPtr weakEntry(boost::any_cast<WeakEntryPtr>(conn->getContext()));
EntryPtr entry(weakEntry.lock());
if (entry)
{
connectionBuckets_.back().insert(entry);
dumpConnectionBuckets();
}
}
从connection对象中取出Entry的弱引用 , 将其提升为强引用EntryPtr,加到队列的最后一个桶,Entry的引用计数加1.
然后,就结束了。程序竟然已经实现了我们想要的功能。上述代码能够处理我们之前分析的每一种情况。
当timer 超时时,Entry被挤出队列,如果在这之前没有新消息到达,则它的引用计数减一,变成0,Entry被析构,连接断开。如果在这之前有新消息来过,则Entry的另一次强引用被加到队列的最后一个桶,即使这次被挤出,但引用计数不是0,不会被析构,连接还活着。
参考文献
第2节的内容和图片均参考:https://blog.csdn.net/Solstice/article/details/6395098