netty之分隔符和定长解码器解决之道

特此说明:我参考了李林锋老师写的《netty权威指南》一书,支持大家买正版书学习。学会了,赶紧写下来,不但为了加深记忆也希望对大家有所帮助!

上节我们讲解了LineBasedFrameDecoder和StringDecoder的使用,如果大家理解了这二个东西,那么这一章学起来将是轻车熟路。话不多说开始吧。

本章我们将讲解一下内容:

DelimiterBasedFrameDecoder(可以自动完成以分隔符做结束标志的消息解码)
FixedLengthFrameDecoder(可以自动完成对定长消息的解码)

在本章开始先给大家奉送代码,大家可以对照着学习。

基础知识

TCP在以流的方式进行数据传输中,上层的应用协议为了对消息进行区分,往往采用如下4种方式。

1.消息长度固定。当累计读取了定长(length)的报文后,我们就认为读到了一个完整的
2.将回车换行符作为消息结束符,例如FTP协议,这种方式在文本协议中应用比较广泛。
3.将特殊的分隔符作为消息的结束标志。如本章自定义的"$_"符,另外回车符就是种特殊的结束分隔符。
4.通过在消息头中定义长度字段来标识消息的总长度。

Netty很友好的对这4种应用做了统一的抽象,提供了4种解码器来解决对应的问题。大家在使用起来非常方便。我们再也不需要自己对读取的报文进行人工解码,也不需要考虑TCP的粘包和拆包。

正式开始

DelimiterBasedFrameDecoder应用开发

DelimiterBasedFrameDecoder可以自动完成以分隔符作为结束标记的消息解码。

下面我们实现一个功能:服务端收到客户端的消息后打印消息,并发送给客户端。

服务端核心代码
public class ChildChannelHandler extends
        ChannelInitializer<SocketChannel> {

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
//      ch.pipeline().addLast(new FixedLengthFrameDecoder(2));
        //下面这两句就是配置以“$_”为分隔符的解码器,怎么样简单吧
        ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
        //1024 是单条消息的最大长度,如果达到该长度后仍然没有找到分隔符就会抛出异常,这点大家要特别注意。delimiter就是我们的分隔符。
        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));

        ch.pipeline().addLast(new StringDecoder());
        ch.pipeline().addLast(new TimeServerHandler());
    }
}

解析 在initChannel方法中,我们配置了分割符“$_”,需要注意参数1024是单条消息的最大长度,如果达到该长度后仍然没有找到分隔符就会抛出异常。delimiter就是我们的分隔符。

继续看代码:

public class TimeServerHandler extends SimpleChannelInboundHandler<Object> {
    private int counter;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg)
            throws Exception {
            String  body = (String) msg;
            //打印消息,需要注意的是客户端发过来的消息,有“$_”分隔符,但是接收到消息并没有这个分隔符,说明DelimiterBaseFrameDecoder自动对消息进行解码,并去掉了“$_”. 
            System.out.println("This is "+(++counter)+"times receive client:["+body+"]");
            body+="$_";//现在我们要把消息重新发回给客户端,所以要重新把这个“$_”分割符加上
            ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
            ctx.writeAndFlush(echo);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }
}

解析 在注释中已经解释的很清楚了,这里就不解释了。

客户端核心代码

客户端的代码和服务器端基本上一样。

public class TimeClient {
    public void connect(int port,String host) throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY,true)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
