java架构学习——11. Netty高级

本文深入探讨TCP粘包和拆包现象,并提供三种解决方案:1) 消息定长;2) 包尾添加特殊分隔符;3) 消息头携带消息体长度。通过实例代码展示如何在Netty中应用这些方法来处理粘包和拆包问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇博文主要包含:

  • TCP粘包与拆包
  • 粘包与拆包的解决办法
    -消息定长,报文大小固定长度
    -包尾添加特殊分隔符
    -将消息分为消息头和消息体

一、TCP粘包、拆包问题解决方案

  1. 什么是粘包/拆包
    一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。
    下面可以看一张图,是客户端向服务端发送包:
    在这里插入图片描述
    1.) 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。
    2.)第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。
    3.) 第三种情况,Data2被分离成Data2_1和Data2_2,并且Data2_1在Data1之前到达了服务端,这种情况就产生了拆包。
    由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包.

  2. 解决办法

  • 消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
    sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
  • 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
    ByteBuf buf = Unpooled.copiedBuffer("_mayi".getBytes());
    sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
  • 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段

代码演示:
1)maven依赖

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>5.0.0.Alpha2</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.jboss.marshalling/jboss-marshalling -->
		<dependency>
			<groupId>org.jboss.marshalling</groupId>
			<artifactId>jboss-marshalling</artifactId>
			<version>1.3.19.GA</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.jboss.marshalling/jboss-marshalling-serial -->
		<dependency>
			<groupId>org.jboss.marshalling</groupId>
			<artifactId>jboss-marshalling-serial</artifactId>
			<version>1.3.18.GA</version>
			<scope>test</scope>
		</dependency>

2)服务器端与客户端代码:

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
 * netty5.0 简单案例,项目中建议使用4.X版本
 *
 */
public class Netty5Demo {
}

class ServerHandler5 extends ChannelHandlerAdapter {
	// 当通道被调用,执行该方法
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("Server msg:" + value);
		// 回复给客户端 “您好!”
		String res = "好的...";
		ctx.writeAndFlush(Unpooled.copiedBuffer(res.getBytes()));
	}

}
//服务器端
class NettyServer5 {
	public static void main(String[] args) throws InterruptedException {
		System.out.println("服务器端已经启动....");
		// 1.创建2个线程,一个负责接收客户端连接, 一个负责进行 传输数据
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		NioEventLoopGroup cGroup = new NioEventLoopGroup();
		// 2. 创建服务器辅助类
		ServerBootstrap b = new ServerBootstrap();
		b.group(pGroup, cGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
				// 3.设置缓冲区与发送区大小
				.option(ChannelOption.SO_SNDBUF, 32 * 1024)
				.option(ChannelOption.SO_RCVBUF, 32 * 1024)
				.childHandler(new ChannelInitializer<SocketChannel>() {
					@Override
					protected void initChannel(SocketChannel sc) throws Exception {
						//设置为String类型
						sc.pipeline().addLast(new StringDecoder());
						sc.pipeline().addLast(new ServerHandler5());
					}
				});
		ChannelFuture cf = b.bind(8090).sync();
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
		cGroup.shutdownGracefully();
	}
}


class ClientHandler5 extends ChannelHandlerAdapter {
	// 当通道被调用,执行该方法
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("client msg:" + value);
		super.channelRead(ctx, msg);
	}
}

//客户端
class NettyClient5{
	
	public static void main(String[] args) throws InterruptedException {
		System.out.println("客户端已经启动.......");
		//创建负责接收客户端连接
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		Bootstrap b = new Bootstrap();
		b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {

			@Override
			protected void initChannel(SocketChannel ch) throws Exception {

				ch.pipeline().addLast(new StringDecoder());
				ch.pipeline().addLast(new ClientHandler5());
			}
		});
		ChannelFuture cf = b.connect("127.0.0.1",8090).sync();
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		Thread.sleep(1000);
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		// 等待客户端端口号关闭
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
	}
}

运行结果:
在这里插入图片描述
在这里插入图片描述

3)消息定长,服务器端与客户端代码演示:

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
 * netty5.0 简单案例,项目中建议使用4.X版本
 *
 */
public class Netty5Demo {
}

class ServerHandler5 extends ChannelHandlerAdapter {
	// 当通道被调用,执行该方法
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("Server msg:" + value);
		// 回复给客户端 “您好!”
		String res = "好的...";
		ctx.writeAndFlush(Unpooled.copiedBuffer(res.getBytes()));
	}

}

