Java NIO通道概览与文件通道【源码笔记】

目录
一、通道概览
    1.概念示意图
    2.Channel接口继承关系
二、文件通道使用
    1.文件通道类图
    2.文件通道示例
三、文件通道开启源码
    1.通道开启示例
    2.RandomAccessFile创建源码
    3.文件打开源码
    4.开启通道源码
四、ByteBuffer写入通道源码
五、强制刷盘源码
六、通道重置位点源码
七、读取数据到ByteBuffer源码
    1.JDK源码跟踪
    2.Native源码跟踪
八、通道关闭源码   
    1.JDK源码追踪
    2.Native方法源码跟踪
九、文件截取源码
一、通道概览
1.概念示意图
概念示意图.jpg

系统I/O即字节的传输,Channel即传输的通道,文件或网络Socket服务即传输的目的地。

2.Channel接口继承关系

实现Channle的接口

AsynchronousByteChannel, AsynchronousChannel, ByteChannel, GatheringByteChannel, InterruptibleChannel, MulticastChannel, NetworkChannel, ReadableByteChannel, ScatteringByteChannel, SeekableByteChannel, WritableByteChannel

实现Channle的类

AbstractInterruptibleChannel, AbstractSelectableChannel, AsynchronousFileChannel, AsynchronousServerSocketChannel, AsynchronousSocketChannel, DatagramChannel, FileChannel, Pipe.SinkChannel, Pipe.SourceChannel, SelectableChannel, ServerSocketChannel, SocketChannel

12个接口继承关系

channel接口继承图.jpg

小结:由图可以看出直接继承Channel接口的接口由5个分别为:AsynchronousChannel、NetworkChannel、ReadableByteChannel、WritableByteChannel、InterruptibleChannel。其他接口和类都从这5个接口派生。两个字节操作接口ReadableByteChannel、WritableByteChannel,即:通道只能在字节缓冲区上操作。

二、文件通道使用
1.文件通道类图
FileChannel类图.jpg
2.文件通道示例

以示例方式串下文件通道的基本操作,示例内容为:将字符串写入文件,再读出来打印。

File file = new File("/Users/yongliang/mytest/channletst.tmp");

RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel fileChannel = randomAccessFile.getChannel(); // @1
ByteBuffer buffer = ByteBuffer.allocate(20);

String str = "It's a good day!";

buffer.put(str.getBytes("UTF-8"));
buffer.flip();
while(buffer.hasRemaining()) {
    fileChannel.write(buffer); // @2
}

fileChannel.force(false); // @3

System.out.println("FileChannel current position="+fileChannel.position());

fileChannel.position(0); // @4
ByteBuffer buffer2 = ByteBuffer.allocate(16);
int data = fileChannel.read(buffer2); // @5

System.out.println("length=" + data);
System.out.println("content=" + new String(buffer2.array()));

fileChannel.close(); // @6

@1 开启文件通道
@2 将ByteBuffer数据写入FileChannel
@3 强制刷盘
@4 FileChannel重置到开始位置
@5 从FileChannel中读取数据到ByteBuffer
@6 关闭FileChannel

小结:梳理了FileChannle的继承关系以及通过一个示例说明FileChannle的基本操作。

三、文件通道开启源码
1.通道开启示例
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
FileChannel fileChannel = randomAccessFile.getChannel(); 
2.RandomAccessFile创建源码

代码位置:java.io.RandomAccessFile

public RandomAccessFile(File file, String mode)
    throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    int imode = -1;
    if (mode.equals("r"))
        imode = O_RDONLY;
    else if (mode.startsWith("rw")) {
        imode = O_RDWR;
        rw = true;
        if (mode.length() > 2) {
            if (mode.equals("rws"))
                imode |= O_SYNC;
            else if (mode.equals("rwd"))
                imode |= O_DSYNC;
            else
                imode = -1;
        }
    } // @1
    
    // ...
    
    fd = new FileDescriptor(); // @2
    fd.attach(this);
    path = name;
    open(name, imode); // @3
}

@1 r只读模式;rw读写模式;rws模式保证数据同步写入磁盘;rwd模式保证数据和元数据同步写入磁盘。
@2 初始化文件描述符,此时初始值为-1
@3 open调用本地方法打开文件

小结:RandomAccessFile通过Native的open方法打开一个文件。

3.文件打开源码

调用链条

1.open(name, imode);
2.open0(String name, int mode)
3.Java_java_io_RandomAccessFile_open0
4.fileOpen
5.handleOpen

代码位置:jdk/src/solaris/native/java/io/io_util_md.c

