典型回答
Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
Java NIO: Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Java NIO: Non-blocking IO(非阻塞IO)
Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。
Java NIO: Selectors(选择器)
Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
什么是NIO?
-
IO 阻塞类型 block-IO
-
NIO 非阻塞类型 No-Block-IO 通过Channel+Buffer实现IO操作
-
标准的IO基于字节和字符流进行操作,而NIO是基于通道和缓冲区进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。
-
JavaNIO可以让你非阻塞的使用IO。例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它,从缓冲区写入通道也类似。
-
JavaNIO引入了选择器的概念。选择器用于监听多个通道的事件(比如:连接打开、数据到达)。因此,单个线程可以监听多个数据通道。
-
传统IO是单向
-
NIO面向缓冲流,是双向的
Buffer的数据存取
-
一个用于特定基本数据类型的容器,Buffer实质上是一个数组,所有缓冲区都是抽象类Buffer的子类
-
Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的。
-
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
缓冲区理解
-
在NIO中,缓冲区又分为非直接缓冲区和直接缓冲区。
-
非直接缓冲区建立在JVM的内存中。
-
直接缓冲区建立在物理内存中,可以提高效率。
IO/NIO的底层原理
1、BIO
-
Blocking IO ,阻塞IO,同步阻塞IO
-
阻塞的理解:①accept 阻塞的 ②IO进行操作的时候,要么读,要么写,单线程环境下而言
-
BIO中不可能只有一个客户端进行连接
-
BIO存在的问题
- 每一个Socket连接服务器端都会立即开启一个线程处理(连接不开启线程,执行IO操作再开启线程)
- 每个IO操作完成之后,线程就会销毁(好不容易开启的线程不能轻易销毁)
2、NIO
-
New IO 同步非阻塞
-
非阻塞:客户端连接服务器的时候-------服务器端 连接请求(记录下来,问问你要不要进行IO操作)
-
Selector(Accept、Readable、Writable)
-
Buffer(我是中间的桥梁,所有的IO操作都经过,这样就可以进行读写同时进行)
NIO重点补充
-
IO(BIO)和NIO区别:本质就是阻塞和非阻塞的区别
-
阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。
-
非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。
-
IO为同步阻塞形式,NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞通讯模型NIO2.0(AIO)。
-
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
-
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
-
AIO(NIO2.0):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
1、NIO与IO简介区别
-
NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行读写操作。
-
传统IO面向流(输入流,输出流),阻塞IO
-
NIO面向缓冲区,非阻塞IO,选择器
2、通道和缓冲区
-
Channel负责传输,Buffer负责存储
-
应用程序不能直接对Channel进行读写操作,必须通过Buffer来进行。即Channel是通过Buffer来读写数据的。通道可以异步地读写。
-
通道表示打开到IO设备(文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
-
0 <= mark <= position <= limit <= capacity
private int mark = -1; // 记住目前的position,调用reset()方法可以将position变回之前记住的位置
private int position = 0; // 用于指明下一个可以被读出的或者写入的缓冲区位置索引。
private int limit; // 第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可被读,也 不可以被写。
private int capacity; // 缓冲区的容量表示该Buffer的最大数据容量,缓冲区的容量不能为负值,创建后不能改变。
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
-
通道用于源节点与目标节点的连接,负责缓冲区中数据的传输,通道本身不存储数据,因此需要配合缓冲区进行传输。
-
java.nio.channels.Channel 接口
- FileChannel:从文件读取数据
- SocketChannel:读写TCP网络协议数据
- ServerSocketChannel:监听TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
- DatagramChannel:读写UDP网络协议数据
FileChannel
FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。
打开FileChannel
- 通过使用一个InputStream、OutputStream或RandomAccessFile等IO流来获取一个FileChannel实例。
FileChannel inChannel = new FileInputStream("filename").getChannel();
从FileChannel读取数据
ByteBuffer buffer = ByteBuffer.allocate(capacity);
int bytesRead = inChannel.read(buffer); // read方法返回-1则到达文件末尾
写入数据到FileChannel
- 获取一个通道:FileChannel outChannel = new FileOutputStream("filename").getChannel();
- 创建缓冲区,将数据放入缓冲区
ByteBuffer buffer = ByteBuffer.allocate( capacity );
for (int i = 0; i < message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
把缓冲区数据写入通道中:outChannel.write(buffer);
Channel与Buffer的例子
public class ReadFile {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("ReadFile.java");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
while (fileChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(byteBuffer);
System.out.println(charBuffer);
byteBuffer.clear();
}
fileChannel.close();
fileInputStream.close();
}
}
SocketChannel
打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
从SocketChannel读取数据
ByteBuffer buf = ByteBuffer.allocate(256);
int bytesRead = socketChannel.read(buf);
写入数据到SocketChannel
String newData = "New String to write to file...";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
非阻塞模式
- 可以设置 SocketChannel 为非阻塞模式(non-blocking mode)。设置之后,就可以在异步模式下调用connect(),read() 和write()了。
connect()
- 如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
while (!socketChannel.finishConnect()) {
// wait, or do something else
}
write()
- 非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。
read()
- 非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。
ServerSocketChannel
打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
while (true) {
// 监听连接
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
非阻塞模式
- 在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的ull将是n
- 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);
while (true) {
// 监听连接
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
//do something with socketChannel...
}
}
DatagramChannel
因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。
打开DatagramChannel
DatagramChannel channel = DatagramChannel.open();
// 可以在UDP端口上接收数据包
channel.socket().bind(new InetSocketAddress(8000));
接收数据
ByteBuffer buf = ByteBuffer.allocate(256);
buf.clear();
// 通过receive()方法从DatagramChannel接收数据
// receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。
channel.receive(buf);
发送数据
String newData = "New String to write to file...";
ByteBuffer buf = ByteBuffer.allocate(256);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
// 通过send()方法从DatagramChannel发送数据
int bytesSent = channel.send(buf, new InetSocketAddress("127.0.0.1", 8000));
-
这个例子发送一串字符到”127.0.0.1”服务器的UDP端口8000。 因为服务端并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证。
3、获取通道的三种方法
-
1.java 针对支持通道的类提供了getChannel()方法
- 本地IO
- FileInputStream/FileOutputStream
- RandomAccessFile
- 网络IO
- Socket
- ServerSocket
- DatagramSocket
- 本地IO
-
2.在JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
-
3.在JDK 1.7 中的 NIO.2 的Files 工具类的 newByteChannel()
4、分散读取和聚集写入
-
通道之间的数据传输
- transferFrom()
- transferTo()
-
分散读取:指从Channel中读取的数据分散到多个Buffer中(按照缓冲区的顺序,从Channel中读取的数据依次将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中写。
- Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。
-
聚集写入:将多个缓冲区的数据聚集到通道中
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
- buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。
5、字符集:Charset
-
编码:字符串-->字节数组
-
解码:字节数组-->字符串
6、NIO的非阻塞式网络通信
-
SelectionKey: 表示SelectionChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
-
当调用register(Selector sel, int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。
-
可以监听的事件类型(可使用SelectionKey的四个常量表示):
- 读:SelectionKey.OP_READ
- 写:SelectionKey.OP_WRITE
- 连接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
-
若注册时不止监听一个事件,则可以使用位或操作符连接
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
package selector;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class WebServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
// 注册 channel,并且指定感兴趣的事件是 Accept
ssc.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(128);
writeBuff.put("received".getBytes());
writeBuff.flip();
while (true) {
int nReady = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) {
// 创建新的连接,并且把连接注册到selector上,而且,
// 声明这个channel只对读操作感兴趣。
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
socketChannel.read(readBuff);
readBuff.flip();
System.out.println("received : " + new String(readBuff.array()));
key.interestOps(SelectionKey.OP_WRITE);
}
else if (key.isWritable()) {
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuff);
key.interestOps(SelectionKey.OP_READ);
}
it.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
7、管道(Pipe)
- Java NIO 管道是2个线程之间的单向数据连接。
- Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
public void test()throws IOException{
//1.获取管道
Pipe pipe = Pipe.open();
//2.将缓冲区中的数据写入通道
ByteBuffer buf = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通向单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
//3.读取缓冲区中的数据
Pipe.SourceChannel sourceChannel = pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(),0,len));
sourceChannel.close();
sinkChannel.close();
}
完整NIO例子
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO服务端
*/
public class NIOServer {
//通道管理器
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
* @param port 绑定的端口号
* @exception IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
//当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @exception IOException
*/
@SuppressWarnings("unchecked")
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
//当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 客户端请求连接事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给客户端发送信息哦
channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
//在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取客户端发来的信息 的事件
* @param key
* @exception IOException
*/
public void read(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}
/**
* 启动服务端测试
* @exception IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO客户端
*/
public class NIOClient {
//通道管理器
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip,int port) throws IOException {
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
//用channel.finishConnect();才能完成连接
channel.connect(new InetSocketAddress(ip,port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
@SuppressWarnings("unchecked")
public void listen() throws IOException {
// 轮询访问selector
while (true) {
selector.select();
// 获得selector中选中的项的迭代器
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key
.channel();
// 如果正在连接,则完成连接
if(channel.isConnectionPending()){
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给服务端发送信息哦
channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
//和服务端的read方法一样
}
/**
* 启动客户端测试
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOClient client = new NIOClient();
client.initClient("localhost",8000);
client.listen();
}
}