提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
TCP 粘包和拆包基本介绍
- TCP是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端) 都要有一一成对的socket,因此,发送端为了将多个发给接收端的包,更有效的发 给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合 并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于 分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
- 由于TCP无消息保护边界, 需要在接收端处理消息边界问题,也就是我们所说的粘 包、拆包问题, 看一张图
- TCP粘包、拆包图解
假设客户端分别发送了两个数据包D1和D2给 服务端,由于服务端一次读取到字节数是不确 定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包, 分别是D1和D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1和D2 粘合在一起,称之为TCP粘包
- 服务端分两次读取到了数据包,第一次读 取到了完整的D1包和D2包的部分内容,第 二次读取到了D2包的剩余内容,这称之为 TCP拆包
- 服务端分两次读取到了数据包,第一次读 取到了D1包的部分内容D1_1,第二次读取 到了D1包的剩余部分内容D1_2和完整的D2 包。
TCP 粘包和拆包现象实例
在编写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<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyServerHandler());
}
}
服务器端处理器
public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
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<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyClientHandler());
}
}
客户端处理器:
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
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的粘包或拆包,服务器就无法辨别接受的数据是否是完整的。
netty解决 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<Void> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
System.out.println("MyMessageDeCoder decode 被调用");
int length = in.readInt();
byte[] content = new byte[length];
in.readBytes(content);
//封装成MessageProtocol对象,放入out,传给下一个handler处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
out.add(messageProtocol);
}
}
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
System.out.println("MyMessageEncoder encode 被调用");
out.writeInt(msg.getLen());
out.writeBytes(msg.getContent());
}
}
服务器端代码:
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<MessageProtocol> {
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<SocketChannel> {
@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<MessageProtocol> {
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<SocketChannel> {
@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 被调用