基础知识:
TCP/IP 网络协议栈分为应用层(Application)、传输层(Transport)、网络层(Network)和链路层(Link)四层
通信过程中,每层协议都要加上一个数据首部(header),称为封装(Encapsulation),如下图所示
如图:
产生原因:
数据发送端发送数据给缓冲区Buffer太大,导致一个完整的数据包被分为几部分发送给Buffer,然而缓冲buffer等到数据满了以后会自动把数据发送的数据链路层去,这样就导致分包了。
TCP协议定义有一个选项叫做最大报文段长度(MSS,Maximum Segment Size),该选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。在一定程度上MSS应该能尽可能多地承载用户数据,用于在传输通路上又可能避免分片,但是在复杂的网络环境下确定这个长度值非常困难,那么在这样的情况下在传输过程中产生分包,粘包就很常见了
数据帧的有效载荷(payload)比以太网的最大传输单元(MTU)大的时候,进行了IP分片
解决方案:
1.消息定长,比如定一个100,那么读取端每次读取数据就截取100个长度的数据,然后交给业务成去做解析
2.在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。
3.读取缓存的数据是不定长的,所以我们把读取到的数据添加到我们自己的一个byte[]数组中,然后根据我们的业务逻辑来找到指定的特殊协议头部,协议长度,协议尾部,然后从我们的byte[]中获取一个完整的数据包,然后再对数据包进行业务解析就可以得到正确结果
大多数的应用中,你说是定长消息,不太实用。而特殊字符就要根据客户的需求书要求了,还有行业标准,想来想去还是用第三种方案:
我同事说,为什么不一个字节一个字节读取,读到协议头部就开始组装,根据协议读取整体长度还有包尾,读不到我需要的包头我就全部扔掉,一开始我觉得挺好,因为感觉socket连接报文不是很频繁,不会出现太大量的数据,应该不会出现粘包,分包的情况,大部分会每次读取正好是一条完整的数据,但是根据墨菲定律来说:凡是可能会出错的就一定会出错。
第一步,定义一个缓存
public byte[] Buffer { set; get; }
public int DataCount { set; get; }
public MsgPackProcess(int buffSize) {
Buffer = new byte[buffSize];
DataCount = 0;
}
第二步,缓存的写入方法
///
/// 获取剩余字节数
///
///
public int GetReserveCount() {
return Buffer.Length - DataCount;
}
///
/// 写入缓存。判断缓存的大小
///
///
///
///
public void WriteBuffer(byte[] buffer, int offset, int count) {
if (GetReserveCount() >= count)
{
Array.Copy(buffer, offset, Buffer, DataCount, count);//
}
else {
int totalSize = Buffer.Length + count;//- GetReserveCount()总大小-空余大小
byte[] tempBuffer = new byte[totalSize];
Array.Copy(Buffer, 0, tempBuffer, 0, DataCount);//复制以前的数据
Array.Copy(buffer, offset, tempBuffer, DataCount, count);//复制新写入的数据
//DataCount = DataCount + count;
Buffer = tempBuffer;//替换
}
DataCount = DataCount + count;
}
//写入缓存数据
public void WriteBuffer(byte[] buffer){
WriteBuffer(buffer, 0, buffer.Length);
}
第三步,缓存的读取,因为socket我这是长连接,所以只要有数据传输,我就会写入缓存,同样只要是缓存有数量在协议内的数据我就会读取,所以,轮询读取:
while (true)
{
lock (client.revobj) //因为是对同一个缓存操作,为了避免数据混乱,使用线程锁
{
if (client.mp.GetDataCount() >= 6) 长度达到标准开始执行
{
tmp = client.mp.Buffer;
for (int i = 0; i < tmp.Length; i++) 找到包头的位置
{
if (tmp[i] == 0xeb)
{
index = i;
break;
}
}
if (index == -1) 这一步,在考虑要不要删掉,我想每次取完一个完整的信息包,如果缓存里不
{ 存在包头,就是信息混乱,把他初始化,然后继续轮询
client.mp.Buffer = new byte[0];
client.mp.DataCount = 0;
continue;
}
try {
bytes = tmp.Skip(index).Take(1).ToArray(); 根据协议取包头
header = nt.ByteArrayToHexString(bytes);
bytes = tmp.Skip(index + 1).Take(1).ToArray(); 取类型
msgType = nt.ByteArrayToHexString(bytes);
if (msgType != "31" && msgType != "32" && msgType != "33" && msgType != "34" && msgType != "35" && msgType != "36") //判断类型是否在协议内 如果不在去除上一个包头,继续轮询下一个包头位置
{
byte[] ot = new byte[client.mp.GetDataCount() - 1];
Buffer.BlockCopy(tmp, index + 1, ot, 0, ot.Length);
client.mp.Buffer = ot;
client.mp.DataCount = client.mp.Buffer.Length;
continue;
}
bytes = tmp.Skip(index + 2).Take(1).ToArray();
msn = nt.ByteArrayToHexString(bytes);
bytes = tmp.Skip(index + 3).Take(2).ToArray();
dataLen = nt.ByteArrayToHexString(bytes); 获取包的长度
string tmpdl = "00";
//包长度
tmpdl = nt.hexToDec(dataLen);
//去0转化
int dl = nt.removZero(tmpdl);
if (client.mp.GetDataCount() < (dl + 6)) 判断缓存的长度是否和报文的长度匹配。如果小于则跳出等 待
{
flag++;
//等待两秒 超过时间重启socket 并清空缓存
if (flag > 3) 如果等待三次6秒仍然无信息输入,不符合要求,则清空缓 存,重新连接socket
{
flag = 0;
client.mp.Clear();
reConnectionSocket();
}
Thread.Sleep(2000);
continue;
}
bytes = tmp.Skip(index + 5).Take(dl).ToArray(); //余下的都是读取报文内容的
data = nt.ByteArrayToHexString(bytes);
bytes = tmp.Skip(index + 5 + dl).Take(1).ToArray();
end = nt.ByteArrayToHexString(bytes);
result = header + msgType + msn + dataLen + data + end; 组装好争取报文
AppLog.Info(LoggerEnum.LogType.RECV.ToString(), result);
byte[] bty = new byte[client.mp.GetDataCount() - dl - 6]; 根据协议将取出来的报文从缓存内清
Buffer.BlockCopy(tmp, (index + 5 + dl + 1), bty, 0, bty.Length); 除
client.mp.Buffer = bty;
client.mp.DataCount = client.mp.Buffer.Length;
ReciveMsgPro(result); 处理正确的报文信息
} catch (Exception ex) {
AppLog.Info(LoggerEnum.LogType.OTHER.ToString(), "解析缓存失败 " + ex.Message.ToString());
}
}
}
Thread.Sleep(200);
}
大体是这个意思,能力有限,有待提高!不过,将不会的学会了一点仍然很开心,每天进步一丢丢,就感觉今天没有虚度