Java NIO提供了与标准IO不同的IO工作方式:
Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
使用场景
NIO
优势在于一个线程管理多个通道;但是数据的处理将会变得复杂;
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,采用这种;
传统的IO
适用于一个线程管理一个通道的情况;因为其中的流数据的读取是阻塞的;
如果需要管理同时打开不太多的连接,这些连接会发送大量的数据;
NIO vs IO区别
NIO vs IO之间的理念上面的区别(NIO将阻塞交给了后台线程执行)
IO是面向流的,NIO是面向缓冲区的
Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方;
NIO则能前后移动流中的数据,因为是面向缓冲区的
IO流是阻塞的,NIO流是不阻塞的
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
选择器
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
Java NIO 由以下几个核心部分组成:
- Channels
- Buffers
- Selectors
基本上,所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。
Channel的实现: (涵盖了UDP 和 TCP 网络IO,以及文件IO)
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
读数据:
- int bytesRead = inChannel.read(buf);
写数据:
- int bytesWritten = inChannel.write(buf);
Buffer
Buffer实现: (byte, char、short, int, long, float, double )
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Buffer使用
读数据
flip()方法
将Buffer从写模式切换到读模式
调用flip()方法会将position设回0,并将limit设置成之前position的值。
buf.flip();
(char) buf.get()
读取数据
Buffer.rewind()
将position设回0,所以你可以重读Buffer中的所有数据
limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)
Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用
Buffer.reset()方法,恢复到Buffer.mark()标记时的position
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
clear()方法会:
清空整个缓冲区。
position将被设回0,limit被设置成 capacity的值
compact()方法:
只会清除已经读过的数据;任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
将position设到最后一个未读元素正后面,limit被设置成 capacity的值
写数据
buf.put(127);
Buffer的三个属性
capacity:含义与模式无关;Buffer的一个固定的大小值;Buffer满了需要将其清空才能再写;
ByteBuffer.allocate(48);该buffer的capacity为48byte
CharBuffer.allocate(1024);该buffer的capacity为1024个char
position:含义取决于Buffer处在读模式还是写模式(初始值为0,写或者读操作的当前位置)
写数据时,初始的position值为0;其值最大可为capacity-1
将Buffer从写模式切换到读模式,position会被重置为0
limit:含义取决于Buffer处在读模式还是写模式(写limit=capacity;读limit等于最多可以读取到的数据)
写模式下,limit等于Buffer的capacity
切换Buffer到读模式时, limit表示你最多能读到多少数据;
Selector
概述
Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
使用
创建:Selector selector = Selector.open();
注册通道:
channel.configureBlocking(false);
//与Selector一起使用时,Channel必须处于非阻塞模式
//这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式(而套接字通道都可以)
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
//第二个参数表明Selector监听Channel时对什么事件感兴趣
//SelectionKey.OP_CONNECT SelectionKey.OP_ACCEPT SelectionKey.OP_READ SelectionKey.OP_WRITE
//可以用或操作符将多个兴趣组合一起
SelectionKey
包含了interest集合 、ready集合 、Channel 、Selector 、附加的对象(可选)
int interestSet = key.interestOps();可以进行类似interestSet & SelectionKey.OP_CONNECT的判断
使用:
select():阻塞到至少有一个通道在你注册的事件上就绪了
selectNow():不会阻塞,不管什么通道就绪都立刻返回
selectedKeys():访问“已选择键集(selected key set)”中的就绪通道
close():使用完selector需要用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效
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();//注意这里必须手动remove;表明该selectkey我已经处理过了;
}
Java测试关键代码
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel(); //从一个InputStream outputstream中获取channel
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
文件通道
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
读数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
写数据
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);
}
Socket 通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
读数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
写数据
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()) {
socketChannel.write(buf);
}
ServerSocket 通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
Datagram 通道(channel的读写操作与前面的有差异)
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));
JAVA NIO vs IO
当我们学习了Java NIO和IO后,我们很快就会思考一个问题:
什么时候应该使用IO,什么时候我应该使用NIO
在下文中我会尝试用例子阐述java NIO 和IO的区别,以及它们对你的设计会有什么影响
Java NIO和IO的主要区别
面向Stream和面向Buffer
Java NIO和IO之间最大的区别是IO是面向流(Stream)的,NIO是面向块(buffer)的,所以,这意味着什么?
面向流意味着从流中一次可以读取一个或多个字节,拿到读取的这些做什么你说了算,这里没有任何缓存(这里指的是使用流没有任何缓存,接收或者发送的数据是缓存到操作系统中的,流就像一根水管从操作系统的缓存中读取数据)而且只能顺序从流中读取数据,如果需要跳过一些字节或者再读取已经读过的字节,你必须将从流中读取的数据先缓存起来。
面向块的处理方式有些不同,数据是先被 读/写到buffer中的,根据需要你可以控制读取什么位置的数据。这在处理的过程中给用户多了一些灵活性,然而,你需要额外做的工作是检查你需要的数据是否已经全部到了buffer中,你还需要保证当有更多的数据进入buffer中时,buffer中未处理的数据不会被覆盖
阻塞IO和非阻塞IO
所有的Java IO流都是阻塞的,这意味着,当一条线程执行read()或者write()方法时,这条线程会一直阻塞知道读取到了一些数据或者要写出去的数据已经全部写出,在这期间这条线程不能做任何其他的事情
java NIO的非阻塞模式(Java NIO有阻塞模式和非阻塞模式,阻塞模式的NIO除了使用Buffer存储数据外和IO基本没有区别)允许一条线程从channel中读取数据,通过返回值来判断buffer中是否有数据,如果没有数据,NIO不会阻塞,因为不阻塞这条线程就可以去做其他的事情,过一段时间再回来判断一下有没有数据
NIO的写也是一样的,一条线程将buffer中的数据写入channel,它不会等待数据全部写完才会返回,而是调用完write()方法就会继续向下执行
Selectors
Java NIO的selectors允许一条线程去监控多个channels的输入,你可以向一个selector上注册多个channel,然后调用selector的select()方法判断是否有新的连接进来或者已经在selector上注册时channel是否有数据进入。selector的机制让一个线程管理多个channel变得简单。
NIO和IO对应用的设计有何影响
选择使用NIO还是IO做你的IO工具对应用主要有以下几个方面的影响
1、使用IO和NIO的API是不同的(废话)
2、处理数据的方式
3、处理数据所用到的线程数
处理数据的方式
在IO的设计里,要一个字节一个字节从InputStream 或者Reader中读取数据,想象你正在处理一个向下面的基于行分割的流
-
Name:Anna
-
Age: 25
-
Email: anna@mailserver.com
-
Phone:1234567890
处理文本行的流的代码应该向下面这样
-
InputStream input = ... ; // get the InputStream from the client socket
-
-
BufferedReader 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()阻塞知道一整行都被读取
NIO的实现会有一些不同,下面是一个简单的例子
-
ByteBuffer buffer = ByteBuffer.allocate(48);
-
-
int bytesRead = inChannel.read(buffer);
注意第二行从channel中读取数据到ByteBuffer,当这个方法返回你不知道是否你需要的所有数据都被读到buffer了,你所知道的一切就是有一些数据被读到了buffer中,但是你并不知道具体有多少数据,这使程序的处理变得稍微有些困难
想象一下,调用了read(buffer)方法后,只有半行数据被读进了buffer,例如:“Name: An”,你能现在就处理数据吗?当然不能。你需要等待直到至少一整行数据被读到buffer中,在这之前确保程序不要处理buffer中的数据
你如何知道buffer中是否有足够的数据可以被处理呢?你不知道,唯一的方法就是检查buffer中的数据。可能你会进行几次无效的检查(检查了几次数据都不够进行处理),这会令程序设计变得比较混乱复杂
-
ByteBuffer buffer = ByteBuffer.allocate(48);
-
-
int bytesRead = inChannel.read(buffer);
-
-
while(! bufferFull(bytesRead) ) {
-
bytesRead = inChannel.read(buffer);
-
}
bufferFull方法负责检查有多少数据被读到了buffer中,根据返回值是true还是false来判断数据是否够进行处理。bufferFull方法扫描buffer但不能改变buffer的内部状态
is-data-in-buffer-ready 循环柱状图如下
总结
NIO允许你用一个单独的线程或几个线程管理很多个channels(网络的或者文件的),代价是程序的处理和处理IO相比更加复杂
如果你需要同时管理成千上万的连接,但是每个连接只发送少量数据,例如一个聊天服务器,用NIO实现会更好一些,相似的,如果你需要保持很多个到其他电脑的连接,例如P2P网络,用一个单独的线程来管理所有出口连接是比较合适的
如果你只有少量的连接但是每个连接都占有很高的带宽,同时发送很多数据,传统的IO会更适合
NIO图解
demo-nio
package com.common.nio;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
public class TestJavaNio {
public static String pathname = "d://read.txt";
public static String filename = "d://write.txt";
@SuppressWarnings("resource")
public static void main(String[] args) {
readNIO();
writeNIO();
//testReadAndWriteNIO();
}
public static void readNIO() {
FileInputStream fin = null;
try {
fin = new FileInputStream(new File(pathname));
FileChannel channel = fin.getChannel();
int capacity = 1000;// 字节
ByteBuffer bf = ByteBuffer.allocate(capacity);
System.out.println("限制是:" + bf.limit() + ",容量是:" + bf.capacity() + " ,位置是:" + bf.position());
int length = -1;
while ((length = channel.read(bf)) != -1) {
/*
* 注意,读取后,将位置置为0,将limit置为容量, 以备下次读入到字节缓冲中,从0开始存储
*/
bf.clear();
byte[] bytes = bf.array();
System.out.println("start..............");
String str = new String(bytes, 0, length);
System.out.println(str);
//System.out.write(bytes, 0, length);
System.out.println("end................");
System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity() + "位置是:" + bf.position());
}
channel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void writeNIO() {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(filename));
FileChannel channel = fos.getChannel();
ByteBuffer src = Charset.forName("utf8").encode("你好你好你好你好你好");
// 字节缓冲的容量和limit会随着数据长度变化,不是固定不变的
System.out.println("初始化容量和limit:" + src.capacity() + ","
+ src.limit());
int length = 0;
while ((length = channel.write(src)) != 0) {
/*
* 注意,这里不需要clear,将缓冲中的数据写入到通道中后 第二次接着上一次的顺序往下读
*/
System.out.println("写入长度:" + length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void testReadAndWriteNIO() {
FileInputStream fin = null;
FileOutputStream fos = null;
try {
fin = new FileInputStream(new File(pathname));
FileChannel channel = fin.getChannel();
int capacity = 100;// 字节
ByteBuffer bf = ByteBuffer.allocate(capacity);
System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity() + "位置是:" + bf.position());
int length = -1;
fos = new FileOutputStream(new File(filename));
FileChannel outchannel = fos.getChannel();
while ((length = channel.read(bf)) != -1) {
//将当前位置置为limit,然后设置当前位置为0,也就是从0到limit这块,都写入到同道中
bf.flip();
int outlength = 0;
while ((outlength = outchannel.write(bf)) != 0) {
System.out.println("读," + length + "写," + outlength);
}
//将当前位置置为0,然后设置limit为容量,也就是从0到limit(容量)这块,
//都可以利用,通道读取的数据存储到
//0到limit这块
bf.clear();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}