class NettyServer5 {
	public static void main(String[] args) throws InterruptedException {
		System.out.println("服务器端已经启动....");
		// 1.创建2个线程,一个负责接收客户端连接, 一个负责进行 传输数据
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		NioEventLoopGroup cGroup = new NioEventLoopGroup();
		// 2. 创建服务器辅助类
		ServerBootstrap b = new ServerBootstrap();
		b.group(pGroup, cGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
				// 3.设置缓冲区与发送区大小
				.option(ChannelOption.SO_SNDBUF, 32 * 1024)
				.option(ChannelOption.SO_RCVBUF, 32 * 1024)
				.childHandler(new ChannelInitializer<SocketChannel>() {
					@Override
					protected void initChannel(SocketChannel sc) throws Exception {
					    //消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
						sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
						//设置为String类型
						sc.pipeline().addLast(new StringDecoder());
						sc.pipeline().addLast(new ServerHandler5());
					}
				});
		ChannelFuture cf = b.bind(8090).sync();
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
		cGroup.shutdownGracefully();
	}
}


class ClientHandler5 extends ChannelHandlerAdapter {
	// 当通道被调用,执行该方法
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("client msg:" + value);
		super.channelRead(ctx, msg);
	}
}


class NettyClient5{
	
	public static void main(String[] args) throws InterruptedException {
		System.out.println("客户端已经启动.......");
		//创建负责接收客户端连接
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		Bootstrap b = new Bootstrap();
		b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {

			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
			    //消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
				ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
				ch.pipeline().addLast(new StringDecoder());
				ch.pipeline().addLast(new ClientHandler5());
			}
		});
		ChannelFuture cf = b.connect("127.0.0.1",8090).sync();
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("ganxiaoning".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("ganxiaoning".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		// 等待客户端端口号关闭
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
	}
}

运行结果:
在这里插入图片描述
在这里插入图片描述

4)包尾添加特殊分隔符,服务器端与客户端代码演示:

class ServerHandler5 extends ChannelHandlerAdapter {
	// 当通道被调用,执行该方法
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("Server msg:" + value);
		// 回复给客户端 “您好!”
		String res = "好的...";
		ctx.writeAndFlush(Unpooled.copiedBuffer(res.getBytes()));
	}

}

class NettyServer5 {
	public static void main(String[] args) throws InterruptedException {
		System.out.println("服务器端已经启动....");
		// 1.创建2个线程,一个负责接收客户端连接, 一个负责进行 传输数据
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		NioEventLoopGroup cGroup = new NioEventLoopGroup();
		// 2. 创建服务器辅助类
		ServerBootstrap b = new ServerBootstrap();
		b.group(pGroup, cGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 1024)
				// 3.设置缓冲区与发送区大小
				.option(ChannelOption.SO_SNDBUF, 32 * 1024)
				.option(ChannelOption.SO_RCVBUF, 32 * 1024)
				.childHandler(new ChannelInitializer<SocketChannel>() {
					@Override
					protected void initChannel(SocketChannel sc) throws Exception {
						//消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
						//sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
						
						//包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
						ByteBuf buf = Unpooled.copiedBuffer("_sucess".getBytes());
						sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
						
						//设置为String类型
						sc.pipeline().addLast(new StringDecoder());
						sc.pipeline().addLast(new ServerHandler5());
					}
				});
		ChannelFuture cf = b.bind(8090).sync();
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
		cGroup.shutdownGracefully();
	}
}


class ClientHandler5 extends ChannelHandlerAdapter {
	// 当通道被调用,执行该方法
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		// 接收数据
		String value = (String) msg;
		System.out.println("client msg:" + value);
		super.channelRead(ctx, msg);
	}
}


class NettyClient5{
	
	public static void main(String[] args) throws InterruptedException {
		System.out.println("客户端已经启动.......");
		//创建负责接收客户端连接
		NioEventLoopGroup pGroup = new NioEventLoopGroup();
		Bootstrap b = new Bootstrap();
		b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {

			@Override
			protected void initChannel(SocketChannel ch) throws Exception {
				//消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
				//ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
				
				//包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
				ByteBuf buf = Unpooled.copiedBuffer("_sucess".getBytes());
				ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
				
				ch.pipeline().addLast(new StringDecoder());
				ch.pipeline().addLast(new ClientHandler5());
			}
		});
		ChannelFuture cf = b.connect("127.0.0.1",8090).sync();
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("ganxiaoning_sucess".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("ganxiaoning".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁_sucess".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("甘小宁".getBytes()));
		// 等待客户端端口号关闭
		cf.channel().closeFuture().sync();
		pGroup.shutdownGracefully();
	}
}

运行结果:
在这里插入图片描述
项目中建议使用包尾添加特殊分隔符的方式进行粘包和拆包

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值