大型mmo服務(wù)器架構(gòu)介紹----網(wǎng)絡(luò)底層篇

上一篇介紹線程架構(gòu),現(xiàn)在介紹網(wǎng)絡(luò)底層是怎么在這個架構(gòu)上工作的
首先網(wǎng)絡(luò)io在windows下我們選擇select,linux情況下我們使用epoll,這篇文章主要是使用epoll

首先聊聊內(nèi)存對齊

內(nèi)存對齊

#pragma pack(push)
#pragma pack(4)
struct PacketHead {
    unsigned short MsgId;
};
#pragma pack(pop)

這段代碼的含義是:自定義的協(xié)議頭,并且這個PacketHead的結(jié)構(gòu)體是4字節(jié)對齊,服務(wù)端和客戶端都遵從這個協(xié)議頭的結(jié)構(gòu)。
在網(wǎng)絡(luò)傳輸過程中,連續(xù)發(fā)送5條10KB的消息,在邏輯上認(rèn)為它是一條一條發(fā)送的,但在真實的網(wǎng)絡(luò)傳輸過程中卻不是嚴(yán)格按照一條一條數(shù)據(jù)到達(dá)接收端的,可能一次收到5KB,也可能一次就收到15KB,這就是網(wǎng)絡(luò)編程中常說的粘包問題。那么我們?nèi)绾闻袛嗍盏搅艘粋€完整協(xié)議呢?為了解決這個問題,需要在邏輯層手動為它加上一個協(xié)議頭,這個協(xié)議頭的定義是自由的,但最重要的一個數(shù)據(jù)是size,表示本協(xié)議的大小。
我們規(guī)定這個協(xié)議的格式遵循:
2字節(jié)(unsigned short) + 4字節(jié)(PacketHead) +body
其中開始的2字節(jié)是代表本協(xié)議的大?。ㄖ劣诒緟f(xié)議的大小包括不包括自己 看設(shè)計者怎么考慮了,兩種都行,我們選擇包含自己)。
收到網(wǎng)絡(luò)數(shù)據(jù)的時候,首先緩沖區(qū)中年將已收到的數(shù)據(jù)大小與一個協(xié)議頭的大小(2字節(jié))相比較,如果小于一個協(xié)議頭,就不處理,等到大于等于一個協(xié)議頭的時候,再把協(xié)議頭讀出來,然后取到協(xié)議的size,如果緩沖區(qū)的size還沒大于等于這個size就說明數(shù)據(jù)還沒接受完,等到數(shù)據(jù)大于等于這個size了,就說明一個完整的協(xié)議發(fā)送完畢了,如果這個數(shù)據(jù)解析完了還剩下數(shù)據(jù) ,那么剩下的數(shù)據(jù)就是下一個協(xié)議頭,然后依舊按照之前的邏輯反復(fù)操作就行。


image.png

這篇文章講pragma的 不懂的可以看看 https://www.cnblogs.com/yangguang-it/p/7392726.html

協(xié)議體

在早期的游戲編程中,協(xié)議內(nèi)容一般是由程序員自定義一個結(jié)構(gòu)類型。以登錄為例,它的結(jié)構(gòu)類型的定義可能如下:

struct AccountCheck{
    unsigned short Version;
    char Account[128];
    char Password[128];
};

結(jié)構(gòu)類型定義完成之后,在代碼中實現(xiàn)序列化,并轉(zhuǎn)化為二進(jìn)制串。這個結(jié)構(gòu)類型一旦定義,客戶端和服務(wù)端就必須使用相同的格式。當(dāng)數(shù)據(jù)從客戶端到達(dá)服務(wù)端時,以同樣的規(guī)則反序列化,生成一個結(jié)構(gòu)體(Struct)。
自定義結(jié)構(gòu)類型有一定的優(yōu)勢,執(zhí)行效率相對來說比較高,序列化與反序列化都是清晰可見的。但自定義結(jié)構(gòu)類型有一個致命的缺點,當(dāng)客戶端和服務(wù)端協(xié)議結(jié)構(gòu)不一致時,容易引起異?;蛘咤礄C(jī),必須解決這類兼容問題。特別是對于在線游戲,有人對協(xié)議進(jìn)行分析試探的時候,傳來的協(xié)議可能是錯誤的。我們必須有一個根本的認(rèn)識,從網(wǎng)絡(luò)傳來的協(xié)議任何時候都是不可靠的,它有可能是一個偽客戶端。
另一方面,在上面自定義的結(jié)構(gòu)類型的結(jié)構(gòu)體中加了一個Version字段,隨著游戲上線的時間增長,我們要修改原來的協(xié)議變得十分煩瑣。因為既要考慮到舊的結(jié)構(gòu)體,又要處理新的結(jié)構(gòu)體。常用的辦法就是增加Version字段,同一個協(xié)議的每一個不同的版本都需要處理。
現(xiàn)在不需要這么復(fù)雜的步驟了,有了一個可替代方案,就是Google提供的Protocol Buffer開源項目,簡稱Protobuf。Protobuf是跨平臺的,并提供多種語言版本,也就是說,服務(wù)端和客戶端的編程語言可以不一致,數(shù)據(jù)卻可以通用。序列化和反序列化功能Protobuf都已經(jīng)完成了,不需要我們過多關(guān)心,這樣可以把編碼的重心放在游戲邏輯上。

