JavaNIO库是在JDK1.4中引入的。NIO弥补了原来同步阻塞I/O的不足。
下面是NIO的三个主要概念
1.缓冲区Buffer
Buffer是一个对象,它包含一些要写入或者要读取的数据
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,他是直接读到缓冲区中;在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实质上是一个数组。通常是一个是字节数组ByteBuffer
2.通道Channel
Channel是一个通道,网络数据通过Channel读取和写入。通道和流的不同之处在于通道是双向的,流只是在一个方向上移动(InputStream或OutputStream),而通道可以用于读、写或者二者同时进行。
3.多路复用器Selector
Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或写时间,这个Channel就处于就绪状态,会被Selector出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替了传统的select实现,所以它并没有最大连接句柄1024/2048的限制。
只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
下面用一个例子来演示NIO
package com.hueason.netty.n1.nio;
/**
* Created by Administrator on 2017/4/19.
*/
public class TimeServer {
public static void main(String[] args) {
int port = 8080;
if(args != null && args.length >0){
port = Integer.valueOf(args[0]);
}
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start();
}
}
package com.hueason.netty.n1.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.Date;
import java.util.Iterator;
import java.util.Set;
/**
* Created by Administrator on 2017/4/19.
*/
public class MultiplexerTimeServer implements Runnable{
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
public MultiplexerTimeServer(int port) {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port),1024);
servChannel.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 selectionKeys = selector.selectedKeys();
Iterator 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 (IOException e) {
e.printStackTrace();
}
}
//多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if(selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if(key.isValid()){
//处理新接入的请求信息
if(key.isAcceptable()){
//Accept the new connection
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//Add the new connection to the selector
sc.register(selector,SelectionKey.OP_READ);
}
if(key.isReadable()){
//Read the 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 server receive order "+body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(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);
}
}
}
package com.hueason.netty.n1.nio;
/**
* Created by Administrator on 2017/4/19.
*/
public class TimeClient {
public static void main(String[] args) {
int port = 8080;
if(args != null && args.length>0){
port = Integer.valueOf(args[0]);
}
new Thread(new TimeClientHandler("127.0.0.1",port),"TimeClient-001").start();
}
}
package com.hueason.netty.n1.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.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* Created by Administrator on 2017/4/19.
*/
public class TimeClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop;
public TimeClientHandler(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 (Exception e) {
e.printStackTrace();
System.exit(1);
}
while(!stop){
try {
selector.select(1000);
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}
}
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
//多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
if(selector != null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
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{
//读到0字节,忽略
}
}
}
}
private 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 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);
}
}
}
从代码可以看出来,NIO编程的难度比同步阻塞BIO大很多,而且这个例子没有考虑半包情况。所以它的劣势就是代码复杂,开发难度大
NIO的优点
1)客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像BIO客户端那样被同步阻塞。
2)SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用
3)线程模型的优化:由于JDK的Selector在Linux上通过epoll实现,没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector可以同属处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。非常适合做高性能、高负载的网络服务器。