Netty解决TCP粘包拆包问题

本文详细介绍了TCP粘包/拆包的概念及原因,并通过Netty展示了三种解决方案:LineBasedFrameDecoder按行解码,DelimiterBasedFrameDecoder自定义分隔符解码,以及FixedLengthFrameDecoder固定长度解码。通过对示例的改造,成功避免了TCP粘包问题。
摘要由CSDN通过智能技术生成

什么是TCP粘包/拆包

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP是基于字节流传输的,就像河流一样,数据“流”式传输,数据中间没有分界。

TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以一个完整的数据包可能会拆分成多个包传输,或者多个数据包可能会合并成一个包传输,这就是所谓的TCP的粘包/拆包问题。如下图所示:

在这里插入图片描述

正常业务逻辑上,Client需要向Server发送D1和D2两个数据包(如上图第一种情况);但实际在TCP传输的过程中,可能会将D1和D2合并为一个数据包发送(如上图第二种情况);也有可能D2被拆分成两个数据包,其中一个跟D1合并成了一个数据包(如上图第三种情况);又或者D1被拆分成两个数据包,其中一个跟D2合并成了一个数据包(如上图第四种情况)。

还有一种可能,当服务端接收滑窗非常小,而D1、D2数据又很大的时候,可能会分多次才能将D1、D2两个数据接收完全。

TCP粘包/拆包发送的原因

TCP发送粘包/拆包的原因主要有以下几个:
(1)应用程序write写入的字节大小大于套接口发送缓冲区大小。
(2)进行MSS(最大报文段长度)大小的TCP分段。当TCP报文段的长度大于MSS时,要进行分段传输。
(3)以太网帧的payload大于MTU(最大传输单元)进行IP分片。一个IP数据报在以太网中 传输,如果它的长度大于该MTU值,就要进行分片传输,使得每片数据报的长度小于MTU。

使用Netty未考虑TCP粘包的示例

仍然还是用之前的客户端向服务端请求时间的案例来改造。之前的示例没有出现粘包/拆包问题是因为客户端与服务器之间只是一问一答,并没有太大的流量压力。这次为了呈现问题,我们将服务器端增加一个计数,每收到一个TCP数据包计数加一;在客户端上,循环发送100条请求,每接收到一条服务端的回复也计数加一。

服务端NettyServerTest类:

package com.test.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServerTest {
   
	
	public void bind(int port) throws InterruptedException {
   
		// NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器
		// 第一个经常被叫做‘boss’,用来接收进来的连接
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		// 第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
   
			// ServerBootstrap 是一个启动 NIO 服务的辅助启动类
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup)
				.channel(NioServerSocketChannel.class)
				// 设置 socket 的参数选项
				.option(ChannelOption.SO_BACKLOG, 1024)
				.childHandler(new childChannelHandler());
			// 绑定端口,同步等待成功
			ChannelFuture f = b.bind(port).sync();
			
			// 等待服务端监听端口关闭
			f.channel().closeFuture().sync();
			
		}finally {
   
			// 优雅退出,释放线程资源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}
	
	/**
	 * ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel
	 * @author 
	 *
	 */
	private class childChannelHandler extends ChannelInitializer<SocketChannel>{
   

		@Override
		protected void initChannel(SocketChannel arg0) throws Exception {
   
			arg0.pipeline().addLast(new ServerTestHandler());
		}
		
	}

	public static void main(String[] args) throws InterruptedException {
   
		int port = 8888;
		new NettyServerTest().bind(port);
	}

}

服务端ServerTestHandler类:

package com.test.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ServerTestHandler extends ChannelInboundHandlerAdapter  {
   
	
	private int counter;
	
	/**
	 * 每当从客户端收到新的数据时,channelRead()方法会在收到消息时被调用
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
   
		ByteBuf buf = (ByteBuf) msg;
		byte[] req = new byte[buf.readableBytes()];
		buf.readBytes(req);
		String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
		System.out.println("The time server receive order : " + body + " ; the counter is : " + ++counter);
		String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
				System.currentTimeMillis()).toString() : "BAD ORDER";
			currentTime = currentTime + System.getProperty("line.separator");
		ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
		ctx.write(resp);
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
   
		// 将消息发送发送队列中的消息写入到SocketChannel中发送给对方
		ctx.flush();
	}
	
	/**
	 * exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,
	 * 即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
   
		System.out.println(cause.getMessage());
		ctx.close();
	}
}

客户端NettyClientTest:

package com.test.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClientTest {
   

	public void connect(int port, String host) throws Exception {
   
		// 配置客户端NIO线程组
		EventLoopGroup group = new NioEventLoopGroup();
		try {
   
			Bootstrap b = new Bootstrap();
			b.group(group).channel(NioSocketChannel.class)
				.option(ChannelOption.TCP_NODELAY, true)
				.handler(new ChannelInitializer<SocketChannel>() {
   

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
   
					// TODO Auto-generated method stub
					ch.pipeline().addLast(new ClientTestHandler());
				}
				
			});
			// 发起异步连接操作
			ChannelFuture f = b.connect(host, port).sync();
			// 等待客户端链路关闭
			f.channel().closeFuture().sync();
		} finally {
   
			// 优雅退出,释放NIO线程组
			group.shutdownGracefully();
		}
	}
	
	public static void main(String[] args) throws Exception {
   
		int port = 8888;
		new NettyClientTest().connect(port, "127.0.0.1");
	}

}

客户端ClientTestHandler类:

package com.test.netty;

import java.util.logging.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ClientTestHandler extends Cha
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值