在以前的一个项目中,使用五台主机进行tcp通讯,一台为服务机,其余为客户机。当其中一台客户机数据的发生变化,要经过服务机通知其它客户机必须作出相应的状态变化,也就是数据不能丢弃或丢失。因为当时未考虑到数据TCP粘包,拆包的问题,在数据包格式不正确时直接执行了return,致使了程序未达到预期的要求,花了好久时间才找到问题的所在,因此写下这篇博客,来加深一下印象,须要tcp实时完整通讯是避免再次入坑。web
发生TCP粘包或拆包有不少缘由,现列出常见的几点,可能不全面,欢迎补充,c#
一、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。网络
二、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。async
三、要发送的数据小于TCP发送缓冲区的大小,TCP将屡次写入缓冲区的数据一次发送出去,将会发生粘包。tcp
四、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。学习
等等。测试
如今假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据能够分为三种,现列举以下:spa
第一种状况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种状况不在本文的讨论范围内。.net
第二种状况,接收端只收到一个数据包,因为TCP是不会出现丢包的,因此这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种状况因为接收端不知道这两个数据包的界限,因此对于接收端来讲很难处理。code
第三种状况,这种状况有两种表现形式,以下图。接收端收到了两个数据包,可是这两个数据包要么是不完整的,要么就是多出来一块,这种状况即发生了拆包和粘包。这两种状况若是不加特殊处理,对于接收端一样是很差处理的。
客户端:
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