一、什么是NIO
有人称之为New I/O,因为它是相对于之前的I/O库是新的,不过在NIO之前是BIO,即阻塞I/O,所以NIO的目标是让Java支持非阻塞的I/O,所以有人也称之为非阻塞I/O。
NIO是在JDK1.4中引入的,它提供了更高效的I/O操作方式。
二、NIO的基本概念
1、缓冲区(Buffer)
它代表要写入/读取的数据,为什么要引入Buffer呢,是为了异步化读写数据的流程从而系统的吞吐能力,在BIO中系统将数据直接写入或者从Stream中读取,而在NIO中写入数据时先写到缓冲区中,再将数据从缓存区中发往内核进行实际的发送,不再直接写入到Stream中,这样将应用的发送和实际物理的发送隔离开来,从而实现程序的异步化,从而提高系统的吞吐量,当然这样也会增加系统的复杂性。
最常用的缓冲区是ByteBuffer,它提供了基于byte的缓冲操作,还有其它一些缓存,如CharBuffer、ShortBuffer等,除了Boolean之外每一种Java基本类型都有对应的缓冲区。
2、通道(Channel)
通过通道可以读取、写入数据,就像水管一样,网络数据的读取和写入都是通过Channel来完成的。通道是双向的,即可以同时双向操作,且同时支持读写操作。
说起来可能有些抽象,其实在其它系统中也有相应的概念,请看我以前的文章从RabbitMQ Channel设计看连接复用 ,可以理解Channel表示一次客户端和服务端收发数据的一个过程,它和连接还不一样,连接代表的物理的联系,对应一个TCP连接,而通道是个逻辑的概念,可以在一个连接发生多次。
实际打交道比较多的是ServerSocketChannel和SocketChannel,前者代表服务端,后者表示客户端。
3、多路复用器(Selector)
这个是NIO编程的基础,多路复用器可以理解为对通道的管理,因为实际数据的收发都是在通道上完成的,实际的情况是需要同时处理多个通道,如果全由应用去维护是非常麻烦的,多路复用器就是做这个事情的,我们把通道注册进去,然后注册需要感兴趣的事件,多路复用器就在相应事件发生的时候回调我们。
三、NIO编程实践
接下来就写一个echo的服务器熟悉下整个流程,这个服务器很简单,客户端发送什么它就返回什么。
先梳理下服务端的编写过程:
1、打开ServerSocketChannel;
2、绑定并监听地址;
3、创建多路复用器Selector;
4、将ServerSocketChannel注册到多路复用器Selector,并监听Accept事件;
5、当有新连接的时候Accept之后再注册到Selector里,并监听Read事件;
完整代码如下:
public class EchoServer implements Runnable{
private Selector selector;
private ServerSocketChannel servChannel;
private static int port = 8080;
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
EchoServer echoServer = new EchoServer();
new Thread(echoServer, "NIO-EchoServer-001").start();
}
public EchoServer() {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port), 1024);
servChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
/**
* 主函数
*/
@Override
public void run () {
try {
while (true) {
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}finally {
//关闭多路复用器,自动关闭其上的Channel
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 处理请求
* @param key
* @throws IOException
*/
private void handleInput(SelectionKey key) throws IOException {
if (!key.isValid()) {
return;
}
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//将新进来的连接加入到多路复用器,并监听读事件
sc.register(selector, SelectionKey.OP_READ);
}
//读取消息,这个是前面Accept后的Socket
if (key.isReadable()) {
// Read the data
SocketChannel sc = (SocketChannel) key.channel();
//这里先写死1024字节,实际场景按需分配
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
doWrite(sc, body);
} else if (readBytes < 0) {
key.cancel();
sc.close();
} else {
// 读到0字节,忽略
}
}
}
/**
* 写入数据
* @param channel
* @param response
* @throws IOException
*/
private void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}
}
}
相对来说客户端就简单些,步骤如下:
1、打开SocketChannel;
2、设置SocketChannel为非阻塞模式;
3、调用SocketChannel的connect方法连接到服务器,连接成功后将其绑定到Selector上,并监听Read事件;
因流程和上面无太多区别,这里就不贴代码了。