【关于TCP那些事】--TCP粘包半包问题原因常用处理办法

【关于TCP那些事】--TCP粘包半包问题原因常用处理办法

543发表于2019-04-24

熟悉的TCP编程的都知道,无论是客户端或服务端,在发送和接收数据的时候都需要考虑TCP底层的粘包/半包机制。

那么什么是TCP粘包和拆包呢?TCP是一个“流”协议,所谓流,就是没有界限的一串数据。网络中传输的数据流你可以想象成水龙头里面的水,水流并没有界限。所以TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包合并成一个在的数据包发送,就是所谓的TCP粘包半包问题。


之所以出现粘包和半包现象,是因为TCP当中,只有流的概念,没有包的概念. 

半包 
指接受方没有接受到一个完整的包,只接受了部分,这种情况主要是由于TCP为提高传输效率,将一个包分配的足够大,导致接受方并不能一次接受完。(在长连接和短连接中都会出现)。 

粘包与分包 
指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。分包是指在出现粘包的时候我们的接收方要进行分包处理。(在长连接中都会出现) 

什么时候需要考虑半包的情况? 
我们了解到Socket内部默认的收发缓冲区大小大概是8K,但是我们在实际中往往需要考虑效率问题,重新配置了这个值,来达到系统的最佳状态。 
一个实际中的例子:用mina作为服务器端,使用的缓存大小为10k,这里使用的是短连接,所以不用考虑粘包的问题。 
问题描述:在并发量比较大的情况下,就会出现一次接受并不能完整的获取所有的数据。 
粘包与分包常用处理方式: 
1.通过【包长+包体】的协议形式,当服务器端获取到指定的包长时才说明获取完整。 

发送信息前构造含包大小的字节:


public byte[] BuildMsg<T>(T model)
{
	byte[] msgBuffer;
	var msgBodyBuffer = MessagePackSerializer.Serialize<T>(model);
	using (MemoryStream ms = new MemoryStream())
	{
		var boyLengthBytes = BitConverter.GetBytes(msgBodyBuffer.Length);
		if (BitConverter.IsLittleEndian)
		{
			Array.Reverse(boyLengthBytes);//java大端对齐和小端对齐与windows是反的,java默认是big endian,网络字节也是大端(符合人的思维)
		}
		ms.Write(boyLengthBytes, 0, 4);
		ms.Write(msgBodyBuffer, 0, msgBodyBuffer.Length);
		msgBuffer = ms.ToArray();
		ms.Close();
	}
	return msgBuffer;
}



接收信息:


byte[] msgHeader = new byte[4];//4字节为包内容大小。
clientSocket.Receive(msgHeader, 0, 4, SocketFlags.Partial);
Array.Reverse(msgHeader);//网络字节序列默认为大端,java默认为大端,c#默认为小端。所以要转换一下,否则获取包大小不正确。
int msgLength = BitConverter.ToInt32(msgHeader, 0);


byte[] msgContent = new byte[msgLength];
var receiveMsgByteCount = 0;
var leftByteCount = msgLength;
while (true) { //myClientSocket.Available=默认8192,8K //处理粘包问题 int readByteLength = clientSocket.Receive(msgContent, receiveMsgByteCount, clientSocket.Available > leftByteCount ? leftByteCount : clientSocket.Available, SocketFlags.Partial);//默认次最多收8192字节 receiveMsgByteCount += readByteLength; leftByteCount -= readByteLength; if (receiveMsgByteCount >= msgContent.Length) break; }

2.指定包的结束标识,这样当我们获取到指定的标识时,说明包获取完整。 



其实对于java的一些框架已经有比较成熟的方案。比如nettty中的LineBasedFrameDecoder、DelimiterBasedFrameDecoder、FixedLengthBasedFrameDecoder。如查要自定协议,使用netty的比较复杂的LengthFieldBasedFrameDecoder。



小编蓝狐