Packet具體實現(xiàn)

class Packet : public Buffer {
public:
    //Packet();
    Packet(const int msgId, SOCKET socket);
    ~Packet();

    template<class ProtoClass>
    ProtoClass ParseToProto()
    {
        ProtoClass proto;
        proto.ParsePartialFromArray(GetBuffer(), GetDataLength());
        return proto;
    }

    template<class ProtoClass>
    void SerializeToBuffer(ProtoClass& protoClase)
    {
        auto total = protoClase.ByteSizeLong();
        while (GetEmptySize() < total)
        {
            ReAllocBuffer();
        }

        protoClase.SerializePartialToArray(GetBuffer(), total);
        FillData(total);
    }

    void Dispose() override;
    void CleanBuffer();

    char* GetBuffer() const;
    unsigned short GetDataLength() const;
    int GetMsgId() const;
    void FillData(unsigned int size);
    void ReAllocBuffer();
    SOCKET GetSocket() const;

private:
    int _msgId;
    SOCKET _socket;
};

先聊成員:
1._msgId: protobuf對應(yīng)的msgId
2._socket:packet主要是接收網(wǎng)絡(luò)數(shù)據(jù),那么必定是某個connect函數(shù)返回的socket。這個socket成員就與之對應(yīng)

在講成員函數(shù)之前,先看看這個類繼承于Buffer對象,大概理解就是一個緩沖區(qū)。至于緩沖區(qū)干了什么,我們再看Buffer類是啥:

Buffer類

class Buffer :public IDisposable
{
public:
    virtual unsigned int GetEmptySize();
    void ReAllocBuffer(unsigned int dataLength);
    unsigned int GetEndIndex() const
    {
        return _endIndex;
    }

    unsigned int GetBeginIndex() const
    {
        return _beginIndex;
    }

    unsigned int GetTotalSize() const
    {
        return _bufferSize;
    }

protected:
    char* _buffer{ nullptr };
    unsigned int _beginIndex{ 0 }; // 
    unsigned int _endIndex{ 0 };

    unsigned int _bufferSize{ 0 }; // 
};

看關(guān)鍵成員:char* _buffer;
原來這個緩沖區(qū)就是一個char字符數(shù)組
并且有一個開始index,和結(jié)束index,同時還有一個已經(jīng)存儲數(shù)據(jù)的大小記錄字段_bufferSize;再仔細(xì)看,原來這個是個環(huán)形的緩沖區(qū),為什么是環(huán)形,后面仔細(xì)說明。

再看關(guān)鍵函數(shù)ReAllocBuffer

void Buffer::ReAllocBuffer(const unsigned int dataLength)
{
    if (_bufferSize >= MAX_SIZE) {
        std::cout << "Buffer::Realloc except!! " << std::endl;
    }

    char* tempBuffer = new char[_bufferSize + ADDITIONAL_SIZE];
    unsigned int _newEndIndex;
    if (_beginIndex < _endIndex)
    {
        ::memcpy(tempBuffer, _buffer + _beginIndex, _endIndex - _beginIndex);
        _newEndIndex = _endIndex - _beginIndex;
    }
    else
    {
        if (_beginIndex == _endIndex && dataLength <= 0)
        {
            _newEndIndex = 0;
        }
        else 
        {
            ::memcpy(tempBuffer, _buffer + _beginIndex, _bufferSize - _beginIndex);
            _newEndIndex = _bufferSize - _beginIndex;

            if (_endIndex > 0)
            {
                ::memcpy(tempBuffer + _newEndIndex, _buffer, _endIndex);
                _newEndIndex += _endIndex;
            }
        }
    }

    _bufferSize += ADDITIONAL_SIZE;

    delete[] _buffer;
    _buffer = tempBuffer;

    _beginIndex = 0;
    _endIndex = _newEndIndex;

    //std::cout << "Buffer::Realloc. _bufferSize:" << _bufferSize << std::endl;
}

