Netty网络编程-入门篇(三)-TCP粘包拆包问题

TCP粘包拆包源于TCP协议的流特性,可能导致数据混淆。解决方案包括消息定长、添加特殊分隔符或使用Netty的FixedLengthFrameDecoder。Netty通过分隔符拆包和消息定长策略能有效解决这一问题。
摘要由CSDN通过智能技术生成

TCP粘包、拆包:
TCP是一个“流”协议,所谓流就是没有界限的一串数据。大家可以想象下如果河里的水就好比数据,他们是连成一片的,没有分界线,TCP底层并不了解上层业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包的划分,在业务上,我们一个完整的包可能会被TCP分成多个包进行发送,也可能吧多个小包封装成一个大的数据包发送出去,这就是所谓的粘包、拆包问题。
TCP粘包、拆包发生的原因:
1、应用程序write写入的字节大小大于套接字发送缓冲区的大小
2、进行MSS大小的TCP分段
3、以太网帧的payload大于MTU进行IP分片
粘包拆包问题的解决方案,根据业界主流协议的有三种方案:
1.消息定长,例如每个报文的大小固定为200个字节,如果不够,控温补空格;
2.在包尾部增加特殊字符进行分割,例如加回车等
3.将消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑的处理

Netty如何去解决粘包拆包问题:
1.分隔符拆包
2.消息定长

先上菜1(通过自定义特殊分隔符来解决粘包问题):
Server端代码:

import java.nio.ByteBuffer;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
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.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class Server {

	public static void main(String[] args) throws Exception{
		//1 创建2个线程,boss是负责接收客户端的连接,worker负责进行数据传输
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		
		//2 创建服务器辅助类
		ServerBootstrap b = new ServerBootstrap();
		b.group(bossGroup, workerGroup)
		 .channel(NioServerSocketChannel.class)
		 .childHandler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				//设置特殊分隔符和解码器
//				ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
//				sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ServerHandler());
			}
		 })
		 .option(ChannelOption.SO_BACKLOG, 1024)
		 .option(ChannelOption.SO_SNDBUF, 32*1024)
		 .option(ChannelOption.SO_RCVBUF, 32*1024)
		 .option(ChannelOption.SO_KEEPALIVE, true);
		
		ChannelFuture cf = b.bind(8088).sync();
		
		//等待服务器监听端口关闭
		cf.channel().closeFuture().sync();
		
		bossGroup.shutdownGracefully();
		workerGroup.shutdownGracefully();
		
	}
	
	private static class ServerHandler extends ChannelHandlerAdapter {

		@Override
		public void channelActive(ChannelHandlerContext ctx) throws Exception {
			System.out.println(" server channel active... ");
		}

		@Override
		public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
			String request = (String)msg;
			System.out.println("Server :" + msg);
			String response = "服务器响应:" + msg + "$_";
			ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
		}

		@Override
		public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
			System.out.println(" server channel read complete... ");
		}

		@Override
		public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) throws Exception {
			ctx.close();
		}
	}
	
}

Client端代码:

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
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.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.ReferenceCountUtil;

public class Client {

	public static void main(String[] args) throws Exception {
		EventLoopGroup group = new NioEventLoopGroup();
		Bootstrap b = new Bootstrap();
		b.group(group)
		 .channel(NioSocketChannel.class)
		 .handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				//对消息包添加分隔符和解码器
//				ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
//				sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));	
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ClientHandler());
			}
		});
		
		ChannelFuture cf = b.connect("127.0.0.1", 8088).sync();
		
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("mynameisljj$_".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("whoareyou$_".getBytes()));
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("nishuosha??$_".getBytes()));
		
		//等待Client端口关闭
		cf.channel().closeFuture().sync();
		group.shutdownGracefully();
		
	}
	
	private static class ClientHandler extends ChannelHandlerAdapter {
		@Override
		public void channelActive(ChannelHandlerContext ctx) throws Exception {
			System.out.println("client channel active... ");
		}

		@Override
		public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
			try {
				System.out.println("Client: " + (String)msg);//由于事先在管道中使用解码器将ByteBuf解码成字符串了,传进来的msg就是字符串对象
			} finally {
				ReferenceCountUtil.release(msg);
			}
		}

		@Override
		public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
			System.out.println(" client channel read complete... ");
		}

		@Override
		public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
			ctx.close();
		}
	}
}

上面的代码中客户端连续向服务器发送消息:

cf.channel().writeAndFlush(Unpooled.wrappedBuffer("mynameisljj$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("whoareyou$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("nishuosha??$_".getBytes()));

会长生粘包的问题,明明是发送三次请求,由于发送的速度太快,在加上TCP的”流“性质,服务端将三次请求的数据当作一次来处理了,因此输出:

server channel active... 
Server :mynameisljj$_whoareyou$_nishuosha??$_       //看到了没,产生了粘包的问题
server channel read complete... 

客户端这样处理,每发送一次消息的时候线程都睡1秒,这时候服务端就能识别客户端发送的是请求包了:

cf.channel().writeAndFlush(Unpooled.wrappedBuffer("mynameisljj$_".getBytes()));
TimeUnit.SECONDS.sleep(1);
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("whoareyou$_".getBytes()));
TimeUnit.SECONDS.sleep(1);
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("nishuosha??$_".getBytes()));

但是上面这样处理实际环境中得慢到什么时候啊,因此可以通过添加自定义的特殊分隔符的形式来解决的问题:

ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));	

上菜2(通过消息定长的方式来解决粘包问题):
io.netty.handler.codec
Class FixedLengthFrameDecoder //netty中的定长译码器
一种译码器,它将接收到的字节数按固定的字节数分开。例如,如果你收到以下四个片段包:
±–±-----±----------±----+
| A | BC | DEFG | HI |
±–±-----±----------±----+
一个FixedLengthFrameDecoder(3)将它们解码成以下三个具有固定长度的数据包:
±------±-------±------+
| ABC | DEF | GHI |
±------±-------±------+

以上的做法很简单,直接添加:

sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值