java nio使用方法(转)

I/O即输入输出,指的是计算机和外界的接口,或者是单个程序同计算机其他部分的接口。 在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统每次处理一个字节,输入流(input stream)生产一个字节,输出流(output stream)消费一个字节。这种工作模式下,非常容易给流数据创建过滤器(filters),而且也很容易将多个过滤器串起来,每个过滤器针对流过自己的字节做相应处理。另一方面,在这种工作模式下面向流的IO通常很慢。而在Java 1.4中推出了NIO(New I/O),这是一个面向块的I/O系统,系统以块为单位处理数据,每个操作都会生产或者消费一“块”数据,以块为单位处理数据会比以字节(流)为单位处理数据快很多。但是面向块的IO系统同时也损失了一些优雅而简单的操作方式。 

在NIO中有几个核心对象需要掌握:缓冲区(Buffer)、通道(Channel)、选择器(Selector)。

缓冲区Buffer

缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,它们之间的继承关系如下图所示:

下面是一个简单的使用IntBuffer的例子:

import java.nio.IntBuffer; 
 
public class TestIntBuffer { 
    public static void main(String[] args) { 
        // 分配新的int缓冲区,参数为缓冲区容量 
        // 新缓冲区的当前位置将为零,其界限(限制位置)将为其容量。它将具有一个底层实现数组,其数组偏移量将为零。 
        IntBuffer buffer = IntBuffer.allocate(8); 
 
        for (int i = 0; i < buffer.capacity(); ++i) { 
            int j = 2 * (i + 1); 
            // 将给定整数写入此缓冲区的当前位置,当前位置递增 
            buffer.put(j); 
        } 
 
        // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0 
        buffer.flip(); 
 
        // 查看在当前位置和限制位置之间是否有元素 
        while (buffer.hasRemaining()) { 
            // 读取此缓冲区当前位置的整数,然后当前位置递增 
            int j = buffer.get(); 
            System.out.print(j + "  "); 
        } 
 
    } 
 
} 

运行后可以看到: 

在后面我们还会继续分析Buffer对象,以及它的几个重要的属性。

通道Channel

通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。它们之间的继承关系如下图所示:

使用NIO读取数据

在第一个练习中,我们首先从文件中读取一些数据。如果使用老的IO,只需要简单的创建FileInputStream,然后从中读取数据。在NIO中,事情变得有些不同了,首先需要从FileInputStream中获取Channel对象,然后使用这个Channel去读文件。 

在NIO系统中任何时刻执行一个读操作时,都要从Buffer中读,但不是直接从Channel中读。由于所有的数据都需要通过Buffer承载,所以需首先从Channel中把数据读进Buffer。

因此,从文件中读数据一共有三步:

1. 从FileInputStream中获取Channel

2. 创建Buffer

3. 从Channel中把数据读入Buffer

下面是一个简单的使用NIO从文件中读取数据的例子:

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public class TestChannelRead {
 
	public static void main(String[] args) throws Exception {
		FileInputStream fileInputStream = new FileInputStream("D:\\test.txt");
		// 获取通道
		FileChannel fileChannel = fileInputStream.getChannel();
 
		// 创建缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
 
		// 读取数据到缓冲区
		fileChannel.read(buffer);
 
		// 重设buffer,将limit设置为position,position设置为0
		buffer.flip();
 
		// 查看在position和limit之间是否有元素
		while (buffer.hasRemaining()) {
			// 读取buffer当前位置的整数
			byte b = buffer.get();
			System.out.print((char) b);
		}
 
		fileInputStream.close();
	}
 
}

你一定发现,在这里,并没有告诉Channel把多少内容读进Buffer。在每个Buffer中都有一套完整的内部计数系统来跟踪已经读了多少数据了,Buffer中还剩多少空间。关于Buffer的计数系统在随后的“Buffer内部原理”中讲解。

使用NIO写入数据

使用NIO写入数据与读取数据的过程类似,同样数据不是直接写入Channel,而是先将数据写入Buffer,可以分为下面三个步骤:

1. 从FileOutputStream获取Channel

2. 创建Buffer并且把数据放到Buffer中

3. 将Buffer中的数据写入Channel

下面是一个简单的使用NIO向文件中写入数据的例子:

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public class TestChannelWrite {
 
	private static byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101,
			115, 46 };
 
	public static void main(String[] args) throws Exception {
		FileOutputStream fileOutputStream = new FileOutputStream("D:\\test.txt");
		// 获取通道
		FileChannel fileChannel = fileOutputStream.getChannel();
 
		// 创建缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		// 数据存入缓冲区
		for (int i = 0; i < message.length; ++i) {
			buffer.put(message[i]);
		}
		// 重设buffer,将limit设置为position,position设置为0
		buffer.flip();
 
		// 将buffer中的数据写入
		fileChannel.write(buffer);
 
		fileOutputStream.close();
	}
 
}

