必须的知识储备
1、boost的circular_buffer。
2、c++的shared_ptr和weak_ptr。
3、Timing wheel。
代码实现
主要数据结构
1、Entry结构持有连接(TCPConn)的弱引用。
2、Entry的析构函数实现了连接的主动关闭。本质上就是把连接的管理交给了Entry结构。
typedef std::weak_ptr<evpp::TCPConn> WeakTCPConnPtr;
struct Entry
{
explicit Entry(const WeakTCPConnPtr& weakConn)
: weakConn_(weakConn)
// , paccServer(server)
{
}
~Entry()
{
evpp::TCPConnPtr conn = weakConn_.lock(); //提升为强引用
if (conn)
{
conn->Close(); //会回调OnConnection,即使对端时非正常断开tcp连接
//paccServer->connections_.erase(conn); //关闭掉应用层的conn,2017-10-19,这个不需要处理,因为网络库会回调OnConnection,在哪里已经关闭了
//当客户端保活失败时,uid_conns跟mac_conns_不需要处理,只在要发送消息时,转换成强引用失败,再去删除这两个容器中的元素
AK_LOG_WARN << "client " << conn->remote_addr() << " keepalive failed";
}
}
WeakTCPConnPtr weakConn_;
//AccessServer *paccServer; //设置成静态成员,节省内存
};
实现类AccessServer。
1、connectionBuckets_变量使用boost的circular_buffer实现了轮盘。
2、connectionBuckets_放的是std::unordered_set,说明轮盘的一个格子可以存放多个连接。
class AccessServer
{
public:
AccessServer(evpp::EventLoop* loop, const std::string& addr, const std::string& name, uint32_t thread_num);
void Start();
public:
//friend struct Entry;
//message已经是一条完整的消息了
void OnStringMessage(const evpp::TCPConnPtr& conn, std::string& message);
public:
//对于akcs来说,就是在这里讲请求设备上报状态的消息下发了,同时保存设备的相关状态信息(含地址信息\联动系统等)
void OnConnection(const evpp::TCPConnPtr& conn);
void onHeartBeatTimer()
{
{
std::lock_guard<std::mutex> lock(buckets_mutex_);
connectionBuckets_.push_back(Bucket());
}
{
std::lock_guard<std::mutex> lock(reportstatu_buckets_mutex_);
connectionReportStatuBuckets_.push_back(Bucket2());
}
}
public:
typedef std::shared_ptr<Entry> EntryPtr;
typedef std::weak_ptr<Entry> WeakEntryPtr;
typedef std::unordered_set<EntryPtr> Bucket;
typedef boost::circular_buffer<Bucket> WeakConnectionList;
private:
std::mutex buckets_mutex_;
WeakConnectionList connectionBuckets_;
evpp::TCPServer server_;
AkcsMsgCodec codec_;
std::mutex mutex_;
}
Timing Wheel的实现
初始化轮盘。
1、connectionBuckets_.resize(20)。这个动作后connectionBuckets_就是满的,容量是20。记住这点很重要。
AccessServer::AccessServer(evpp::EventLoop* loop, const std::string& addr, const std::string& name, uint32_t thread_num)
: server_(loop, addr, name, thread_num)
, codec_(this)
{
//codec_.SetMsgCallback( //codec_要先初始化,才能用SetMsgCallback
// std::bind(&AccessServer::OnStringMessage, this, std::placeholders::_1, std::placeholders::_2));
server_.SetConnectionCallback(
std::bind(&AccessServer::OnConnection, this, std::placeholders::_1));
//tcp应用层有消息的时候,就调用该函数
server_.SetMessageCallback(
std::bind(&AkcsMsgCodec::OnMessage, &codec_, std::placeholders::_1, std::placeholders::_2));
loop->RunEvery(evpp::Duration(10.0), std::bind(&AccessServer::onHeartBeatTimer, this));
connectionBuckets_.resize(20); //保活200s = 10*20 设备端180s发心跳包
connectionReportStatuBuckets_.resize(30);
}
连接的处理。
1、新的连接进来后,会用Entry去装这个连接,然后插入connectionBuckets_桶的最后面。可以看到执行完OnConnection后,entry的引用计数是1。
2、思考为什么要持有entry的弱引用weakEntry,并放到连接的上下文?下面给出答案。
void AccessServer::OnConnection(const evpp::TCPConnPtr& conn) //
{
AK_LOG_INFO << conn->AddrToString() << " is " << (conn->IsConnected() ? "UP" : "DOWN");
if (conn->IsConnected())
{
EntryPtr entry(new Entry(conn));
{
std::lock_guard<std::mutex> lock(buckets_mutex_);
connectionBuckets_.back().insert(entry);
}
WeakEntryPtr weakEntry(entry);
evpp::Any any_tmp(weakEntry);
conn->set_context(EVPP_CONN_ANY_CONTEXT_HB_INDEX, any_tmp);
}
else
{
if (conn->context(EVPP_CONN_ANY_CONTEXT_HB_INDEX).IsEmpty())
{
AK_LOG_ERROR << "it is impossible!";
//return
}
//assert(!conn->context().IsEmpty());
WeakEntryPtr weakEntry(evpp::any_cast<WeakEntryPtr>(conn->context()));
}
}
定时器的处理。
很简单就是插入一个空桶,实现让轮盘转起来的效果。
进一步说connectionBuckets_.push_back(Bucket())后,connectionBuckets_的front就被移除了,里面的Entry的引用计数都减1。那么如果某个Entry的引用计数到0,就执行了析构函数,关闭持有的这个连接。
void onHeartBeatTimer()
{
{
std::lock_guard<std::mutex> lock(buckets_mutex_);
connectionBuckets_.push_back(Bucket());
}
}
收到消息的处理。
解答上面为什么要在上下文存放Entry的弱引用,答案就在下面的代码当中。
如果有新消息进来,那么我们先从上下文获取到Entry的弱引用,就可以进一步取到entry—>EntryPtr entry(weakEntry.lock());
然后再把这个entry放到桶的末尾,即增加了这个entry的引用计数。本质上就是刷新了连接的保活时间。
void AccessServer::OnStringMessage(const evpp::TCPConnPtr& conn, std::string& message)
{
//开始业务处理
OnSocketMsg(conn, message);
//刷新保活时间,原则上是要在codec_里面的消息回调中保活,为避免造成类结构的侵入,挪到这里.
//在客户端传输消息很慢时,即一个保活周期内无法完成一条完整的消息的传输时,会造成假性保活失败
//assert(!conn->context().IsEmpty());
WeakEntryPtr weakEntry(evpp::any_cast<WeakEntryPtr>(conn->context(EVPP_CONN_ANY_CONTEXT_HB_INDEX)));
EntryPtr entry(weakEntry.lock());//原子操作,把它提升为强引用 EntryPtr,然后放到当前的 timing wheel 队尾。
//保证~Entry()不被调用->不会析构,从而保证不执行conn->shutdown()->fd不会被关闭
if (entry)
{
//AK_LOG_INFO << "tcp keepalive %s successful", __FUNCTIONW__, conn->remote_addr().c_str());
{
std::lock_guard<std::mutex> lock(buckets_mutex_); //加锁,从原理上是不需要的。。。
connectionBuckets_.back().insert(entry);
}
}
}
举个栗子
为了说明简单,轮盘(connectionBuckets_)的大小是3,定时器10秒。
连接进来,然后啥都不干被关闭
1、0:01,连接A进来。那么connectionBuckets_[2]这个集合就放入了一个entryA,引用计数是1。
2、0:11,执行OnTimer。这时候放入一个空桶,这时候connectionBuckets_[0]是空桶,connectionBuckets_[1]放着entryA,connectionBuckets_[2]是空桶。
3、0:21,执行OnTimer。这时候放入一个空桶,这时候connectionBuckets_[0]放着entryA,connectionBuckets_[1]是空桶,connectionBuckets_[2]是空桶。
4、0:31,执行OnTimer。这时候放入一个空桶,那么connectionBuckets_[0]就要被清空,那么entryA的引用计数就变成了0,执行析构函数,关闭连接A。
连接进来,消息进来
1、0:01,连接A进来。那么connectionBuckets_[2]这个集合就放入了一个entryA,引用计数是1。
2、0:11,执行OnTimer。这时候放入一个空桶,这时候connectionBuckets_[0]是空桶,connectionBuckets_[1]放着entryA,connectionBuckets_[2]是空桶。
3、0:13,消息进来,执行OnMessage。这时候onnectionBuckets_[0]是空桶,connectionBuckets_[1]放着entryA,connectionBuckets_[2]也插入了entryA。即entryA的引用计数变成了2.
4、0:21,执行OnTimer。这时候放入一个空桶,这时候connectionBuckets_[0]放着entryA,connectionBuckets_[1]放着entryA,connectionBuckets_[2]是空桶。
5、0:31,执行OnTimer。这时候放入一个空桶,那么connectionBuckets_[0]就要被清空,那么entryA的引用计数就从2变成了1,不会执行析构函数。这边可以看到0:13消息进来刷新保活的作用了。
。。。。。。
以此类推。