//                  ch.pipeline().addLast(new FixedLengthFrameDecoder(2));
                    ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
                    ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            ChannelFuture f = b.connect(host,port).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            System.out.println("");
        }finally{
            group.shutdownGracefully();
        }

    }
    /**
     * 入口
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception{
        int port = 9090; //监听端口号
        new TimeClient().connect(port, "localhost");
    }
}

解析 主要看initChannel方法,我们接受到消息后,添加解码器DelimiterBasedFrameDecoder和StringDecoder,最后添加TimeClientHandler处理类,最后添加到pipeline中。

接着看代码:

public class TimeClientHandler extends SimpleChannelInboundHandler {
    private int counter;
    static final String ECHO_REQ = "Hi, myfriend , welcom to netty.$_";

    public TimeClientHandler(){
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for(int i=0;i<10;i++){
            ByteBuf echo = Unpooled.copiedBuffer(ECHO_REQ.getBytes());
            ctx.writeAndFlush(echo);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println("This is:"+(++counter)+"times receive server:["+msg+"]");
    }

}

看到没,在刚开始的静态常量 ECHO_REQ 的最后有一个“$_”分隔符。在channelActive方法中,我们循环发送10条消息给服务器端。

运行结果

服务器端

This is 1times receive client:[Hi, myfriend , welcom to netty.]
This is 2times receive client:[Hi, myfriend , welcom to netty.]
This is 3times receive client:[Hi, myfriend , welcom to netty.]
This is 4times receive client:[Hi, myfriend , welcom to netty.]
This is 5times receive client:[Hi, myfriend , welcom to netty.]
This is 6times receive client:[Hi, myfriend , welcom to netty.]
This is 7times receive client:[Hi, myfriend , welcom to netty.]
This is 8times receive client:[Hi, myfriend , welcom to netty.]
This is 9times receive client:[Hi, myfriend , welcom to netty.]
This is 10times receive client:[Hi, myfriend , welcom to netty.]

客户端

This is:1times receive server:[Hi, myfriend , welcom to netty.]
This is:2times receive server:[Hi, myfriend , welcom to netty.]
This is:3times receive server:[Hi, myfriend , welcom to netty.]
This is:4times receive server:[Hi, myfriend , welcom to netty.]
This is:5times receive server:[Hi, myfriend , welcom to netty.]
This is:6times receive server:[Hi, myfriend , welcom to netty.]
This is:7times receive server:[Hi, myfriend , welcom to netty.]
This is:8times receive server:[Hi, myfriend , welcom to netty.]
This is:9times receive server:[Hi, myfriend , welcom to netty.]
This is:10times receive server:[Hi, myfriend , welcom to netty.]

客户端和服务器端分别收到的10条消息,和我们的预期一样,说明了DelimiterBasedFrameDecoder可以自动采用分隔符做码流标识的消息进行解码。

FixedLengthFrameDecoder应用开发

FixedLengthFrameDecoder 是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题,非常方便。下面来看代码

服务器核心代码:
public class ChildChannelHandler extends
        ChannelInitializer<SocketChannel> {

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        //重点在这条上,我设置了6个字符做为一条消息的长度。
        ch.pipeline().addLast(new FixedLengthFrameDecoder(6));
        ch.pipeline().addLast(new StringDecoder());
        ch.pipeline().addLast(new TimeServerHandler());
    }
}

这个不做解释了很简单,看注释。

public class TimeServerHandler extends SimpleChannelInboundHandler<Object> {
    private int counter;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg)
            throws Exception {
            String  body = (String) msg;
            System.out.println("This is "+(++counter)+"times receive client:["+body+"]");
            body+="$_";
            ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
            ctx.writeAndFlush(echo);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }
}

这个也不解释了,就是收到消息后,打印再发给客户端。

客户端核心代码:
public class TimeClient {
    public void connect(int port,String host) throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group).channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY,true)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    //往这看,核心代码在这
                    ch.pipeline().addLast(new FixedLengthFrameDecoder(6));

                    ch.pipeline().addLast(new StringDecoder());
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });
            ChannelFuture f = b.connect(host,port).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            System.out.println("");
        }finally{
            group.shutdownGracefully();
        }

    }
    /**
     * 入口
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception{
        int port = 9090; //监听端口号
        new TimeClient().connect(port, "localhost");
    }
}

不解释,看下面代码

public class TimeClientHandler extends SimpleChannelInboundHandler {
    private int counter;
    static final String ECHO_REQ = "Hi, myfriend , welcom to netty.$_";

    public TimeClientHandler(){
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for(int i=0;i<2;i++){
            ByteBuf echo = Unpooled.copiedBuffer(ECHO_REQ.getBytes());
            ctx.writeAndFlush(echo);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println("This is:"+(++counter)+"times receive server:["+msg+"]");
    }
}

不解释了,很简单,在channelActive方法中循环发送2条消息(为什么现在不发10条了呢,嘿嘿,太多了,放不开。)

有趣的东西来了,大家看打印

服务器的打印:

This is 1times receive client:[Hi, my]
This is 2times receive client:[friend]
This is 3times receive client:[ , wel]
This is 4times receive client:[com to]
This is 5times receive client:[ netty]
This is 6times receive client:[.$_Hi,]
This is 7times receive client:[ myfri]
This is 8times receive client:[end , ]
This is 9times receive client:[welcom]
This is 10times receive client:[ to ne]
This is 11times receive client:[tty.$_]

客户端打印:

This is:1times receive server:[Hi, my]
This is:2times receive server:[$_frie]
This is:3times receive server:[nd$_ ,]
This is:4times receive server:[ wel$_]
This is:5times receive server:[com to]
This is:6times receive server:[$_ net]
This is:7times receive server:[ty$_.$]
This is:8times receive server:[_Hi,$_]
This is:9times receive server:[ myfri]
This is:10times receive server:[$_end ]
This is:11times receive server:[, $_we]
This is:12times receive server:[lcom$_]
This is:13times receive server:[ to ne]
This is:14times receive server:[$_tty.]

如果你仔细看就会发现,哎?!为什么服务器收到11条消息,而客户端收到14条消息呢?在这里我就不告诉大家了,大家仔细想想。

FixedLengthFrameDecoder解析
FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的拆包/粘包问题,非常实用。无论一次收到多少数据,它都会按照我们给定的长度进行解码。如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并且等待下个包到达后进行拼包,直到读到一个完整的包。

结尾

DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,
FixedLengthFrameDecoder用于对固定长度的消息进行自动解码。有了这二种解码器,再结合其他的解码器,可以轻松完成对很多消息的自动解码,而且不再考虑拆包/粘包的读半包问题,极大提高了开发效率。

在本章开始先给大家奉送代码,大家可以对照着学习。

好了,就讲到这里吧。今天我们讲解了LineBasedFrameDecoder和StringDecoder的使用,因为我也是现学现卖,讲解不到的地方,希望大家谅解。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序编织梦想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值