Netty(3) 分隔符和定长解码器的应用

分隔符和定长解码器的应用

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

  1. 消息长度固定,累计读取到长度总和为定长LEN的报文后,就认定读取到了一个完成的消息;将计数器置位,重新开始下一个数据报;
  2. 将回车换行符作为消息的结束符。例如FTP协议,这种方式在文本协议中应用比较广泛;
  3. 将特殊的分隔符作为消息的结束标志,回车换行符就是一种特殊的结束分隔符;
  4. 通过在消息头中定义长度字段来表示消息的总长度;

Netty对上面的4种应用做了统一的抽象,提供了四种解码器来解决对应的问题,使用起来非常方便。有了这些解码器,用户便不需要自己对读取报文进行人工解码,也不需要考虑TCP粘包和拆包。

前面我们学习了LineBasedFrameDecoder解决TCP粘包问题,在这一章我们继续学习另外两个——DelimiterBasedFrameDecoderFixedLengthFrameDecoder,前者可以完成以分隔符做结束标志的消息的解码,后者可以自动完成对定长消息的解码,它们都能解决TCP粘包/拆包导致的读半包问题。

一、DelimiterBasedFrameDecoder应用开发

通过对DelimiterBasedFrameDecoder的使用,我们可以自动完成以分隔符作为码流结束标识的消息的解码,下面通过经典的Echo服务为例进行演示。EchoServer接收到EchoClient的请求消息后,将其打印出来,然后将原始消息返回个客户端,消息以”$_“作为分隔符。

1.1 服务端开发

EchoServer代码:

public class EchoServer {

    public void bind(int port) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try{
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,100)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
                            ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            //绑定端口,同步等待成功
            ChannelFuture f = b.bind(port).sync();
            //等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int port = 8080;
        if(args!=null && args.length>0){
            try{
                port = Integer.parseInt(args[0]);
            }catch (NumberFormatException e){
                //
            }
        }
        new EchoServer().bind(port);
    }
}

EchoServerHandler代码:

public class EchoServerHandler extends ChannelHandlerAdapter {
    int count = 0;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String body = (String) msg;
        System.out.println("The is " + ++count + "times receive client:[" + body + "]");
        body += "$_";
        ByteBuf echo = Unpooled.copiedBuffer(body.getBytes());
        ctx.writeAndFlush(echo);
    }

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

1.2客户端开发

EchoClient代码:

public class EchoCilent {
    public void connect(int port, String host) throws InterruptedException {
        NioEventLoopGroup 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 {
                            ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
                            ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            //发起异步连接
            ChannelFuture f = b.connect(host, port).sync();
            //等待客户端链路关闭
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int port = 8080;
        try{
            if(args!=null && args.length>0){
                port = Integer.parseInt(args[0]);
            }
        }catch(NumberFormatException e){

        }
        new EchoCilent().connect(port,"127.0.0.1");
    }
}

EchoClientHandler代码:

public class EchoClientHandler extends ChannelHandlerAdapter {
    private int count = 0;

    static final String ECHO_REQ = "Hi,Welcome to Netty.$_";

    public EchoClientHandler() {
    }

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

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

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

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

1.3运行代码

在这里插入图片描述

在这里插入图片描述

二、FixedLengthFrameDecoder应用开发

这是一个固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者吧u需要考虑TCP的粘包/拆包问题。非常实用。

2.1 服务端开发

在服务端的ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再一次增加字符串解码器和EchoServerHandler,代码如下:

EchoServer代码:

public class EchoServer {
    public void bind(int port) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,100)
                    .childHandler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });

            //绑定端口
            ChannelFuture f = bootstrap.bind(port).sync();
            //等待关闭
            f.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int port = 8080;
        try {
            if(args!=null && args.length>0){
                port = Integer.parseInt(args[0]);
            }
        }catch (NumberFormatException e){

        }
        new EchoServer().bind(port);
    }
}

EchoServerHandler代码:

public class EchoServerHandler extends ChannelHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("Receive client : [" + msg + "]");
    }

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

2.2利用telnet 命令模拟客户端连接

win+r打开终端:

在这里插入图片描述

输入cmd然后回车,就能打开终端:

在这里插入图片描述

在终端输入:telnet localhost 8080,然后回车:

在这里插入图片描述

进入这个页面,说明连接成功了,我们按下·ctrl+l快捷键打开telnet回显功能,否则你看不到自己输入的东西:

在这里插入图片描述

出现Microsoft Telnet,我们直接回车:

在这里插入图片描述

重新回到空白的页面,然后输入信息,此刻面板出现我们输入的信息,无论我们输入多少内容,服务端都会接收,当长度满足定长解码器设置的长度,那么服务端就会将此消息当成一次消息:

在这里插入图片描述

服务端打印消息:

在这里插入图片描述

后面的信息省略了,因为超出了我们设置的20的长度。

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值