nio是java New IO的简称,在jdk1.4里提供的新api.
Sun官方标榜的特性如下:
1.为所有的原始类型提供(Buffer)缓存支持.
2.字符集编码解码解决方案。
3.一个新的原始I/O抽象Channel.
4.支持锁和内存映射文件的文件访问接口。
5.提供多路(non-bloking)非阻塞式的高伸缩性网络I/O.
NIO真的象官方说的这么强大吗?首先看下面的这个实例:普通IO(带缓冲区的)读写文件和NIO读写文件的性能对比.
甚至还稍微低一些,那么NIO的优势在哪里呢?下面首先通过一个实例来展示一下基于NIO的多路非阻塞网络服务器.
由于对于CPU而言,线程的开销是很重要的,无限创建线程会让操作系统崩溃,因此,比较好的方法是在系统启动的时候创建一个动态的线程池,
例如Tomcat就是采用这种解决方案,然而,这种解决方案在高并发的情况下也不是很乐观,当线程池大小超过CPU瓶颈的时候,
速度就明显降低了.然而有了nio以后,服务端无需再创建多个线程,也无需创建线程池,仅仅只需要1个线程即可以处理全部客户端请求,
通过这一个线程nio服务器将每一个客户端连接请求注册到特定的Selector对象上,这就可以在单线程中利用Selector对象管理大量
并发的网络连接,更好的利用了系统资源.
nio通信过程:
当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,从SelectionKey中可以找到发生的事件和该事件
所发生的具体的SelectableChannel,以获得客户端发送过来的数据/
由于在非阻塞网络I/O中采用了事件触发机制,处理程序可以得到系统的主动通知,从而可以实现底层网络I/O无阻塞,流畅地读写,
而不像在原来的阻塞模式下处理程序需要不断循环等待.使用NIO,可以编写出性能更好,更易扩展的并发型服务器程序.
nio的设计原理:
设计原理有点像设计模式中的观察者模式,由Selector去轮流咨询各个SocketChannel通道是否有事件发生,如果有,则选择出所有的Key
集合,然后传递给处理程序.我们通过每个key就可以获取客户端的SocketChannel,从而进行通信.
如果Selector发现所有通道都没有事件发生,则线程进入睡眠状态Sleep,即阻塞,等到客户端有事件发生时会自动唤醒选择器selector.
知识补充:
Sun官方标榜的特性如下:
1.为所有的原始类型提供(Buffer)缓存支持.
2.字符集编码解码解决方案。
3.一个新的原始I/O抽象Channel.
4.支持锁和内存映射文件的文件访问接口。
5.提供多路(non-bloking)非阻塞式的高伸缩性网络I/O.
NIO真的象官方说的这么强大吗?首先看下面的这个实例:普通IO(带缓冲区的)读写文件和NIO读写文件的性能对比.
直接看代码:IoMainTest和NioMainTest.
<span style="font-size:12px;"><span style="font-size:12px;">package com.ilucky.nio.one.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author IluckySi
* @date 20140618
*/
public class IoMainTest {
public static void main(String[] args) {
File fileRead = new File("D:/fileRead.txt");
File fileWrite = new File("D:/fileIOWrite.txt");
FileInputStream fis = null;
BufferedInputStream bis = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
fis = new FileInputStream(fileRead);
bis = new BufferedInputStream(fis);
fos = new FileOutputStream(fileWrite);
bos = new BufferedOutputStream(fos);
byte[] buffer=new byte[10240];
int length = -1;
long start = System.currentTimeMillis();
//length = bis.read(buffer)意思是读取10KB字节放入缓存并返回读到字节的长度.
while((length = bis.read(buffer))!=-1){
bos.write(buffer, 0, length);
bos.flush();
}
long end = System.currentTimeMillis();
System.out.println("本次IO操作耗时: " + (end - start) + "毫秒!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(bos != null) {
bos.close();
bos =null;
}
if(fos != null) {
fos.close();
fos = null;
}
if(bis != null) {
bis.close();
bis = null;
}
if(fis != null) {
fis.close();
fis = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
输出结果(注意文件大小是291MB, 每次读取10KB, 共输出5次结果):
本次IO操作耗时: 940毫秒!
本次IO操作耗时: 861毫秒!
本次IO操作耗时: 925毫秒!
本次IO操作耗时: 904毫秒!
本次IO操作耗时: 962毫秒!
*/
/**
输出结果(注意文件大小是1.02G, 每次读取10KB, 共输出3次结果):
本次IO操作耗时: 34880毫秒!
本次IO操作耗时: 29267毫秒!
本次IO操作耗时: 31435毫秒!
*/
/**
io分析:
1.首先,正确理解byte[] buffer=new byte[10240]; 这相当于一个缓冲区, 即每次读取10KB放到缓存中,
因为java中输入输出流类都是按单字节的读写方式进行IO操作的,也就是说每次读写一个字节的数据,
这样很大程度上影响了系统的性能,因此,java又专门提供了高IO效率的缓冲类(这里是通过装饰者模式添加的),
每次读写一个缓冲区的数据,这样很大程度上提高了系统的性能.
2.其次,write(byte[] b, int offset, int length)中的offset是指字节数组的偏移量.
在网上的一些资料中经常看到这样的写法write(buffer),这样写是有问题的,
经过多次测试,这么写每次write后的文件都比源文件大一些,究其原因,是因为读到最后一次时如果不够10KB,
他也会按10KB写到目的文件中.
3.然后是flush方法,flush是把缓冲区中的内容全部写到文件上,如果没有他,可能有很多内容还存在于缓存中,即内容还有丢失的可能.
4.最后是close方法,close是文件完成真正的标志,通过调用close方法,来表示目的文件是一个完整的文件.
*/
</span></span>
<span style="font-size:12px;"><span style="font-size:12px;">package com.ilucky.nio.one.nio;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author IluckySi
* @date 20140618
*/
public class NioMainTest {
public static void main(String[] args) {
File fileRead = new File("D:/fileRead.txt");
File fileWrite = new File("D:/fileIOWrite.txt");
FileInputStream fis = null;
FileChannel fic = null;
FileOutputStream fos = null;
FileChannel foc = null;
try {
fis = new FileInputStream(fileRead);
fic = fis.getChannel();
fos = new FileOutputStream(fileWrite);
foc = fos.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(10240);
long start = System.currentTimeMillis();
//把通道的数据读入缓冲区.
while(fic.read(byteBuffer) != -1) {
byteBuffer.flip();
//将缓冲区的数据写入另一个通道.
foc.write(byteBuffer);
byteBuffer.clear();
}
long end = System.currentTimeMillis();
System.out.println("本次IO操作耗时: " + (end - start) + "毫秒!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(foc != null) {
foc.close();
foc =null;
}
if(fos != null) {
fos.close();
fos = null;
}
if(fic != null) {
fic.close();
fic = null;
}
if(fis != null) {
fis.close();
fis = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
输出结果(注意文件大小是291MB, 每次读取10KB, 共输出5次结果):
本次IO操作耗时: 910毫秒!
本次IO操作耗时: 1163毫秒!
本次IO操作耗时: 937毫秒!
本次IO操作耗时: 997毫秒!
本次IO操作耗时: 900毫秒!
*/
/**
输出结果(注意文件大小是1.02G, 每次读取10KB, 共输出3次结果):
本次IO操作耗时: 37308毫秒!
本次IO操作耗时: 31083毫秒!
本次IO操作耗时: 35427毫秒!
*/
</span></span>
通过代码我们可以看出针对简单的读写操作NIO并不占优势,基本上和普通IO(带缓冲区的)读写文件性能不相上下,甚至还稍微低一些,那么NIO的优势在哪里呢?下面首先通过一个实例来展示一下基于NIO的多路非阻塞网络服务器.
1.创建服务器,直接看代码NioServer.
<span style="font-size:12px;"><span style="font-size:12px;">package com.ilucky.nio.two.nio;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author IluckySi
* @date 20140618
*/
public class NioServer {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public static void main(String[] args) throws IOException {
//创建服务器.
NioServer server = new NioServer(9911);
//启动服务器.
server.start();
}
public NioServer(int port) throws IOException {
//创建选择器.
selector = Selector.open();
//创建通道.
serverSocketChannel = ServerSocketChannel.open();
//创建服务器.
ServerSocket serverSocket = serverSocketChannel.socket();
//为服务器绑定监听端口.
serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
//设置通道为非阻塞模式.
serverSocketChannel.configureBlocking(false);
//为通道注册选择器,并设置其支持Accept操作.
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public void start() {
System.out.println("Nio Server启动成功!");
try {
while (true) {
//当通道上有数据到来时继续执行,否则会一直阻塞.
selector.select();
//遍历所有注册过选择器的通道.
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iterator.next();
//删除SelectionKey,防止重复处理.
iterator.remove();
//分发SelectionKey,进行业务处理.
dispatch(selectionKey);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void dispatch(SelectionKey selectionKey) {
//判断SelectionKey对应的通道是否支持ACCEPT操作.
if (selectionKey.isAcceptable()) {
try {
//获取客户端连接的通道.
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = (SocketChannel) serverSocketChannel.accept();
//设置通道为非阻塞模式.
socketChannel.configureBlocking(false);
//在客户端连接成功后,为了可以接收到客户端发送过来的消息,需要为通道注册读权限.
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
//判断SelectionKey对应的通道是否支持READ操作.
if (selectionKey.isReadable()) {
try {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
//将ByteBuffer转化为为UTF-16编码的字符串.
String message = Charset.forName("UTF-16").newDecoder().decode(buffer).toString();
System.out.println("接收到来自客户端"+ socketChannel.socket().getRemoteSocketAddress() + "的消息: "+ message);
//将字符串转化为为UTF-16编码的ByteBuffer.
buffer = ByteBuffer.wrap("success".getBytes("UTF-16"));
//向客户端写回消息.
socketChannel.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//操作结束后取消操作.
selectionKey.cancel();
if (selectionKey.channel() != null) {
try {
selectionKey.channel().close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
}
</span></span>
2.创建客户端,直接看代码NioClientOne和NioClientTwo(两个文件内容一样).
<span style="font-size:12px;"><span style="font-size:12px;">package com.ilucky.nio.two.nio;
import java.io.IOException;
import java.net.InetAddress;
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.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
/**
* @author IluckySi
* @date 20140618
*/
public class NioClientOne {
private Selector selector;
private SocketChannel socketChannel;
public static void main(String[] args) throws IOException {
//模拟业务: 隔5秒钟向服务器发送一条消息.
for (int i = 0; i < 5; i++) {
NioClientOne client = new NioClientOne(9911);
client.sendMessage("Hello! Nio Server, I am NioClientOne!");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public NioClientOne(int port) throws IOException {
//创建选择器.
selector = Selector.open();
//创建通道.
socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(), port));
//设置通道为非阻塞模式.
socketChannel.configureBlocking(false);
//为通道注册选择器,并设置其支持READ操作
socketChannel.register(selector, SelectionKey.OP_READ);
//启动读取线程.
new Thread(new NioClientReadThread()).start();
}
public void sendMessage(String message) throws IOException {
//将字符串转化为为UTF-16编码的ByteBuffer.
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes("UTF-16"));
//向服务器写消息.
socketChannel.write(writeBuffer);
}
class NioClientReadThread implements Runnable {
public void run() {
try {
while (true) {
//当通道上有数据到来时继续执行,否则会一直阻塞.
selector.select();
//遍历所有注册过选择器的通道.
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey)iterator.next();
//删除SelectionKey,防止重复处理.
selector.selectedKeys().remove(selectionKey);
//分发SelectionKey,进行业务处理.
dispatch(selectionKey);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
public void dispatch(SelectionKey selectionKey) {
//判断SelectionKey对应的通道是否支持READ操作.
if (selectionKey.isReadable()) {
try {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
buffer.flip();
//将ByteBuffer转化为为UTF-16编码的字符串.
String message = Charset.forName("UTF-16").newDecoder().decode(buffer).toString();
System.out.println("接收到来自服务器" + socketChannel.socket().getRemoteSocketAddress() + "的消息: "+ message);
} catch (Exception e) {
e.printStackTrace();
} finally {
//操作结束后取消操作.
selectionKey.cancel();
if (selectionKey.channel() != null) {
try {
selectionKey.channel().close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
}
}
}</span></span>
看完了基于NIO的多路非阻塞网络服务器,再回顾一下阻塞服务器. 1.创建服务器,直接看代码IoServer.
<span style="font-size:12px;"><span style="font-size:12px;">package com.ilucky.nio.two.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author IluckySi
* @date 20140618
*/
public class IoServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(9911);
System.out.println("Io Server启动成功!");
while (true) {
Socket socket = null;
PrintWriter writer = null;
BufferedReader reader = null;
try {
// 如果没有消息阻塞在这里.
socket = serverSocket.accept();
writer = new PrintWriter(socket.getOutputStream());
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("接收到来自客户端"+ socket.getRemoteSocketAddress() + "的消息: "+ reader.readLine());
writer.println("success!");
writer.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(reader != null) {
reader.close();
reader = null;
}
if(writer != null) {
writer.close();
writer = null;
}
if(socket != null) {
socket.close();
socket = null;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
</span></span>
2.创建客户端,直接看代码IoClientOne和IoClientTwo.
<span style="font-size:12px;"><span style="font-size:12px;">package com.ilucky.nio.two.io;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* @author IluckySi
* @date 20140618
*/
public class IoClientOne {
public static void main(String[] args) throws Exception {
//模拟业务: 隔5秒钟向服务器发送一条消息.
for(int i = 0; i < 5; i++) {
Socket client = new Socket("127.0.0.1", 9911);
PrintWriter writer = new PrintWriter(client.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
writer.println("Hello! Io Server, I am IoClientOne!");
writer.flush();
System.out.println("接收到来自服务器" + client.getRemoteSocketAddress() + "的消息: "+ reader.readLine());
writer.close();
reader.close();
client.close();
Thread.sleep(5000);
}
}
}
</span></span>
通过上面的两个小例子我们对nio有了更深的认识.
nio服务器和传统服务器的对比:
传统的java网络编程中都是在服务端创建一个ServerSocket,然后为每一个客户端单独创建一个线程处理请求,由于对于CPU而言,线程的开销是很重要的,无限创建线程会让操作系统崩溃,因此,比较好的方法是在系统启动的时候创建一个动态的线程池,
例如Tomcat就是采用这种解决方案,然而,这种解决方案在高并发的情况下也不是很乐观,当线程池大小超过CPU瓶颈的时候,
速度就明显降低了.然而有了nio以后,服务端无需再创建多个线程,也无需创建线程池,仅仅只需要1个线程即可以处理全部客户端请求,
通过这一个线程nio服务器将每一个客户端连接请求注册到特定的Selector对象上,这就可以在单线程中利用Selector对象管理大量
并发的网络连接,更好的利用了系统资源.
nio通信过程:
当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,从SelectionKey中可以找到发生的事件和该事件
所发生的具体的SelectableChannel,以获得客户端发送过来的数据/
由于在非阻塞网络I/O中采用了事件触发机制,处理程序可以得到系统的主动通知,从而可以实现底层网络I/O无阻塞,流畅地读写,
而不像在原来的阻塞模式下处理程序需要不断循环等待.使用NIO,可以编写出性能更好,更易扩展的并发型服务器程序.
nio的设计原理:
设计原理有点像设计模式中的观察者模式,由Selector去轮流咨询各个SocketChannel通道是否有事件发生,如果有,则选择出所有的Key
集合,然后传递给处理程序.我们通过每个key就可以获取客户端的SocketChannel,从而进行通信.
如果Selector发现所有通道都没有事件发生,则线程进入睡眠状态Sleep,即阻塞,等到客户端有事件发生时会自动唤醒选择器selector.
知识补充: