• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

深入浅出NIO之Channel、Buffer

技术杂谈 勤劳的小蚂蚁 3个月前 (01-25) 67次浏览 已收录 0个评论 扫描二维码

前言

Java NIO 由以下几个核心部分组成: 
1 、Buffer
2、Channel 
3、Selector
传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。
NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。
本文着重介绍Channel和Buffer的概念以及在文件读写方面的应用和内部实现原理。

Buffer

A buffer is a linear, finite sequence of elements of a specific primitive type.
一块缓存区,内部使用字节数组存储数据,并维护几个特殊变量,实现数据的反复利用。 
1、mark:初始值为-1,用于备份当前的position; 
2、position:初始值为0,position表示当前可以写入或读取数据的位置,当写入或读取一个数据后,position向前移动到下一个位置; 
3、limit:写模式下,limit表示最多能往Buffer里写多少数据,等于capacity值;读模式下,limit表示最多可以读取多少数据。 
4、capacity:缓存数组大小
mark():把当前的position赋值给mark
  1. publicfinalBuffer mark(){
  2.    mark = position;
  3.    returnthis;
  4. }
reset():把mark值还原给position
  1. publicfinalBuffer reset(){
  2.    int m = mark;
  3.    if(m <0)
  4.        thrownewInvalidMarkException();
  5.    position = m;
  6.    returnthis;
  7. }
clear():一旦读完Buffer中的数据,需要让Buffer准备好再次被写入,clear会恢复状态值,但不会擦除数据。
  1. publicfinalBuffer clear(){
  2.    position =0;
  3.    limit = capacity;
  4.    mark =-1;
  5.    returnthis;
  6. }
flip():Buffer有两种模式,写模式和读模式,flip后Buffer从写模式变成读模式。
  1. publicfinalBuffer flip(){
  2.    limit = position;
  3.    position =0;
  4.    mark =-1;
  5.    returnthis;
  6. }
rewind():重置position为0,从头读写数据。
  1. publicfinalBuffer rewind(){
  2.    position =0;
  3.    mark =-1;
  4.    returnthis;
  5. }
目前Buffer的实现类有以下几种:
  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • MappedByteBuffer

ByteBuffer

A byte buffer,extend from Buffer
ByteBuffer的实现类包括”HeapByteBuffer”和”DirectByteBuffer”两种。

HeapByteBuffer

  1. publicstaticByteBuffer allocate(int capacity){
  2.    if(capacity <0)
  3.        thrownewIllegalArgumentException();
  4.    returnnewHeapByteBuffer(capacity, capacity);
  5. }
  6. HeapByteBuffer(int cap,int lim){  
  7.    super(-1,0, lim, cap,newbyte[cap],0);
  8. }
HeapByteBuffer通过初始化字节数组hd,在虚拟机堆上申请内存空间。

DirectByteBuffer

  1. publicstaticByteBuffer allocateDirect(int capacity){
  2.    returnnewDirectByteBuffer(capacity);
  3. }
  4. DirectByteBuffer(int cap){
  5.    super(-1,0, cap, cap);
  6.    boolean pa = VM.isDirectMemoryPageAligned();
  7.    int ps =Bits.pageSize();
  8.    long size =Math.max(1L,(long)cap +(pa ? ps :0));
  9.    Bits.reserveMemory(size, cap);
  10.    longbase=0;
  11.    try{
  12.        base=unsafe.allocateMemory(size);
  13.    }catch(OutOfMemoryError x){
  14.        Bits.unreserveMemory(size, cap);
  15.        throw x;
  16.    }
  17.    unsafe.setMemory(base, size,(byte)0);
  18.    if(pa &&(base% ps !=0)){
  19.        // Round up to page boundary
  20.        address =base+ ps -(base&(ps -1));
  21.    }else{
  22.        address =base;
  23.    }
  24.    cleaner =Cleaner.create(this,newDeallocator(base, size, cap));
  25.    att =null;
  26. }
DirectByteBuffer通过unsafe.allocateMemory在物理内存中申请地址空间(非jvm堆内存),并在ByteBuffer的address变量中维护指向该内存的地址。 unsafe.setMemory(base, size, (byte) 0)方法把新申请的内存数据清零。

Channel

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.
NIO把它支持的I/O对象抽象为Channel,Channel又称“通道”,类似于原I/O中的流(Stream),但有所区别: 
1、流是单向的,通道是双向的,可读可写。 
2、流读写是阻塞的,通道可以异步读写。 
3、流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入,如下所示:
目前已知Channel的实现类有:
  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel

A channel for reading, writing, mapping, and manipulating a file. 一个用来写、读、映射和操作文件的通道。
FileChannel的read、write和map通过其实现类FileChannelImpl实现。
read实现
  1. publicint read(ByteBuffer dst)throwsIOException{
  2.    ensureOpen();
  3.    if(!readable)
  4.        thrownewNonReadableChannelException();
  5.    synchronized(positionLock){
  6.        int n =0;
  7.        int ti =-1;
  8.        try{
  9.            begin();
  10.            ti = threads.add();
  11.            if(!isOpen())
  12.                return0;
  13.            do{
  14.                n =IOUtil.read(fd, dst,-1, nd);
  15.            }while((n ==IOStatus.INTERRUPTED)&& isOpen());
  16.            returnIOStatus.normalize(n);
  17.        }finally{
  18.            threads.remove(ti);
  19.            end(n >0);
  20.            assertIOStatus.check(n);
  21.        }
  22.    }
  23. }
