Timing Wheel的c++实现

必须的知识储备

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消息进来刷新保活的作用了。

。。。。。。

以此类推。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值