文章目录提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
- TCP 粘包和拆包基本介绍
- TCP 粘包和拆包现象实例
- netty解决 TCP 粘包和拆包解决方案
- 看一个具体的实例:
TCP 粘包和拆包基本介绍
- TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端) 都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发 给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合 并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于 分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于TCP无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘 包、拆包问题, 看一张图
- TCP粘包、拆包图解
假设客户端分别发送了两个数据包D1和D2给 服务端,由于服务端一次读取到字节数是不确 定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包, 分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2 粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读 取到了完整的D1包和D2包的部分内容,第 二次读取到了D2包的剩余内容,这称之为 TCP拆包
- 服务端分两次读取到了数据包,第一次读 取到了D1包的部分内容D1_1,第二次读取 到了D1包的剩余部分内容D1_2和完整的D2 包。
在编写Netty 程序时,如果没有做处理,就会发生粘包和拆包的问题
看一个具体的实例:
服务器:
public class MyServer {
public static void main(String[] args) throws Exception {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(6000).sync();
System.out.println("server is ready");
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务器端通道初始化类
public class MyServerInitializer extends ChannelInitializer{ @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyServerHandler()); } }
服务器端处理器
public class MyServerHandler extends SimpleChannelInboundHandler{ private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); String message = new String(buffer, Charset.forName("utf-8")); System.out.println("服务器接收到的数据"+message+" "); System.out.println("服务器接收到的消息量"+(++this.count)); //服务器会送数据给客户端 ByteBuf responseByteBuf = Unpooled.copiedBuffer(UUID.randomUUID().toString()+" ", Charset.forName("utf-8")); ctx.writeAndFlush(responseByteBuf); } }
客户端:
public class Myclient {
public static void main(String[] args) throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new MyclientInitializer());
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
客户端通道初始化类
public class MyclientInitializer extends ChannelInitializer{ @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new MyClientHandler()); } }
客户端处理器:
public class MyClientHandler extends SimpleChannelInboundHandler{ private int count; @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { byte[] buffer = new byte[msg.readableBytes()]; msg.readBytes(buffer); String message = new String(buffer, Charset.forName("utf-8")); System.out.println("客户端接收到的消息"+message); System.out.println("客户端接收到的消息的数量"+(++this.count)); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //使用for循环发送10条数据 hello,server for (int i=0;i<10;i++){ ByteBuf buf = Unpooled.copiedBuffer("hello,server" + i, CharsetUtil.UTF_8); ctx.writeAndFlush(buf); } } }
服务器接收到的数据hello,server0hello,server1hello,server2hello,server3hello,server4hello,server5hello,server6hello,server7hello,server8hello,server9 服务器接收到的消息量1 服务器接收到的数据hello,server0 服务器接收到的消息量1 服务器接收到的数据hello,server1 服务器接收到的消息量2 服务器接收到的数据hello,server2hello,server3hello,server4hello,server5 服务器接收到的消息量3 服务器接收到的数据hello,server6 服务器接收到的消息量4 服务器接收到的数据hello,server7hello,server8hello,server9 服务器接收到的消息量5 服务器接收到的数据hello,server0 服务器接收到的消息量1 服务器接收到的数据hello,server1 服务器接收到的消息量2 服务器接收到的数据hello,server2hello,server3 服务器接收到的消息量3 服务器接收到的数据hello,server4hello,server5hello,server6 服务器接收到的消息量4 服务器接收到的数据hello,server7hello,server8 服务器接收到的消息量5 服务器接收到的数据hello,server9 服务器接收到的消息量6
现象:
一个起了三个客户端给服务器发送相同的消息,服务器分别是1次全部接收、5次全部接收、6次全部接收
这就说明发生了TCP的粘包或拆包,服务器就无法辨别接受的数据是否是完整的。
- 使用自定义协议 + 编解码器 来解决
- 关键就是要解决 服务器端每次读取数据长度的问题, 这个问题解决,就不会出现服 务器多读或少读数据的问题,从而避免的TCP 粘包、拆包 。
- 要求客户端发送 5 个 Message 对象, 客户端每次发送一个 Message 对象
- 服务器端每次接收一个Message, 分5次进行解码, 每读取到 一个Message , 会回 复一个Message 对象 给客户端.
代码:
首先定义客户端和服务器端的协议:
package com.atguigu.bio.nio.protocoltcp;
//协议包
public class MessageProtocol {
private int len;//关键
private byte[] content;
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
自定义编码和解码器
public class MyMessageDeCoder extends ReplayingDecoder{ @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
服务器端代码:
public class MyServer {
public static void main(String[] args) throws Exception {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(6000).sync();
System.out.println("server is ready");
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class MyServerHandler extends SimpleChannelInboundHandler {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
//接受到数据并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务器接收到信息如下");
System.out.println("长度="+len);
System.out.println("内容="+new String(content,Charset.forName("utf-8")));
System.out.println("服务器接收到的消息包数量="+(++this.count));
//回复消息
String responseContent = UUID.randomUUID().toString();
byte[] responseContent2 = responseContent.getBytes("utf-8");
int length = responseContent2.length;
//构建协议包
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setContent(responseContent2);
messageProtocol.setLen(length);
ctx.writeAndFlush(messageProtocol);
}
}
public class MyServerInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageDeCoder());
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyServerHandler());
}
}
客户端
public class Myclient {
public static void main(String[] args) throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new MyclientInitializer());
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
public class MyClientHandler extends SimpleChannelInboundHandler {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("客户端接收消息如下");
System.out.println("长度="+len);
System.out.println("内容="+new String(content,Charset.forName("utf-8")));
System.out.println("客户端接收到的消息包数量="+(++this.count));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//发送十条数据“天黑打老虎”
for (int i=0;i<5;i++){
String mes = "天黑打老虎";
byte[] content = mes.getBytes(Charset.forName("utf-8"));
int length = mes.getBytes(Charset.forName("utf-8")).length;
//创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setContent(content);
messageProtocol.setLen(length);
ctx.writeAndFlush(messageProtocol);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常消息="+cause.getMessage());
ctx.close();
}
}
public class MyclientInitializer extends ChannelInitializer {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyMessageDeCoder());
pipeline.addLast(new MyClientHandler());
}
}
测试:
客户端:
MyMessageEncoder encode 被调用 MyMessageEncoder encode 被调用 MyMessageEncoder encode 被调用 MyMessageEncoder encode 被调用 MyMessageEncoder encode 被调用 MyMessageDeCoder decode 被调用 客户端接收消息如下 长度=36 内容=e56bbb54-a444-42ea-b810-4b1e03007baf 客户端接收到的消息包数量=1 MyMessageDeCoder decode 被调用 客户端接收消息如下 长度=36 内容=96194cc6-e090-44c2-a0d6-8d3c97b86ac1 客户端接收到的消息包数量=2 MyMessageDeCoder decode 被调用 客户端接收消息如下 长度=36 内容=f17b9deb-3a2e-42c4-bf81-6f419e75057a 客户端接收到的消息包数量=3 MyMessageDeCoder decode 被调用 客户端接收消息如下 长度=36 内容=dd965499-4685-4f34-be61-661d52f0ad34 客户端接收到的消息包数量=4 MyMessageDeCoder decode 被调用 客户端接收消息如下 长度=36 内容=a49d42e0-31ff-45b9-8f01-b49a6e821369 客户端接收到的消息包数量=5
服务器:
MyMessageDeCoder decode 被调用 服务器接收到信息如下 长度=15 内容=天黑打老虎 服务器接收到的消息包数量=1 MyMessageEncoder encode 被调用 MyMessageDeCoder decode 被调用 服务器接收到信息如下 长度=15 内容=天黑打老虎 服务器接收到的消息包数量=2 MyMessageEncoder encode 被调用 MyMessageDeCoder decode 被调用 服务器接收到信息如下 长度=15 内容=天黑打老虎 服务器接收到的消息包数量=3 MyMessageEncoder encode 被调用 MyMessageDeCoder decode 被调用 服务器接收到信息如下 长度=15 内容=天黑打老虎 服务器接收到的消息包数量=4 MyMessageEncoder encode 被调用 MyMessageDeCoder decode 被调用 服务器接收到信息如下 长度=15 内容=天黑打老虎 服务器接收到的消息包数量=5 MyMessageEncoder encode 被调用



