Java NIO概述
来源:互联网 发布:看网络电视用什么盒子好 编辑:程序博客网 时间:2024/06/06 19:48
Java NIO由以下几个核心部分组成:
- Channels
- Buffers
- Selectors
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
IONIOStream orientedBuffer orientedBlocking IONon blocking IO Selectors
Java NIO与IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(Channel)。
选择器(Selector)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
NIO和IO如何影响应用程序的设计
无论您选择IO或NIO工具箱,可能会影响您应用程序设计的以下几个方面:
- 对NIO或IO类的API调用。
- 数据处理。
- 用来处理数据的线程数。
Name: AnnaAge: 25Email: anna@mailserver.comPhone: 123467890
该文本行的流可以这样处理:
InputStream input = ...; // get the InputStream from the client socketBufferedReader reader = new BufferedReader(new InputStreamReader(input));String nameLine = reader.readLine();String ageLine = reader.readLine();String emailLine = reader.readLine();String phoneLine = reader.readLine();
请注意处理状态由程序执行多久决定。换句话说,一旦reader.readLine()方法返回,你就知道肯定文本行已读完,readLine()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个readLine()调用返回的时候,你知道这行包含年龄等。正如你可以看到的,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则:
而一个NIO的实现会有所不同,下面是一个简单的例子:
ByteBuffer buffer = ByteBuffer.allocate(48);int byteRead = inChannel.read(buffer);
注意第二行,从通道读取字节到ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。
假设你第一次read(buffer)调用后,读入缓冲区的数据只有半行,例如,“Name: An”,你能处理数据吗?显然不能,需要等待,直到整行数据读入缓存,在此之前,对数据的任何处理毫无意义。
所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?好了,你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且使程序设计方案杂乱不堪。例如:
ByteBuffer buffer = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buffer);while(! bufferFull(bytesRead)) {bytesRead = inChannel.read(buffer);}
bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满了。
bufferFull()方法扫描缓冲区,但必须保持与bufferFull()方法被调用之前的状态相同,如果没有,下一个读入缓冲区的数据可能无法读到正确的位置。这是不可能的,但却是需要注意的又一问题。
如果缓冲区已满,它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”:
总结
NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势,一个线程多个连接的设计方案如下图所示:
如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先找到一个Buffer,或者总是要从一个Buffer中写入。
- FileChannel:从文件中读写数据。
- DatagramChannel:能通过UDP读写网络中的数据。
- SocketChannel:能通过TCP读写网络中的数据。
- ServerSocketChannel:可以监听新来的TCP连接,像Web服务器那样。对每一个新来的连接都会创建一个SocketChannel。
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");FileChannel inChannel = aFile.getChannel();ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buf);while(bytesRead != -1) {System.out.println("Read " + bytesRead);buf.flip();while(buf.hasRemaining()) {System.out.print((char) buf.get());}buf.clear();bytesRead = inChannel.read(buf);}aFile.close();
注意buf.flip()的调用,首先读取数据到Buffer,然后反转Buffer,接着再从Buffer中读取数据。下面会详细介绍Buffer的更多细节。
- 写入数据到Buffer
- 调用flip()方法
- 从Buffer中读取数据
- 调用clear()方法或者compact()方法
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");FileChannel inChannel = aFile.getChannel();// create buffer with capacity of 48 bytesByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buf); // read into bufferwhile(bytesRead != -1) {buf.flip(); // make buffer ready for readwhile(buf.hasRemaining()) {System.out.print((char) buf.get()); // read 1 byte at a time}buf.clear(); // make buffer ready for writingbytesRead = inChannel.read(buf);}aFile.close();
- capacity
- position
- limit
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
// create buffer with capacity of 48 bytesByteBuffer buf = ByteBuffer.allocate(48);
这是分配一个可存储1024个字符的CharBuffer:
CharBuffer buf = CharBuffer.allocate(1024);
- 从Channel写到Buffer。
- 通过Buffer的put()方法写到Buffer里。
int bytesRead = inChannel.read(buf); // read into buffer
通过put方法写数据到Buffer的例子:
buf.put(127);
put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如,写到一个指定的位置,或者把一个字节数组写入到Buffer。更多Buffer实现的细节参考JavaDoc。
- 从Buffer读取数据到Channel。
- 使用get()方法从Buffer中读取数据。
// read from buffer into channelint bytesWritten = inChannel.write(buf);
使用get()方法从Buffer中读取数据的例子:
byte aByte = buf.get();
get方法有很多版本,允许你以不同的方式从Buffer中读取数据。例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。
buffer.mark();// call buffer.get() a couple of times, e.g. during parsingbuffer.reset(); // set position back to mark
- 有相同的类型(byte、char、int等)。
- Buffer中剩余的byte、char等的个数相等。
- Buffer中所有剩余的byte、char等都相同。
- 第一个不相等的元素小于另一个Buffer中对应的元素。
- 所有元素都相等,但第一个Buffer比另一个Buffer先耗尽(第一个Buffer的元素个数比另一个少)。
ByteBuffer header = ByteBuffer.allocate(128);ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = {header, body};channel.read(bufferArray);
注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从Channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
ByteBuffer header = ByteBuffer.allocate(128);ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = {header, body};channel.write(bufferArray);
bufferArray数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到Channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");FileChannel fromChannel = fromFile.getChannel();RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");FileChannel toChannel = toFile.getChannel();long position = 0;long count = fromChannel.size();toChannel.transferFrom(position, count, fromChannel);
方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于count个字节,则所传输的字节数要小于请求的字节数。
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");FileChannel fromChannel = fromFile.getChannel();RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");FileChannel toChannel = toFile.getChannel();long position = 0;long count = fromChannel.size();fromChannel.transferTo(position, count, toChannel);
是不是发现这个例子和前面那个例子特别相似?除了调用方法的FileChannel对象不一样外,其他的都一样。
Selector selector = Selector.open();
channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
- Connect
- Accept
- Read
- Write
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
在下面还会继续提到interest集合。
- interest集合
- ready集合
- Channel
- Selector
- 附加的对象(可选)
int interestSet = selectionKey.interestOps();boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
可以看到,用“位与”操作interest集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest集合中。
int readySet = selectionKey.readyOps();
可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:
selectionKey.isAcceptable();selectionKey.isConnectable();selectionKey.isReadable();selectionKey.isWritable();
Channel channel = selectionKey.channel();Selector selector = selectionKey.selector();
selectionKey.attach(theObject);Object attachObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
- int select()
- int select(long timeout)
- int selectNow()
Set selectedKeys = selector.selectedKeys();
当向Selector注册Channel时,Channel.register()方法会返回一个SelectionKey对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法返回这些对象。
Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if(key.isAcceptable()) {// a connection was accepted by a ServerSocketChannel.} else if(key.isConnectable()) {// a connection was established with a remote server.} else if(key.isReadable()) {// a channel is ready for reading} else if(key.isWritable()) {// a channel is ready for writing}keyIterator.remove();}
这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。
Selector selector = Selector.open();channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ);while(true) {int readyChannels = selector.select();if(readyChannels == 0)continue;Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if(key.isAcceptable()) {// a connection was accepted by a ServerSocketChannel.} else if(key.isConnectable()) {// a connection was established with a remote server.} else if(key.isReadable()) {// a channel is ready for reading} else if(key.isWritable()) {// a channel is ready for writing}keyIterator.remove();}}
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = inChannel.read(buf);
首先分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。
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);}
注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。
channel.close();
long pos = channel.position();channel.position(pos+123);
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1——文件结束标志。
long fileSize = channel.size();
channel.truncate(1024);
这个例子截取文件的前1024个字节。
channel.force(true);
- 打开一个SocketChannel并连接到互联网上的某台服务器。
- 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
socketChannel.close();
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = socketChannel.read(buf);
首先,分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。
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没有要写的字节为止。
socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));while(! socketChannel.finishConnect()) {// wait, or do something else ...}
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(9999));while(true) {SocketChannel socketChannel = serverSocketChannel.accept();// do something with socketChannel...}
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.close();
while(true) {SocketChannel socketChannel = serverSocketChannel.accept();// do something with socketChannel...}
当然,也可以在while循环中使用除了true以外的其他退出准则。
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...}
DatagramChannel channel = DatagramChannel.open();channel.socket().bind(new InetSocketAddress(9999));
ByteBuffer buf = ByteBuffer.allocate(48);buf.clear();channel.receive(buf);
receive()方法会将接收到的数据包内容复制到指定的Buffer。如果Buffer容不下收到的数据,多出的数据将被丢弃。
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));
这个例子发送一串字符到“jenkov.com”服务器的UDP端口80。因为服务器并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证。
channel.connect(new InetSocketAddress("jenkov.com", 80));
当连接后,也可以使用read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证。这里有几个例子:
int bytesRead = channel.read(buf);int bytesWrite = channel.write(buf);
Pipe pipe = Pipe.open();
Pipe.SinkChannel sinkChannel = pipe.sink();
通过调用SinkChannel的write()方法,将数据写入SinkChannel,像这样:
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()) {sinkChannel.write(buf);}
Pipe.SourceChannel sourceChannel = pipe.source();
调用source通道的read()方法来读取数据,像这样:
ByteBuffer buf = ByteBuffer.allocate(48);int bytesRead = sourceChannel.read(buf);
read()方法返回的int值会告诉我们多少字节被读进了缓冲区。
- Java NIO: NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- JSON
- 最小生成树(克鲁斯卡尔算法)
- 半年总结
- Spring结合EasyUI导入和导出excel
- storm的利用并行度提高处理速度的几点感想
- Java NIO概述
- [软件测试]
- Linux入门级需要掌握的命令
- Google ZXing系列讲解(三)——ZXing 目录结构与主体流程
- javaweb接受跨域请求设置
- 并查集--PAT.A1107.Social Clusters
- 用jQuery实现菜单隐藏
- oj2494: 大写字母转小写字母
- synchronized同步方法