文件传输利器之netty splice和fileRegion

因为项目需要需要做一个文件快速传输的工具。参考kafka和rocketmq的经验,文件传输有几个好用的方法,nio,零拷贝、顺序写入,内存映射(使用pagecache)。

一、什么是nio

在看了kafka代码相关实现之后,打算使用nio来作为通讯方式。

nio有以下概念:channel、selector、buffer,网上资料很多,这里说下自己的理解

channel相当于一个通道,写数据和读数据都是用这个通道,selector可以注册很多channel,selector可以标明感兴趣的事件,通过interestops来设定,selector通过非阻塞的poll方法,当感兴趣的事件,比如write事件、read事件、connect事件就绪时,readyops里会有值,之后用户再根据不同的事件来做不同处理,但是处理完之后需要手动把readyops删除才行,要不下次还会处理到。

这里为什么会有write事件?如果不使用write事件,那么在channel.write时会阻塞直到buffer有空闲可以发送,这样子对系统的性能有很大浪费,如果通过注册write事件,当可以发送时才发送事件才更加合理。

buffer是一个存储数据的容器,有mark、position、limit、capacity几个概念

position是指将要处理的下一个字节的位置,limit是指当前最大的字节数,position不能大于limit,limit是当前写入的字节数,mark标记之后使用reset可以复原position到mark的位置。

buffer还有几个操作flip compact,flip是在write之后将position置为0,方便从头读。compatc的用处是因为nio非阻塞的特点,导致write和read不是阻塞的,每次写多少,读多少都不确定,在读的时候可能有些数据没有读完,如果这个时候flip,那么会将剩下的数据覆盖掉,compact之后会将没有读完的数据往前移动,将position指向没有读完数据的下一个字节,这样子切换到写的时候就不会覆盖没有读取的数据。

二、什么是netty

在了解这些概念之后准备开始实现,发现单纯用tcp,咨询了同事,有tcp黏包的问题,后续还需要考虑tcp的安全性问题,于是在他的推荐下,然后rocketmq也是使用的netty作为底层组件,开始学习netty。

netty支持很多通讯方式,nio、oio等等,在此基础上抽象出一组api,可以不用关心底层是采用什么通讯方式。netty有eventloop、encoder、decoder、channelhandler、channelcontext 、byteBuf等概念,接下来说一说我自己的理解。

eventloopgroup相当于线程池,eventloop相当于selector,eventloop注册了很多channel,有channel事件就绪就触发相应的handler,是单线程的,不用担心多线程的问题,所以在handler里可以放心大胆的写代码,encoder相当于特殊的outboundhandler,decoder相当于特殊的inboundhandler,eventloop会将channelpipline里的所有handler都调用一遍,调用channelcontext write方法会触发outboundhandler,所有handler执行顺序如下:
在这里插入图片描述
这张图片来自netty in action,说明了handler执行顺序,inboundhandler会从前往后先执行,然后从后往前执行outboundhandler。inboundhandler调用write方法之后会调用outboundhandler,最后一个outboundhandler需要放在inboundhandler前面,要不触发不了。

decoder和encoder可以解决tcp黏包问题,tcp黏包问题用一句话概况就是,tcp在发送时都是按照字节来发送,有时候会好几个包一起发送,在接受时就不知道应该怎么解析。encoder按照一定规则编码tcp发送的包,decoder按照相同规则解析。netty提供了很多decoder和encoder,比如LineBasedFrameDecoder,是按照换行符来解析,如果碰到换行符就认为可以读取数据了,就会把收到的所有字节读到bytebuf里,形成一个个bytebuf消息给handler处理;比如LengthFieldBasedFrameDecoder是根据包的长度来判断是不是该读取消息,如果设定包的长度为10,那么会等10个字节到达才会读取。

byteBuf和nio里的byteBuffer差不多,是byteBuffer加强版,byteBuf有readerIndex、writerIndex,readerIndex是当前读到哪里,writerIndex是当前写到哪里。

三、什么是零拷贝

零拷贝网上介绍的文章也很多,我谈下自己的理解。一个socket发送过程是cpu将数据读到应用的内存中,经过处理,然后cpu将数据发送到socket的缓存里,这样子就需要多次cpu操作,零拷贝是将数据直接发送到socket缓存而不经过cpu和用户内存,可以加快数据传输速度。Java提供的零拷贝有filechannel的transferTo方法,对应netty的实现就是fileRegion。

四、代码实现

当数据到达服务端要存储时,要从socket零拷贝到file,在netty中找到了NioSocketChannel封装了java 的socket,但是提供的javaChannel()方法是protected方法,拿不到,因此没有办法用filechannel的transferFrom方法,找了资料,发现netty 4.0.28之后EpollSocketChannel可以使用spliceTo方法。spilce是将socket和文件之间直接建立管道,不用cpu参与,是比filechannel的transferFrom方法更加高效的方法,但是只能在unix系统上使用,windows不能用。
下图是defaultFileRegion的代码实现,
在这里插入图片描述
最终调用的是filechannel的transferTo方法,由于transferTo 方法没有办法保证把所有字节都发送出去,在transferTo外层还有代码是循环调用直到所有字节都写入。

下面是服务端收到数据之后利用spliceTo直接从socket将数据落地成文件:
在这里插入图片描述
ctx.channel().config().setAutoRead(true)是为了直接读取文件,不进入handler处理

五、实验结果

在linux机器上使用scp和netty实现文件传输进行对比
对一个174M的文件,scp显示的传输速度是57M/s,需要3秒钟传输完毕,使用netty实现的文件传输在splice传输结束时显示的耗时是1.5秒。相对于scp,使用Netty实现的文件传输速度更快。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>