上次多人聊天的小项目仅限于发送字符串,这次改进一下,发送一个自定义的聊天消息类,类中包括用户数据和聊天内容。
想看上次的多人聊天小项目的可以点击一下这里。
如何发送自定义类
发送自定义类分三步
- 计算类中的成员变量一共占用多少个字节。
- 创建相对应大小的字节数组(A),将成员变量序列化成字节数组添加A字节数组中。
- 发送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(发送/接收自定义的聊天消息类)
未完待续…后续更新接收自定义类!
以上就是这篇文章的所有内容了,此为个人学习记录,如有哪个地方写的有误,劳烦大佬指出,感谢,希望对各位看官有所帮助!