1、TCP 粘包和拆包基本介绍
- 图解
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
1)、服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包
2)、服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包
3)、服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包
4)、服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。
2. 解决方案
- 使用自定义协议 + 编解码器 来解决
- 重点是要解决服务器端每次读取数据长度的问题, 这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的TCP 粘包、拆包
3. 代码实现
- 编写服务端
MyServer
public class MyServer {
public static void main(String[] args) {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitalizer());
ChannelFuture channelFuture = bootstrap.bind(8888).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e) {
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
- 编写
MyServerInitalizer
public class MyServerInitalizer 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());
}
}
- 编写
MyServerHandler
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
System.out.println("服务器收到的消息: " + new String(msg.getContent(), Charset.forName("utf-8")));
System.out.println("服务器收到的消息长度: " + msg.getLen());
//回送给客户端
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setContent("hello, 客户端".getBytes(Charset.forName("utf-8")));
messageProtocol.setLen("hello, 客户端".getBytes().length);
ctx.writeAndFlush(messageProtocol);
}
}
- 编写编码器
MyMessageEncoder
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());
}
}
- 编写解码器
MyMessageDecoder
public class MyMessageDecoder extends ReplayingDecoder<MessageProtocol> {
@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 messageProtocol = new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
//下一个handler处理
out.add(messageProtocol);
}
}
- 编写
MessageProtocol
封装内容
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;
}
}
- 编写客户端
MyClient
public class MyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new MyClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888).sync();
channelFuture.channel().closeFuture().sync();
group.shutdownGracefully();
}
}
- 编写客户端
MyClientInitializer
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());
}
}
- 编写客户端
MyClientHandler
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
System.out.println("客户端收到消息: " + new String(msg.getContent(), Charset.forName("utf-8")));
System.out.println("客户端收到消息: " + msg.getLen());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//发送给服务端
for (int i = 0; i < 5; i++) {
String message = "今天天气冷,吃火锅";
byte[] content = message.getBytes(Charset.forName("utf-8"));
int length = message.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 {
ctx.close();
}
}
- 效果
服务端:
客户端:
分析:当客户端发送5次内容给服务器,此时会连续调用5次编码器,服务端接受到内容解码后打印,并回送5次编码后的hello,客户端
给客户端,此时服务端打印内容后看到编码器被调用了,客户端接收后经过解码器解码后打印。