Java_java_io_RandomAccessFile_open0(JNIEnv *env,
jobject this, jstring path, jint mode)
{
int flags = 0;
if (mode & java_io_RandomAccessFile_O_RDONLY)
    flags = O_RDONLY;
else if (mode & java_io_RandomAccessFile_O_RDWR) {
    flags = O_RDWR | O_CREAT;
    if (mode & java_io_RandomAccessFile_O_SYNC)
        flags |= O_SYNC;
    else if (mode & java_io_RandomAccessFile_O_DSYNC)
        flags |= O_DSYNC;
}
fileOpen(env, this, path, raf_fd, flags);
}

代码位置:jdk/src/solaris/native/java/io/io_util_md.c

fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
WITH_PLATFORM_STRING(env, path, ps) {
    FD fd;

#if defined(__linux__) || defined(_ALLBSD_SOURCE)
    /* Remove trailing slashes, since the kernel won't */
    char *p = (char *)ps + strlen(ps) - 1;
    while ((p > ps) && (*p == '/'))
        *p-- = '\0';
#endif
    fd = handleOpen(ps, flags, 0666);
    if (fd != -1) {
        SET_FD(this, fd, fid);
    } else {
        throwFileNotFoundException(env, path);
    }
} END_PLATFORM_STRING(env, ps);
}

代码位置:jdk/src/solaris/native/java/io/io_util_md.c

handleOpen(const char *path, int oflag, int mode) {
FD fd;
RESTARTABLE(open64(path, oflag, mode), fd); // @1
if (fd != -1) {
    struct stat64 buf64;
    int result;
    RESTARTABLE(fstat64(fd, &buf64), result);
    if (result != -1) {
        if (S_ISDIR(buf64.st_mode)) {
            close(fd);
            errno = EISDIR;
            fd = -1;
        }
    } else {
        close(fd);
        fd = -1;
    }
}
return fd;
#define RESTARTABLE(_cmd, _result) do { \
    do { \
        _result = _cmd; \ // @2
    } while((_result == -1) && (errno == EINTR)); \
} while(0)

@1 通过open调用链条跟踪,最后调用open64函数来打开一个文件,并返回文件描述符
@2 将文件描述符赋值

open64函数说明

The open64() function, similar to the open() function, opens a file and returns a number called a file descriptor. open64() differs from open() in that it automatically opens the file with the O_LARGEFILE flag set.

小结:RandomAccessFile的创建,即通过open64()函数打开一个文件返回文件描述符。

4.开启通道源码
public final FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, true, rw, this);
        }
        return channel;
    }
}

public static FileChannel open(FileDescriptor var0, String var1, boolean var2, boolean var3, Object var4) {
    return new FileChannelImpl(var0, var1, var2, var3, false, var4);
}

private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4, boolean var5, Object var6) {
    this.fd = var1;
    this.readable = var3;
    this.writable = var4;
    this.append = var5;
    this.parent = var6;
    this.path = var2;
    this.nd = new FileDispatcherImpl(var5);
}

小结:开启通道即创建FileChannelImpl实例。

四、ByteBuffer写入通道源码

给予以上fileChannel.write(buffer)示例分析。

代码位置:sun.nio.ch.write

public int write(ByteBuffer var1) throws IOException {
   // ...
    synchronized(this.positionLock) {
        // ...
        try {
            this.begin(); // @1
            // ...
            do {
                // @2
                var3 = IOUtil.write(this.fd, var1, -1L, this.nd);
            } while(var3 == -3 && this.isOpen());

            int var5 = IOStatus.normalize(var3);
            return var5;
        } finally {
            this.threads.remove(var4);
            this.end(var3 > 0); // @3
            assert IOStatus.check(var3);

        }
    }
}

@1/@3 可中断I/O操作,另文分析
@2 执行IOUtil.write操作

代码位置:sun.nio.ch.write

static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    // ...
    int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
    // ...
}

代码位置:sun.nio.ch.writeFromNativeBuffer

private static int writeFromNativeBuffer(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    // ...
    if (var2 != -1L) {
        var9 = var4.pwrite(var0, ((DirectBuffer)var1).address() + (long)var5, var7, var2); // @1
    } else {
        var9 = var4.write(var0, ((DirectBuffer)var1).address() + (long)var5, var7); // @2
    }

    // ...
    return var9;
}
}

@1 将缓冲区写入channel调用pwrite本地方法
@2 将缓冲区写入channel调用write本地方法

代码位置:jdk/src/solaris/native/sun/nio/fs/UnixNativeDispatcher.c

Java_sun_nio_fs_UnixNativeDispatcher_write(JNIEnv* env, jclass this, jint fd,
    jlong address, jint nbytes)
{
    ssize_t n;
    void* bufp = jlong_to_ptr(address);
    RESTARTABLE(write((int)fd, bufp, (size_t)nbytes), n); // @1
    if (n == -1) {
        throwUnixException(env, errno);
    }
    return (jint)n;
}

