Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战

文章目录

  • TCP粘包/拆包分析
    • 什么是TCP粘包/拆包
    • 粘包/拆包产生原因
      • Nagle算法
      • 以太网的MTU(最大传输单元)限制
      • TCP的MSS(最大报文段)限制
    • 解决粘包/拆包问题
    • Netty粘包/拆包解决方案
      • 消息定长
      • 增加分隔符
      • 自定义长度
        • LengthFieldBasedFrameDecoder参数分析
        • LengthFieldBasedFrameDecoder图解参数使用
  • 集成MessagePack序列化框架实战(基于自定义长度解决粘包/拆包)


TCP粘包/拆包分析

什么是TCP粘包/拆包

TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题

图解说明TCP粘包/拆包:
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第1张图片
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下 4 种情况

  1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有发生粘包和拆包
  2. 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包
  3. 服务端分两次读取到了两个数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这被称为 TCP 拆包
  4. 服务端分两次读取到了两个数据包,第一次读取到了 D1 包的部分内容 D1_1,第 二次读取到了 D1 包的剩余内容 D1_2 和 D2 包的整包

特别要注意的是,如果TCP的接收方的滑动窗口非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包

粘包/拆包产生原因

具体的三个原因:

  1. Nagle算法
  2. TCP的MSS(最大报文段)限制
  3. 以太网的MTU(最大传输单元)限制

Nagle算法

由于 TCP 协议本身的机制客户端与服务器会维持一个连接,数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用 Nagle 算法(可配置是否启用)对较小的数据包进行合并然后再发送。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次再取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象

Nagle算法的目的:TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的消息内容和40字节的头部。这样的情况对于高负载的网络来说是无法接受的。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块

Nagle算法的规则:

  1. 如果SO_SNDBUF(发送缓冲区)中的数据长度达到MSS,则允许发送
  2. 如果该SO_SNDBUF(发送缓冲区)中含有FIN标志位,则表示请求关闭连接,则先将SO_SNDBUF(发送缓冲区)中的剩余数据发送,再关闭
  3. 启动TCP_NODELAY,禁用了Nagle算法,允许小包的发送
  4. TCP_CORK选项,开启时,内核将阻塞不完整的报文,当关闭此选项时,发送阻塞的报文(不完整的报文指的是应用层发送的数据长度不足一个MSS长度)
  5. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送

以太网的MTU(最大传输单元)限制

MTU是以太网传输数据方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes。刨去以太网帧的帧头: (DMAC目的MAC地址:6Bytes+SMAC源MAC地址:6Bytes+Type域:2bytes) 14Bytes和帧尾 CRC校验部分4Bytes,那么剩下承载上层协议的数据域最大就只能有1500Bytes,我们就把它称之为MTU

如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成若干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上

TCP的MSS(最大报文段)限制

MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度。并且MSS是根据MTU计算出来的,因此当发送的数据满足MSS时,必然也满足MTU。而上述我们知道MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和Ip Header,因此刨去这两个部分,就是TCP协议一次可以发送的实际应用数据的最大大小,也就是MSS即 MSS长度=MTU长度-IP Header-TCP Header

TCP Header的长度是20字节,IPv4中IP Header长度是20字节,IPV6中IP Header长度是40字节,因此:在IPV4中,TCP的MSS可以达到1460byte;在IPV6中,TCP的MSS可以达到1440byte

发送方发送数据时,当SO_SNDBUF(发送缓冲区)中的数据量大于MSS时,操作系统会将数据进行拆分,使得每一部分都小于MSS,这就是拆包

解决粘包/拆包问题

前面我们知道了TCP是一个没有界限数据流协议,并且协议本身无法避免粘包/拆包的发生,因此我们只能在应用层协议上,进行相应的控制。通常在制定数据传输时,可以使用如下方法:

  1. 消息定长,每次读取固定长度的内容作为一条完整的消息
  2. 增加分隔符,按照指定的分隔符分离出消息内容,比如回车换行符进行分割,如FTP协议
  3. 消息头+消息体的自定义长度,消息头存储消息开始的标识以及消息读取的长度,服务端根据消息头解析出消息长度,然后向后读取该长度的消息内容

Netty粘包/拆包解决方案

消息定长

对于使消息定长的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足。其使用也比较简单,只需要在构造函数中指定每个消息的长度即可。

