Netty编解码&粘包拆包&心跳检测与重连&零拷贝

1、 BIO/NIO/AIO

2、netty组件解析

3、Netty编解码&粘包拆包&心跳检测与重连&零拷贝

      Netty心跳检测代码实例

编解码

Netty涉及到的编解码组件有channel、channelHandler和channelPipe等,详细解析如下:

channelHandler:处理入站、出站数据的应用程序逻辑处理容器。例如,实现        channelInBoundHandler接口(或channelInBoundHandlerAdapter),就可以接受入站时间和数据,这些数据会被你的业务逻辑程序处理。当你要给连接的客户端发送响应时,也可以用channelInBoundHandler冲刷数据。你的业务要逻辑通常写在一个或者多个channelInBoundHandler中。channelOutBoundHandler是处理出战数据的。

channelPipeline:联想我们写的NIO程序,启动ServerBootstrap前,设置了bootstrap的各种属性,采用的链式编程,其中有调用本身的.childHandler(new ChannelInitializer<SocketChannel>() {})方法,而其中就是往pipeline中加入我们自己编写的入站数据逻辑处理的handler,那么这个我们自定义的handler是怎么调用的呢?这就是靠channelPipeline。

channelPipeline是一个双向链表,有属性值next、pre与前后handler建立连接,并当存在入站事件时,数据先经过head到tail,入站的handler都继承ChannelInboundHandler(ChannelInboundHandlerAdapter)。出站数据流经过tail到head,出站的handler都继承ChannelOutboundHandler(ChannelOutboundHandlerAdapter)。

当通过Netty发送或接受消息时,就会发生一次数据转换。大家都知道,数据在网络传输中是以二进制的字节码传输的,所以,当我们消息出站时,程序数据(比如java对象)会被编码成字节,如果是入站,则会被解码成另一种格式(比如java对象)。

Netty提供了一系列的编解码器,这些编解码器都实现了ChannelInboundHandler或ChannelOutboundHandler接口。在这些类中,channelRead方法被重写。以入站为例,对于每个从入站channel读取的消息,都会调用channelRead方法,在这个方法中调用程序设置的解码器的decode()方法进行解码,并将解码后的数据发到channelPipeline的下一个ChannelInboundHandler。netty提供了很多编解码器,比如StringEncoder/StringDecoder,ObjectEncoder和ObjectDecoder等。解码器调用如下:

//设置String的编解码器
bootstrap.group(bossGroup, workerGroup)
                //使用NioServerSocketChannel作为服务器的通道
                .channel(NioServerSocketChannel.class)
                //初始化服务器连接队列大小,服务器处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
                //多个客户端同时来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ChannelInitializer<SocketChannel>() { //创建通道初始化对象,设置初始化参数
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        //对workerGroup的socketchannel设置处理器
                        channel.pipeline().addLast(new NettyServerHandler());
                        channel.pipeline().addLast(new StringEncoder());
                        channel.pipeline().addLast(new StringDecoder());
                    }
                });

//StringDecoder继承MessageToMessageDecoder
//MessageToMessageDecoder是一个实现了ChannelInboundHandlerAdapter的handler
//重写了ChannelInboundHandlerAdapter的channelRead()方法
@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I cast = (I) msg;
                try {
                    decode(ctx, cast, out);
...
...
//其中调用的decode()方法就是StringDecoder的decode()方法
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        out.add(msg.toString(charset));
    }

粘包拆包

TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不了解上层业务数据的具体含义,它会根据TCP缓冲区的情况进行数据包划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP拆包和粘包的问题。面向流的通信是无消息保护边界的。

如下图,client发送D1和D2两个数据包,但server可能会收到如下几种情况的数据:

解决方案:

1、消息定长度:传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格;

2、在数据包尾部添加特殊分隔符:比如下划线、中等线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不能出现分隔符;

3、发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。

Netty提供了多个解码器,可以进行分包操作,如下:

LineBasedFrameDecoder(回车换行分包)

DelimiterBaseFrameDecoder(特殊分隔符分包)

FiredLengthFrameDecoder(固定长度报文分包)

心跳检测与重连

先了解下长连接、短连接,其实两者并没有实现上的不同,只是针对时间而言,某client与server能保持长时间的连接状态而不断连就是长连接,反之则是短连接。

所谓心跳,就是在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的WRITER_IDLE的IdleStateEvent事件;
allIdleTimeSeconds:读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件。

注:这三个参数默认的时间单位是秒。若需要指定其他时间单位,可以使用另一个构造方法:

public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }

 该类的channelRead()方法没有逻辑处理,只是将数据移交给下一个channel

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
            reading = true;
            firstReaderIdleEvent = firstAllIdleEvent = true;
        }
        ctx.fireChannelRead(msg);
    }

 接下来,看下该类的channelActive方法,主要是initialize(ctx)

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // This method will be invoked only if this handler was added
        // before channelActive() event is fired.  If a user adds this handler
        // after the channelActive() event, initialize() will be called by beforeAdd().
        initialize(ctx);
        super.channelActive(ctx);
    }

 继续往下,比如我们触发的是ReaderIdleTimeoutTask 的task。

   private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        initOutputChanged(ctx);

        lastReadTime = lastWriteTime = ticksInNanos();
        if (readerIdleTimeNanos > 0) {
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (writerIdleTimeNanos > 0) {
            writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

 继续看tak的run()方法

        @Override
        protected void run(ChannelHandlerContext ctx) {
            long nextDelay = readerIdleTimeNanos;
            if (!reading) {
                //ticksInNanos():当前时间,lastReadTime:上次读取channel消息的时间
                //当nextDelay<=0时,表示设置的超时时间<上次读取的间隔时间,应触发
                nextDelay -= ticksInNanos() - lastReadTime;
            }

            if (nextDelay <= 0) {
                // Reader is idle - set a new timeout and notify the callback.
                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

                boolean first = firstReaderIdleEvent;
                firstReaderIdleEvent = false;

                try {
                    //触发的是一个READER_IDLE事件
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // Read occurred before the timeout - set a new timeout with shorter delay.
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }

 继续看channelIdle(ctx, event);方法,执行ChannelHandlerContext.fireUserEventTriggered(IdleStateEvent evt),实现的是AbstractChannelHandlerContext.fireUserEventTriggered(IdleStateEvent evt),最后是执行的ChannelInboundHandler的userEventTriggered()方法,我们根据IdleStateEvent的不同event处理不同的读写超时事件

private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        initOutputChanged(ctx);

        lastReadTime = lastWriteTime = ticksInNanos();
        //读超时时,进行ReaderIdleTimeoutTask
        if (readerIdleTimeNanos > 0) {
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        //写超时时,进行WriterIdleTimeoutTask
        if (writerIdleTimeNanos > 0) {
            writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        //读/写超时时,进行AllIdleTimeoutTask
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

Netty断线自动重连实现

1、客户端启动连接服务端时,如果因为网络或服务端有问题,导致连接失败,可以发起重连,重连的逻辑在客户端;

2、系统运行过程中网络故障或服务端异常,导致客户端与服务端断开了连接,也需要自动重连,可以在客户端处理数据的handler的channelInactive方法中进行重连。

零拷贝

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值