本篇博客主要介绍数据发送和接收时遇到的粘包和分包现象,以及实现服务端解析收到消息的代码,本节的代码是在第(一)部分的基础上进行的,可以先浏览下第一部分https://blog.csdn.net/s1314_jhc/article/details/80914044
源码下载地址:https://download.csdn.net/download/s1314_jhc/10545585
1. 粘包和分包
粘包和分包是利用Socket在TCP协议下内部的优化机制。粘包指的是发送数据比较频繁,但数据量较少,此时客户端不会直接将数据包发送给服务器,而是会与其它的数据包进行一个结合,例如游戏中的位置信息就是属于频繁发送但数据量小的信息,此时如果每条数据都Send一次,特别耗费性能,将几个数据包粘合再发送,这样服务端只需要调用一次Receive即可接收完毕。
分包是指发送数据量较大时,分成多个数据包进行发送,这么做可以减少网络传输的压力。并且当数据包发生错误,需要重传时,分成小包发送可以减少传输时间。
1.1 粘包
举例:
以第(一)部分1.2.2为例,展示一下分包的作用,在客户端中修改之前的while代码
//向服务端发送数据
for (int i = 0; i < 100; i++) //直接发送100条数据给服务端,代替原有的手动发送
{
byte[] dataSend = Encoding.UTF8.GetBytes(i.ToString()); //字符串转为bype数组发送,用到using System.Text;
clientSocket.Send(dataSend);
}
如果顺利发送100次并且不产生粘包,服务端中的
Console.WriteLine("接收到的消息为:" + msg);
代码应该输出100次
但实际的结果如下,100个数据仅通过三个包进行发送。
1.2 分包
为了演示分包,设置一个很长的string变量,发送给服务端。
string s = "风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计风格几个人你看过呢人家我看过呢人口估计";
clientSocket.Send(Encoding.UTF8.GetBytes(s));
结果如下
可以看出s数组发送给服务端时,通过两个红字的标识,表示服务端把它拆分成了两个包,但在蓝色方框部分,由于汉字占据四个字节,因此进行了拆分,变成了三个问号。
2. 考虑分包粘包情况下的数据发送与解析
如果出现了粘包/分包现象,那么在服务器端接收消息的时候很难判断接收到的消息是否是完整的,之后对消息的处理也会存在问题。
2.1 客户端发送数据的优化
因此解决数据分包和粘包的基本策略如下
1.消息定长,比如定一个100,那么读取端每次读取数据就截取100个长度的数据,然后交给业务成去做解析
2.在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。
3.读取缓存的数据是不定长的,所以我们把读取到的数据添加到我们自己的一个byte[]数组中,然后根据我们的业务逻辑来找到指定的特殊协议头部,协议长度,协议尾部,然后从我们的byte[]中获取一个完整的数据包,然后再对数据包进行业务解析就可以得到正确结果。
在这里采用第3种方法,在客户端新建一个类
class Message
{
public static byte[] GetNewByte(string data)
{
byte[] dataByte = Encoding.UTF8.GetBytes(data); //数据转换成字节数组
int dataLength = dataByte.Length; //数据长度
byte[] byteDataLength = BitConverter.GetBytes(dataLength); //数据长度也转换为字节数组
byte[] newDataByte = byteDataLength.Concat(dataByte).ToArray(); //将数据和长度连接成新的字节数组
return newDataByte;
}
}
在客户端发送时调用,注意到GetNewByte方法设置为静态工具类(static),这么做的原因是:静态类在调用的时候不用实例化(不需要new),执行函数体内容时不保存中间数据,相当于拿个扳手旋了一下螺丝,无论是谁在使用,操作步骤都是一样的,静态类的使用具体可以参见 https://www.cnblogs.com/franky2015/p/4757792.html
并在客户端发送时将
for (int i = 0; i < 100; i++)
{
byte[] dataSend = Encoding.UTF8.GetBytes(i.ToString()); //字符串转为bype数组发送,用到using System.Text;
clientSocket.Send(dataSend);
}
改为
for (int i = 0; i < 100; i++)
{
clientSocket.Send(Message.GetNewByte(i.ToString()));
}
以测试粘包的情况,服务端接收情况如下
图中□的乱码是因为在保存字节长度的时候使用的是BitConverter编码方式,服务端使用的是UTF8,在接收时无法正确解析。因此服务端要根据接收的消息,分割出每条数据的长度。
2.2 服务端接收数据的优化
搞定了客户端的发送,现在解决对字符数组的解析问题,在服务端创建一个类,命名为MsgRecv,用以存储并解析接收到的数据
class MsgRecv
{
private byte[] data = new byte[1024]; //确保一条完整的数据可以存放
private int startIdx = 0; //表示数组中已经存储了多少字节的数据
public byte[] Data //获取的值
{
get { return data; }
}
public int GetStartIdx //开始的位置
{
get { return startIdx; }
}
public int RemainSize //data中剩余数据
{
get { return Data.Length - startIdx; }
}
}
服务端的存储是通过
clientSocket.BeginReceive(dataBuffer, 0, 1024, SocketFlags.None, ReceiveCallBack, clientSocket);
进行,因此将上式修改为
static MsgRecv message = new MsgRecv(); //新建一个MsgRecv对象,用来存储
clientSocket.BeginReceive(message.Data, message.GetStartIdx, message.RemainSize, SocketFlags.None, ReceiveCallBack, clientSocket);
此时通过BeginReceive开始接收数据,接下来处理接收数据后的处理,首先是接收到数据后startIdx的更新,
public void AddCount(int count)
{
startIdx += count;
}
解析数据通过MsgRecv中的一个方法实现,每次调用ReceiveCallBack时执行。
public void ReadMsg() //完成一条消息的解析
{
while(true)
{
if(startIdx <= 4) return; //小于4表示信息不完整,不解析,继续接收下一条
int count = BitConverter.ToInt32(data, 0); //读取数据长度,0表示startIdx的位置
if ((startIdx - 4) >= count)
{
string s = Encoding.UTF8.GetString(data, 4, count);
Console.WriteLine("解析出一条数据:" + s);
Array.Copy(data, count + 4, data, 0, startIdx - 4 - count); //接收完数据后将其余数据往前移,便于下一次解析
startIdx -= (count + 4); //更新startIdx
}
else
{
break;
}
}
}
至此完成了解析的功能,打开服务端与客户端进行测试,可以看到服务端解析出了客户端发出的100条消息。