TCP拆包、粘包问题
熟悉TCP编程的可能都知道,无论是服务器端还是客户端,当我们读取或者发送数据的时候,都需要考虑TCP底层的粘包/拆包机制。
TCP是一个“流”协议,所谓流就是没有界限的遗传数据。大家可以想像下如果河里的水好比数据,他们是连成一片的,没有分界线,TCP底层并不了解上层业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包的划分,也就是说在业务上我们是一个完整的包可能会被TCP分成多个包进行发送,也就可能把多个小包分装成一个打的数据包发送出去,这就是所谓的TCP粘包、拆包问题。
分析TCP粘包、拆包问题产生原因:
1.应用write写入的字节大小大于套接字发送缓冲区的大小
2.进行MSS大小的TCP分段
3.以太网帧的payload大于MTU进行IP分片
TCP拆包、粘包解决方案
粘包拆包问题的解决方案,根据业界主流协议的有三方案:
1.消息定长,例如每个报文的大小固定2000个字节,如果不够空位补空格
2.在包尾部增加特殊字符进行分割,例如回车等
3.将消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务逻辑处理
Netty解决TCP拆包粘包问题
1.分隔符类 DelimiterBasedFrameDecoder (自定义分隔符)
2.FixedLengthFrameDecoder(定长)
自定义分隔示例
maven依赖
<!--netty依赖-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.20.Final</version>
</dependency>
服务端
package com.example.netty.pack;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
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.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
/**
* netty 服务端
*
* @author lanx
* @date 2022/3/20
*/
public class Server {
public static void main(String[] args) throws InterruptedException {
//用户接收客户端连接的线程工作组
EventLoopGroup bossGroup = new NioEventLoopGroup();
//用于接收客户端连接读写操作的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类 帮我我们创建netty服务
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//绑定两个工作组
.channel(NioServerSocketChannel.class)//设置NIO模式
//option 针对于服务端配置; childOption 针对于客户端连接通道配置
.option(ChannelOption.SO_BACKLOG, 1024)//设置tcp缓冲区
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024)//设置发送数据的缓存大小
.childOption(ChannelOption.SO_RCVBUF, 32 * 1024)//设置读取数据的缓存大小
.childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持长连接
//初始化绑定服务通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
//1. 定义特殊字符分割 处理数据
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
/**
* int maxFrameLength 传输数据的最大长度
* ByteBuf delimiter 分隔符
*/
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
sc.pipeline().addLast(new ServerHandler());
}
});
ChannelFuture cf = b.bind(8765).sync();
//释放连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
package com.example.netty.pack;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* 服务端 监听器
*
* @author lanx
* @date 2022/3/20
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当我们的通道被激活的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------服务端通道激活---------");
}
/**
* 当我们通道里有数据进行读取的时候触发的监听
*
* @param ctx netty服务上下文
* @param msg 实际传输的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// NIO 通信 (传输的数据是什么? ---------> Buffer 对象)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
// 从缓冲区获取数据到 req
buf.readBytes(req);
//读取到的数据转换为字符串
String body = new String(req, "utf-8");
System.out.println("服务端读取到数据:" + body);
//响应给客户端的数据
ctx.writeAndFlush(Unpooled.copiedBuffer("netty server response data$_".getBytes()));
// 添加 addListener 可以触发关闭通道监听事件(客户端短连接场景使用)
// .addListener(ChannelFutureListener.CLOSE);
}catch (Exception e){
e.printStackTrace();
}finally {
//释放数据
ReferenceCountUtil.release(msg);
}
}
/**
* 当我们读取完成数据的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------服务端数据读取完毕---------");
}
/**
* 当我们读取数据异常的时候触发的监听
*
* @param ctx
* @param cause
* @throws Exception
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("--------服务端数据读取异常---------");
ctx.close();
}
}
客戶端
package com.example.netty.pack;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
/**
* netty 客户端
*
* @author lanx
* @date 2022/3/20
*/
public class Client {
public static void main(String[] args) throws Exception {
//线程工作组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类 帮我我们创建netty服务
Bootstrap b = new Bootstrap();
b.group(workerGroup)//绑定两个工作组
.channel(NioSocketChannel.class)//设置NIO模式
//初始化绑定服务通道
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
//1. 定义特殊字符分割 处理数据
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
/**
* int maxFrameLength 传输数据的最大长度
* ByteBuf delimiter 分隔符
*/
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf = b.connect("127.0.0.1", 8765).syncUninterruptibly();
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_1$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_2$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_3$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_4$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_5$_".getBytes()));
//释放连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
}
}
package com.example.netty.pack;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* 客户端 监听器
*
* @author lanx
* @date 2022/3/20
*/
public class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当我们的通道被激活的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------客户端通道激活---------");
}
/**
* 当我们通道里有数据进行读取的时候触发的监听
*
* @param ctx netty服务上下文
* @param msg 实际传输的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// NIO 通信 (传输的数据是什么? ---------> Buffer 对象)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
// 从缓冲区获取数据到 req
buf.readBytes(req);
//读取到的数据转换为字符串
String body = new String(req, "utf-8");
System.out.println("客户端读取到数据:" + body);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放数据 (如果你读取数据后又写出去数据就不需要调用此方法,因为底层代码帮忙实现额释放数据)
ReferenceCountUtil.release(msg);
}
}
/**
* 当我们读取完成数据的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------客户端数据读取完毕---------");
}
/**
* 当我们读取数据异常的时候触发的监听
*
* @param ctx
* @param cause
* @throws Exception
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("--------客户端数据读取异常---------");
ctx.close();
}
}
定长示例
服务端
package com.example.netty.leng;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
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.FixedLengthFrameDecoder;
/**
* netty 服务端
*
* @author lanx
* @date 2022/3/20
*/
public class Server {
public static void main(String[] args) throws InterruptedException {
//用户接收客户端连接的线程工作组
EventLoopGroup bossGroup = new NioEventLoopGroup();
//用于接收客户端连接读写操作的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类 帮我我们创建netty服务
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)//绑定两个工作组
.channel(NioServerSocketChannel.class)//设置NIO模式
//option 针对于服务端配置; childOption 针对于客户端连接通道配置
.option(ChannelOption.SO_BACKLOG, 1024)//设置tcp缓冲区
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024)//设置发送数据的缓存大小
.childOption(ChannelOption.SO_RCVBUF, 32 * 1024)//设置读取数据的缓存大小
.childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持长连接
//初始化绑定服务通道
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
//2 定长 处理数据
sc.pipeline().addLast(new FixedLengthFrameDecoder(20));
sc.pipeline().addLast(new ServerHandler());
}
});
ChannelFuture cf = b.bind(8765).sync();
//释放连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
package com.example.netty.leng;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* 服务端 监听器
*
* @author lanx
* @date 2022/3/20
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {
/**
* 当我们的通道被激活的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------服务端通道激活---------");
}
/**
* 当我们通道里有数据进行读取的时候触发的监听
*
* @param ctx netty服务上下文
* @param msg 实际传输的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// NIO 通信 (传输的数据是什么? ---------> Buffer 对象)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
// 从缓冲区获取数据到 req
buf.readBytes(req);
//读取到的数据转换为字符串
String body = new String(req, "utf-8");
System.out.println("服务端读取到数据:" + body);
//响应给客户端的数据
ctx.writeAndFlush(Unpooled.copiedBuffer("netty server response data$_".getBytes()));
// 添加 addListener 可以触发关闭通道监听事件(客户端短连接场景使用)
// .addListener(ChannelFutureListener.CLOSE);
}catch (Exception e){
e.printStackTrace();
}finally {
//释放数据
ReferenceCountUtil.release(msg);
}
}
/**
* 当我们读取完成数据的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------服务端数据读取完毕---------");
}
/**
* 当我们读取数据异常的时候触发的监听
*
* @param ctx
* @param cause
* @throws Exception
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("--------服务端数据读取异常---------");
ctx.close();
}
}
客戶端
package com.example.netty.leng;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
/**
* netty 客户端
*
* @author lanx
* @date 2022/3/20
*/
public class Client {
public static void main(String[] args) throws Exception {
//线程工作组
EventLoopGroup workerGroup = new NioEventLoopGroup();
//辅助类 帮我我们创建netty服务
Bootstrap b = new Bootstrap();
b.group(workerGroup)//绑定两个工作组
.channel(NioSocketChannel.class)//设置NIO模式
//初始化绑定服务通道
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
//为通道进行初始化:数据传输过来的时候会进行拦截和执行 (可以有多个拦截器)
//2 定长 处理数据
sc.pipeline().addLast(new FixedLengthFrameDecoder(20));
sc.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture cf = b.connect("127.0.0.1", 8765).syncUninterruptibly();
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_1$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_2$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_3$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_4$_".getBytes()));
cf.channel().writeAndFlush(Unpooled.copiedBuffer("netty client request data_5$_".getBytes()));
//释放连接
cf.channel().closeFuture().sync();
workerGroup.shutdownGracefully();
}
}
package com.example.netty.leng;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
/**
* 客户端 监听器
*
* @author lanx
* @date 2022/3/20
*/
public class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当我们的通道被激活的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------客户端通道激活---------");
}
/**
* 当我们通道里有数据进行读取的时候触发的监听
*
* @param ctx netty服务上下文
* @param msg 实际传输的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// NIO 通信 (传输的数据是什么? ---------> Buffer 对象)
ByteBuf buf = (ByteBuf) msg;
//定义byte数组
byte[] req = new byte[buf.readableBytes()];
// 从缓冲区获取数据到 req
buf.readBytes(req);
//读取到的数据转换为字符串
String body = new String(req, "utf-8");
System.out.println("客户端读取到数据:" + body);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放数据 (如果你读取数据后又写出去数据就不需要调用此方法,因为底层代码帮忙实现额释放数据)
ReferenceCountUtil.release(msg);
}
}
/**
* 当我们读取完成数据的时候触发的监听
*
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("--------客户端数据读取完毕---------");
}
/**
* 当我们读取数据异常的时候触发的监听
*
* @param ctx
* @param cause
* @throws Exception
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("--------客户端数据读取异常---------");
ctx.close();
}
}