完整代码:https://gitee.com/firewolf/java-io/tree/master/java-io/netty-02-tcppacket
一、TCP粘包/拆包问题
TCP是一个“流”协议,所谓流,也就是一串没界限的数字,在TCP底层是连接成一片的,没有界限,因为不知道业务层含义,所以只会根据TCP缓冲区的实际情况进行划分,导致在业务层来看,一个完整的包可能会被TCP拆分成多个包,也可能把多个小的包封装起来,形成一个大的包。
粘包和拆包可能会出现如下情况:
假设客户端发送两个数据包D1和D2给服务端,由于服务端一次读到的字节数是不固定的,所以可能出现如下几种情况:
- 服务端分两次读取到了两个完整的数据包,分别是D1和D2,没有发生粘包和拆包;
- 服务端一次读取到了两个数据包,D1和D2粘在了一起,发生了粘包;
- 服务端分两次读了两个数据包,第一次读取了完整的D1包和D2包的一部分内容D2_1,第二次读取了D2包的另外一部分D2_2。发生了拆包和粘包;
- 服务端分两次读了两个数据包,第一次读取了D1包的部分D1_1,第二次读取了D1包的另一部分D1_2和D2完整包,发生了粘包和拆包;
- 除此之外,还可能发生多次拆包和粘包;
二、TCP粘包/拆包问题产生的原因
问题产生的原因主要有三个:
- 应用程序write写入的字节大小大于套接口发送缓冲区的大小;
- 进行MSS大小的TCP分段
- 以太网的payload大于MTU进行IP分片
三、TCP粘包/拆包问题的解决策略
- 消息定长,例如每个报文的长度为200,不够的时候,使用空格补齐;
- 在尾部增加回车换行符进行分割,如TCP协议;
- 在尾部增加特殊的字符进行分割,回车换行就是一种特殊的情况;
- 将消息分为消息头和消息体,消息头包含表示消息总长度字段;
- 使用更复杂的应用层协议;
四、TCP粘包/拆包问题演示
这里简单的写一个程序,功能是客户端连接服务端之后,向服务端发送20个字符串,服务端收到后进行显示:
(一)服务端
package com.firewolf.java.io.netty.packagee.paste.origin;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
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 java.util.HashMap;
import java.util.Map;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloServer {
public HelloServer(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MessageServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("启动服务端监听端口:" + port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
class MessageServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String message = new String(bytes);
System.out.println("有客户端消息:" + message);
buf.release();
}
}
public static void main(String[] args) {
new HelloServer(9999);
}
}
(二)客户端
package com.firewolf.java.io.netty.packagee.paste.origin;
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.ChannelInboundHandlerAdapter;
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.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloClient {
public HelloClient(String host, int port) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MessageClientHandler());
}
});
ChannelFuture f = bootstrap.connect(new InetSocketAddress(host, port)).sync();
System.out.println("连接服务器成功-----");
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
class MessageClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//遍历写出100个字符串
for (int i = 0; i < 20; i++) {
String message = "helllo," + i + System.getProperty("line.separator");
ByteBuf buf = Unpooled.buffer(message.getBytes().length);
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
}
public static void main(String[] args) {
new HelloClient("127.0.0.1", 9999);
}
}
启动服务端,再启动客户端,服务端打印信息如下:
有客户端消息:helllo,0
helllo,1
helllo,2
helllo,3
helllo,4
helllo,5
helllo,6
helllo,7
helllo,8
helllo,9
helllo,10
helllo,11
helllo,12
helllo,13
helllo,14
helllo,15
helllo,16
helllo,17
helllo,18
helllo,19
我们会发现,实际上, 服务端一次接受了全部字符串,也就是说,这些包并没有正常的接受到,而是发生了粘包。
五、Netty对粘包/拆包问题解决
为了解决TCP拆包/粘包导致的半读包问题,Netty默认提供了很多编码解码器用于处理半包。
(一)LineBasedFrameDecoder
LineBasedFrameDecoder会对包进行识别,根据包里面的回车(13)换行(10)符进行切割。
1. 服务端代码
package com.firewolf.java.io.netty.packagee.paste.solve;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
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 java.util.HashMap;
import java.util.Map;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloServerSolve {
public HelloServerSolve(int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
//添加编码器
.addLast(new LineBasedFrameDecoder(1024))
.addLast(new StringDecoder())
.addLast(new MessageServerHandler());
}
});
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("启动服务端监听端口:" + port);
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
class MessageServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//由于StringDecoder的功劳,接受消息的时候,我们可以直接转成String
String message = (String) msg;
System.out.println("有客户端消息:" + message);
}
}
public static void main(String[] args) {
new HelloServerSolve(9999);
}
}
服务端有两处改动
- 给启动类BootStrap添加了消息解码器;
- 接受消息的时候直接使用String接受,这个是由于添加了StringDecoder具有的功能。
2.客户端代码
package com.firewolf.java.io.netty.packagee.paste.solve;
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.ChannelInboundHandlerAdapter;
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.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.net.InetSocketAddress;
/**
* 作者:刘兴 时间:2019/5/15
**/
public class HelloClientSolve {
public HelloClientSolve(String host, int port) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new MessageClientHandler());
}
});
ChannelFuture f = bootstrap.connect(new InetSocketAddress(host, port)).sync();
System.out.println("连接服务器成功-----");
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
class MessageClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//遍历写出100个字符串
for (int i = 0; i < 20; i++) {
String message = "helllo," + i + System.getProperty("line.separator");
ByteBuf buf = Unpooled.buffer(message.getBytes().length);
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
}
public static void main(String[] args) {
new HelloClientSolve("127.0.0.1", 9999);
}
}
我们可以看到,客户端没有任何变化。
3.查看效果
再次启动后,打印内容如下:
有客户端消息:helllo,0
有客户端消息:helllo,1
有客户端消息:helllo,2
有客户端消息:helllo,3
有客户端消息:helllo,4
有客户端消息:helllo,5
有客户端消息:helllo,6
有客户端消息:helllo,7
有客户端消息:helllo,8
有客户端消息:helllo,9
有客户端消息:helllo,10
有客户端消息:helllo,11
有客户端消息:helllo,12
有客户端消息:helllo,13
有客户端消息:helllo,14
有客户端消息:helllo,15
有客户端消息:helllo,16
有客户端消息:helllo,17
有客户端消息:helllo,18
有客户端消息:helllo,19
可以看到,这些消息是分开了的,没有了粘包和拆包问题。
LineBasedFrameDecoder:以回车换行符作为对包进行切割;
StringDecoder:把ByteBuf转换成字符串
一般情况下,StringDecoder和LineBasedFrameDecoder会配合使用。
(二)DelimiterBasedFrameDecoder
其实DelimiterBasedFrameDecoder的功能和LineBasedFrameDecoder类似,只不过会比LineBasedFrameDecoder更加强大,支持我们自己指定用来切割的字符(串),使用方法和LineBasedFrameDecoder几乎是一模一样,这里只是简单的贴出部分代码:
1.服务端添加解码器
这里使用_$进行分割:
socketChannel.pipeline()
//添加编码器
.addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer("_$".getBytes())))
2.发送消息的时候,带上_$结尾
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//遍历写出100个字符串
for (int i = 0; i < 20; i++) {
String message = "helllo," + i + "_$";
ByteBuf buf = Unpooled.buffer(message.getBytes().length);
buf.writeBytes(message.getBytes());
ctx.writeAndFlush(buf);
}
}
(三)FixedLengthFrameDecoder
FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行解码。
同样,这里只是简单的贴出发生变化的代码(相对于DelimiterBasedFrameDecoder)。
1. 服务端添加解码器
socketChannel.pipeline()
//添加编码器
.addLast(new FixedLengthFrameDecoder(20))
这里设置长度为20
2. 客户端
没有任何变化,就不再贴出
效果如下:
有客户端消息:helllo,0_$helllo,1_$
有客户端消息:helllo,2_$helllo,3_$
有客户端消息:helllo,4_$helllo,5_$
有客户端消息:helllo,6_$helllo,7_$
有客户端消息:helllo,8_$helllo,9_$
有客户端消息:helllo,10_$helllo,11
有客户端消息:_$helllo,12_$helllo,
有客户端消息:13_$helllo,14_$helll
有客户端消息:o,15_$helllo,16_$hel
有客户端消息:llo,17_$helllo,18_$h
可以看到,得到的字符串长度都是20,而且,最后一个由于长度不足20,就没有显示出来。
六、总结
自行阅读上面几个解码器的原代码我们可以看到,这些解码器都是继承了ByteToMessageDecoder,对原始获取到的ByteBuf进行了处理,也就是说,我们也可以通过继承ByteToMessageDecoder来实现自己的解码器。