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