MMORPG游戏核心技术之网络通信-接收篇(一)

发表于2016-05-21
评论1 4.5k浏览

序言

时间如梭,岁月是一把杀猪刀,一晃眼从一个偏偏少年,已经变成了一位标准大叔了。在游戏行业滚摸爬打已经快9年了。之前一直梦想着做一款家喻户晓的游戏。惭愧,一直到现在还没有实现自己的梦想。我还在自己的梦想的路上,与大家共勉之。

不得不说的事,经历了十年的游戏进化史,经典的MMORPG模式已经被改的面目全非,十个开发商九个都在估量着换个美工即得一款新的网游,再也找不回当年玩《传奇》与《奇迹》的一丝感觉,也怀念大学的时候省着钱买月卡玩游戏的日子。悲哀,悲剧!

然而令大家欣慰的是客户端技术日益变化,3D网游逐渐向高画质,高细节,多操作方向发展,玩家们已经能享受更好的视觉体验,更无与伦比的操作。相信在灿烂的2016年,会出现更多好的游戏。

本文以unity引擎为例,阐述大型MMORPG游戏中遇见的核心技术。


正文



网络通信,大家听起来肯定都不陌生,在我认识中很多人使用protobuf作为网络消息协议,网上关于protobuf的介绍很多,这里我不在做进一步描述,当然也有使用skynet和sproto协议。我下面要介绍的是如何使用c#完成自定义的协议。因为unity开发脚本最好的语言就是C#了,至少C#是我见过最优美的语言。本架构使用的是流套接字,协议为tcp协议。(想了解TCP协议、UDP协议、流套接字、数据报套接字、原始套接字的同学可以自行了解)

 

自定义协议的优势

   1、性能好,效率高

   2、完全可控

   3、纯原生态C#,可读性

 
自定义协议的缺点

   无(重新写一套还是需要一点点时间的!)


函数介绍

   Socket类:Socket 类为网络通信提供了一套丰富的方法和属性。

        BeginReceive方法:socket类提供的异步接送消息的方法。

   EndReceive方法:socket类提供的异步接收消息完之后回调的方法。

   Stream类:Stream 是所有流的抽象基类。 流是字节序列的抽象,例如文件、输入/输出设备、进程中通信管道或 TCP/IP 套接字。

   对,我们需要就是这么几个类还有函数!So easy!


 整套类架构

 


流程

       



NetRunTime类

  Start函数:实例化Socket。使用的是流套接字,协议使用Tcp长连接。大家可能看见了有一个m_bUseWenSocket字段,这个字段是用来控制是否使用Unity5.0新加的功能WebGl的通信的。关于这一段,之后我会在新的章节进行介绍。需要了解的朋友也可以留言!

1
2
3
4
5
6
7
8
9
10
11
12
public void Start(bool bClient)
{
    if (!GameSet.m_bUseWebSocket)
    {
        m_socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        m_nSend = new NetAsynSend(m_socketClient, m_bClient ? (uint)4096 : (uint)65536);
        //接收的可能多一些
        m_nRece = new NetAsynRecevice(m_socketClient);
        m_nSend.Start();
        m_nRece.Start();
    }
}

   DispatchServerMsg函数:NetAsynRecevice中获取需要处理的NetMssage消息,并且将消息保存到列表m_listRecevice中。之后回调消息中的Onrecv()方法进行逻辑处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void DispatchServerMsg(float fTime, float fDTime)
{
    m_nRece.GetReceviceMsg(ref m_listRecevice);
    foreach(NetMessage msg in m_listRecevice)
    {
        if (msg != null)
        {
            if (!msg.OnRecv())
            {
                Debug.LogWarning("消息处理失败----" + msg.m_uID.ToString());
            }
        }
    }
    m_listRecevice.Clear();
}


NetAsynRecevice类

ReceviceMessage函数:调用BeginReceive,这里最大接收的长度为SocketStream.DEFAULTSOCKETMAXBUFFER,也就是8192,为什么是8192?我也给不出最好的理由,就跟服务器为什么在接收线程开CPU*2 + 2一样,我只能在同屏多人,消息量特别大的时候,测试,这个数字的效率是最好的。关于为什么使用BeginRecive,而不使用Recive?异步嘛,不堵塞!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void __ReceviceMessage()
{
    //接受数据
    if (!GameSet.m_bUseWebSocket)
    {
        if (m_socket != null)
        {
            m_socket.BeginReceive(m_packStream.m_bytes, 0, SocketStream.DEFAULTSOCKETMAXBUFFER, SocketFlags.None, new AsyncCallback(this.ReceviceEnd), null);
        }
    }
    else
    {
        int len = m_webSocket.Recv(ref m_packStream.m_bytes);
        if (len != 0)
        {
            //填充消息
            m_socketStream.Fill(m_packStream, len);
            //产生消息          
            m_socketStream.GenMsg(m_listMsg);
        }
    }
}

   ReceviceEnd函数:BeginReceive的回调,功能只有二个:一、将m_packStream.m_bytes接收到的数据保存到存储区。二、将存储区的数据解析成NetMessage,并且保留m_listMsg中。这里有个字段m_bRecing开关,再接收的时候设置为true,接收完之后设置为false,以保证只能接受同一消息.

