大型mmo服务器架构介绍----网络底层篇

上一篇介绍线程架构,现在介绍网络底层是怎么在这个架构上工作的
首先网络io在windows下我们选择select,linux情况下我们使用epoll,这篇文章主要是使用epoll

首先聊聊内存对齐

内存对齐

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

这段代码的含义是:自定义的协议头,并且这个PacketHead的结构体是4字节对齐,服务端和客户端都遵从这个协议头的结构。
在网络传输过程中,连续发送5条10KB的消息,在逻辑上认为它是一条一条发送的,但在真实的网络传输过程中却不是严格按照一条一条数据到达接收端的,可能一次收到5KB,也可能一次就收到15KB,这就是网络编程中常说的粘包问题。那么我们如何判断收到了一个完整协议呢?为了解决这个问题,需要在逻辑层手动为它加上一个协议头,这个协议头的定义是自由的,但最重要的一个数据是size,表示本协议的大小。
我们规定这个协议的格式遵循:
2字节(unsigned short) + 4字节(PacketHead) +body
其中开始的2字节是代表本协议的大小(至于本协议的大小包括不包括自己 看设计者怎么考虑了,两种都行,我们选择包含自己)。
收到网络数据的时候,首先缓冲区中年将已收到的数据大小与一个协议头的大小(2字节)相比较,如果小于一个协议头,就不处理,等到大于等于一个协议头的时候,再把协议头读出来,然后取到协议的size,如果缓冲区的size还没大于等于这个size就说明数据还没接受完,等到数据大于等于这个size了,就说明一个完整的协议发送完毕了,如果这个数据解析完了还剩下数据 ,那么剩下的数据就是下一个协议头,然后依旧按照之前的逻辑反复操作就行。


image.png

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

协议体

在早期的游戏编程中,协议内容一般是由程序员自定义一个结构类型。以登录为例,它的结构类型的定义可能如下:

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

结构类型定义完成之后,在代码中实现序列化,并转化为二进制串。这个结构类型一旦定义,客户端和服务端就必须使用相同的格式。当数据从客户端到达服务端时,以同样的规则反序列化,生成一个结构体(Struct)。
自定义结构类型有一定的优势,执行效率相对来说比较高,序列化与反序列化都是清晰可见的。但自定义结构类型有一个致命的缺点,当客户端和服务端协议结构不一致时,容易引起异常或者宕机,必须解决这类兼容问题。特别是对于在线游戏,有人对协议进行分析试探的时候,传来的协议可能是错误的。我们必须有一个根本的认识,从网络传来的协议任何时候都是不可靠的,它有可能是一个伪客户端。
另一方面,在上面自定义的结构类型的结构体中加了一个Version字段,随着游戏上线的时间增长,我们要修改原来的协议变得十分烦琐。因为既要考虑到旧的结构体,又要处理新的结构体。常用的办法就是增加Version字段,同一个协议的每一个不同的版本都需要处理。
现在不需要这么复杂的步骤了,有了一个可替代方案,就是Google提供的Protocol Buffer开源项目,简称Protobuf。Protobuf是跨平台的,并提供多种语言版本,也就是说,服务端和客户端的编程语言可以不一致,数据却可以通用。序列化和反序列化功能Protobuf都已经完成了,不需要我们过多关心,这样可以把编码的重心放在游戏逻辑上。

Packet具体实现

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对应的msgId
2._socket:packet主要是接收网络数据,那么必定是某个connect函数返回的socket。这个socket成员就与之对应

在讲成员函数之前,先看看这个类继承于Buffer对象,大概理解就是一个缓冲区。至于缓冲区干了什么,我们再看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 }; // 
};

看关键成员:char* _buffer;
原来这个缓冲区就是一个char字符数组
并且有一个开始index,和结束index,同时还有一个已经存储数据的大小记录字段_bufferSize;再仔细看,原来这个是个环形的缓冲区,为什么是环形,后面仔细说明。

再看关键函数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长度再分配函数。
1.首先判断——bufferSize是否超出了最大长度MAX_SIZE(假设是1024)。
2.生成一个新的长为 _bufferSize + ADDITIONAL_SIZE(设定为10)的char类型数组
3.声明一个新的名为:_newEndIndex变量 (先不说原因 下面会讲)
4.如果beginIndex < endIndex
如图所示

image.png

那么就将beginIndex到endIndex的数据拷贝到新的char*数组中,此时上面的newIndex就应该是:
_newEndIndex = _endIndex - _beginIndex;

image.png

原来第三点为什么要声明新的endIndex的原因是:因为在重新拷贝数据的时候,将内存重新整理过了

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


image.png

这样就可以看出 原来这是环形的缓冲区,那么这个时候也需要进行数据的拷贝以及内存的重新的对齐


image.png

这里的处理方式是分段进行拷贝,先拷贝beginIndex的,再拷贝endIndex的。
最终结果和上面的一致:


image.png

剩下的步骤就不仔细说了,挺简单的。

这样我们就学习到了环形缓冲区的写法,学会之后,再回到pakcet类当中来。

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];
}

这是构造函数主要干这几件事:注册msgId,赋值connectSocket,初始化buffer。

再看别的函数

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;
}

看完会发现,很简单。甚至没有读数据的逻辑,这是为什么呢?
那么就需要来介绍protobuf了

最关键的两个模板函数:

    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的用法了。
首先假设和客户端需要规定一个消息,这个消息数据结构名叫TestMsg,里面有两个成员 一个msg,一个index


image.png

并且这个消息的msgId为1


image.png

那么在pakcet的封装中,MsgId就是对应的这个msgId,如果是1 那么packet的数据就可以解析成TestMsg的格式。但是如果不解析的话,那么这条数据将会是二进制的,怎么解析数据呢?
就用到了上面两个模板函数
当用户拿到了这个MsgId为1的数据 需要将这个packet反序列化成我们需要的TestMsg结构,只需要调用


image.png

这样protoObj就可以获取到 TestMsg的 id和index字段供我们使用了。

于此同时 另外一个函数是在发送方,想要发一个数据的时候,将这个数据打包成packet,并进行序列化


image.png

这样我们就不需要关心这个数据是怎么变成二进制的,全靠protobuf帮助就行了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容