3.2 游戏网络通信开发
网络游戏开发过程缺少不了通信内容,网络细节的处理在框架中已经有了完整的实现,详细内容可以参考框架中网络模块章节。本节主要介绍如何利用框架封装好的网络消息处理机制来发送和接收消息。
NetWorkManager是核心的网络处理模块,处理客户端网络相关的所有功能。这里介绍如何通过NetWorkManager连接服务器端、接收消息。
3.2.1 设置服务器信息
通过Init初始化函数,可以设置服务器端IP地址、端口等。本节调用此函数来指定连接的服务器。代码如下所示。
在这里涉及几个参数,如服务器地址、端口号等。接下来,介绍每个参数的意义。
◎地址与端口
Init函数的前两个参数指定服务器地址与端口。服务器地址是一台主机的标识符,一般将其定义为字符串类型;端口号用来标识主机中的某一个进程,通常将其定义为Int类型。代码如下所示。
◎服务器类型
本游戏的服务器分为3种类型:登录服务器(LoginServer),平衡服务器(BalanceServer),网关服务器(GateServer)。各种服务器的功能介绍如下。
□ LoginServer:登录服务器,负责校验账号与密码,并返回所有可用的服务器的列表。
□ BalanceServer:平衡服务器,记录客户端登录情况并返回网关服务器的IP与地址。
□ GateServer:网关服务器,负责功能逻辑,所以最终客户端要连接的服务器是网关服务器。
◎是否接收网络消息
最后一个参数是个布尔值,表示是否接收消息。False代表在执行原来的处理消息函数;True代表可以在任意地方接收消息,需要注意的是必须提前注册接收消息的事件。
3.2.2 网络信息处理
当服务器地址与端口设置完成后,需要在脚本的Update函数逐帧处理网络通信信息,包括服务器连接检测、消息的收发。因此,需要在Update函数中调用如下代码。
3.2.3 消息序列化与反序列化
在通信过程中,为了尽可能地缩短消息体的长度,通常消息是以二进制流进行传递的,在消息发送时,需要将消息体编码为二进制流;在消息接收后,需要对消息进行解码,并反序列化后生成对象。为了进一步介绍序列化和反序列化的基本原理,下面用一个示例来学习如何对消息体进行序列和反序列化的操作。这个示例的目标是将包含人物ID与name的对象进行序列化与反序列化操作。
◎消息序列化(创建消息体)
c#自带序列化模块,可序列化的消息类型有很多种,比如所有继承UnityEngine.Object的类、所有的基本数据类型、Unity自定义数据类型(需带有Serializable标签),静态字段与属性则不能序列化。下面通过示例自定义序列化的类型和格式,来介绍如何序列化和反序列化一个对象,完整地展示用自己的代码进行编解码的过程。
首先,新建一个脚本TestSerialize,双击打开。在脚本中创建一个类Info,包含两个字段,ID与name,并在TestSerialize中的Start函数中创建Info的对象并赋值,如下所示。
小提示
Int32需引用Systerm的命名空间。
◎消息序列化(序列化编码实现)
在此脚本中新建一个类,命名为SerializeTool,并挂载到场景中的Camera,接下来在此类中创建序列化与反序列化的工具函数。序列化函数Encode的功能是将消息体写入数据流。序列化的规则是通信双方定义好的,Info类对象要对ID与name字段进行序列化,序列化后的格式如图3-3所示,前四个字节为ID,第五个字节为name字符串长度,从第六个字节开始是name的详细内容。因为id为Int32类型,表示是32位整数,所以包含32个bit,每个字节包含8个Bit,因此需要4个字节存储数据。这里假定字符串不超过一个字节的长度,最大为255。
图3-3
Encode代码的序列化主要利用内存流MemoryStream中Write函数,将数据转换到二进制并存储在MemoryStream对象中,因此创建一个MemoryStream。MemoryStream派生自基类Stream,实现了对内存数据读/写的功能。其中Write函数将值从缓存区写入MemoryStream流对象。WriteByte函数负责从缓存区写入MemoytStream流对象一个字节。使用Write时需要添加三个参数,如下所示。
□ buffer:表示要写入的字节数组。
□ offset:是指Buffer中的字节偏移量,从此处开始写入。
□ count:是指最多写入的字节数。
小提示
使用MemoryStream时需要引用System.IO的命名空间。
第一个写入数据是Value中的ID字段。Value指代的是创建的Info,在Start函数中需要用Encode函数。因为Write函数写入的是字节数组,所以需要先将ID转换为字节数组,利用Systerm中的BitConverter.GetBytes可以实现将Int32转换为字节数组,再写入ms。写入完成后,ms中包含着ID对应的字节流内容,如图3-4所示。这是执行到第一次写入ID时ms中的数据,通过打断点获取。
图3-4
第二个写入的数据是name的长度,利用WriteByte写入。它与Wirte函数的区别是写入一个字节,且直接写入获取名称的长度。写入后的结果如图3-5所示。
图3-5
第三个写入的数据是name本身,同样利用Write函数。首先将name字符串转换为字节数组nameData。利用System.Text.Encoding.UTF8.GetBytes将字符串转换为字节数组。写入完成后如图3-6所示。
图3-6
小结
以上便是对象序列化的过程,简单来说是将字节数组中的数据写入MemoryStream中。那么如何将MemoryStream中二进制的数据流转化为对象呢?接下来介绍反序列化过程。
◎消息反序列化
反序列化的过程与序列化的过程都是以MemoryStream为媒介,反序列化过程是利用Read函数读取字节数组后转换为相应的数据。反序列化对象过程代码如下所示。
Read使用的语法:
buffer:将读取的内容输出到字节数组。
offset:读取字节数组位置的偏移量。
count:要读取的字符数。
创建一个Info对象,用来保存反序列化后的结果。调用此函数时需要在Start中调用Decode函数进行反序列化,参数是字节数组,这里可以使用Encode函数得到字节数组。
然后读取4个字节,转换为Int32类型。先定义一个字节数组,用于缓存读取的ID数据。再利用BitConverter.ToInt32将字节数据转换为Int32类型的ID。
再次读取name数据并将其转换为字符串。在创建缓存字节数组时,需要知道name的长度,所以要先获取name的长度。根据长度去创建大小适合的数据缓存区。读取数据到缓冲区。完成后利用System.Text.Encoding.UTF8.GetString将字节数组转换为字符串,并存储在name字段中。结果如图3-7所示。
图3-7
◎ProtoBuffer反序列化应用
对于消息的反序列化处理,本书利用的是PortocolBuffer中的工具。Message Decode封装了反序列化的过程,因此可以直接通过此函数对消息进行反序列化,使用方式如下:
尖括号中表示需要传入消息的类型,消息类型是客户端与服务器端共同定义好的通信协议,如AskGateAddressRet。本书中涉及的消息体类型可以直接通过工程作为参考。
小括号中需要传入客户端接收到的消息体,此消息体是二进制的数据流。通过此方式转换后返回真实的消息体。
小提示
ProtobufMsg在Common.Tools命名空间内,并且不同的消息体类型包含在不同的命名空间内,所以需要引用命名空间。快速引用命名空间的方法:鼠标定位在消息类型上或者ProtobufMsg上,单击鼠标右键,选择Resolve→Using...命令,便可以快速添加命名空间。