1
2
3
4
5
6
7
8
9
10
private void ReceviceEnd(IAsyncResult ar)
{
    int num = m_socket.EndReceive(ar);
    //填充消息
    m_socketStream.Fill(m_packStream, num);
    //产生消息          
    m_socketStream.GenMsg(m_listMsg);
    //处理完消息再接受消息
    m_bRecing = false;
}


SocketInputStream类

   Fill函数:将_packStream.m_bytes接收到的数据保存到存储区的具体实现。函数中存在一个存储概念。m_Head为存储消息的头位置,m_Tail为存储消息的尾位置,m_BufferLen为整个消息的存储长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public bool Fill(SocketStream stream, int recLen)
{
    int nFree = 0;
    if (m_Head <= m_Tail)
    {
        // H   T        LEN=10
        // 0123456789
        // abcd......
        nFree = m_m_BufferLen - m_Tail;
        if (nFree >= recLen)
        {
            //直接填充数据
            m_steam.Seek(m_Tail); //尾部写入数据
            m_steam.Write(ref stream.m_bytes, recLen);
            m_Tail += recLen;
        }
        else
        {
            if (!ReSize(recLen + 1))
                return false;
            m_steam.Write(ref stream.m_bytes, recLen);
            m_Tail += recLen;
        }
    }
    else
    {
        //这种清空应该不会出现
        Debug.LogError(string.Format("socket通信出现错误!!{0},{1}", m_Head, m_Tail));
        //     T  H     LEN=10
        // 0123456789
        // abcd...efg
    }
    return true;
}

GenMsg函数:将存储区的消息解释成为NetMessage。整个的函数最重要的处理是将流数据放入到一个消息解析类里面进行二进制解释,对应将解析到的NetMessage.具体的解释思路很简单。比如战斗模块+释放技能.对应的mainKeyRootCombat,对应的subKeyUseSkill。之后你就能得到需要处理的消息类GSCL_UseSkill。当然朋友们别忘记组包了,再消息没有接受完之后需要等到下次消息接收后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private bool GenMsg(BetterList lstMsg)
{
    //如果不够读出一份完整数据。回读
    int iMinSize = StringHelper.s_ShortSize;
    m_steam.Seek(m_Head); //头部读数据
    int nNeedReadBuffer = m_Tail - m_Head;
    ushort uLen = 0;
    if (nNeedReadBuffer < iMinSize)
    {
        return false; ;
    }
    uLen = m_steam.ReadUShort();
    //等待组包
    if (nNeedReadBuffer < uLen + 2)
    {
        m_steam.Seek(m_Head);
        return false;
    }
    ushort uId  = m_steam.ReadUShort();
    //一级消息解释。获取对应的cmdRootBase类
    CmdRootBase cmdRoot = CmdRootSinkManager.Instance().getCmdRootById(uId);
    //在对应的cmdRootbase进行解析,获取对应的类,返回对应的类
    NetMessage msg = cmdRoot.ByteToNetMessage(ref m_steam);
  
    if (NetMessage.m_NeedCrypter)
        msg.m_uDataLenght = (ushort)(uLen - STREAM_KEY_XXTEA.GetSize());
    else
        msg.m_uDataLenght = (ushort)uLen;
    //类里面进行对应的字节解释,将服务器数据保存到相应的类数据里面
    if (!msg.FromByte(ref m_steam))
    {
        //放弃这条消息
        m_Head = m_Head + uLen + 2;
        m_steam.Seek(m_Head);
        Debug.LogWarning("消息的字节处理出现了问题" + msg.GetType().Name);
        return true; //
    }
    //将类保存到lstmsg列表,之后统一处理
    lstMsg.Add(msg);
  
    //继续解释下一个类消息
    m_Head = m_steam.GetOffset();
    return true;
}



总结

    自定义消息协议,思路很清楚,实现的方式也不是很复杂。当然这一套我也经历过几个项目使用,总体来说上手简单。如果有什么不明白的也可以留言。我这边会找时间回答。下一篇,将会给大家介绍发送篇。


 
放松篇

最近的几张图片再程序员之间疯狂的传播。程序员下班与市场部下班。

一花一世界一叶一菩提!

 



如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论