@1 本地函数执行write方法

pwrite函数说明

The pwrite() function writes nbyte bytes from buf to the file associated with file_descriptor. The offset value defines the starting position in the file and the file pointer position is not changed

write函数说明

The write() function writes nbyte bytes from buf to the file or socket associated with file_descriptor. nbyte should not be greater than INT_MAX (defined in the <limits.h> header file). If nbyte is zero, write() simply returns a value of zero without attempting any other action.

小结:将ByteBuffer写入FileChannel,底层通过pwrite()和write()将字节写入到文件。

五、强制刷盘源码

以下源码给予fileChannel.force()进行跟踪展开。

代码位置:sun.nio.ch.FileDispatcher

public void force(boolean var1) throws IOException {
     // ...
     var2 = this.nd.force(this.fd, var1); // @1
     // ...
            
}

int force(FileDescriptor var1, boolean var2) throws IOException {
   return force0(var1, var2);
}

static native int force0(FileDescriptor var0, boolean var1) throws IOException; // @2

@1 调用FileDispatcherImpl#force强制刷盘
@2 调用native方法force0

代码位置:jdk/src/solaris/native/sun/nio/ch/FileDispatcherImpl.c

Java_sun_nio_ch_FileDispatcherImpl_force0(JNIEnv *env, jobject this,
                                          jobject fdo, jboolean md)
{
    jint fd = fdval(env, fdo);
    int result = 0;

    if (md == JNI_FALSE) {
        result = fdatasync(fd); // @1
    } else {
#ifdef _AIX
        int getfl = fcntl(fd, F_GETFL);
        if (getfl >= 0 && (getfl & O_ACCMODE) == O_RDONLY) {
            return 0;
        }
#endif
        result = fsync(fd); // @2
    }
    return handle(env, result, "Force failed");
}

@1 如果fileChannel.force(false)执行fdatasync()函数
@2 如果fileChannel.force(true)执行fsync()函数

fdatasync()函数说明

fdatasync() is similar to fsync(), but does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be correctly handled.

fsync()函数说明

The fsync() function transfers all data for the file indicated by the open file descriptor file_descriptor to the storage device associated with file_descriptor. fsync() does not return until the transfer is complete, or until an error is detected.

小结:FileChannel.force(false)调用Native函数fdatasync()同步刷盘,不写入元数据;FileChannel.force(true)调用Native函数fsync()同步刷盘,同时写入元数据信息;元数据包括修改人、修改时间等信息。

六、通道重置位点源码

给予fileChannel.position(0)进行源码追踪。

代码位置:sun.nio.ch.FileChannelImpl

public FileChannel position(long var1) throws IOException {
        this.ensureOpen();
        // ...
        do {
            var4 = this.nd.seek(this.fd, var1);
        } while(var4 == -3L && this.isOpen());
        // ...
    }
}

long seek(FileDescriptor var1, long var2) throws IOException {
    return seek0(var1, var2);
}

static native long seek0(FileDescriptor var0, long var1) throws IOException; // @1

@1 调用Native lseek()函数重置位点。

lseek()函数说明

The lseek() function changes the current file offset to a new position in the file. The new position is the given byte offset from the position specified by whence. After you have used lseek() to seek to a new location, the next I/O operation on the file begins at that location.

小结: 由以上源码可以看出,fileChannel.position(0)通过Native方法seek0来实现位点重置,底层为lseek()函数重置文件位点。

七、读取数据到ByteBuffer源码

给予fileChannel.read(buffer2)进行源码跟踪

1.JDK源码跟踪

代码位置:sun.nio.ch.FileChannelImpl

public int read(ByteBuffer var1) throws IOException {
    // ...
    if (this.isOpen()) {
        do {
            var3 = IOUtil.read(this.fd, var1, -1L, this.nd);
        } while(var3 == -3 && this.isOpen());

        int var12 = IOStatus.normalize(var3);
        return var12;
    }
    // ...
}
static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    // ...
    try {
        int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
        var5.flip();
        if (var6 > 0) {
            var1.put(var5);
        }

    } finally {
        Util.offerFirstTemporaryDirectBuffer(var5);
    }
    // ...
}
private static int readIntoNativeBuffer(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
    if (var2 != -1L) {
        var9 = var4.pread(var0, ((DirectBuffer)var1).address() + (long)var5, var7, var2);
    } else {
        var9 = var4.read(var0, ((DirectBuffer)var1).address() + (long)var5, var7);
    }

    if (var9 > 0) {
        var1.position(var5 + var9);
    }

    return var9;
    }
    }

小结:从JDK源码fileChannel.read最终调用Native的pread和read来读取。

