笨木头  2018-04-18 17:36     Unity3D,Game Framework     阅读(12796)     评论(14)
转载请注明,原文地址: http://www.benmutou.com/archives/2630
文章来源:笨木头与游戏开发
本文 Game Framework 版本:3.1.1

本文 Unity3D 版本:2017.3

更多GF教程和实例:https://github.com/mutouzdl/gameframework_demo.git

 
转载请注明,原文地址:http://www.benmutou.com/archives/2630 文章来源:笨木头与游戏开发


关于NetWork这一块,我折腾了两个多星期了,因为每天只有1个多小时的时候,思路很不连续,一度想放弃。

还好坚持下来了,其实NetWork这块的使用不难,只是我自己掉进坑里了。

没错,我要来和大家分享我的坑,以及演示NetWork从连接到发送数据,再到接收数据的处理。

1.大致流程

网络组件的使用大致使用流程如下:

a.创建频道(NetworkChannel)

b.绑定频道辅助器(NetworkChannelHelper)

c.注册频道的消息监听类(PacketHandler),即服务端发送某类消息过来时,客户端有对应的类进行接收处理

d.频道连接服务端

e.使用频道辅助器(NetworkChannelHelper)序列化消息对象,通过频道发送消息

f.服务端接收消息并回应客户端

f.消息监听类(PacketHandler)处理服务端回应的消息

 

在使用上,大致就是这么个逻辑,真的很简单,我也不知道自己是怎么掉坑里的。

由于本Demo的代码较多,不适合把每个细节都贴出来讲解,请大家结合本篇文章查看源码(Demo8)

