大型mmo服务器架构介绍----线程篇

本篇是打算介绍一下目前常见的大型mmo服务器架构的源码,其实目前见过的几个框架在思想上模型基本上大同小异,本人公司的代码由于不方便展示,于是使用开源的框架进行解析,主要理解大致思想,抓重点

有没有想过一个问题?
服务器在启动的一刻,客户端和服务器究竟做了哪些准备工作?以及玩家登陆的时候,客户端又是怎么样和服务器通讯的呢?
本篇也是系列的第一篇,打算先从底层线程的设计开始讲起
该框架将每个任务进行封装,然后通过线程管理器分别分发到对应的线程处理。
也是mmo服务器的基础线程框架。

ThreadObject类

ThreadObject类其实是线程处理事件的一个最小单位
里面包含了需要处理的函数(通过std::function包装)

class Thread;
class ThreadObject : public MessageList
{
public:
    virtual bool Init() = 0; //初始化函数
    virtual void RegisterMsgFunction() = 0;//消息注册
    virtual void Update() = 0;    //更新

    void SetThread(Thread* pThread);
    Thread* GetThread() const;
    bool IsActive() const;
    void Dispose() override;
    
protected:
    bool _active{ true };
    Thread* _pThread{ nullptr };

该类很简单,继承于MessageList(这个后续说)
并且主要成员就两个,_active是否已经执行过,_pThread为当前执行这个线程对象的线程指针(就是这玩意是在哪个线程上执行的)

如何使用这个类呢,只需要继承这个类,然后将这个类的 虚函数实现就行
具体用法后面再结合一起说

Thread类

介绍完object类自然就是介绍Thread类啦,顾名思义 ThreadObject对象就是在某个Thread对象上运行的

class Packet;
class ThreadObject;

class ThreadObjectList: public IDisposable
{
public:
    void AddObject(ThreadObject* _obj);
    void Update();
    void AddPacketToList(Packet* pPacket);
    void Dispose() override;

protected:
    // 本线程的所有对象    
    std::list _objlist;
    std::mutex _obj_lock;
};

class Thread : public ThreadObjectList, public SnObject {
public:
    Thread();
    void Start();
    void Stop();
    bool IsRun() const;
   
private:
    bool _isRun;
    std::thread _thread;
};

可以看到上面的代码,Thread类继承于ThreadObjectList,而ThreadObjectList里就保存了一个list,里面存放所有ThreadObject对象的指针,并且提供了方法接口,将ThreadObject对象指针存入到list中,而主Thread类保存了当前进程的指针,使用的是C++11后的std::thread库,并且也封装了start和stop方法,方便使用。

void ThreadObjectList::AddObject(ThreadObject* obj)
{
    std::lock_guard guard(_obj_lock);//对当前线程上锁

    // 在加入之前初始化一下
    if (!obj->Init())
    {
        std::cout << "AddObject Failed. ThreadObject init failed." << std::endl;
    }
    else
    {
        obj->RegisterMsgFunction(); //运行threadObject的消息注册函数
        _objlist.push_back(obj); //将threadobject对象指针保持至list
      
        //保持成功后 将当前线程的指针保存至这个threadObject对象的thread成员上
        const auto pThread = dynamic_cast(this);
        if (pThread != nullptr)
            obj->SetThread(pThread);
    }
}

上面的函数就是将ThreadObject对象指针存入ThreadObjectList::_objlist对象的过程,重要的三点:
1:存入之前先对object初始化
2:注册消息函数
3:成功之后保存当前线程指针

Thread::Thread()
{
    this->_isRun = true;
}

void Thread::Stop()
{
    if (!_isRun)
    {
        _isRun = false;
        if (_thread.joinable()) _thread.join();
    }
}

void Thread::Start()
{
    _isRun = true;
    _thread = std::thread([this]()
    {
        while (_isRun)
        {
            Update();
        }
    });
}

bool Thread::IsRun() const
{
    return _isRun;
}

上述代码就是Thread类的方法实现,非常简单明了,这里注意 thread对象只要在运行,那么就会一直执行ThreadObiectList对象的update函数 这也是当前线程的主循环

那么我们来看看update函数都干了些什么吧~

void ThreadObjectList::Update()
{
    std::list _tmpObjs; //
    _obj_lock.lock();
    std::copy(_objlist.begin(), _objlist.end(), std::back_inserter(_tmpObjs));
    _obj_lock.unlock();

    for (ThreadObject* pTObj : _tmpObjs)
    {
        pTObj->ProcessPacket();
        pTObj->Update();

        // 非激活状态,删除
        if (!pTObj->IsActive())
        {
            _obj_lock.lock();
            _objlist.remove(pTObj);
            _obj_lock.unlock();

            pTObj->Dispose();
            delete pTObj;
        }
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

上述代码其实也不难,本质就是把原来的list上的Object拷贝一份,然后对拷贝的这份的每个object指针进行处理
分别对每个object执行
1: processPacket(这个先无视 是MessageList里的方法,理解成处理消息就行)
2:Update(这个函数是虚函数,具体逻辑自己实现,也是我们需要真正执行逻辑的地方)
3:执行完update之后 判断这个object是否已经没用了(被抛弃了) 如果已经没用了,那么就将这个obj移除掉

最终再过一毫秒继续运行该函数

ThreadMgr类

class Packet;
class ThreadObject;
class Network;

class ThreadMgr :public Singleton, public ThreadObjectList
{
public:
    ThreadMgr();
    void StartAllThread();
    bool IsGameLoop();
    void NewThread();
    bool AddObjToThread(ThreadObject* obj);
    void AddNetworkToThread(APP_TYPE appType, Network* pNetwork);

    // message
    void DispatchPacket(Packet* pPacket);
    void SendPacket(Packet* pPacket);

    void Dispose() override;

private:
    Network* GetNetwork(APP_TYPE appType);

private:
    uint64 _lastThreadSn{ 0 }; // 实现线程对象均分

    std::mutex _thread_lock;
    std::map _threads;

    // NetworkLocator
    std::mutex _locator_lock;
    std::map _networkLocator;
};

不需要全部看懂意思 只需要大概了解思想
抓重点
1:继承自Singleton 说明每个进程只允许一个对象,不管从哪个线程获取到这个ThreadMgr都是指向同一个对象
2: std::map _threads; 该对象拥有所有线程对象的指针,并且是以key = 线程id value =线程指针的形式保存
3:可以通过该对象创建新的进程
4:可以通过该对象直接将ThreadObject对象平均分配到线程中,而不需要我们自己决定,实现了负载均衡

接下来看具体方法实现:

void ThreadMgr::StartAllThread()
{
    auto iter = _threads.begin();
    while (iter != _threads.end())
    {
        iter->second->Start();
        ++iter;
    }
}

bool ThreadMgr::IsGameLoop()
{
    for (auto iter = _threads.begin(); iter != _threads.end(); ++iter)
    {
        if (iter->second->IsRun())
            return true;
    }

    return false;
}

void ThreadMgr::NewThread()
{
    std::lock_guard guard(_thread_lock);
    auto pThread = new Thread();
    _threads.insert(std::make_pair(pThread->GetSN(), pThread));
}

上面代码通俗易懂 不需要解释~

// 平均加入各线程中
bool ThreadMgr::AddObjToThread(ThreadObject* obj)
{
    std::lock_guard guard(_thread_lock);

    // 找到上一次的线程 
    auto iter = _threads.begin();
    if (_lastThreadSn > 0)
    {
        iter = _threads.find(_lastThreadSn);
    }

    if (iter == _threads.end())
    {
        // 没有找到,可能没有配线程
        std::cout << "AddThreadObj Failed. no thead." << std::endl;
        return false;
    }

    // 取到它的下一个活动线程
    do
    {
        ++iter;
        if (iter == _threads.end())
            iter = _threads.begin();
    } while (!(iter->second->IsRun()));

    auto pThread = iter->second;
    pThread->AddObject(obj);
    _lastThreadSn = pThread->GetSN();
    //std::cout << "add obj to thread.id:" << pThread->GetSN() << std::endl;

    return true;
}

主要来看如何进行负载均衡的
看了代码就能理解,其实就是平均分配
在把obj加入到某个线程之后 记录这个线程的线程id,下次别的ojb来的时候就在这个线程id对应的mapKey的下一位key对应的线程上增加该ojb
假设 线程有 1 2 3
第一次是加到1 上
第二次是加到2 上
第三次是加到3 上
如此反复

那么到此为止,整体的线程框架就能了解了

image.png

MessageList

image.png

上文说道,在ThreadObject上继承了MessageList这个类,那么这个类是啥呢?

先考虑一个问题:
假设一个消息Id为 Msg1,携带着Packet数据,从客户端传来,按照现在的情况是 ThreadMgr管理类会将这个msg分配给每个Thread,并且每个ThreadObject都可以处理这个数据。
但是如果我只希望Thread1里的某个ThreadObject处理这条数据就行了,别的ThreadObject处理别的MsgId,实现消息的过滤,应该怎么办呢?
MessageList的用处就是在这里,实现消息过滤,每个ThreadObject监听自己感兴趣的msgId。
看代码!

class Packet;

typedef std::function HandleFunction;

class MessageList
{
public:
    void RegisterFunction(int msgId, HandleFunction function);//注册这个消息Id 对应的回调
    bool IsFollowMsgId(int msgId);//是否是这个ThreadObjc所需要的消息id
    void ProcessPacket();//处理Packet
    void AddPacket(Packet* pPacket); //添加Patcket

protected:
    std::mutex _msgMutex; //锁
    std::list _msgList; //消息队列
    std::map _callbackHandle; //回调map 
};

从上面可以大致知道,每个ThreadObject都有一个MessageList,这样每个ThreadObject在声明的时候就可以通过RegisterFunction这个函数来注册自己需要监听的消息。

void MessageList::RegisterFunction(int msgId, HandleFunction function)
{
    std::lock_guard guard(_msgMutex);
    _callbackHandle[msgId] = function;
}

当消息来到的时候,通过IsFollowMsgId 来判断是不是ThreadObject所需要的msgId,如果是的话,就调用 AddPacket函数,将这个消息的packet保存下来,供后续ProcessPacket函数使用。

void MessageList::AddPacket(Packet* pPacket)
{
    std::lock_guard guard(_msgMutex);
    _msgList.push_back(pPacket);
}
image.png

上图逻辑就是每个Thread在分发消息的时候 根据每个ThreadObject关心的MsgId进行分发的。

void MessageList::ProcessPacket()
{
    std::list tmpList;
    _msgMutex.lock();
    std::copy(_msgList.begin(), _msgList.end(), std::back_inserter(tmpList));
    _msgList.clear();
    _msgMutex.unlock();

    for (auto packet : tmpList)
    {
        const auto handleIter = _callbackHandle.find(packet->GetMsgId());
        if (handleIter == _callbackHandle.end())
        {
            std::cout << "packet is not hander. msg id;" << packet->GetMsgId() << std::endl;
        }
        else
        {
            handleIter->second(packet);
        }
    }

    tmpList.clear();
}

处理消息的逻辑大致如下
1.声明一个tmpList,存储
2 上锁 把_msgList复制到tmpList里处理 清空_msgList 解锁

  1. 遍历tmpList 获取每个packet的 MsgId,并且通过_callbackHandle map找到对应MsgId需要处理的函数对象,调用该函数。

提问:为什么这里要上锁呢?
其实是因为ThreadMgr是一个单例对象,这个对象在线程1和线程2其实指向的都是同一个对象,那么就会造成共享资源的问题,所以需要对Thread和ThreadObect做访问临界资源上锁的处理。

提问:MsgId是什么
实际上可以理解成 是和服务器和客户端规定的协议 ,比如大家规定当msgId等1时,这个消息代表的含义是XXX。那么双方彼此就可以通过MsgId获取到含义,从而处理这个消息。
目前用的最普遍的消息序列化工具是protobuf

什么是protobuf? 官方文档对 protobuf 的定义:protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,可用于数据通信协议和数据存储等.

你可能感兴趣的:(大型mmo服务器架构介绍----线程篇)