FileChannelImpl的read方法通过IOUtil的read实现:
  1. staticint read(FileDescriptor fd,ByteBuffer dst,long position,
  2.                NativeDispatcher nd)IOException{
  3.    if(dst.isReadOnly())
  4.        thrownewIllegalArgumentException("Read-only buffer");
  5.    if(dst instanceofDirectBuffer)
  6.        return readIntoNativeBuffer(fd, dst, position, nd);
  7.    // Substitute a native buffer
  8.    ByteBuffer bb =Util.getTemporaryDirectBuffer(dst.remaining());
  9.    try{
  10.        int n = readIntoNativeBuffer(fd, bb, position, nd);
  11.        bb.flip();
  12.        if(n >0)
  13.            dst.put(bb);
  14.        return n;
  15.    }finally{
  16.        Util.offerFirstTemporaryDirectBuffer(bb);
  17.    }
  18. }
通过上述实现可以看出,基于channel的文件数据读取步骤如下: 
1、申请一块和缓存同大小的DirectByteBuffer bb。 
2、读取数据到缓存bb,底层由NativeDispatcher的read实现。 
3、把bb的数据读取到dst(用户定义的缓存,在jvm中分配内存)。 
read方法导致数据复制了两次
write实现
  1. publicint write(ByteBuffer src)throwsIOException{
  2.    ensureOpen();
  3.    if(!writable)
  4.        thrownewNonWritableChannelException();
  5.    synchronized(positionLock){
  6.        int n =0;
  7.        int ti =-1;
  8.        try{
  9.            begin();
  10.            ti = threads.add();
  11.            if(!isOpen())
  12.                return0;
  13.            do{
  14.                n =IOUtil.write(fd, src,-1, nd);
  15.            }while((n ==IOStatus.INTERRUPTED)&& isOpen());
  16.            returnIOStatus.normalize(n);
  17.        }finally{
  18.            threads.remove(ti);
  19.            end(n >0);
  20.            assertIOStatus.check(n);
  21.        }
  22.    }
  23. }
和read实现一样,FileChannelImpl的write方法通过IOUtil的write实现:
  1. staticint write(FileDescriptor fd,ByteBuffer src,long position,
  2.                 NativeDispatcher nd)throwsIOException{
  3.    if(src instanceofDirectBuffer)
  4.        return writeFromNativeBuffer(fd, src, position, nd);
  5.    // Substitute a native buffer
  6.    int pos = src.position();
  7.    int lim = src.limit();
  8.    assert(pos <= lim);
  9.    int rem =(pos <= lim ? lim - pos :0);
  10.    ByteBuffer bb =Util.getTemporaryDirectBuffer(rem);
  11.    try{
  12.        bb.put(src);
  13.        bb.flip();
  14.        // Do not update src until we see how many bytes were written
  15.        src.position(pos);
  16.        int n = writeFromNativeBuffer(fd, bb, position, nd);
  17.        if(n >0){
  18.            // now update src
  19.            src.position(pos + n);
  20.        }
  21.        return n;
  22.    }finally{
  23.        Util.offerFirstTemporaryDirectBuffer(bb);
  24.    }
  25. }
通过上述实现可以看出,基于channel的文件数据写入步骤如下: 
1、申请一块DirectByteBuffer,bb大小为byteBuffer中的limit – position。 
2、复制byteBuffer中的数据到bb中。 
3、把数据从bb中写入到文件,底层由NativeDispatcher的write实现,具体如下:
  1. privatestaticint writeFromNativeBuffer(FileDescriptor fd,
  2.        ByteBuffer bb,long position,NativeDispatcher nd)
  3.    throwsIOException{
  4.    int pos = bb.position();
  5.    int lim = bb.limit();
  6.    assert(pos <= lim);
  7.    int rem =(pos <= lim ? lim - pos :0);
  8.    int written =0;
  9.    if(rem ==0)
  10.        return0;
  11.    if(position !=-1){
  12.        written = nd.pwrite(fd,
  13.                            ((DirectBuffer)bb).address()+ pos,
  14.                            rem, position);
  15.    }else{
  16.        written = nd.write(fd,((DirectBuffer)bb).address()+ pos, rem);
  17.    }
  18.    if(written >0)
  19.        bb.position(pos + written);
  20.    return written;
  21. }
write方法也导致了数据复制了两次

Channel和Buffer示例

  1. File file =newRandomAccessFile("data.txt","rw");
  2. FileChannel channel = file.getChannel();
  3. ByteBuffer buffer =ByteBuffer.allocate(48);
  4. int bytesRead = channel.read(buffer);
  5. while(bytesRead !=-1){
  6.    System.out.println("Read "+ bytesRead);
  7.    buffer.flip();
  8.    while(buffer.hasRemaining()){
  9.        System.out.print((char) buffer.get());
  10.    }
  11.    buffer.clear();
  12.    bytesRead = channel.read(buffer);
  13. }
  14. file.close();
注意buffer.flip() 的调用,首先将数据写入到buffer,然后变成读模式,再从buffer中读取数据。

总结

通过本文的介绍,希望大家对Channel和Buffer在文件读写方面的应用和内部实现有了一定了解,努力做到不被一叶障目。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:深入浅出NIO之Channel、Buffer
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00