再次注意,我们没有必要告诉Channel总共要写多少数据,Buffer的内部计数系统会跟踪已经写了多少数据了,还剩多少空间可以使用。

本文介绍了Java NIO中三个核心概念中的两个,并且看了两个简单的示例,分别是使用NIO进行数据的读取和写入,下篇将会介绍Buffer内部实现。

在上一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道。本文为NIO入门学习的第二篇,将会分析NIO中的缓冲区Buffer的内部原理。

在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如果我们使用get()方法从缓冲区获取数据或者使用put()方法把数据写入缓冲区,都会引起缓冲区状态的变化。

在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:

position

指定了下一个将要被写入或者读取的元素索引。在从Channel读取数据到Buffer时,position变量用来跟踪截止目前为止从Channel中读出了多少数据,在从Buffer中向Channel写数据时,position变量用来跟踪截止目前为止向Channel写入了多少数据。

limit

在从Channel中读取数据到Buffer中时,limit变量指示了还剩多少空间可供存放数据,在从Buffer向Channel写数据时,limit变量指示了还剩多少数据可以写入。position正常情况下小于或者等于limit。

capacity

指示Buffer最多能够存储的数据。实际上,它指示了底层array的容量,或者至少是底层array允许使用的空间数量。Limit永远不会大于capacity。

 

接下来我们将逐一检查每个细节,并且也看看为什么这样的设计适合典型的读/写(输入/输出)处理。我们假设从一个Channel拷贝数据到另一个Channel。

首先新建一个容量大小为10的ByteBuffer对象,在初始化的时候,position设置为0,如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入索引为0的字节。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自索引为0的字节。limit和 capacity被设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其它两个将会随着使用而变化。

现在我们可以从读通道中读取一些数据到缓冲区中。如果读取到4个字节数据,则此时position的值为4,即下一个将要被写入的字节索引为4,而limit仍然是10,如下图所示:

下一步把读取到的数据写入到写通道中,在此之前必须调用flip()方法,该方法将会完成两件事情:

1. 把limit设置为当前的position值

2. 把position设置为0

position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多,如下图所示:

我们现在可以将数据从缓冲区写入通道了,这会导致position的增加而limit保持不变,但position不会超过limit的值,所以在读取我们之前写入到缓冲区中的4个字节之后,position和limit的值都为4,如下图所示:

在数据写入到写通道完毕后,调用clear()方法能够把所有的状态变化设置为初始化时的值,该方法将会完成两件事情:

1. 把limit设置为capacity值

2. 把position设置为0

clear()方法会重置Buffer以便接收更多的字节,如下图所示:

最后我们用一段代码来验证这个过程,如下所示:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
 
public class TestBufferField {
 
	public static void main(String[] args) throws Exception {
		FileInputStream fileInStream = new FileInputStream("D:\\test1.txt");
		// 获取读通道
		FileChannel fcin = fileInStream.getChannel();
 
		FileOutputStream fileOutStream = new FileOutputStream("D:\\test2.txt");
		// 获取写通道
		FileChannel fcout = fileOutStream.getChannel();
 
		// 创建缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(10);
		output("初始化", buffer);
 
		// 从通道fcin读取数据到缓冲区
		fcin.read(buffer);
		output("调用read()", buffer);
 
		// 重设buffer,将limit设置为position,然后将position设置为0
		buffer.flip();
		output("调用flip()", buffer);
 
		// 将缓冲区中的数据写入通道fcout
		fcout.write(buffer);
		output("调用write()", buffer);
 
		// 重设buffer,将limit设置为容量capacity,position设置为0
		buffer.clear();
		output("调用clear()", buffer);
 
		fileInStream.close();
		fileOutStream.close();
	}
 
	public static void output(String step, Buffer buffer) {
		System.out.println(step + " : ");
		System.out.print("position: " + buffer.position() + ", ");
		System.out.print("limit: " + buffer.limit() + ", ");
		System.out.println("capacity: " + buffer.capacity());
		System.out.println();
	}
 
}

输出结果为:

 

缓冲区的分配

在前面的几个例子中,我们已经看过了,在创建一个缓冲区对象时,会调用静态方法allocate()来指定缓冲区的容量,其实调用 allocate()相当于创建了一个指定大小的数组,并把它包装为缓冲区对象。或者我们也可以直接将一个现有的数组,包装为缓冲区对象,如下示例代码所示:

