1、问题背景
传输层除了有TCP协议外还有UDP协议。
首先,UDP不会发生粘包或拆包现象。因为UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。
但TCP是基于字节流的,在基于流的传输里(如TCP/IP),接收到的数据会先被存储到一个socket接收缓冲里。不幸的是,基于流的传输并不是一个数据包队列,而是一个字节队列。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行化包的划分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包进行发送,也有多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。粘包拆包现象发生如图所示:
2、发生粘包或拆包的直接原因
原因与解决方式,网上一查有很多,服务端 和 客户端 都会造成粘包、半包问题,以下列出常见原因。
- 服务端:
- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
- 接收端:
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
3、粘包问题的解决策略
TCP以流的方式进行数据传输,由于底层TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,上层的应用协议为了对消息进行区分,业界主流的解决方案归纳如下:
- 固定消息长度,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息,如果不够,空位补空格;将计数器置位,重新开始读取下一个数据。
- 将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应该比较广泛。
- 将特殊的分隔符作为消息结束标志,回车换行符就是一种特殊的结束分隔符。
- 通过在消息头中定义长度字段来标识消息的总长度。通常设计思路为消息头的第一个字段使用int32来表示消息的总长度。
- 更复杂的应用层协议。
4、netty提供的处理方式
Netty对上述前四种应用做了统一抽象,提供了Decoder来解决对应的问题,使用起来非常方便。只要能够熟练的掌握这些类库的使用,TCP粘包问题就可以变得非常容易,这个就是其他NIO框架和JDK原生的NIO API无法匹敌的。有了这些Decoder,用户不需要对自己读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。
4.1、Decoder API
- LineBasedFrameDecoder(消息以回车换行符结尾的处理)
- StringDecoder
- DelimiterBasedFrameDecoder(消息以自定义分隔符结尾的处理)
- FixedLengthFrameDecoder
4.2、上述API使用方式及原理
4.2.1、LineBasedFrameDecoder 和 StringDecoder示例
- 服务端:
@Override
public void run() {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//add decoder
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(MAX_LEN));
socketChannel.pipeline().addLast(new StringDecoder());
}
});
//绑定端口
ChannelFuture future = b.bind(port).sync();
//等待服务端监听端口关闭
logger.debug("server start!");
future.channel().closeFuture().sync();
logger.debug("server end");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
- 客户端:
@Override
public void run() {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//add decoder
socketChannel.pipeline().addLast(new LineBasedFrameDecoder(MAX_LEN));
socketChannel.pipeline().addLast(new StringDecoder());
}
});
try {
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
group.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 原理分析:
LineBasedFrameDecoder + StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。其工作原理是它依次遍历ByteBuf中的可读字节,判断是否有”\n” 或者”\r\n”, 如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器, 支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。回车换行解码器实际上是一种特殊的 DelimiterBasedFrameDecoder 解码器
如果连续读取到最大长度之后,仍然没有发现换行符就会抛出异常,同时忽略掉之前读到的异常码流。StringDecoder的功能非常简单,就是将接收到的对象换成字符串,然后继续调用后面的Handller。
4.2.2、DelimiterBasedFrameDecoder+StringDecoder示例
选定特殊字符作为收发双方发送数据包的分隔符,注:此方式需确定数据包中不含该分隔符,否则数据包会被拆包。若服务器与客户端之间相互发送的数据均含特定分隔符"_#",则C、S两端initChannel方法中都需添加该解码器。
- 服务端:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 告诉DelimiterBasedFrameDecoder以_#作为分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("_#".getBytes());
ChannelPipeline pipeline = ch.pipeline();
//1024表示单条消息的最大长度,当达到该长度还没有找到分隔符,则抛出TooLongFrameException
pipeline.addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
pipeline.addLast(new StringDecoder());
}
- 客户端:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("_#".getBytes());
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
pipeline.addLast(new StringDecoder());
}
注:消息尾需加上"_#" ,即ctx.writeAndFlush (yourMessage+"_#");
DelimiterBasedFrameDecoder 原理分析:解码时,判断当前已经读取的 ByteBuf 中是否包含分隔符 ByteBuf,如果包含,则截取对应的 ByteBuf 返回。
4.2.3、定义消息长度字段编解码示例
- 服务端
public class SocketServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加自定义解码处理器
pipeline.addLast(new SelfDefineDecodeHandler());
pipeline.addLast(new ServerBusinessHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8090).sync();
channelFuture.channel().closeFuture().sync();
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
自定义解码处理器:
/**
* 定长消息数据格式
*
* | length | msg | 头部length用4字节存储,存储的长度为消息体msg的总长度
*
*
* */
public class SelfDefineDecodeHandler extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf bufferIn, List<Object> out) throws Exception {
if (bufferIn.readableBytes() < 4) {
return;
}
//返回当前buff中readerIndex索引
int beginIndex = bufferIn.readerIndex();
//在当前readerIndex基础上读取4字节并返回,同时增加readIndex
int length = bufferIn.readInt();
/**
* 1.当可读数据小于length,说明包还没有接收完全
* 2.开始可读为beginindex,此时读完readInt后需要重置readerindex
* 3.重置readerindex后继续等待下一个读事件到来
* */
if (bufferIn.readableBytes() < length) {
//重置当前的readerindex为beginindex
bufferIn.readerIndex(beginIndex);
return;
}
//4字节存放length,这里整个消息长度为4+length,跳过当前消息,增大bufferIn的readindex,bufferIn中数组可复用
bufferIn.readerIndex(beginIndex + 4 + length);
//Returns a slice of this buffer's sub-region.
//取出当前的整条消息并存入otherByteBufRef中
ByteBuf otherByteBufRef = bufferIn.slice(beginIndex, 4 + length);
/**
* 1.每一个bytebuf都有一个计数器,每次调用计数器减1,当计数器为0时则不可用。
* 2.当前bytebuf中数据包含多条消息,本条信息会通过out返回被继续封装成一个新的bytebuf返回下一个hander处理
* 3.retain方法是将当前的bytebuf计数器加1
* */
otherByteBufRef.retain();
out.add(otherByteBufRef);
}
}
服务端业务处理器:
public class ServerBusinessHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
/**
* 1.读数据,这里收到的是一个完整的消息数据,从上述decoder方法中的out传递到当前逻辑
* 2.对消息进一步的解码
* */
ByteBuf buf = (ByteBuf)msg;
int length = buf.readInt();
assert length == (8);
byte[] head = new byte[4];
buf.readBytes(head);
String headString = new String(head);
assert "head".equals(headString);
byte[] body = new byte[4];
buf.readBytes(body);
String bodyString = new String(body);
assert "body".equals(bodyString);
}
}
- 客户端
public class SocketClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.handler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new SocketClientHandler());
}});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8090).sync();
channelFuture.channel().closeFuture().sync();
}
finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客户端消息处理器
public class SocketClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//暂不处理
}
/**
* 1.写数据的逻辑,先写入消息的总长度
* 2.分别写入消息体的内容
* 3.以bytebuf的方式发送数据
* */
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
UnpooledByteBufAllocator allocator = new UnpooledByteBufAllocator(false);
ByteBuf buffer = allocator.buffer(20);
buffer.writeInt(8);
buffer.writeBytes("head".getBytes());
buffer.writeBytes("body".getBytes());
ctx.writeAndFlush(buffer);
}
}
此外,本人觉得https://www.jianshu.com/p/adc2de3691c7基于netty实现了后台简易聊天程序, 可加深初学者理解,供参考。
另,netty相关分析可参考资料:
零拷贝:https://www.cnblogs.com/xys1228/p/6088805.html
channelHandler:https://www.cnblogs.com/krcys/p/9297092.html
https://www.jianshu.com/p/a9bcd89553f5
心跳保活处理:https://blog.csdn.net/u013967175/article/details/78591810
编解码处理:https://www.cnblogs.com/itdragon/p/8384014.html
本文参考以下大神博客:
http://www.importnew.com/26577.html