前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java NIO实现原理之Channel

Java NIO实现原理之Channel

作者头像
Monica2333
发布2020-06-19 17:52:24
8230
发布2020-06-19 17:52:24
举报
文章被收录于专栏:码农知识点码农知识点

Channel类似与流,通道的特点:

既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。

通道可以异步地读写。

通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

channel的类结构图如下:

channel类结构.png

其中:

AbstractInterruptibleChannel:NIO中可中断channel的基本实现,可参考

Java NIO中线程的中断机制

ReadableByteChannel,WritableByteChannel,SelectableChannel,NetworkChannel这些接口或类都是字如其名,分别提供了io传输中的功能,如read,write。。

具体的channel实现类常见的如下:

FileChannel: 从文件中读写数据。

DatagramChannel:能通过UDP读写网络中的数据。

SocketChannel: 能通过TCP读写网络中的数据。

ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

简单的FileChannel用法为:

代码语言:javascript
复制
public class FileChannelText {
    public static void main(String args[]) throws IOException {
        //1.创建一个RandomAccessFile(随机访问文件)对象
        RandomAccessFile raf=new RandomAccessFile("D:\\aaa.txt", "rw");
        //通过RandomAccessFile对象的getChannel()方法。FileChannel是抽象类。
        FileChannel inChannel=raf.getChannel();
        //2.创建一个读数据缓冲区对象
        ByteBuffer buf=ByteBuffer.allocate(48);
        //3.从通道中读取数据
        int bytesRead = inChannel.read(buf);
        //创建一个写数据缓冲区对象
        ByteBuffer buf2=ByteBuffer.allocate(48);
        //写入数据
        buf2.put("filechannel test".getBytes());
        buf2.flip();
        inChannel.write(buf);
        while (bytesRead != -1) {

            System.out.println("Read " + bytesRead);
            //Buffer有两种模式,写模式和读模式。在写模式下调用flip()之后,Buffer从写模式变成读模式。
            buf.flip();
           //如果还有未读内容
            while (buf.hasRemaining()) {
                System.out.print((char) buf.get());
            }
            //清空缓存区
            buf.clear();
            bytesRead = inChannel.read(buf);
        }
        //关闭RandomAccessFile(随机访问文件)对象
        raf.close();
    }
}

其中FileChannel的.read实现如下:

代码语言:javascript
复制
public int read(ByteBuffer dst) throws IOException {
  //确保channel是打开状态
    ensureOpen();
    if (!readable)
        throw new NonReadableChannelException();
    synchronized (positionLock) {
        int n = 0;
        int ti = -1;
        try {
            begin();
            ti = threads.add();
            if (!isOpen())
                return 0;
            do {
                n = IOUtil.read(fd, dst, -1, nd);
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            return IOStatus.normalize(n);
        } finally {
            threads.remove(ti);
            end(n > 0);
            assert IOStatus.check(n);
        }
    }
}

可以看到读取数据逻辑是 n = IOUtil.read(fd, dst, -1, nd);

IOUtil.read(fd, dst, -1, nd)的实现:

代码语言:javascript
复制
static int read(FileDescriptor fd, ByteBuffer dst, long position,
                NativeDispatcher nd) IOException {
    if (dst.isReadOnly())
        throw new IllegalArgumentException("Read-only buffer");
    if (dst instanceof DirectBuffer)
        return readIntoNativeBuffer(fd, dst, position, nd);

    // Substitute a native buffer
    ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
    try {
        int n = readIntoNativeBuffer(fd, bb, position, nd);
        bb.flip();
        if (n > 0)
            dst.put(bb);
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

如果ByteBuffer dst是DirectBuffer,则直接将数据从内核复制到dst,如果是堆内内存,则需要先创建一个临时DirectBuffer bb,将数据拷贝到bb之后,再将数据拷贝到ByteBuffer dst中,经过了两次数据复制。

Q:那为什么操作系统不直接访问Java堆内的内存区域了?

A:这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地址,那么该内存地址指向的是Java堆内内存的话,那么如果在操作系统正在访问这个内存地址的时候,Java在这个时候进行了GC操作,而 GC操作 会涉及到数据的移动操作,GC经常会进行先标志在压缩的操作。即,将可回收的空间做标志,然后清空标志位置的内存,然后会进行一个压缩,压缩就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。

同样,write实现堆内内存也经过了两次复制。

FIleChannelimpl.write

代码语言:javascript
复制
ublic int write(ByteBuffer src) throws IOException {
    ensureOpen();
    if (!writable)
        throw new NonWritableChannelException();
    synchronized (positionLock) {
        int n = 0;
        int ti = -1;
        try {
            begin();
            ti = threads.add();
            if (!isOpen())
                return 0;
            do {
                n = IOUtil.write(fd, src, -1, nd);
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            return IOStatus.normalize(n);
        } finally {
            threads.remove(ti);
            end(n > 0);
            assert IOStatus.check(n);
        }
    }
}

IOUtil.write(fd, src, -1, nd)的实现:

代码语言:javascript
复制
static int write(FileDescriptor fd, ByteBuffer src, long position,
                 NativeDispatcher nd) throws IOException {
    if (src instanceof DirectBuffer)
        return writeFromNativeBuffer(fd, src, position, nd);
    // Substitute a native buffer
    int pos = src.position();
    int lim = src.limit();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);
    ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
    try {
        bb.put(src);
        bb.flip();
        // Do not update src until we see how many bytes were written
        src.position(pos);
        int n = writeFromNativeBuffer(fd, bb, position, nd);
        if (n > 0) {
            // now update src
            src.position(pos + n);
        }
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

参考资料:

https://www.jianshu.com/p/052035037297

https://www.jianshu.com/p/007052ee3773

http://ifeve.com/channels/

https://segmentfault.com/a/1190000014869494

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档