NIO是jdk1.4版本以后发布的新特性,主要类位于包java.nio
下,NIO主要由以下几个部分组成:
- Buffer
缓冲区,通常用于普通类与通道之间交换数据。缓冲区提供了一个会合点:通道既可提取放在缓冲区中的数据(写),也可向缓冲区存入数据供读取(读)。 - Channel
通道,和传统IO类中的流概念有些相似,但IO流是单向的,通道则是双向的,基于NIO的数据交换都从通道中经过,可以将通道中的数据写入缓冲区,也可以读取缓冲区的数据进通道。 - Selector
选择器,基于多路复用的IO模型,作用类似于多路复用器,允许通过一个线程处理多个注册的Channel。下图表示了单线程一个选择器同时处理三个通道的情况:
Buffer
缓冲区是包在一个对象内的基本数据元素数组。Buffer类相比一个简单数组的优点是它将关于数据的数据内容和信息包含在一个单一的对象中。Buffer类以及它专有的子类定义了一个用于处理数据缓冲区的API。
缓冲区维护了四个属性来操作内部数据:
1.Capacity
容量,缓冲区所能容纳的最大数据字节值,一旦固定即不可改变。
2.Limit
上界,缓冲区中第一个不能被操作的位置,可等同于现存数据计数。
3.Position
下一个操作位,由put和get方法更新。
4.Mark
备忘位置,不太常用。
缓冲区有读和写两种模式,一般通过filp()
和clear()
来切换这两种操作模式。
public abstract class 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;
}
}
position和limit的含义取决于Buffer处在读模式还是写模式:
Channel
Channel是一个通道,类似于输入输出流的概念。区别在于流是单向的,一般在传统IO模型中进行数据读写需要输入输出流搭配使用,但是Channel则提供了双向数据通信的机制。
Channel的继承关系比Buffer要复杂,其具体实现依赖于不同的操作系统。大体上来说,IO通道可以分为两大类:文件通道和socket通道,常用的Channel类有FileChannel
,SocketChannel
,ServerSocketChannel
和DatagramChannel
,适用于文件IO和网络流IO。
常用的Channel类一般都实现了ReadableByteChannel
和WriteableByteChannel
,因此他们具有双向读写能力:
public class NIOTest {
public static void main(String[] args) throws IOException {
NIOTest nt = new NIOTest();
ReadableByteChannel src = Channels.newChannel(System.in);
WritableByteChannel dest = Channels.newChannel(System.out);
nt.channelCopy(src, dest);
src.close();
dest.close();
}
private void channelCopy(ReadableByteChannel src, WritableByteChannel dest) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(src.read(buffer) != -1){
buffer.flip();
while (buffer.hasRemaining()) {
dest.write(buffer);
}
buffer.clear();
}
}
}
通道内容的复制即可通过这两个类来实现。
Selector
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。
SelectableChannel
提供了实现通道的可选择性所需要的公共方法,FileChannel
并未继承此抽象类,因此不具有可选择性。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,应该关注何种IO操作。
SelectionKey
封装了特定的通道与特定的选择器的注册关系,指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
以下通过简单的回声服务器来实例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;
/**
* Created by Administrator on 2017/7/19.
* Intellij IDEA
*/
public class EchoServer {
private Selector selector;
private int port;
private ByteBuffer buffer = ByteBuffer.allocate(1024);
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws IOException {
new EchoServer(8000).start();
}
private void start() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(port));
serverSocketChannel.configureBlocking(false);
this.selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//ServerHandler handler = new ServerHandler();
while (true) {
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
try {
if (key.isAcceptable())
handleAccept(key);
else if (key.isReadable())
handleRead(key);
else if (key.isWritable())
handleWrite(key);
} catch (Exception e) {
keys.remove();
continue;
}
keys.remove();
}
}
}
private void handleWrite(SelectionKey key) throws IOException {
buffer.clear();
SocketChannel socketChannel = (SocketChannel) key.channel();
String message = "[message from server]";
buffer.put(message.getBytes());
buffer.flip();//prepare for write
socketChannel.write(buffer);//write to client channel
System.out.println("Server send message to client"+message);
socketChannel.register(selector, SelectionKey.OP_READ);
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
buffer.clear();//clear this buffer for next read event
int length = socketChannel.read(buffer);
if (length > 0){
String message = new String(buffer.array(), 0, length);
System.out.println("Server received message from client:" + message);
socketChannel.register(selector, SelectionKey.OP_WRITE);
}
}
private void handleAccept(SelectionKey key) throws IOException {
//waiting for client to connect
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}
- 客户端,接收服务端发送的消息并发送消息给服务端
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;
/**
* Created by Administrator on 2017/7/19.
* Intellij IDEA
*/
public class EchoClient {
private int port;
private Selector selector;
private ByteBuffer buffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws IOException {
new EchoClient(8000).start();
}
public EchoClient(int port) {
this.port = port;
}
public void start() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
this.selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress(8000));//connect to server.
while (true){
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
if (key.isConnectable())
handleConnect(key);
else if (key.isReadable())
handleRead(key);
else if (key.isWritable())
handleWrite(key);
keys.remove();
}
}
}
private void handleWrite(SelectionKey key) throws IOException {
buffer.clear();
SocketChannel socketChannel = (SocketChannel) key.channel();
String message = "#message from client#";
buffer.put(message.getBytes());
buffer.flip();
socketChannel.write(buffer);
System.out.println("Client send message to server:" + message);
socketChannel.register(selector, SelectionKey.OP_READ);
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
buffer.clear();
int length = socketChannel.read(buffer);//read data from channel
if (length > 0) {
String serverMessage = new String(buffer.array(), 0, length);
System.out.println("Client received message from server::" + serverMessage);
socketChannel.register(selector, SelectionKey.OP_WRITE);
}
}
private void handleConnect(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
if (socketChannel.isConnectionPending()) {
socketChannel.finishConnect();
buffer.clear();
buffer.put("Message from client".getBytes());
buffer.flip();
socketChannel.write(buffer);
}
socketChannel.register(selector, SelectionKey.OP_READ);
}
}
可见,基于NIO的socket编程确实比传统Blocking IO模型要复杂,但无疑效率更高。这个实例中,Server端仅开启了一个线程用于处理IO操作,由于Selector的引入使得IO操作不再需要频繁切换线程上下文,该Selector不断轮询注册在它上面的key,完成相应的IO操作。