这里需要注意的是,FixedLengthFrameDecoder只是一个解码一器,Netty也只提供了一个解码器,这是因为对于解码是需要等待下一个包的进行补全的,代码相对复杂,而对于编码器,用户可以自行编写,因为编码时只需要将不足指定长度的部分进行补全即可

增加分隔符

对于通过分隔符进行粘包和拆包问题的处理,Netty提供了两个编解码的类,LineBasedFrameDecoderDelimiterBasedFrameDecoder。这里LineBasedFrameDecoder的作用主要是通过换行符,即\n或者\r\n对数据进行处理;而DelimiterBasedFrameDecoder的作用则是通过用户指定的分隔符对数据进行粘包和拆包处理。同样的,这两个类都是解码器类,而对于数据的编码,也即在每个数据包最后添加换行符或者指定分割符的部分需要用户自行进行处理

自定义长度

Netty提供了LengthFieldBasedFrameDecoder自定义长度解码器,用来解析带长度字段数据包

LengthFieldBasedFrameDecoder参数分析

  1. maxFrameLength: 发送数据包的最大长度
  2. lengthFieldOffset:长度域的偏移量,描述的是发送的字节数组跳过多少个字节后才是长度域
  3. lengthFieldLength:长度域的长度
  4. lengthAdjustment:长度域的修正值,Netty在读取到数据包的长度值 N 后, 认为接下来的 N 个字节都是需要读取的,但是根据实际情况,有可能需要增加 N 的值,也有可能需要减少 N 的值,具体增加多少,减少多少,写在这个参数里
  5. initialBytesToStrip:表示接收到的发送数据包中需要去除initialBytesToStrip个字节数
  6. failFast :如果为 true,则表示读取到长度域值的超过 maxFrameLength,就抛出一个TooLongFrameException,而为 false 表示只有当真正读取完长度域的值表示的字节之后, 才会抛出TooLongFrameException,默认情况下设置为 true,建议不要修改,否则可能会造 成内存溢出

LengthFieldBasedFrameDecoder图解参数使用

数据包大小: 14B = 长度域2B + “HELLO! NETTY”(12B)Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第2张图片
数据包大小: 14B = 长度域2B + “HELLO! NETTY”(12B),解码后会丢掉长度域
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第3张图片
数据包大小: 16B = 长度域2B + “CAFE”(2B)+“HELLO! NETTY”(12B),在长度域后添加2个字节的Header
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第4张图片
数据包大小: 16B = “CAFE”(2B)+长度域2B + “HELLO! NETTY”(12B),在长度域前添加2个字节的Header,解码后会丢掉长度域
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第5张图片
数据包大小: 17B = “CA”(1B)+长度域3B +“FE”(1B)+ “HELLO! NETTY”(12B),在长度域前后各添加1个字节的Header,解码后会丢掉长度域
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第6张图片

集成MessagePack序列化框架实战(基于自定义长度解决粘包/拆包)

引入依赖:

<dependency>
       <groupId>org.msgpack</groupId>
       <artifactId>msgpack</artifactId>
       <version>0.6.12</version>
</dependency>

首先,定义发送的消息实体

//MessagePack提供的注解,表明这是一个需要序列化的实体类
@Message
public class LogEvent {

	private String msg;
	
	private long time;

	private long msgId;
	

	@Override
	public String toString() {
		return "LogEvent [msg=" + msg + ", time=" + time + ", msgId=" + msgId + "]";
	}
    ....
}

定义服务端,服务端首先基于LengthFieldBasedFrameDecoder自定义长度解码器,对客户端发送的报文进行解码,然后在将解码后的字节交由MessagePack进行反序列化成目标实例,最后再由业务Handler来处理

public class MessagePackServer {

	private static final int PORT=8761;
	private static EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
	private static ServerBootstrap bootstrap = new ServerBootstrap();
	
	public static void startServer(){
		try {
			bootstrap.group(eventLoopGroup).
			 channel(NioServerSocketChannel.class).
			   localAddress(new InetSocketAddress(PORT)).childHandler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					//解决粘包/拆包问题,添加自定义长度解码器
					ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
					//添加自定义messagepack反序列化器
					ch.pipeline().addLast(new MessagePackDecoder());
					//添加业务handler
					ch.pipeline().addLast(new MessagePackServerHandler());
				}
			});
		    ChannelFuture future=bootstrap.bind().sync();
		    System.out.println("服务器启动完成,等待客户端的连接和数据.....");
		    future.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			eventLoopGroup.shutdownGracefully();
		}
	}

	public static void main(String[] args) {
		startServer();
	}
}

