前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Netty 之 FileRegion 文件传输

Netty 之 FileRegion 文件传输

作者头像
java404
发布2019-03-13 16:21:20
4.9K1
发布2019-03-13 16:21:20
举报
文章被收录于专栏:java 成神之路java 成神之路

概述

Netty 传输文件的时候没有使用 ByteBuf 进行向 Channel 中写入数据,而使用的 FileRegion。下面通过示例了解下 FileRegion 的用法,然后深入源码分析 为什么不使用 ByteBuf 而使用 FileRegion。

示例 (Netty example 中的示例)

代码语言:javascript
复制
public final class FileServer {

    public static void main(String[] args) throws Exception {

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup 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
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     p.addLast(
                             new StringEncoder(CharsetUtil.UTF_8),
                             new LineBasedFrameDecoder(8192),
                             new StringDecoder(CharsetUtil.UTF_8),
                             new ChunkedWriteHandler(),
                             // 自定义 Handler
                             new FileServerHandler());
                 }
             });

            // 起动服务
            ChannelFuture f = b.bind(8080).sync();

            // 等待服务关闭
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

从示例中可以看出 ChannelPipeline 中添加了自定义的 FileServerHandler()。 下面看下 FileServerHandler 的源码,其它几个 Handler 的都是 Netty 中自带的,以后会分析这些 Handler 的具体实现原理。

代码语言:javascript
复制
public class FileServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        RandomAccessFile raf = null;
        long length = -1;
        try {
            raf = new RandomAccessFile(msg, "r");
            length = raf.length();
        } catch (Exception e) {
            ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
            return;
        } finally {
            if (length < 0 && raf != null) {
                raf.close();
            }
        }

        ctx.write("OK: " + raf.length() + '\n');
        if (ctx.pipeline().get(SslHandler.class) == null) {
            // 传输文件使用了 DefaultFileRegion 进行写入到 NioSocketChannel 中
            ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
        } else {
            // SSL enabled - cannot use zero-copy file transfer.
            ctx.write(new ChunkedFile(raf));
        }
        ctx.writeAndFlush("\n");
    }
}

从 FileServerHandler 中可以看出,传输文件使用了 DefaultFileRegion 进行写入到 NioSocketChannel 里。 我们知道向 NioSocketChannel 里写数据,都是使用的 ByteBuf 进行写入。这里为啥使用 DefaultFileRegion 呢?

DefaultFileRegion 源码

代码语言:javascript
复制
public class DefaultFileRegion extends AbstractReferenceCounted implements FileRegion {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultFileRegion.class);
    // 传输的文件
    private final File f;
    // 文件的其实坐标
    private final long position;
    // 传输的字节数
    private final long count;
    // 已经写入的字节数
    private long transferred;
    // 传输文件对应的 FileChannel
    private FileChannel file;

    /**
     * Create a new instance
     *
     * @param file     要传输的文件
     * @param position  传输文件的其实位置
     * @param count     传输文件的字节数
     */
    public DefaultFileRegion(FileChannel file, long position, long count) {
        if (file == null) {
            throw new NullPointerException("file");
        }
        if (position < 0) {
            throw new IllegalArgumentException("position must be >= 0 but was " + position);
        }
        if (count < 0) {
            throw new IllegalArgumentException("count must be >= 0 but was " + count);
        }
        this.file = file;
        this.position = position;
        this.count = count;
        f = null;
    }
    ....
}

transferTo() 方法

DefaultFileRegion 中有一个很重要的方法 transferTo() 方法

代码语言:javascript
复制
    @Override
    public long transferTo(WritableByteChannel target, long position) throws IOException {
        long count = this.count - position;
        if (count < 0 || position < 0) {
            throw new IllegalArgumentException(
                    "position out of range: " + position +
                    " (expected: 0 - " + (this.count - 1) + ')');
        }
        if (count == 0) {
            return 0L;
        }
        if (refCnt() == 0) {
            throw new IllegalReferenceCountException(0);
        }
        // Call open to make sure fc is initialized. This is a no-oop if we called it before.
        open();

        long written = file.transferTo(this.position + position, count, target);
        if (written > 0) {
            transferred += written;
        }
        return written;
    }

这里可以看出 文件 通过 FileChannel.transferTo 方法直接发送到 WritableByteChannel 中。 通过 Nio 的 FileChannel 可以使用 map 文件映射的方式,直接发送到 SocketChannel中,这样可以减少两次 IO 的复制。 第一次 IO:读取文件的时间从系统内存中拷贝到 jvm 内存中。 第二次 IO:从 jvm 内存中写入 Socket 时,再 Copy 到系统内存中。 这就是所谓的零拷贝技术。

写入 FileRegion
代码语言:javascript
复制
public abstract class AbstractNioByteChannel extends AbstractNioChannel {

    private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            ......
        } else if (msg instanceof FileRegion) {
            FileRegion region = (FileRegion) msg;
            if (region.transferred() >= region.count()) {
                in.remove();
                return 0;
            }

            long localFlushedAmount = doWriteFileRegion(region);
            if (localFlushedAmount > 0) {
                in.progress(localFlushedAmount);
                if (region.transferred() >= region.count()) {
                    in.remove();
                }
                return 1;
            }
        } else {
            throw new Error();
        }
        return WRITE_STATUS_SNDBUF_FULL;
    }

从 ChannelOutboundBuffer 中获取 FileRegion 类型的节点。 然后调用 NioSocketChannel.doWriteFileRegion() 方法进行写入。

NioSocketChannel.doWriteFileRegion()
代码语言:javascript
复制
public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {

    @Override
    protected long doWriteFileRegion(FileRegion region) throws Exception {
        final long position = region.transferred();
        return region.transferTo(javaChannel(), position);
    }

这里调用 FileRegion.transferTo() 方法,使用 基于文件内存映射技术进行文件发送。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.02.19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 示例 (Netty example 中的示例)
  • DefaultFileRegion 源码
  • transferTo() 方法
    • 写入 FileRegion
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档