引言

做Unity项目 使用C#的Socket与服务器进行通信时,消息的解析有多种方案,比如protobuffer、Marshal、或BindaryReader/BindaryWriter等,但各有优缺点,再考虑到加/解密、压缩与解压缩、跨平台,最终还要考虑性能,其实最合适的方案只有一个。

 

Socket的处理

         C#提供了两种方式的连接、收发数据的方式,一种是同步处理,一种是异步处理。

  1. 同步处理方式,需要自己使用多线程进行处理。连接时需要启动一个线程,发消息时启动一个线程,收消息启动一个线程。这样更可控,但如果经验不足,也容易出Bug,再者Unity(Mono)对线程的支持并不那么完美(可能出现线程挂起后,无法恢复),导致致命Bug。
  2. 异步处理,可以减少对线程的依赖,建议使用这种方式。

 

关于连接的处理,这里暂时不细述。

 

基于ProtoBuffer的协议

Protobuffer是个很好的方式,提供了一套完整的解决方案,有描述文件,有转换工具,多语言支持,使用简单,易于维护。

但protobuf性能损耗比较高,对于通信量大且频繁的游戏,就不太适用。

而且处理加密/解密、压缩/解压时,还要做额外的处理,并且累加了性能消耗,使性能降到不可接受的范围。

基于Marshal与C#序列化的协议

c/c++在序列化或反序列化内存二进制数据时,相对比较容易,大部门情况下,只需对内存块和Struct进行操作即可,无需编码太多解析代码。使用Marshal就是类型这种方,反序列过程是从托管内存传Byte[]与Struct进去,返回反序列好的Struct对像;序列化过程是把Struct对象传入非托管内存,转化为Byte[]。

         具体实现方式这里不再陈述,百度上随便一搜就可以看到很多例子。

         这种方式,实现起来也相对比较容易,解析性能比较高,特别是对比较大且复杂的消息。但托管内存与非托管内存的转换过程,会产生相应大小的临时对象,在Unity中会触发GC,导致性能折半。

         一个致命的问题,如果结构中含有数组,会触发JIT,iOS系统只允许AOT,所以一但需要处理数组(非值类型的字段)在iOS上就会直接Crash。

         因此,这种方式并不是一个好的选择,更不是通用的选择。

 

基于MemoryStream的协议解决

         在C#的System.IO命名空间下,有BinaryReader与BinaryWriter两个类,分别处理对MemeoryStream的读写。

         我们从Socket中接收到Byte[]后,通过种方式按读出每个字段,实现反序列化:

 

const int
Byte[] dataFromSocket = new Byte[MAX_BUFFER_SIZE];
 
MemoryStream ms = new MemoryStream();
BinaryReader br = new BinaryReader(ms);
            ms.Write(dataFromSocket, 0, MAX_BUFFER_SIZE);
 
            br.ReadInt16();
            br.ReadBoolean();
            br.ReadChars(10);
            ....
        
 
把C#对象,按顺序写入字段的值,实现序列化生成Byte[],并传给Socket发送出去:
MemoryStream bwms = new MemoryStream(MAX_BUFFER_SIZE);
BinaryWriter bw = new BinaryWriter(bwms);
 
            bw.Write(0);//有很多重载,可以支持很多数据类型的写入
            ...
            bwms.ToArray();//转化为Byte[],通过Socket发送

    这种方式需要按照字段的顺序进行读取或写入,如果是通过程序员手动去写,工作量相当大的,面且很维护,增/删字段简直会让人崩溃,面且非常容易出错。

然而,我们可以通过编写一个工具,把protobuffer的消息定义文件(或别的定义方式)转化为有序的读/写代码,就像protobuffer生成的类文件一样。

这种方式,相比前两种,性能是最好,不但读/写方便,而且可以控制中间临时内存的创建数量(可使用对象池),大大减少Unity的GC。

 

目前我现在的项目主要使用这种方式,并开发了相应的工具。

 

Lua中使用的消息解析

         Lua也有多种方式来解析Socket二进制消息,比如逐字段解析、或使用protobuffer(protoc-gen-lua)等。

         逐字段解析需要自己开发工具,并编写解析代码,目前没采用。

        

目前推荐使用protoc-gen-lua结合pb.c来处理,方案比较成熟,可参考网上的教程。

 

 

结语

         对与unity的socket通信,还有非常多的细节,这里暂未提及