Netty使用经验总结

不得不说的netty里面万能tcp粘包解决组合(变长协议)

  • LengthFieldPrepender
  • LengthFieldBasedFrameDecoder

我认为netty里面提供的这两个东西,组合使用能够解决在任何场景的tcp粘包的问题,所以在用netty解决tcp粘包的时候,一定要学会这套组合拳,使用这两个东西就是说发送消息的时候,将消息的长度带上,然后根据消息的长度来进行拆包。(这两个东西在<<netty权威指南>>没有介绍到)

LengthFieldPrepender

这个是用在发送端的,主要就是说明消息的长度值占整个消息体的长度是多少,注意是消息的长度值,比如这条消息的长度是128个字节,那么我发送消息的时候是要把128这个数值当成消息体一起发送出去的,说的就是128这个数占多少长度。

LengthFieldBasedFrameDecoder

这个是用在接收端的,上面说的,假设这个128占2个字节,那么我在解码的时候我需要跳过这2个字节,然后后面解释消息体了,然后我解析出128个字节的消息体。

这2个东西在网上有很多文章有比较详细的介绍,随便谷歌一下,可以搜出很多。

堆内、外内存在netty中

先说缓冲区

缓冲区这个概念大家都熟悉,最常用的就是搞个数组来用作缓冲区,比如byte[]数组,io操作中肯定要涉及到的,而Java nio中缓冲区的体现都是Bytebuffer,那么在Netty中呢,缓冲区的体现是Bytebuf,ByteBuf实际是netty在ByteBuffer上做的封装,更先进,更好用,这里不说先进到哪个地方,需要的同学在网上查一查,或者看看代码就知道了。

堆 内、外内存

我们平时在Java应用中使用到的都是用到的Java的堆内内存,在nio出来之后,我们对于一些在io上的操作我们可以使用到效率更高的堆外内存,因为它减少了内核态到用户态的数据拷贝,所以当然使用这种在读写文件这类操作上,效率肯定会提高不少的。

重点来了

  • 在原生的nio中,如果我把数据放到堆外内存去发送,接收端是可以分得清这个数据是在堆外内存还是在堆外内存过来的。
  • 如果使用netty发送数据,接收端是分不清这个数据到底是不是来自堆外内存还是堆内内存,因为netty在最终发送数据的时候,把数据都放到了堆外内存,然后再发送。即如果这个数据是从堆内内存中申请的空间,那么把数据复制到堆外内存,如果是堆外内存申请的空间,就可以直接发走。

netty文件零拷贝之FileRegion

netty在网络传输数据的时候最终其实都只支持两种数据类型,byttebuf和FileRegion,FileRegion其实最终都是转化为了byttebuf,然后将数据发送出去了。

FileRegion其实就是直接堆外内存将文件发走,看起来很爽的样子,但是netty自己也说了,这个在大文件传输的时候效率不高,好吧,我要同步的blk文件也不是很大128M,试一试这个吧,其实我们一听这种关于region的东西,那肯定就是将文件分段,然后发送对吧,而且这也是处理大文件的常见姿势,这个FileRegion也确实可以将文件分段,但是netty在网络传输的时候有个坑爹的操作,就是在发送数据之后即调用netty的发送函数channel.write(data)这个函数之后,它会自动帮你清理这个数据申请的内存空间,这样就减少了内存溢出,或者是文件流没有关闭的情况,坑就坑在这个它会帮你自动关闭文件流的这个操作上,因为fileRegion其实也是需要打开文件流再创建出FileRegion的,那如果我这个文件被分成了两段,那么我就要打开两次文件流,这个明显降低效率啊,意思就是你这个FileRegion不是在大文件传输上效率不高的问题上了,是根本就不能用了.下面是它对这个FileRegion在源码里面备注的原话:

* If your operating system (or JDK / JRE) does not support zero-copy file
 * transfer, sending a file with {@link FileRegion} might fail or yield worse
 * performance.  For example, sending a large file doesn't work well in Windows.

堆外内存nio的byttebuffer转netty的bytebuf

