在之前的一个项目中,使用五台主机进行tcp通信,一台为服务机,其他为客户机。当其中一台客户机数据的发生变化,要通过服务机通知其它客户机必须做出相应的状态变化,也就是数据不能丢弃或丢失。由于当时未考虑到数据TCP粘包,拆包的问题,在数据包格式不正确时直接执行了return,导致了程序未达到预期的要求,花了很久时间才找到问题的所在,所以写下这篇博客,来加深一下印象,需要tcp实时完整通信是避免再次入坑。
发生粘包,拆包的原因:
发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
等等。
表现形式:
现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。
第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
解决方式:
原因和现象是查阅资料或摘抄自别人的博客,比较容易理解,我们主要做的是怎么解决这种问题:客户端:
static TcpClient tcpClient; static BinaryReader br; static BinaryWriter bw; static bool isConnect = false; static void Main(string[] args) { tcpClient = new TcpClient(); reConnect(); Console.ReadLine(); } private static bool ConnectServer(string ip, int port) { try { tcpClient.Connect(IPAddress.Parse(ip), port); } catch (Exception ex) { Console.WriteLine(ex.Message); return false; } try { NetworkStream networkStream = tcpClient.GetStream(); //将网络流作为二进制读写对象 br = new BinaryReader(networkStream); bw = new BinaryWriter(networkStream); Thread threadReceive = new Thread(new ThreadStart(ThreadMethod)); threadReceive.IsBackground = true; threadReceive.Start(); isConnect = true; } catch (Exception ex) { Console.WriteLine(ex.Message); isConnect = false; reConnect(); return false; } return true; } private static void ThreadMethod() { byte[] nHeadLen = new byte[4]; //包含长度的数据首部 bool isFirstReadHead = true; bool isFirstReadData = true; try { while (isConnect) { if (isFirstReadHead) { nHeadLen = br.ReadBytes(4); //从字节流中读取四个字节,如果字节数小于四个,则继续读取 } if (nHeadLen.Length < 4) { nHeadLen = nHeadLen.Concat(br.ReadBytes(4 - nHeadLen.Length)).ToArray(); } if (nHeadLen.Length == 4) //读到一个整形的数据后,读指定长度的内容 { isFirstReadHead = false; int dataLen = BitConverter.ToInt32(nHeadLen, 0); byte[] data = new byte[dataLen]; data = br.ReadBytes(dataLen); if (data.Length < dataLen) //如果读到数据没有到指定长度,继续读取 { data = nHeadLen.Concat(br.ReadBytes(dataLen - nHeadLen.Length)).ToArray(); } else { Console.WriteLine(System.Text.Encoding.Default.GetString(data)); Array.Clear(nHeadLen, 0, nHeadLen.Length); isFirstReadHead = true; } } } } catch(Exception ex) { Console.WriteLine(ex.ToString()); onClose(); tcpClient = new TcpClient(); //如果服务端断开,上一个连接关闭,必须重新创建对象 isConnect = false; reConnect(); } } private static async Task reConnect() { await Task.Run(()=> { while(!isConnect) { Thread.Sleep(2000); ConnectServer("127.0.0.1", 12334); Console.WriteLine("Reconnecting......."); } Console.WriteLine("Connect Success********"); } ); } private static void onClose() { if (isConnect) { br.Close(); bw.Close(); tcpClient.Close(); } }
string ms1 = "first_data";
string ms2 = "second_data";
string ms3 = "third_data_test";
客户端分别需要的ms1,ms2,ms3
在方法一的模式下,服务端需要发送的数据是 ms1.length (byte)+ ms1(byte),ms2.length (byte)+ ms2(byte),ms2.length (byte)+ ms3(byte)
模拟粘包发送,将三个数据包一起发送:ms1.length (byte)+ ms1(byte)+ ms2.length (byte)+ ms3(byte)+ ms2.length (byte)+ ms3(byte)
主要代码:
byte[] bt = BitConverter.GetBytes(ms1.Length); bt = bt.Concat(System.Text.Encoding.Default.GetBytes(ms1)).ToArray(); bt = bt.Concat(BitConverter.GetBytes(ms2.Length)).ToArray(); bt = bt.Concat(System.Text.Encoding.Default.GetBytes(ms2)).ToArray(); bt = bt.Concat(BitConverter.GetBytes(ms3.Length)).ToArray(); bt = bt.Concat(System.Text.Encoding.Default.GetBytes(ms3)).ToArray(); bw.Write(bt);
然后使用For循环模拟拆包发送,使用for循环按字节发送每一个数据:
客户端仍然可以输出所需的数据。
测试方式不太严谨,解决方式也是根据自己的理解去写的,可能存在不太合理的地方,欢迎指正:
c#源码:点击打开链接
欢迎加群一起深入学习WPF/.Net/C#:Hello WPF