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();
}
}
}