【Unity】Socket网络通信(TCP) - 发送自定义类/消息类型的区分

上次多人聊天的小项目仅限于发送字符串,这次改进一下,发送一个自定义的聊天消息类,类中包括用户数据和聊天内容。
想看上次的多人聊天小项目的可以点击一下这里。

如何发送自定义类

发送自定义类分三步

  1. 计算类中的成员变量一共占用多少个字节。
  2. 创建相对应大小的字节数组(A),将成员变量序列化成字节数组添加A字节数组中。
  3. 发送A字节数组。

序列化自定义类

因为每一个需要发送出去的自定义类都需要将其序列化,所以可以写一个抽象类,将序列化和计算成员变量一共占用多少个字节的方法写成抽象函数,让所有继承这个抽象类的类重写自己的计算方法和序列化方法。

public abstract class BaseData
{
    
    
    /// <summary>
    /// 获得自定义数据类的字节长度
    /// </summary>
    /// <returns>长度</returns>
    public abstract int GetDataBytesLength();

    /// <summary>
    /// 序列化数据类
    /// </summary>
    /// <returns>返回序列化后的字节数组</returns>
    public abstract byte[] SerializeData();
}

我们可以在这个抽象类里面,写一些序列化常用的变量的方法,例如序列化int、string等类型,继承这个抽象类后可以直接调用,方便变量的序列化。我这里就随便写了几种,常用变量类型(string比较特殊除外)都可以借助BitConverter.GetBytes这个方法来序列化,需要引用System命名空间,这个方法可以将常用的数据类型转化成字节数组完成序列化。
进去BitConverter这个类里面可以看到,GetBytes方法有很多的重载。
在这里插入图片描述
序列化常用类型实现代码如下:

using System;
using System.Text;

public abstract class BaseData
{
    
    
    /// <summary>
    /// 获得自定义数据类的字节长度
    /// </summary>
    /// <returns>长度</returns>
    public abstract int GetDataBytesLength();

    /// <summary>
    /// 序列化数据类
    /// </summary>
    /// <returns>返回序列化后的字节数组</returns>
    public abstract byte[] SerializeData();

    /// <summary>
    /// 序列化int类型数据
    /// </summary>
    /// <param name="bytes">序列化后存放的位置</param>
    /// <param name="data">需要序列化的数据</param>
    /// <param name="index">从bytes中的哪个位置开始存</param>
    protected void SerializeInt(byte[] bytes, int data, ref int index)
    {
    
    
    	//CopyTo方法可以将调用此方法的数组拷贝到bytes中,从bytes的index处开始
        BitConverter.GetBytes(data).CopyTo(bytes, index);
        //一个int数据占用4个字节,所以index加4位
        index += 4;
    }

    /// <summary>
    /// 序列化long类型数据
    /// </summary>
    /// <param name="bytes">序列化后存放的位置</param>
    /// <param name="data">需要序列化的数据</param>
    /// <param name="index">从bytes中的哪个位置开始存</param>
    protected void SerializeLong(byte[] bytes, long data, ref int index)
    {
    
    
        BitConverter.GetBytes(data).CopyTo(bytes, index);
        index += 8;
    }

    /// <summary>
    /// 序列化string类型数据
    /// </summary>
    /// <param name="bytes">序列化后存放的位置</param>
    /// <param name="data">需要序列化的数据</param>
    /// <param name="index">从bytes中的哪个位置开始存</param>
    protected void SerializeString(byte[] bytes, string data, ref int index)
    {
    
    
    	//序列化string需要用Encoding来帮助序列化,这个类需要引用命名空间using System.Text;
        byte[] strBytes = Encoding.UTF8.GetBytes(data);

        //因为string类型长度是不固定的,所以序列化string类型需要先保存string数据转化成字节数组的长度
        SerializeInt(bytes, strBytes.Length, ref index);

        strBytes.CopyTo(bytes, index);
        index += strBytes.Length;
    }

    /// <summary>
    /// 序列化自定义类数据
    /// </summary>
    /// <param name="bytes">序列化后存放的位置</param>
    /// <param name="data">需要序列化的数据</param>
    /// <param name="index">从bytes中的哪个位置开始存</param>
    protected void SerializeCustomClass(byte[] bytes, BaseData data, ref int index)
    {
    
    
    	//因为每个数据类都是继承BaseData的,所以这里用父类装子类传入数据类
    	//直接用数据类实现好的序列化函数进行序列化
        data.SerializeData().CopyTo(bytes,index);
        index += data.GetDataBytesLength();
    }
}

