IO基础知识——2、Java NIO之Buffer、Channel

2、Java NIO之Buffer、Channel

目录

2、NIO之Buffer、Channel

2.1 Buffer类

2.2 FileChannel

2.3. SocketChannel

2.3.1 打开 SocketChannel

2.3.2 从 SocketChannel 读取数据

2.3.3 写入 SocketChannel

2.3.4 非阻塞模式

2.4 ServerSocketChannel

2.5. DatagramChannel


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

2.1 Buffer类

Buffer类位于java.nio包下,它一个用于特定基本类型数据的容器。缓冲区是特定基本类型元素的线性有限序列。除内容外,缓冲区的基本属性还包括容量、限制和位置:

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
  • capacity 缓冲区的容量 是它所包含的元素的数量。缓冲区的容量不能为负并且不能更改。
  • limit 缓冲区的限制 是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负,并且不能大于其容量。
  • position 缓冲区的位置 是下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。
  • mark 初始值为-1,用于备份当前的position。

对于每个非 boolean 基本类型,此类都有一个子类与之对应。

目前Buffer的实现类有以下几种:
 

ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer

ByteBuffer
ByteBuffer的实现类包括"HeapByteBuffer"和"DirectByteBuffer"两种。

  • 非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存
  • 直接缓存区:是在虚拟机内存外开辟的内存,IO操作直接进行,不再对其进行复制,但创建和销毁开销大

直接缓冲区与非直接缓冲区具体见  https://blog.csdn.net/u010365717/article/details/97807219

非直接缓冲区写入步骤:
1.创建一个临时的直接ByteBuffer对象。
2.将非直接缓冲区的内容复制到临时缓冲中。
3.使用临时缓冲区执行低层次I/O操作。
4.临时缓冲区对象离开作用域,并最终成为被回收的无用数据。

如果采用直接缓冲区会少一次复制过程,如果需要循环使用缓冲区,用直接缓冲区可以很大地提高性能。虽然直接缓冲区使JVM可以进行高效的I/O操作,但它使用的内存是操作系统分配的,绕过了JVM堆栈,建立和销毁比堆栈上的缓冲区要更大的开销。

public static ByteBuffer allocate(int capacity)分配一个新的字节缓冲区。

 public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }
HeapByteBuffer(int cap, int lim) {            
     super(-1, 0, lim, cap, new byte[cap], 0);
 }
ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
              byte[] hb, int offset)
 {
     super(mark, pos, lim, cap);
     this.hb = hb;
     this.offset = offset;
 }

HeapByteBuffer通过初始化字节数组hd,在虚拟机堆上申请内存空间。再看看IntBuffer的分配

 HeapIntBuffer(int cap, int lim) {            // package-private
        super(-1, 0, lim, cap, new int[cap], 0);
        hb = new int[cap];
        offset = 0;
    }

果然和预料的一样,创建一个int数组。
public static ByteBuffer allocateDirect(int capacity)分配新的直接字节缓冲区。

public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {                  
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

DirectByteBuffer通过unsafe.allocateMemory申请堆外内存,并在ByteBuffer的address变量中维护指向该内存的地址。
unsafe.setMemory(base, size, (byte) 0)方法把新申请的内存数据清零。

使用Buffer读写数据一般遵循以下四个步骤:

  • 写入数据到Buffer
  • 调用flip()方法
  • 从Buffer中读取数据
  • 调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

2.2 FileChannel

用于读取、写入、映射和操作文件的通道。

FileChannel的read、write和map通过其实现类FileChannelImpl实现。

public abstract int read(ByteBuffer dst)throws IOException
将字节序列从此通道读入给定的缓冲区。尝试最多从该通道中读取 r 个字节,其中 r 是调用此方法时缓冲区中剩余的字节数,即 dst.remaining()。返回:读取的字节数,可能为零,如果该通道已到达流的末尾,则返回 -1

public int read(ByteBuffer dst) throws IOException {
    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);
        }
    }
}
//IOUtil的read实现:
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);
    }
}

通过上述实现可以看出,基于channel的文件数据读取步骤如下:

如果申请的ByteBuffer是DirectBuffer类型,则直接将我们申请的内存传入,也就是将读到数据数据放入们申请的内存,然后返回。

  • 如果是堆内存,则申请一块和缓存同大小的DirectByteBuffer bb。
  • 读取数据到缓存bb,底层由NativeDispatcher的read实现。
  • 把bb的数据读取到dst(用户定义的缓存,在jvm中分配内存)。
  • 如果是直接内存,则拷贝一次,如果是堆内存,read方法导致数据复制了两次。

public abstract int write(ByteBuffer src)throws IOException将字节序列从给定的缓冲区写入此通道。
 

public 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实现:
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);
    }
}

通过上述实现可以看出,基于channel的文件数据写入步骤如下:

  • 和read过程类似,如果是直接内存,writeFromNativeBuffer方法传入bytebuffer,内部通过FileDispatcher进行写操作。
  • 申请一块DirectByteBuffer,bb大小为byteBuffer中的limit - position。
  • 复制byteBuffer中的数据到bb中。
  • 把数据从bb中写入到文件,底层由NativeDispatcher的write实现

综合例子:
 

	public class NIOBuffer {

		public static void main(String args[])  {
			try {

			RandomAccessFile file = new RandomAccessFile("d:/data.txt", "rw");
			FileChannel channel = file.getChannel();
			ByteBuffer buffer = ByteBuffer.allocate(4);

			int bytesRead = channel.read(buffer);
			while (bytesRead != -1) {
			    System.out.println("Read " + bytesRead);
			    buffer.flip();
			    while(buffer.hasRemaining()){
			        System.out.print((char) buffer.get());
			    }
			    buffer.clear();
			    bytesRead = channel.read(buffer);
			}
			
			file.close();
		}catch(Exception e) {
			e.printStackTrace();
		}
	}		

注意buffer.flip() 的调用,首先将数据写入到buffer,然后变成读模式,再从buffer中读取数据。

2.3. SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。:

2.3.1 打开 SocketChannel

方式1.
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));

方式2. 
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.baidu.com", 80));

2.3.2 从 SocketChannel 读取数据

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = socketChannel.read(buf);

分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。 
read()方法返回的int值表示读了多少字节进Buffer里。如果返回的是-1,表示已经读到了流的末尾

2.3.3 写入 SocketChannel

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

注意SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。

2.3.4 非阻塞模式

SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("test end!");

以上为阻塞式读,当执行到read出,线程将阻塞,控制台将无法打印test end!。

SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("www.baidu.com", 80));
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("test end!");

以上为非阻塞读,控制台将打印test end!。

2.3.4.1 connect()

如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。像这样:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...
}

2.3.4.2 write()

非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。

2.3.4.3 read()

非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。


2.4 ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。ServerSocketChannel类在 java.nio.channels包中。

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    //do something with socketChannel...
}

accept()
通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。

非阻塞模式
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null.如:
 

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
    }
}

2.5. DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包

打开 DatagramChannel

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));

这个例子打开的 DatagramChannel可以在UDP端口9999上接收数据包。

接收数据

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);

receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。

发送数据

通过send()方法从DatagramChannel发送数据

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));

ServerSocketChannel:用于监听新的TCP连接的通道,负责读取&响应,通常用于服务端的实现。

SocketChannel:用于发起TCP连接,读写网络中的数据,通常用于客户端的实现。

DatagramChannel:上述两个通道基于TCP传输协议,而这个通道则基于UDP,用于读写网络中的数据。

FileChannel:从文件读取数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值