Netty 之 FileRegion 文件傳輸

概述

Netty 傳輸文件的時候沒有使用 ByteBuf 進行向 Channel 中寫入數據,而使用的 FileRegion。下面通過示例瞭解下 FileRegion 的用法,然後深入源碼分析 爲什麼不使用 ByteBuf 而使用 FileRegion。

示例 (Netty example 中的示例)

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 的具體實現原理。

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 源碼

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() 方法

    @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

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()
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() 方法,使用 基於文件內存映射技術進行文件發送。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章