public class BufferWrap { 
 
    public void myMethod() 
    { 
        // 分配指定大小的缓冲区 
        ByteBuffer buffer1 = ByteBuffer.allocate(10); 
         
        // 包装一个现有的数组 
        byte array[] = new byte[10]; 
        ByteBuffer buffer2 = ByteBuffer.wrap( array ); 
    } 
} 

缓冲区分片

在NIO中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用slice()方法可以创建一个子缓冲区,让我们通过例子来看一下:

import java.nio.*; 
 
public class Program { 
    static public void main( String args[] ) throws Exception { 
        ByteBuffer buffer = ByteBuffer.allocate( 10 ); 
         
        // 缓冲区中的数据0-9 
        for (int i=0; i<buffer.capacity(); ++i) { 
            buffer.put( (byte)i ); 
        } 
         
        // 创建子缓冲区 
        buffer.position( 3 ); 
        buffer.limit( 7 ); 
        ByteBuffer slice = buffer.slice(); 
         
        // 改变子缓冲区的内容 
        for (int i=0; i<slice.capacity(); ++i) { 
            byte b = slice.get( i ); 
            b *= 10; 
            slice.put( i, b ); 
        } 
         
        buffer.position( 0 ); 
        buffer.limit( buffer.capacity() ); 
         
        while (buffer.remaining()>0) { 
            System.out.println( buffer.get() ); 
        } 
    } 
} 

在该示例中,分配了一个容量大小为10的缓冲区,并在其中放入了数据0-9,而在该缓冲区基础之上又创建了一个子缓冲区,并改变子缓冲区中的内容,从最后输出的结果来看,只有子缓冲区“可见的”那部分数据发生了变化,并且说明子缓冲区与原缓冲区是数据共享的,输出结果如下所示:

只读缓冲区

只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化:

import java.nio.*; 
 
public class Program { 
    static public void main( String args[] ) throws Exception { 
        ByteBuffer buffer = ByteBuffer.allocate( 10 ); 
         
        // 缓冲区中的数据0-9 
        for (int i=0; i<buffer.capacity(); ++i) { 
            buffer.put( (byte)i ); 
        } 
 
        // 创建只读缓冲区 
        ByteBuffer readonly = buffer.asReadOnlyBuffer(); 
         
        // 改变原缓冲区的内容 
        for (int i=0; i<buffer.capacity(); ++i) { 
            byte b = buffer.get( i ); 
            b *= 10; 
            buffer.put( i, b ); 
        } 
         
        readonly.position(0); 
        readonly.limit(buffer.capacity()); 
         
        // 只读缓冲区的内容也随之改变 
        while (readonly.remaining()>0) { 
            System.out.println( readonly.get()); 
        } 
    } 
} 

如果尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

直接缓冲区

直接缓冲区是为加快I/O速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java虚拟机将尽最大努 力直接对它执行本机I/O操作。也就是说,它会在每一次调用底层操作系统的本机I/O操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数据。要分配直接缓冲区,需要调用allocateDirect()方法,而不是allocate()方法,使用方式与普通缓冲区并无区别,如下面的拷贝文件示例:

import java.io.*; 
import java.nio.*; 
import java.nio.channels.*; 
 
public class Program { 
    static public void main( String args[] ) throws Exception { 
        String infile = "c:\\test.txt"; 
        FileInputStream fin = new FileInputStream( infile ); 
        FileChannel fcin = fin.getChannel(); 
         
        String outfile = String.format("c:\\testcopy.txt"); 
        FileOutputStream fout = new FileOutputStream( outfile );     
        FileChannel fcout = fout.getChannel(); 
         
        // 使用allocateDirect,而不是allocate 
        ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 ); 
         
        while (true) { 
            buffer.clear(); 
             
            int r = fcin.read( buffer ); 
             
            if (r==-1) { 
                break; 
            } 
             
            buffer.flip(); 
             
            fcout.write( buffer ); 
        } 
    } 
} 

内存映射文件I/O

内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O是通过使文件中的数据出现为 内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。如下面的示例代码:

import java.io.*; 
import java.nio.*; 
import java.nio.channels.*; 
 
public class Program { 
    static private final int start = 0;<span style="font-family: FangSong_GB2312; font-size: 13px;"> 
    static private final int size = 1024; 
     
    static public void main( String args[] ) throws Exception { 
        RandomAccessFile raf = new RandomAccessFile( "c:\\test.txt", "rw" ); 
        FileChannel fc = raf.getChannel(); 
         
        MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 
          start, size ); 
         
