1、TCP粘包、拆包问题
1.1TCP粘包/拆包问题
TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
1.2TCP粘包/拆包发生的原因
1.应用程序write写入的字节大小大于套接口发送缓冲区大小。
2.进行MSS(TCP协议定义的最大报文段长度)大小的TCP分段。
3.以太网帧的payload大于MTU最大传输单元)进行IP分片。
2、TCP粘包问题的解决策略
2.1TCP粘包问题解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。业界的主流协议的解决方案,可以归纳如下:
1. 消息定长,报文大小固定长度,例如每个报文的长度固定为200字节,如果不够空位补空格;
2. 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分;
3. 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段;
4. 更复杂的自定义应用层协议。
2.2netty粘包问题解决策略
Netty提供了多个解码器,可以进行分包的操作,分别是:
* LineBasedFrameDecoder (回车换行解码器)
* DelimiterBasedFrameDecoder(添加特殊分隔符报文来分包)
* FixedLengthFrameDecoder(使用定长的报文来分包)
* LengthFieldBasedFrameDecoder(通过在消息头定义长度字段来标识消息总长度)
2.2.1LineBasedFrameDecoder
LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的HandleroLineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。
代码实现
服务器端
package com.xyz.netty.decoder1; 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; import io.netty.handler.codec.LineBasedFrameDecoder; 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、定义两个线程组 EventLoopGroup pGroup = new NioEventLoopGroup();//一个是用于处理服务器端接收客户端连接的 EventLoopGroup cGroup = new NioEventLoopGroup();//一个是进行网络通信的(网络读写的) try { //2、创建辅助工具类,用于服务器通道的一系列配置 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(pGroup, cGroup)//绑定俩个线程组 .channel(NioServerSocketChannel.class)//指定NIO的模式 .option(ChannelOption.SO_BACKLOG, 1024) .option(ChannelOption.SO_SNDBUF, 32*1024) .option(ChannelOption.SO_RCVBUF, 32*1024) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //3、处理业务 ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); //设置字符串形式的解码 ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new StringDecoder()); //处理业务 ch.pipeline().addLast(new ServerHandler()); } }); //4、进行绑定 ChannelFuture cf = bootstrap.bind(8765).sync(); System.out.println("server start...."); //5、等待关闭 cf.channel().closeFuture().sync(); } finally{ pGroup.shutdownGracefully(); cGroup.shutdownGracefully(); } } }
服务器端管理类
package com.xyz.netty.decoder1; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; public class ServerHandler extends ChannelHandlerAdapter{ @Override public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception { //读取客户端发过来的信息 String body = (String) msg; System.out.println("服务器接收到客户端的信息为 " + body); //给客户端回应信息 String response = "server 已经接收到信息, 信息为 " + body + "\n"; ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes())); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception { ctx.close(); } }
客户端
package com.xyz.netty.decoder1; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; 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.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class Client { public static void main(String[] args) throws Exception{ //1、创建线程组 EventLoopGroup group = new NioEventLoopGroup(); try { //2、创建辅助工具类,用于服务器通道的一系列配置 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //3 在这里配置具体数据接收方法的处理 ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); //设置字符串解码 ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new ClientHandler()); } }); //4、建立连接 ChannelFuture cf = bootstrap.connect("127.0.0.1", 8765).sync(); System.out.println("Client connet....."); //5、发送信息 cf.channel().writeAndFlush(Unpooled.wrappedBuffer("Hello\nworld\n!\n".getBytes())); cf.channel().writeAndFlush(Unpooled.wrappedBuffer("你好\n世界\n!\n".getBytes())); //6、等待关闭 cf.channel().closeFuture().sync(); } finally{ group.shutdownGracefully(); } } }
客户端管理类
package com.xyz.netty.decoder1; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.util.ReferenceCountUtil; public class ClientHandler extends ChannelHandlerAdapter{ @Override public void channelRead(ChannelHandlerContext ctx, Object msg)throws Exception { try { //读取服务器端发过来的信息 String body = (String) msg; System.out.println("客户端接收到服务端的响应消息 " + body); } finally{ ReferenceCountUtil.release(msg); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)throws Exception { ctx.close(); } }
2.2.2DelimiterBasedFrameDecoder
首先创建分隔符缓冲对象ByteBuf,我使用“$一”作为分隔符。创建DelimiterBasedFrameDecoder对象,将其加入到ChannelPipeline中。DelimiterBasedFrameDecoder有多个构造方法,这里我选择了传递两个参数的:第一个1024表示单条消息的最大长度,当达到该长度后仍然没有查找到分隔符,就抛出TooLongFrameException异常,防止由于异常码流缺失分隔符导致的内存溢出;第二个参数就是分隔符缓冲对象。
代码实现(和LineBasedFrameDecoder有区别的地方客户端处理类保持不变)
服务器端
bootstrap.group(pGroup, cGroup)//绑定俩个线程组 .channel(NioServerSocketChannel.class)//指定NIO的模式 .option(ChannelOption.SO_BACKLOG, 1024) .option(ChannelOption.SO_SNDBUF, 32*1024) .option(ChannelOption.SO_RCVBUF, 32*1024) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //3、处理业务 //创建分隔符缓冲对象ByteBuf ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf)); //设置字符串形式的解码 ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new StringDecoder()); //处理业务 ch.pipeline().addLast(new ServerHandler()); } });
服务器管理类
//给客户端回应信息 String response = "server 已经接收到信息, 信息为 " + body + "$_"; ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
客户端
package com.xyz.netty.decoder2; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; 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.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class Client { public static void main(String[] args) throws Exception{ //1、创建线程组 EventLoopGroup group = new NioEventLoopGroup(); try { //2、创建辅助工具类,用于服务器通道的一系列配置 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //3 在这里配置具体数据接收方法的处理 //创建分隔符缓冲对象ByteBuf ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes()); ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf)); //设置字符串解码 ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new ClientHandler()); } }); //4、建立连接 ChannelFuture cf = bootstrap.connect("127.0.0.1", 8765).sync(); System.out.println("Client connet....."); //5、发送信息 cf.channel().writeAndFlush(Unpooled.wrappedBuffer("Hello$_world$_!$_".getBytes())); cf.channel().writeAndFlush(Unpooled.wrappedBuffer("你好$_世界$_!$_".getBytes())); //6、等待关闭 cf.channel().closeFuture().sync(); } finally{ group.shutdownGracefully(); } } }
2.2.3FixedLengthFrameDecoder
是固定长度解码器,它能按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包等问题。利用FixedLengthFrameDecoder解码,无论一次性接收到多少的数据,他都会按照构造函数中设置的长度进行解码;如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包,到达后进行拼包,直到读取完整的包。
不同编码中英文字母和中文所占的字节长度
英文字母
字节数 : 1;编码:GBK
字节数 : 1;编码:UTF-8
中文
字节数 : 2;编码:GBK
字节数 : 3;编码:UTF-8
代码实现
服务器端
.childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //3、处理业务 ch.pipeline().addLast(new FixedLengthFrameDecoder(6)); //设置字符串形式的解码 ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new StringDecoder()); //处理业务 ch.pipeline().addLast(new ServerHandler()); } });
服务器管理类
//给客户端回应信息 String response = "server" + body; ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
客户端
package com.xyz.netty.decoder3; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; 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.FixedLengthFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; public class Client { public static void main(String[] args) throws Exception{ //1、创建线程组 EventLoopGroup group = new NioEventLoopGroup(); try { //2、创建辅助工具类,用于服务器通道的一系列配置 Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { //3 在这里配置具体数据接收方法的处理 ch.pipeline().addLast(new FixedLengthFrameDecoder(6)); //设置字符串解码 ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new ClientHandler()); } }); //4、建立连接 ChannelFuture cf = bootstrap.connect("127.0.0.1", 8765).sync(); System.out.println("Client connet....."); //5、发送信息 cf.channel().writeAndFlush(Unpooled.wrappedBuffer("Hello world ! ".getBytes())); cf.channel().writeAndFlush(Unpooled.wrappedBuffer("你好世界! ".getBytes())); //6、等待关闭 cf.channel().closeFuture().sync(); } finally{ group.shutdownGracefully(); } } }