## 什么是NIO
同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
### 和BIO、AIO的区别
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
## 实现原理
同步非阻塞式IO,关键是采用了事件驱动的思想来实现了一个多路转换器。
NIO与BIO最大的区别就是只需要开启一个线程就可以处理来自多个客户端的IO事件,这是怎么做到的呢?
就是多路复用器,可以监听来自多个客户端的IO事件:
A. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
B. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
C. 监听多个客户端的连接请求和接收数据请求同时还能监听自己时候有数据要发送。
## NIO实现过程
### Java NIO核心部分
#### Buffer
Buffer(缓冲区)是一个用于存储特定基本类型数据的容器。除了boolean外,其余每种基本类型都有一个对应的buffer类。Buffer类的子类有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 。
#### Channel
Channel(通道)表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接。Channel接口的常用实现类有FileChannel(对应文件IO)、DatagramChannel(对应UDP)、SocketChannel和ServerSocketChannel(对应TCP的客户端和服务器端)。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
#### Selector
Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。即用选择器,借助单一线程,就可对数量庞大的活动I/O通道实施监控和维护。
## Channel用法
下面是一个使用FileChannel读取数据到Buffer的示例:
```java
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel(); //获得通道
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //buffer从channel中读取数据并返回字节数
while (bytesRead != -1) { //如果存在读取内容
System.out.println("Read " + bytesRead);
buf.flip(); //翻转buffer 为输出buffer内容作准备
while(buf.hasRemaining()){
System.out.print((char) buf.get()); //输出buffer
}
buf.clear(); //清空buffer 为下次读取作准备
bytesRead = inChannel.read(buf); //读取第二段内容
}
aFile.close();
```
Java NIO支持scatter/gather,可以将Channel中的内容写入到不同的buffer内或将不同的buffer的内容写入到Channel内
scatter:
```java
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
```
gather:
```java
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
```
## Buffer用法
Buffer的读写数据一般遵循下面四个步骤
1. 调用read()方法写入数据到buffer。方法会返回一个int型的读取的字节数,如果没有读取数据则返回-1
2. 调用flip()方法。
3. 调用get()方法从buffer中读取数据。
4. 调用clear()或者compact()方法。clear()会清空整个buffer,而compact()只会清除已经读过的数据,任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
---
### buffer的工作原理
buffer有三个属性,并且根据模式的不同有不同的含义:
1. capacity(容量)
2. position(游标位置):写模式:初始值为0,当前可以写入数据的位置;读模式:会被重置为0,指向当前可读(还未读)的位置。
3. limit(结束位置):写模式:最多能往buffer里写多少数据(limit == capacity);读模式:最多能够读到多少数据。
具体例子可见下图
![来自:Java NIO系列教程(三) Buffer](http://ifeve.com/wp-content/uploads/2013/06/buffers-modes.png)
#### 获得buffer
想要获得一个buffer对象,首先要进行分配。代码如下:
```java
ByteBuffer buf = ByteBuffer.allocate(48);
```
#### 写buffer
写buffer有两种方式:
1. 从Channel写到Buffer。
```java
int bytesRead = inChannel.read(buf); //返回读取字节数
```
2. put方法
```java
buf.put(127);
```
#### 读buffer
首先要调用**filp()**方法,将buffer从写模式切换到读模式。调用filp()会将position设回0,并将limit设置成之前position的位置。
读buffer也有两种方式:
1. 从Buffer读取数据到Channel
```java
int bytesWritten = inChannel.write(buf); //将buffer内所有内容写入channel
```
2. 使用get()方法
```java
byte aByte = buf.get();
```
==注意:get()方法每次都只读取一个位置的内容,并且会移动position至下一位置。想要读取完整数据,请循环调用get()==
rewind():将position设回0,可以重读buffer内的内容。limit保持不变
#### 清除buffer
一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。
1. clear()
如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
2. compact()
compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。**limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。**
==注意:调用compact()之后buffer的模式转换为写模式。如果想要读取buffer,请重新调用filp()==
#### mark()与reset()
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
```java
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
```
### Selector
#### Selector是什么?为什么使用Selector?
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
但是现在多核CPU的普遍存在使得上下文切换的开销变得没有那么巨大了。在设计时,应该将Selector和多线程结合使用,以充分发挥处理机的能力
#### Selector的实现
1. Selector的创建
通过调用Selector.open()方法创建一个Selector,如下:
```java
Selector selector = Selector.open();
```
2. 向Selector注册通道
为了将Channel和Selector配合使用,必须将channel注册到selector上。
```java
channel.configureBlocking(false); //Selector只能和非阻塞通道配合使用
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
```
注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:Connect、Accept、Read、Write。这四种事件用SelectionKey的四个常量表示:
+ SelectionKey.OP_CONNECT
+ SelectionKey.OP_ACCEPT
+ SelectionKey.OP_READ
+ SelectionKey.OP_WRITE
通道触发了一个事件意思是该事件已经就绪。
如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
```java
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
由上例可见,interest集合是按标志位排布的
2. SelectionKey
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些令我们感兴趣的属性:
+ interest集合
+ ready集合
+ Channel
+ Selector
+ 附加的对象(可选)
可以通过ready集合来分发不同就绪Channel的操作。例如:
```java
while (true)
{
selector.select();
Iterator<SelectionKey> ite = selector.selectedKeys().iterator();
while (ite.hasNext())
{
SelectionKey key = ite.next();
if (key.isAcceptable())
{
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
else if (key.isReadable())
{
recvAndReply(key);
}
ite.remove();
}
}
```
**这里就体现了Selector作为NIO的核心的关键作用。当有请求到来(Channel相关的兴趣就绪),select()就会解除阻塞并返回,通过访问Selector.selectedKeys()可以获得所有就绪的通道结合。对其进行遍历,通过对不同的状态判断进行不同的处理甚至创造新的线程。这里就是NIO的实现思路。**
真正实现多路复用的服务器代码见下:
```java
package com.github.sources.network.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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NioServer {
private int port;
private Selector selector;
private ExecutorService service = Executors.newFixedThreadPool(5); //创建线程池,分发请求
public static void main(String[] args){
new NioServer(8080).start(); //创建8080端口监听服务器
}
public NioServer(int port) {
this.port = port;
}
public void init() {
ServerSocketChannel ssc = null;
try {
ssc = ServerSocketChannel.open(); //打开Socket通道
ssc.configureBlocking(false); //取消阻塞
ssc.bind(new InetSocketAddress(port)); //绑定端口
selector = Selector.open(); //新建多路选择器
ssc.register(selector, SelectionKey.OP_ACCEPT); //注册通道
System.out.println("NioServer started ......");
} catch (IOException e) {
e.printStackTrace();
}finally {
}
}
// 连接到来时的处理:新建通道,绑定新的兴趣READ
public void accept(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept(); //新的Socket请求(注意不是ServerSocket)
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
System.out.println("accept a client : " + sc.socket().getInetAddress().getHostName());
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
this.init();
while (true) {
try {
int events = selector.select(); //一旦有通道在注册的兴趣上就绪就会返回
if (events > 0) {
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
//核心:通过不同的就绪的兴趣,分发不同的请求
while (selectionKeys.hasNext()) {
SelectionKey key = selectionKeys.next();
selectionKeys.remove();
if (key.isAcceptable()) {
accept(key);
} else {
service.submit(new NioServerHandler(key));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static class NioServerHandler implements Runnable{
private SelectionKey selectionKey;
public NioServerHandler(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
@Override
public void run() {
try {
if (selectionKey.isReadable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
buffer.flip();
System.out.println("收到客户端"+socketChannel.socket().getInetAddress().getHostName()+"的数据:"+new String(buffer.array()));
//将数据添加到key中
ByteBuffer outBuffer = ByteBuffer.wrap(buffer.array());
socketChannel.write(outBuffer);// 将消息回送给客户端
selectionKey.cancel();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
```
### SocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:
+ 打开一个SocketChannel并连接到互联网上的某台服务器。
+ 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
#### 读取SocketChannel
调用read()方法即可写入到指定的buffer中
```java
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
```
#### 写入SocketChannel
写数据到SocketChannel用的是SocketChannel.write()方法,该方法以一个Buffer作为参数。
```java
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没有要写的字节为止。
### Pipe
pipe和上面两个通道不一样,他是用于本地的两个线程之间的单向数据链接。
Pipe有一个source通道和一个sink通道(一个管道包装了两个通道),数据会从sink写入管道,然后从source读出。
Pipe的原理如下:
![image](http://ifeve.com/wp-content/uploads/2013/06/pipe.bmp)
#### 创建管道
```
Pipe pipe = Pipe.open();
```
#### 写管道
首先要访问sink通道,然后像其他通道那样从buffer写入sink通道
```java
Pipe.SinkChannel sinkChannel = pipe.sink(); //获取管道的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);
}
```
#### 读管道
同理
```java
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
//注意是一次性读出
int bytesRead = sourceChannel.read(buf);
```
## 参考文献
> [关于BIO和NIO的理解](https://www.cnblogs.com/zedosu/p/6666984.html)
> [Java NIO系列教程](http://ifeve.com/overview/)
> [Java NIO原理与简单实现](https://blog.csdn.net/u013857458/article/details/82424104)
> [Java NIO示例](https://blog.csdn.net/u010889616/article/details/80686236)
【学习】 Java 非阻塞(NIO)初学笔记