NIO,有人称为New IO这是相对于之前传统io的叫法。但是与之前传统的相比,New IO类库的目标是让java支持非阻塞IO,因此是指非阻塞IO。
NIO类库的简介
1、缓冲区buffer
2、 通道Channel
Channel是一个通道,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或是OutputStream子类),而通道可以用于读、写或者两者同时进行。
实际上Channle可以分为2大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。
3、多路复用器Selector
多路复用器Seletor,它是java nio的编程基础,熟练掌握Seletor对于NIO编程至关重要。多路复用器提供选择已经就绪的任务能力。简单的来讲Selector会不断地轮训注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel集合,进行后续的IO操作。
一个多路复用器Selector可以同时轮询多个Channel,由于JDk使用了epoll()代替传统的的select实现,所以它没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
下面给出nio编程的序列图和对应的源码。
步骤一:打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道:
//打开serverSocketChannel用于监听客户端的连接
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
selector = Selector.open();
步骤四:将ServerSocketChannel注册到Selector的多路复用器上,监听Accept事件。
//注册连接事件类型
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
步骤五:多路复用器在run线程方法的无限循环体内轮询准备就绪的Key,示例代码如下:
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
.......
// do with io event
}
步骤六:多路复用器监听到有新的客户端接入,处理新接入的请求,完成tcp的三次握手,建立物理连接,示例代码如下:
//处理新接入的请求信息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//注册读取事件
sc.register(selector, SelectionKey.OP_READ);
}
上面设置了客户端链路为非阻塞模式,并且将新的客户度的连接注册到多路复用器上,监听读操作,读取客户端发送的网络消息。
步骤七:异步读取客户端请求消息到缓冲区,如下:
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
步骤八:进行消息的编解码,如遇到半包的消息指针reset,继续读取后续的报文。
TimeServer代码如下:
public class TimeSever {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
}
}
//多路复用类,用来轮询多路复用器selector
MultiplexerTimerServer timerServer = new MultiplexerTimerServer(port);
new Thread(timerServer, "NIO-MultiplexerTimeServer-001").start();
}
}
有关MultplexerTimeServer的代码如下:
package nio.zou;
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.Date;
import java.util.Iterator;
import java.util.Set;
public class MultiplexerTimerServer implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop;
public MultiplexerTimerServer(int port) {
//创建selector线程
try {
selector = Selector.open();
//打开serverSocketChannel用于监听客户端的连接
serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
//注册连接事件类型
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("The time server is start in port :" + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
selector.select(1000);
//获取就绪事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.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 (Exception e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
//处理新接入的请求信息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//注册读取事件
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
//read data
SocketChannel sc = (SocketChannel) key.channel();
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");
System.out.println("the time sever recevie order:" + body);
String currentTime = "QUERY TIME ORDER".equals(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
doWrite(sc, currentTime);
} else if (readBytes < 0) {
//对端链路进行关闭
key.cancel();
sc.close();
} else
//读到0字节进行忽略
;
}
}
}
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);
}
}
}
需要注意的是ServerSocketChannel设置为异步非阻塞模式,它的backlog设置为1024(最大的连接数)。
线程的run方法是在while循环体中遍历selector,它的休眠时间为1s。无论是否有读写等事件发生,selector每隔1s被唤醒一次。通过对就绪的Channel集合进行迭代,可以进行网络的异步读写操作。这里作为入门的例子,没有考虑tcp的写半包和读半包问题。
NIO客户端的时序图如下:
步骤一:打开SocketChannel,绑定客户端地址,如下:
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
设置为非阻塞。
步骤二:异步连接服务端,并向selector注册事件:
private void doConnect() throws IOException {
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
步骤三:在多路复用器进行轮训就绪的key,代码如下:
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.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 (Exception e) {
e.printStackTrace();
System.exit(1);
}
其代码如下:
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (Exception e) {
}
}
new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient").start();
}
}
package nio.zou;
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;
import java.util.Set;
public class TimeClientHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandle(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.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 (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
}
public void doWrite(SocketChannel sc) throws IOException {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
if (!writeBuffer.hasRemaining()) {
System.out.println("send order 2 server succeed");
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.exit(1);
}
}
if (key.isReadable()) {
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");
System.out.println("now is :" + body);
this.stop = true;
} else if (readBytes < 0) {
key.cancel();
sc.close();
} else {
;//忽略
}
}
}
}
private void doConnect() throws IOException {
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
}