2.Native源码跟踪
Java_sun_nio_ch_FileDispatcherImpl_read0(JNIEnv *env, jclass clazz,
                             jobject fdo, jlong address, jint len)
{
    jint fd = fdval(env, fdo);
    void *buf = (void *)jlong_to_ptr(address);

    return convertReturnVal(env, read(fd, buf, len), JNI_TRUE);
}

JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_pread0(JNIEnv *env, jclass clazz, jobject fdo,
                            jlong address, jint len, jlong offset)
{
    jint fd = fdval(env, fdo);
    void *buf = (void *)jlong_to_ptr(address);

    return convertReturnVal(env, pread64(fd, buf, len, offset), JNI_TRUE);
}

read()函数说明

read() attempts to read up to count bytes from file descriptor fd
into the buffer starting at buf.

pread64()函数说明

The pread() and pread64() functions perform the same action as read(), except that they read from a given position in the file without changing the file pointer. The pread64() function is a large-file support version of pread().

小结:分别调用了Native的read()函数和pread64()函数,都是从文件描述符读取数据到ByteBuffer中,pread64()支持大文件读取。

八、通道关闭源码

给予fileChannel.close()进行追踪

1.JDK源码追踪

代码位置:java.nio.channels.spi.AbstractInterruptibleChannel

public final void close() throws IOException {
    synchronized (closeLock) {
        if (!open)
            return;
        open = false;
        implCloseChannel();
    }
}
protected void implCloseChannel() throws IOException {
    if (this.fileLockTable != null) {
        Iterator var1 = this.fileLockTable.removeAll().iterator();

        while(var1.hasNext()) {
            FileLock var2 = (FileLock)var1.next();
            synchronized(var2) {
                if (var2.isValid()) {
                    this.nd.release(this.fd, var2.position(), var2.size());
                    ((FileLockImpl)var2).invalidate();
                }
            }
        }
    }

    this.threads.signalAndWait();
    if (this.parent != null) {
        ((Closeable)this.parent).close();
    } else {
        this.nd.close(this.fd);
    }

}

void release(FileDescriptor var1, long var2, long var4) throws IOException {
    release0(var1, var2, var4);
}

小结: 通道关闭调用最终调用Native方法release0。

2.Native方法源码跟踪
Java_sun_nio_ch_FileDispatcherImpl_release0(JNIEnv *env, jobject this,
                                         jobject fdo, jlong pos, jlong size)
{
    jint fd = fdval(env, fdo);
    jint lockResult = 0;
    struct flock64 fl;
    int cmd = F_SETLK64;

    fl.l_whence = SEEK_SET;
    if (size == (jlong)java_lang_Long_MAX_VALUE) {
        fl.l_len = (off64_t)0;
    } else {
        fl.l_len = (off64_t)size;
    }
    fl.l_start = (off64_t)pos;
    fl.l_type = F_UNLCK;
    lockResult = fcntl(fd, cmd, &fl);
    if (lockResult < 0) {
        JNU_ThrowIOExceptionWithLastError(env, "Release failed");
    }
}

fcntl()函数说明

The fcntl() function performs various actions on open descriptors, such as obtaining or changing the attributes of a file or socket descriptor.

F_SETLK64

Sets or clears a file segment lock for a large file. You must specify a third argument of type struct flock64

小结:FileChannel的关闭操作,通过调用fcntl()函数参数为F_SETLK64,清理该文件上的锁。

九、文件截取源码

给予fileChannel.truncate(10)来进行源码跟踪。

public FileChannel truncate(long var1){
     var4 = this.nd.truncate(this.fd, var1);
}

static native int truncate0(FileDescriptor var0, long var1) throws IOException;

Java_sun_nio_ch_FileDispatcherImpl_truncate0(JNIEnv *env, jobject this,
                                             jobject fdo, jlong size)
{
    return handle(env,
                  ftruncate64(fdval(env, fdo), size),
                  "Truncation failed");
}
ftruncate64函数说明
The truncate() and ftruncate() functions cause the regular file named by path or referenced by fd to be truncated to a size of precisely length bytes.

If the file previously was larger than this size, the extra data is lost. If the file previously was shorter, it is extended, and the extended part reads as null bytes ('\0').

The file offset is not changed.

If the size changed, then the st_ctime and st_mtime fields (respectively, time of last status change and time of last modification; see stat(2)) for the file are updated, and the set-user-ID and set-group-ID permission bits may be cleared.

With ftruncate(), the file must be open for writing; with truncate(), the file must be writable.

小结:文件的截取通过Native函数ftruncate64来实现,从文件开始位置截取指定的长度。

总结:本文梳理了通道接口继承关系,以文件通道FileChannel的示例入手,跟踪每个操作的Native方法,以及给出这些Native方法的调用源码和说明。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容