使用Netty进行TCP的拆包与粘包

使用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一般作为解决比较复杂的通信协议的解码器

举个例子

对于复杂通信协议 一般都将所需要的信息包含在一个对象中

参数字节长度说明
Head1数据包头 存放一些标识数据 例如数据类型,数据来源等等
Length2数据长度 标记了数据体(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




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值