这里用序列化int类型的方法解释一下传入的参数
protected void SerializeInt(byte[] bytes, int data, ref int index)
第一个参数bytes存放计算需要发送自定义类的成员变量序列化后的二进制数据,所有成员变量序列化后都需要存放到这个byte[]里面。
第二个参数data是需要序列化的数据。
第三个参数index是指从bytes数组中的哪个位置开始存。因为bytes数组是存所有数据的大容器,所以每个数据存进bytes数组中都需要记录存了多少个字节,下一个数据保存到bytes数组的时候才能知道要从bytes数组的哪个位置开始存。

需要注意的是,因为string的长度不是固定的,保存string类型的数据到bytes数组中前,需要先保存string转换成字节数组的长度,后面反序列化成数据时,才能清楚接下来的多少位是string的二进制数据。

实现了这个抽象类就可以开始写数据类了,这里写了一个PlayerInfo的玩家数据类,继承BaseData,并实现抽象函数。

using System.Text;

public class PlayerInfo : BaseData
{
    
    
    public long playerID;
    public string name;
    public int level;

	//计算PlayerInfo所包含的数据(playerID,name,level)一共占用多少个字节
    public override int GetDataBytesLength()
    {
    
    
    	//long类型占用8字节
    	//因为需要用一个int来存string类型所占用的字节长度,所以需要多加一个int的长度
    	//字符串需要先转换成字节数组再获取占用字节长度
    	//所以总长度是long(8)+ int(4)+ string + int(4)
        return 8 + 4 + Encoding.UTF8.GetBytes(name).Length + 4;
    }
    
    //实现序列化方法
    public override byte[] SerializeData()
    {
    
    
    	//获取总字节长度,更具总字节长度创建byte[]
        byte[] bytes = new byte[GetDataBytesLength()];
        int index = 0;
        //序列化成员变量
        SerializeLong(bytes, playerID, ref index);
        SerializeString(bytes, name, ref index);
        SerializeInt(bytes, level, ref index);
        //将字节数组返回
        return bytes;
    }
}

增加消息ID,区分消息类型

因为我们这次发送的是包含PlayerInfo玩家数据的聊天消息,所以这里还需要新建一个聊天消息类,因为考虑到后面可能还会发送别的消息,例如组队消息等等,所以需要对消息做一下区分,后面收到消息才能知道是收到了哪个类型的消息,所以这里我给每个消息定义一个int类型的消息ID放到消息的头部,收到消息后,先解析4个字节获取到消息的ID,就清楚收到的是哪个类型的消息了。

这里我们定义一个消息的基类,继承BaseData类。

public class BaseMsg : BaseData
{
    
    
	//因为是消息基类,不需要实现序列化等方法
    public override int GetDataBytesLength()
    {
    
    
        throw new System.NotImplementedException();
    }
    
    public override byte[] SerializeData()
    {
    
    
        throw new System.NotImplementedException();
    }

	//这里新增一个获取消息ID的虚函数
    public virtual int GetId()
    {
    
    
        return 0;
    }
}

实现聊天消息类,代码如下

using System.Text;
using System;

public class ChatMsg : BaseMsg
{
    
    
	//聊天消息类包含玩家数据playerInfo和聊天内容chatStr
    public PlayerInfo playerInfo;
    public string chatStr;

	//实现计算总字节数方法
    public override int GetDataBytesLength()
    {
    
    
    	//因为多加了一个int类型的消息ID,所以需要多加4个字节
        return 4 + playerInfo.GetDataBytesLength() + 4 + Encoding.UTF8.GetBytes(chatStr).Length;
    }

	//实现序列化方法
    public override byte[] SerializeData()
    {
    
    
        byte[] bytes = new byte[GetDataBytesLength()];

        int index = 0;

		//后面收到消息需要先解析消息ID,所以将其序列化放到bytes的头部
		//解析出ID就知道是哪个类型的消息了
        BitConverter.GetBytes(GetId()).CopyTo(bytes, index);
        index += 4;
        
        //序列化playerInfo玩家数据
        SerializeCustomClass(bytes, playerInfo, ref index);
        //序列化聊天内容
        SerializeString(bytes, chatStr, ref index);
        return bytes;
    }

	//获得消息ID
    public override int GetId()
    {
    
    
    	//这里自定义消息ID并返回出去
        return 1001;
    }
}

序列化自定义类就完成了,需要序列化就直接调用SerializeData方法就能获得序列化后的字节数组,将字节数组发送出去就可以了。

客户/服务端Demo源文件:多人聊天室demo(发送/接收自定义的聊天消息类)

未完待续…后续更新接收自定义类!

以上就是这篇文章的所有内容了,此为个人学习记录,如有哪个地方写的有误,劳烦大佬指出,感谢,希望对各位看官有所帮助!

猜你喜欢

转载自blog.csdn.net/weixin_43453797/article/details/128399051