文章目录
使用Netty进行TCP的拆包与粘包
1.拆包与粘包
1.拆包发生原因
-
发送的数据大于套接字缓冲区剩余大小。
-
发送的数据大于**MTU(最大传输单元)**大小。
2.粘包发生原因
-
发送端原因导致的粘包,客户端在发送A包时,先将A包放入发送缓存,由于Nagle算法判断其发送的可用数据(去头数据)过小等待一小段时间,这时又发送了B包,系统将A和B合成一个大包发送给服务端。服务端读到大包,无法区分A和B包。
-
接收端原因导致的粘包,服务端缓存接收到客户端发送的A包,服务端应用未能及时读取缓存,此时服务端缓存又接收到客户端发送的B包,服务端应用读取缓存,无法区分AB包。
2.Netty中对于拆包与粘包的解决方案-Decoder
1.基于分隔符协议进行的解码
名称 | 描述 |
---|---|
LineBasedFrameDecoder | 根据/n或者/r/n来提取帧 |
DelimiterBasedFrameDecoder | 根据自定义符号提取帧 |
1.LineBasedFrameDecoder 基于回车换行符
客户端
public void connect() {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
// bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new NettyClientInitializer());
Channel channel = bootstrap.connect(host, port).sync().channel();
// 发送json字符串
for (int i = 0; i<5 ; i++) {
String msg = "哲/r/n学/r/n";
channel.writeAndFlush(msg);
}
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
服务端
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
/**
* 分隔符解码器
*/
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
/**
* 业务处理器
*/
pipeline.addLast(new MointorHandler());
}
}
输出结果
哲
学
哲
学
哲
学
哲
学
哲
学
2.DelimiterBasedFrameDecoder 基于自定义分隔符
客户端
public void connect() {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
// bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new NettyClientInitializer());
Channel channel = bootstrap.connect(host, port).sync().channel();
// 发送json字符串
for (int i = 0; i<5 ; i++) {
String msg = "哲♂学♂";
channel.writeAndFlush(msg);
}
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
服务端
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
/**
* 自定义分隔符
*/
ByteBuf byteBuf = Unpooled.copiedBuffer("♂".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(2048,byteBuf));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new MointorHandler());
}
}
输出结果
哲
学
哲
学
哲
学
哲
学
哲
学
2.基于长度进行解码
名称 | 描述 |
---|---|
FixedLengthFrameDecoder | 根据固定长度提取帧 |
LengthFieldBasedFrameDecoder | 提取可变长度帧,该字段的偏移量以及长度在构造函数中指定 |
1.FixedLengthFrameDecoder 基于固定长度解码
客户端
public void connect() {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
// bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new NettyClientInitializer());
Channel channel = bootstrap.connect(host, port).sync().channel();
// 发送json字符串
for (int i = 0; i<5 ; i++) {
String msg = "helloworld";
channel.writeAndFlush(msg);
}
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
服务端
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
/**
* 固定长度解码
*/
pipeline.addLast(new FixedLengthFrameDecoder(5));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new MointorHandler());
}
}
输出结果
hello
world
hello
world
hello
world
hello
world
hello
world
2.LengthFieldBasedFrameDecoder基于可变长度拆帧
LengthFieldBasedFrameDecoder一般作为解决比较复杂的通信协议的解码器
举个例子
对于复杂通信协议 一般都将所需要的信息包含在一个对象中
参数 | 字节长度 | 说明 |
---|---|---|
Head | 1 | 数据包头 存放一些标识数据 例如数据类型,数据来源等等 |
Length | 2 | 数据长度 标记了数据体(body)的长度 |
Body | 可变长度 | 数据体 包含了传输数据的内容 |
下面的一个实体类就是要传输的自定义通讯协议 ,根据这样的通信协议 就可以在客户端和服务端进行交互
@Data
public class MyProtocolBean {
//类型 系统编号 0xA 表示A系统,0xB 表示B系统
private byte type;
//信息标志 0xA 表示心跳包 0xC 表示超时包 0xC 业务信息包
private byte flag;
//内容长度
private int length;
//内容
private String content;
public MyProtocolBean(byte flag, byte type, int length, String content) {
this.flag = flag;
this.type = type;
this.length = length;
this.content = content;
}
}
之后 我们来继承LengthFieldBasedFrameDecoder来进行我们的自定义解码器
自定义解码器
public class MyProtocolDecoder extends LengthFieldBasedFrameDecoder {
/**
* 通信协议中 实体类的为byte+byte+int 字节数为6
*/
private static final int HEADER_SIZE = 6;
/**
*
* @param maxFrameLength 帧的最大长度
* @param lengthFieldOffset length字段偏移的地址
* @param lengthFieldLength length字段所占的字节长
* @param lengthAdjustment 修改帧数据长度字段中定义的值,可以为负数 因为有时候我们习惯把头部记入长度,若为负数,则说明要推后多少个字段
* @param initialBytesToStrip 解析时候跳过多少个长度
* @param failFast 为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常
*/
public MyProtocolDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
//在这里调用父类的方法,实现只得到想要的部分,这里全部都要,也可以只要body部分
in = (ByteBuf) super.decode(ctx,in);
//判断数据是否存在
if(in == null){
return null;
}
//判断数据字节长度是否大于数据头的长度
if(in.readableBytes()<HEADER_SIZE){
throw new Exception("字节数不足");
}
//读取type字段
byte type = in.readByte();
//读取flag字段
byte flag = in.readByte();
//读取length字段
int length = in.readInt();
if(in.readableBytes()!=length){
throw new Exception("标记的长度不符合实际长度");
}
//读取body
byte []bytes = new byte[in.readableBytes()];
in.readBytes(bytes);
return new MyProtocolBean(type,flag,length,new String(bytes,"UTF-8"));
}
}
将自定义的解码器配置到netty中的初始化器中
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
private static final int MAX_FRAME_LENGTH = 1024 * 1024;
private static final int LENGTH_FIELD_LENGTH = 4;
private static final int LENGTH_FIELD_OFFSET = 2;
private static final int LENGTH_ADJUSTMENT = 0;
private static final int INITIAL_BYTES_TO_STRIP = 0;
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
/**
* 自定义解码器
*/
pipeline.addLast(new MyProtocolDecoder(MAX_FRAME_LENGTH,LENGTH_FIELD_OFFSET,LENGTH_FIELD_LENGTH,LENGTH_ADJUSTMENT,INITIAL_BYTES_TO_STRIP,false));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new MointorHandler());
}
}
服务端处理器
@Component
@Slf4j
public class MointorHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
InetSocketAddress insocket = (InetSocketAddress)ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
log.info("服务端获取到客户端["+clientIp+"]的连接");
MyProtocolBean myProtocolBean = (MyProtocolBean)msg; //直接转化成协议消息实体
System.out.println("系统传送的内容是"+myProtocolBean.getContent());
}
}
在客户端当中 配置同样协议的编码器
public class MyProtocolEncoder extends MessageToByteEncoder<MyProtocolBean> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MyProtocolBean myProtocolBean, ByteBuf out) throws Exception {
if(myProtocolBean == null){
throw new Exception("msg is null");
}
out.writeByte(myProtocolBean.getType());
out.writeByte(myProtocolBean.getFlag());
out.writeInt(myProtocolBean.getLength());
out.writeBytes(myProtocolBean.getContent().getBytes(Charset.forName("UTF-8")));
}
}
客户端的连接方法
/**
* 连接方法
*/
public void connect(List<String> msglist) {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.TCP_NODELAY, true);
// bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new NettyClientInitializer());
Channel channel = bootstrap.connect(host, port).sync().channel();
for (String msg:msglist) {
//将需要传输的信息封装到之前设定好的通信协议当中
MyProtocolBean myProtocolBean = new MyProtocolBean((byte)0xA, (byte)0xC, msg.length(), msg);
channel.writeAndFlush(myProtocolBean);
}
channel.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
客户端的入口方法
/**
* 测试入口
*
* @param args
*/
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 8888;
NettyClient nettyClient = new NettyClient(host, port);
List<String> msgList = new ArrayList<String>();
msgList.add("thinking");
msgList.add("in");
msgList.add("java");
msgList.add("thinking in java php is the best");
nettyClient.connect(msgList);
}
这样 启动项目之后 我们获取到结果就是这样
2019-09-16 12:15:48.910 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 服务端获取到客户端[127.0.0.1]的连接
2019-09-16 12:15:48.911 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 系统传送的内容是thinking
2019-09-16 12:15:48.911 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 服务端获取到客户端[127.0.0.1]的连接
2019-09-16 12:15:48.911 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 系统传送的内容是in
2019-09-16 12:15:48.912 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 服务端获取到客户端[127.0.0.1]的连接
2019-09-16 12:15:48.912 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 系统传送的内容是java
2019-09-16 12:15:48.912 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 服务端获取到客户端[127.0.0.1]的连接
2019-09-16 12:15:48.912 INFO 5316 --- [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 系统传送的内容是thinking in java php is the best
6 — [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 服务端获取到客户端[127.0.0.1]的连接
2019-09-16 12:15:48.912 INFO 5316 — [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 系统传送的内容是java
2019-09-16 12:15:48.912 INFO 5316 — [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 服务端获取到客户端[127.0.0.1]的连接
2019-09-16 12:15:48.912 INFO 5316 — [ntLoopGroup-7-1] c.a.c.m.netty.handler.MointorHandler : 系统传送的内容是thinking in java php is the best