這是buffer長度再分配函數(shù)。
1.首先判斷——bufferSize是否超出了最大長度MAX_SIZE(假設(shè)是1024)。
2.生成一個新的長為 _bufferSize + ADDITIONAL_SIZE(設(shè)定為10)的char類型數(shù)組
3.聲明一個新的名為:_newEndIndex變量 (先不說原因 下面會講)
4.如果beginIndex < endIndex
如圖所示

image.png

那么就將beginIndex到endIndex的數(shù)據(jù)拷貝到新的char*數(shù)組中,此時上面的newIndex就應(yīng)該是:
_newEndIndex = _endIndex - _beginIndex;

image.png

原來第三點為什么要聲明新的endIndex的原因是:因為在重新拷貝數(shù)據(jù)的時候,將內(nèi)存重新整理過了

5.如果beginIndex >= endIndex
如圖所示:


image.png

這樣就可以看出 原來這是環(huán)形的緩沖區(qū),那么這個時候也需要進(jìn)行數(shù)據(jù)的拷貝以及內(nèi)存的重新的對齊


image.png

這里的處理方式是分段進(jìn)行拷貝,先拷貝beginIndex的,再拷貝endIndex的。
最終結(jié)果和上面的一致:


image.png

剩下的步驟就不仔細(xì)說了,挺簡單的。

這樣我們就學(xué)習(xí)到了環(huán)形緩沖區(qū)的寫法,學(xué)會之后,再回到pakcet類當(dāng)中來。

packet類方法解析:

Packet::Packet(const int msgId, SOCKET socket)
{
    _socket = socket;  
    _msgId = msgId; 
    CleanBuffer();

    _bufferSize = DEFAULT_PACKET_BUFFER_SIZE;
    _beginIndex = 0;
    _endIndex = 0;
    _buffer = new char[_bufferSize];
}

這是構(gòu)造函數(shù)主要干這幾件事:注冊msgId,賦值connectSocket,初始化buffer。

再看別的函數(shù)

Packet::~Packet()
{
    CleanBuffer();
}

void Packet::Dispose()
{
    _msgId = 0;
    _beginIndex = 0;
    _endIndex = 0;
}

void Packet::CleanBuffer()
{
    if (_buffer != nullptr)
        delete[] _buffer;

    _beginIndex = 0;
    _endIndex = 0;
    _bufferSize = 0;
}

char* Packet::GetBuffer() const
{
    return _buffer;
}

unsigned short Packet::GetDataLength() const
{
    return _endIndex - _beginIndex;
}

int Packet::GetMsgId() const
{
    return _msgId;
}

void Packet::FillData(const unsigned int size)
{
    _endIndex += size;
}

void Packet::ReAllocBuffer()
{
    Buffer::ReAllocBuffer(_endIndex - _beginIndex);
}

SOCKET Packet::GetSocket() const
{
    return _socket;
}

看完會發(fā)現(xiàn),很簡單。甚至沒有讀數(shù)據(jù)的邏輯,這是為什么呢?
那么就需要來介紹protobuf了

最關(guān)鍵的兩個模板函數(shù):

    template<class ProtoClass>
    ProtoClass ParseToProto()
    {
        ProtoClass proto;
        proto.ParsePartialFromArray(GetBuffer(), GetDataLength());
        return proto;
    }
    template<class ProtoClass>
    void SerializeToBuffer(ProtoClass& protoClase)
    {
        auto total = protoClase.ByteSizeLong();
        while (GetEmptySize() < total)
        {
            ReAllocBuffer();
        }

        protoClase.SerializePartialToArray(GetBuffer(), total);
        FillData(total);
    }

說這兩個方法之前 就需要說到protobuff的用法了。
首先假設(shè)和客戶端需要規(guī)定一個消息,這個消息數(shù)據(jù)結(jié)構(gòu)名叫TestMsg,里面有兩個成員 一個msg,一個index


image.png

并且這個消息的msgId為1


image.png

那么在pakcet的封裝中,MsgId就是對應(yīng)的這個msgId,如果是1 那么packet的數(shù)據(jù)就可以解析成TestMsg的格式。但是如果不解析的話,那么這條數(shù)據(jù)將會是二進(jìn)制的,怎么解析數(shù)據(jù)呢?
就用到了上面兩個模板函數(shù)
當(dāng)用戶拿到了這個MsgId為1的數(shù)據(jù) 需要將這個packet反序列化成我們需要的TestMsg結(jié)構(gòu),只需要調(diào)用


image.png

這樣protoObj就可以獲取到 TestMsg的 id和index字段供我們使用了。

于此同時 另外一個函數(shù)是在發(fā)送方,想要發(fā)一個數(shù)據(jù)的時候,將這個數(shù)據(jù)打包成packet,并進(jìn)行序列化


image.png

這樣我們就不需要關(guān)心這個數(shù)據(jù)是怎么變成二進(jìn)制的,全靠protobuf幫助就行了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容