如果我用nio的堆外内存byttebuffer做缓冲区,然后数据发送的时候因为netty只能发送bytebuf的数据结构,而且它也是支持堆外内存的,那么netty是可以直接转过去的,不需要将nio的内核态拷进用户态再拷进netty的内核态,可是可以,但是这个地方你需要慎用!!!

netty在这个转换的时候提供了一个方法bytebuf.wrap(bytterbuffer)这个方法,这个方法实际上底层提供的是一个不安全的堆外内存UnsafeDirectByteBuf,什么意思?意思就是你用这种方式,你收到的数据可能是错的…

netty 里面的高、低水位

这个知识点在netty中也很重要,在<<netty权威指南>>中也没有介绍到。

在netty权威指南一书中,发送消息都是采用的这种方式

ctx.writeAndFlush(message)

看起来这个并没有问题,消息发送之后netty会自动帮你把Message申请的内存空间给释放掉,内存不会溢出,ok,你敢用,它就敢挂。

那么这个问题到底出在哪里?

ctx.writeAndFlush它其实是一个异步的操作,异步肯定是没有问题的,tcp跟流一样,你接收到的消息肯定也是顺序的。那么如果你的带宽发生拥挤、或者消息量巨大的时候,或者就是下面这样简单的代码又会怎样呢?

while(true){
    ctx.writeAndFlush(message)
}

netty的内存清理其实是发生在消息发送成功之后,也就是说,如果发送不成功,这个消息一直在内存中堆着,因为异步,你也可以一直往这个内存里面堆数据,知道内存满了,堆溢出了,注意这里溢出的是堆外内存,因为我们说了netty会把内存全部转化为堆外内存发送出去。 所以我们这里一定要用到netty的高低水位,用于限流的这个东西

  • setWriteBufferHighWaterMark设置高水位
  • setWriteBufferLowWaterMark设置低水位

当流量达到高水位的时候,这个时候netty的channel变成不可写,它有个函数可以判断ctx.channel().isWritable(),如果流量达到高水位的时候这个函数是放回false,当流量将到低水位的时候这个才会放回true。这里面又有个坑!!!下面是一段代码,就是在netty提供的接口里面,我先设置这个高低水位,然后再连接建立之后,我开始发送数据。

public class FilesSenderHandler extends ChannelInboundHandlerAdapter{
//设置高低水位,在创建连接的时候
 @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().config().setWriteBufferHighWaterMark(100 * 1024 * 1024 );
        ctx.channel().config().setWriteBufferLowWaterMark(40 * 1024 * 1024);
    }

//当连接创建之后开始发送数据
@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        while(true){
            //如果已经达到高水位,就等着,等着它将到低水位的时候,再发送数据
            while(!ctx.channel().isWritable()){
                TimeUnit.MILLISECONDS.sleep(30);
            }
            ctx.writeAndFlush(message);
        }
    }
}

有问题吗,看起来对啊,没啥问题吧,当然有问题,问题就是ctx.channel.isWritable()这个值得更新和ChannelInboundHandlerAdapter这个是处于同一个线程,也就是说,while(!ctx.channel().isWritable())这个只要在这里返回false了,它就一直是false了。根本不会继续往下走。网上一大堆一大堆的介绍netty的高低水位怎么怎么样,但是就是没有一个人给你说这个应该新开一个线程去判断…正确的做法应该是下面这样

//当连接创建之后开始发送数据
@Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    // 这里必须要新开一个线程
        new Runnable{
            () -> {
                while(true){
            //如果已经达到高水位,就等着,等着它将到低水位的时候,再发送数据
            while(!ctx.channel().isWritable()){
                TimeUnit.MILLISECONDS.sleep(30);
            }
                ctx.writeAndFlush(message);
                }
            }
        }
    }
}

还有很多小细节,占时就记录这么多了,后面再碰到再更新~

netty是个好东西,要用好有难度。

网上有很多一上来就分析netty源码怎么怎么样,看起来很厉害的样子,但是就是不给你说使用上的关键点,让你怀疑他到底用过没用过~

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值