定义MessagePack反序列化操作,将字节转化为LogEvent实例

public class MessagePackDecoder extends MessageToMessageDecoder<ByteBuf>{

	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
		int readLength=msg.readableBytes();
		byte[] bytes=new byte[readLength];
		//将内容写入到字节数组
		msg.getBytes(msg.readerIndex(),bytes,0,readLength);
		//创建MessagePack序列化器
		MessagePack messagePack = new MessagePack();
		//将字节数组反序列化成目标实例
		out.add(messagePack.read(bytes, LogEvent.class));
	}
}

服务端的业务handler,负责接收数据,并向客户端回复应答

public class MessagePackServerHandler extends ChannelInboundHandlerAdapter{

	private AtomicInteger counter = new AtomicInteger(0);
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		//此时的msg已经被反序列化为目标实例
		LogEvent logEvent = (LogEvent) msg;
		System.out.println("Server Accept:"+logEvent.toString()+" and the counter is:"+counter.incrementAndGet());
		//应答,应答结尾加上换行分割符
		String response =  "message "+logEvent.getMsgId()+" received successfully"+System.getProperty("line.separator");
		ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
	}
}

定义客户端,客户端首先基于LengthFieldPrepender设置长度域,然后将目标实例交由MessagePack进行序列化成字节数据,最后再由业务Handler来处理

public class MessagePackClient {

	private static final int PORT=8761;
	private static final String HOST = "127.0.0.1";
	private static EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
	private static Bootstrap bootstrap = new Bootstrap();
	
	public static void startClient(){
		try {
			bootstrap.group(eventLoopGroup).
			  channel(NioSocketChannel.class).
			    remoteAddress(new InetSocketAddress(HOST, PORT)).handler(new ChannelInitializer<SocketChannel>() {

					@Override
					protected void initChannel(SocketChannel ch) throws Exception {
						//添加自定义长度的编码器
						ch.pipeline().addLast(new LengthFieldPrepender(2));
						//添加MessagePack序列化器
						ch.pipeline().addLast(new MessagePackEncoder());
						//添加应答信息, 按换行符进行分割编码器
						ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
						//添加业务handler
						ch.pipeline().addLast(new MessagePackClientHandler());
					}
				});
			ChannelFuture future=bootstrap.connect().sync();
			System.out.println("客户端启动,并连接到服务端.....");
			future.channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			eventLoopGroup.shutdownGracefully();
		}
	}
	
	public static void main(String[] args) {
		startClient();
	}
}
public class MessagePackEncoder extends MessageToByteEncoder<LogEvent>{

	@Override
	protected void encode(ChannelHandlerContext ctx, LogEvent msg, ByteBuf out) throws Exception {
		MessagePack messagePack = new MessagePack();
		out.writeBytes(messagePack.write(msg));
	}
}
public class MessagePackClientHandler extends SimpleChannelInboundHandler<ByteBuf>{

	private AtomicInteger counter = new AtomicInteger(0);
	
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
		 System.out.println("client Accept["+msg.toString(CharsetUtil.UTF_8)
         +"] and the counter is:"+counter.incrementAndGet());
	}

	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		LogEvent logEvent =new LogEvent();
		for(int i=1;i<=10;i++){
			logEvent.setTime(System.currentTimeMillis());
			logEvent.setMsg("Send request :" + i);
			logEvent.setMsgId(i);
			System.out.println("Send logEvent:"+logEvent.toString());
			ctx.writeAndFlush(logEvent);
		}
	}
}

测试

首先,启动服务端
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第7张图片
接下来,启动客户端,可以看到客户端发出了请求报文,并接收到了服务端的应答报文
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第8张图片
同时服务端也成功收到了客户端的请求报文
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第9张图片
如果服务端不使用自定义长度的解码器,那么服务端就不会接收到完整的报文
Netty框架进阶篇 - 解决TCP粘包和拆包问题以及集成MessagePack序列化框架实战_第10张图片

你可能感兴趣的:(网络编程,netty,网络协议,网络通信,TCP粘包和半包,MessagePack)