2.频道(NetworkChannel

频道是和服务端通信的基础,我们可以创建很多个频道,如聊天频道、战斗频道,不同的频道连接不同服务器,适用于有多个逻辑服务器的游戏。

 

创建频道的同时,我们需要把频道辅助器也一起创建,如:

private GameFramework.Network.INetworkChannel m_Channel;

private NetworkChannelHelper m_NetworkChannelHelper;

// 获取框架网络组件

NetworkComponent Network

    = UnityGameFramework.Runtime.GameEntry.GetComponent<NetworkComponent> ();

// 创建频道

m_NetworkChannelHelper = new NetworkChannelHelper ();

m_Channel = Network.CreateNetworkChannel ("testName", m_NetworkChannelHelper);

 

使用Network组件的CreateNetworkChannel函数即可创建频道,第一个参数是频道名称,第二个参数就是频道辅助器。

具体代码看Demo8_ProcedureLaunch的OnEnter函数。

3.连接服务器

连接服务器很简单,创建好频道后,调用Connet函数即可:

// 连接服务器

m_Channel.Connect (IPAddress.Parse ("127.0.0.1"), 8098);

 

至于服务端,木头已经给大家写(抄)了一个了(Demo8_SocketServer),同样是在Demo8_ProcedureLaunch的OnEnter函数可以看到启动服务端的代码。

至于服务端代码的细节,大家可以忽略。

4.频道辅助器(NetworkChannelHelper

其实要理解Network的使用方式,关键就是理解这个辅助器了,木头也就是在这里掉进坑里的。

频道辅助器是可以自定义的,只要实现了INetworkChannelHelper接口即可。

 

当然,为了学习使用方式,我当然是去官方的StarForce项目里找例子了,于是我把StarForce里的NetworkChannelHelper拷过来用了,包括其它的一些必备类,有这些:



其它的文件先不管,NetworkChannelHelper有三个关键的函数:

Serialize函数:用于序列化消息,这里用的是Protobuf。

DeserializePacketHeader函数:用于反序列化消息头。

DeserializePacket函数:用于反序列化消息内容(不含消息头)。
 

不知道为什么,如果直接用StarForce这个项目的NetworkChannelHelper,不做改动的话,无论如何都无法用好这几个函数,反序列化总是出错。

木头本人没用过Protobuf,所以也是一边研究一边了解这个东西。

 

同时,一开始也不是很了解E大(框架作者)的思路,我硬是想要在不改动这个helper类的前提下,把demo跑通。

我失败了无数次,在无数次的失败过程中,稍微熟悉了Protobuf,以及理解了(也可能是误解?)E大的思路。

改,必须得改。

 

再此,我也求助各位,如果我的改动是错误的,或者思路错了,希望大家能给我指点一二,非常感谢。

5.消息包(Packet

在序列化消息之前,必须得有消息包。

Packet类是框架的消息包抽象类,我们的消息包类都要继承它。

 

而在StarForce的项目了,将消息类型区分为了服务端消息和客户端消息:



这个倒不是必须有的,但我依旧按照框架作者的思路来研究。

因此,对于服务端要解析的消息包和客户端要解析的消息包就要区分开来,所以有了CSPacketBase和SCPacketBase类。

分别表示从客户端发到服务端的消息、从服务端发到客户端的消息



6.消息头(PacketHeader

刚刚说的只是消息的内容,按照框架作者的思路,我们还需要有一个消息头。

消息头的作用是告诉消息接收方,当前的消息是什么类型的消息,消息长度是多少。

而我就是在这里陷入了无数次的失败循环,在后面会说到。

 

既然消息包是区分类型的,那消息头自然也要区分,所有有SCPacketHeader和CSPacketHeader,如:



注意上面代码里的注释,这是我掉入的第一个坑。

 

不知道是不是我误解了框架作者的思路,总之,如果我不在消息头的Id和PacketLength对象里加入ProtoMember特性的话,是无法将这两个字段进行序列化的。

包括这个类头上的ProtoContract特性也是我加的,否则序列化时会报错(UnExpectedType)。

 

大家也看到了,消息头主要有一个Id字段和PacketLength字段。

Id字段用来指定该消息是什么类型,它对应的是消息包(Packet)的Id字段。

PacketLength用来指定消息包的长度,以便解析。

 

来总结一下消息头和消息包:

发送消息时,会将消息头和消息包一起序列化后进行发送,而消息头在序列化前会将Id赋值为消息包的Id,将PacketLength赋值为消息包序列化后的长度。

接收消息时,先解析消息头,根据消息头的Id定位消息包类型,然后根据消息头的PacketLength解析消息包内容。

7.NetworkChannelHelper的序列化操作

接下来就是木头遇到的第二个坑——Serialize函数。

一开始的Serialize函数是长这样的:



CSPacketHeader是客户端发送给服务端的消息头,这里仅仅是创建了一个消息头,然后序列化,接着就把消息包也序列化了。我没有看到任何设置消息头Id和packetLength的操作,我以为有什么厉害的地方自动做了这些事情。

 

但是,这里的Serializer.Serialize(memoryStream, packetHeader)执行是报错的,这个刚刚有提过,因为CSPacketHeader没有指定ProtoContract特性,这样是无法用Protobuf序列化的,所以我给CSPacketHeader加上了这个特性。我不知道我的做法是否有误,如果有错误的地方,希望大家能指正

 

加了特性之后是能顺利序列化了,但是,消息头依然没有进行任何赋值操作。

并且,这样序列化的消息头,在解析时也会报错。

 

最终,我自行给消息头赋值,并且改用SerializeWithLengthPrefix进行序列化,整个流程才得以跑通:



 

再一次求助,我对Protobuf的研究非常的浅(时间精力关系),如果这里也有错误的地方,希望大家能打我脸

 

头部消息的8字节是怎么来的呢?

因为CSPackertHeader的Id和PacketLength两个字段(int类型)是参与了序列化的,两个int类型是8个字节。

由于消息头要记录消息内容的长度,所以,我的做法是先把消息内容序列化,然后再获取它的长度。

在序列化消息内容前,先让Position跳过8,给后面的消息头预留位置,这样消息头后续就能插到流的最前面。

 

另外,NetworkChannelHelper的PacketHeaderLength需要返回8(因为消息头的长度是8)。

在客户端接收到消息的时候,会根据这里设置长度来获取消息头。

8.NetworkChannelHelper的反序列化

由于消息头的序列化方式改了,所以NetworkChannelHelper反序列化消息头的操作也要改动:



这里的改动如果有错误的话,也是希望大家能指出的。

 

客户端收到消息时,会先调研DeserializePacketHeader函数解析消息头。

随后再调研DeserializePacket函数解析消息内容,在这一步,消息头的Id就很有用了,我们要根据Id来区分这个消息的类型,然后再做这个类型的反序列化。

 

至于Id是怎么区分出消息类型的,这个就要看NetworkChannelHelper的Initialize函数了,具体大家看代码,这个函数主要做了以下的事情:

a.将继承了SCPacketBase的类型的类Id和类型保存到一个Dictionary里,以便在DeserializePacket函数里可以根据消息头Id获取消息类型。至于为什么只保存SCPacketBase,因为我们客户端只需要接收来自服务端的消息(SCPacketBase就是来自服务端的消息包类)

b.注册继承了PacketHandlerBase的类,也就是很前面提到的消息监听类

c.订阅一些事件

9.消息监听类(PacketHandler

最后但可能是最重要的一个,就是消息监听类,能发送消息,能接收消息,最终要做的当然就是对消息的处理。

消息监听类就是用来处理不同消息的类,因此,我们可能会有很多很多这样的类。

 

上一步提到,在NetworkChannelHelper的Initialize函数里会注册消息监听类——也就是那些继承了PacketHandlerBase的类。

比如我们的Demo8_HelloPacketHandler:

using GameFramework;

using GameFramework.Network;

using StarForce;

public class Demo8_HelloPacketHandler : PacketHandlerBase {

public override int Id {

get {

return 10;

}

}

public override void Handle (object sender, Packet packet) {

SCHello packetImpl = (SCHello) packet;

Log.Info ("Demo8_HelloPacketHandler 收到消息: '{0}'.", packetImpl.Name);

}

}

这里的Id也是有讲究,一定要和消息类的Id对应,比如我们这个Handler是用来处理Hello消息的,所以Id必须和SCHello类的Id一致:

public class SCHello : SCPacketBase {

public override int Id {

get {

return 10;

}

}

[ProtoMember (1)]

public string Name { get; set; }

public override void Clear () {

}

}

只有Id一致才能正确处理对应的消息。

10.结束

在Network组件上,我大致就是遇到了这些问题,其余的地方,大家看代码就可以了(其实也没有多少代码了)。

服务端代码大家不用管,它在接收到客户端消息后,会回发一条SCHello的消息过来。

大家拉了代码后,运行Demo8的Demo8_Launch场景,若能看到下面的日志,就代表成功了:



 
14 条评论
  • hfxxxxx    2020-12-17 20:21:49

    跟了下代码,消息头序列化的时候会报错跳过,发一个空包过去。不发这个消息头,服务器那头直接去取消息体就是OK的。
    回复
  • 笔记    2020-05-29 03:14:09

    ProtoMember特性是谷歌的Protobuf插件的必须特性 主要用来反序列化时反射包体的变量的
    一般来说包头都只带一个ID就够了 解码时只要知道ID的变量就知道长度的(常用的比如ID是:int:长度4,ushort:长度2)
    所以。。。。
    回复
  • het    2019-06-22 11:31:23

    请问大佬,不想用框架的网络库,自己用GO写网络库的话,是不是C#原生的加载DLL 调用DLL函数就可以了。
    回复
    • 笨木头    2019-06-24 07:32:06

      可以的,但是GO我没用过,能确保兼容你需要的平台就可以了
      回复
  • 666666666    2019-03-28 11:41:43

    多亏了大佬的教程,不然以我自己的悟性....悟一年估计也悟不出来个所以然
    回复
  • zhang    2019-03-07 20:47:41

    在CSPacketHeader,加[ProtoContract]特性, 然后在SCPacketHeader,加[Serializable, ProtoContract(Name = @"SCPacketHeader")] 特性,就可以了。

    NetworkChannelHelper 得 Serialize 方法没有修改。也可以成功
    回复
    • zhang    2019-03-16 16:06:58

      傻逼了,服务器返回得包头大小搞错了,折腾了好几天
      回复
      • GQ    2019-09-02 18:18:08

        他这个demo里的服务端代码是不是有问题?
        回复
      • GQ    2019-09-02 18:19:40

        您好,请问demo的服务端代码是不是有问题?
        回复
  • 郭陆朋    2018-12-07 11:10:05

    给作者点个赞 你这些教程真的可以让新人省了好多时间
    回复
    • 笨木头    2018-12-09 17:05:53

      谢谢。不过网络这块似乎不适用于新的版本了(3.1.6),暂时没精力更新,大家注意一下本教程对应的版本是3.1.2
      回复
  • xpg    2018-04-21 15:40:27

    木头快更新啊,最重要的实体,还有状态机还没介绍一下呢
    回复
    • 笨木头    2018-04-21 17:38:47

      实体介绍过了(比较简略),状态机倒是没有,我最近想先用这框架写个小游戏,这样教程写起来没那么空洞...
      回复
    • 笨木头    2018-04-27 21:41:04

      状态机Demo已经发布了,欢迎食用:http://www.benmutou.com/archives/2643
      回复
发表评论
粤ICP备16043700号

本博客基于 BlazorAnt Design Blazor 开发