3.Netty编解码和粘包拆包

1.Netty组件
 
ChannelHandler
ChannelHandler充当了处理读写数据的应用程序逻辑容器,管理读写数据的生命周期。例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收read事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理写数据的。
 
ChannelPipeline
ChannelPipeline提供了ChannelHandler链的容器。写数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之读数据只调用pipeline里的ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)
 
 
例如:服务端读数据,从head往tail的方向处理
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        // 等待队列
        .option(ChannelOption.SO_BACKLOG,1024)
        // 创建通道初始化对象,设置初始化参数
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 给worker group的channel设置处理器
                ch.pipeline().addLast(new NettyServerHandler());
                ch.pipeline().addLast(new Server2Handler());
                ch.pipeline().addLast(new Server3Handler());
            }
        });

 

 
console输出信息如下:
 
2.编解码器
当通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节
Netty提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
Netty提供了很多编解码器,比如编解码字符串的StringEncoder和StringDecoder,编解码对象的ObjectEncoder和ObjectDecoder等。
当然也可以通过集成ByteToMessageDecoder自定义编解码器。
public class MyEncoder extends MessageToByteEncoder {
    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
        out.writeDouble((Double) msg);
    }
}
public class MyDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        out.add(in.readDouble());
    }
}
ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // 等待队列
                    .option(ChannelOption.SO_BACKLOG,1024)
                    // 创建通道初始化对象,设置初始化参数
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 给worker group的channel设置处理器
//                            ch.pipeline().addLast(new ObjectEncoder());
//                            ch.pipeline().addLast(new ObjectDecoder(className -> Class.forName(className)));
                            ch.pipeline().addLast(new MyDecoder());
                            ch.pipeline().addLast(new MyEncoder());
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * 客户端与服务端channel连接成功后置回调函数
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
//        ctx.writeAndFlush(new User("a",1));
        ctx.writeAndFlush(2.33);
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//        User user = (User) msg;
//        System.out.println("收到服务端的消息:"+user.toString());
        System.out.println("收到服务端的消息:"+msg);
    }
}

 

 
3.Netty粘包拆包
TCP粘包拆包是指发送方发送的若干包数据到接收方接收时粘成一包或某个数据包被拆开接收。如下图所示,client发了两个数据包D1和D2,但是server端可能会收到如下几种情况的数据。
 
为什么出现粘包现象
TCP 是面向连接的, 面向流的, 提供高可靠性服务。 收发两端(客户端和服务器端) 都要有成对的 socket,因此, 发送端为了将多个发给接收端的包, 更有效的发给对方, 使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据, 合并成一个大的数据块, 然后进行封包。 这样做虽然提高了效率, 但是接收端就难于分辨出完整的数据包了, 因为面向流的通信是无消息保护边界的。
 
解决方案
1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符。
2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。
 
4.心跳检测
所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性.
在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 看下它的构造器:
 
public IdleStateHandler(
        int readerIdleTimeSeconds,
        int writerIdleTimeSeconds,
        int allIdleTimeSeconds) {

    this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
         TimeUnit.SECONDS);
}

 

 
这里解释下三个参数的含义:
  • readerIdleTimeSeconds: 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.
  • writerIdleTimeSeconds: 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.
  • allIdleTimeSeconds: 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.
注:这三个参数默认的时间单位是秒。若需要指定其他时间单位,可以使用另一个构造方法:
 
public IdleStateHandler(
        long readerIdleTime, long writerIdleTime, long allIdleTime,
        TimeUnit unit) {
    this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}

 

 
要实现Netty服务端心跳检测机制需要在服务器端的ChannelInitializer中加入如下的代码:
 
pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));

 

初步地看下IdleStateHandler源码,先看下IdleStateHandler中的channelRead方法:
红框代码其实表示该方法只是进行了透传,不做任何业务逻辑处理,让channelPipeline中的下一个handler处理channelRead方法
我们再看看channelActive方法:
这里有个initialize的方法,这是IdleStateHandler的精髓,接着探究:
这边会触发一个Task,ReaderIdleTimeoutTask,这个task里的run方法源码是这样的:
第一个红框代码是用当前时间减去最后一次channelRead方法调用的时间,假如这个结果是6s,说明最后一次调用channelRead已经是6s之前的事情了,你设置的是5s,那么nextDelay则为-1,说明超时了,那么第三个红框代码则会触发下一个handler的userEventTriggered方法:
如果没有超时则不触发userEventTriggered方法。
 
代码示例
服务端
 
// 启动服务端
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        // 等待队列
        .option(ChannelOption.SO_BACKLOG,1024)
        // 创建通道初始化对象,设置初始化参数
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 给worker group的channel设置处理器
                ch.pipeline().addLast(new MessageEncoder());
                ch.pipeline().addLast(new MessageDecoder());
                ch.pipeline().addLast(new IdleStateHandler(6,0,0));
                ch.pipeline().addLast(new IdleServerHandler());
            }
        });
// 启动服务并绑定端口
ChannelFuture channelFuture = serverBootstrap.bind(9000).sync();
System.out.println("netty server start ...");

 

 
空闲超过指定心跳次数,关闭空闲连接
 
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    IdleStateEvent stateEvent = (IdleStateEvent)evt;
    switch (stateEvent.state()){
        case READER_IDLE:
            readIdleTimes ++;
            log.warn("客户端channel 读空闲:"+readIdleTimes+"次");
            break;
        case ALL_IDLE:
        case WRITER_IDLE:
        default:
            break;
    }
    if (readIdleTimes >= MAX_IDLE_HEARTBEAT_TIMES){
        log.info("读空闲超过三次,关闭当前客户端的连接,释放资源:"+ctx.channel().remoteAddress());
        byte[] content = CLOSE_NOTICE.getBytes(CharsetUtil.UTF_8);
        MessageProtocol entity = new MessageProtocol();
        entity.setContent(content);
        entity.setLength(content.length);
        ctx.writeAndFlush(entity);
        ctx.close();
    }
}

 

 
客户端模拟发送心跳包
 
// 客户端启动对象
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {


                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 设置客户端事件处理器
                            ch.pipeline().addLast(new MessageEncoder());
                            ch.pipeline().addLast(new MessageDecoder());
                            ch.pipeline().addLast(new IdleClientHandler());
                        }
                    });
            // 启动客户端,连接服务器
            System.out.println("Netty Client Start...");
            ChannelFuture future = bootstrap.connect("127.0.0.1", 9000).sync();
            while (future.channel().isActive()){
                int randomTime = new Random().nextInt(10);
//                log.info("客户端休眠:"+randomTime+"秒");
                Thread.sleep(randomTime * 1000);
                byte[] content = HEAT_BEAT.getBytes(CharsetUtil.UTF_8);
                MessageProtocol entity = new MessageProtocol();
                entity.setContent(content);
                entity.setLength(content.length);
                future.channel().writeAndFlush(entity);
            }

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值