        mbb.put( 0, (byte)97 ); 
        mbb.put( 1023, (byte)122 ); 
         
        raf.close(); 
    } 
}

在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O。通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有200个线程,而有200个用户都在进行大文件下载,会导致第201个用户的请求无法及时处理,即便第201个用户只想请求一个几KB大小的页面。传统的 Server/Client模式如下图所示:

NIO中非阻塞I/O采用了基于Reactor模式的工作方式,I/O调用不会被阻塞,相反是注册感兴趣的特定I/O事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞I/O的核心对象就是Selector,Selector就是注册各种I/O事件地 方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:

从图中可以看出,当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从 SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。关于 SelectableChannel的可以参考Java NIO使用及原理分析(一)

使用NIO中非阻塞I/O编写服务器处理程序,大体上可以分为下面三个步骤:

1. 向Selector对象注册感兴趣的事件 2. 从Selector中获取感兴趣的事件 3. 根据不同的事件进行相应的处理

接下来我们用一个简单的示例来说明整个过程。首先是向Selector对象注册感兴趣的事件:

/*
* 注册事件
* */ 
protected Selector getSelector() throws IOException { 
    // 创建Selector对象 
    Selector sel = Selector.open(); 
     
    // 创建可选择通道,并配置为非阻塞模式 
    ServerSocketChannel server = ServerSocketChannel.open(); 
    server.configureBlocking(false); 
     
    // 绑定通道到指定端口 
    ServerSocket socket = server.socket(); 
    InetSocketAddress address = new InetSocketAddress(port); 
    socket.bind(address); 
     
    // 向Selector中注册感兴趣的事件 
    server.register(sel, SelectionKey.OP_ACCEPT);  
    return sel; 
} 

创建了ServerSocketChannel对象,并调用configureBlocking()方法,配置为非阻塞模式,接下来的三行代码把该通道绑定到指定端口,最后向Selector中注册事件,此处指定的是参数是OP_ACCEPT,即指定我们想要监听accept事件,也就是新的连接发 生时所产生的事件,对于ServerSocketChannel通道来说,我们唯一可以指定的参数就是OP_ACCEPT。

从Selector中获取感兴趣的事件,即开始监听,进入内部循环:

/*
* 开始监听
* */  
public void listen() {  
    System.out.println("listen on " + port); 
    try {  
        while(true) {  
            // 该调用会阻塞,直到至少有一个事件发生 
            selector.select();  
            Set<SelectionKey> keys = selector.selectedKeys(); 
            Iterator<SelectionKey> iter = keys.iterator(); 
            while (iter.hasNext()) {  
                SelectionKey key = (SelectionKey) iter.next();  
                iter.remove();  
                process(key);  
            }  
        }  
    } catch (IOException e) {  
        e.printStackTrace(); 
    }  
} 

在非阻塞I/O中,内部循环模式基本都是遵循这种方式。首先调用select()方法,该方法会阻塞,直到至少有一个事件发生,然后再使用selectedKeys()方法获取发生事件的SelectionKey,再使用迭代器进行循环。

最后一步就是根据不同的事件,编写相应的处理代码:

/*
* 根据不同的事件做处理
* */ 
protected void process(SelectionKey key) throws IOException{ 
    // 接收请求 
    if (key.isAcceptable()) { 
        ServerSocketChannel server = (ServerSocketChannel) key.channel(); 
        SocketChannel channel = server.accept(); 
        channel.configureBlocking(false); 
        channel.register(selector, SelectionKey.OP_READ); 
    } 
    // 读信息 
    else if (key.isReadable()) { 
        SocketChannel channel = (SocketChannel) key.channel();  
        int count = channel.read(buffer);  
        if (count > 0) {  
            buffer.flip();  
            CharBuffer charBuffer = decoder.decode(buffer);  
            name = charBuffer.toString();  
            SelectionKey sKey = channel.register(selector, SelectionKey.OP_WRITE);  
            sKey.attach(name);  
        } else {  
            channel.close();  
        }  
        buffer.clear();  
    } 
    // 写事件 
    else if (key.isWritable()) { 
        SocketChannel channel = (SocketChannel) key.channel();  
        String name = (String) key.attachment();  
         
        ByteBuffer block = encoder.encode(CharBuffer.wrap("Hello " + name));  
        if(block != null) 
        { 
            channel.write(block); 
        } 
        else 
        { 
            channel.close(); 
        } 
 
     } 
} 

此处分别判断是接受请求、读数据还是写事件,分别作不同的处理。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值