前言
其实作为我个人,NIO,我前面也自己学过一些,但是吧,总觉得没有个概念,也就是说这个知识吧,他不是我的东西,我没法用自己的话去描述,只是简单的说,哦,NIO=New IO,和IO的区别就是不阻塞。然后好像有个选择器,和银行大堂经理一样的,引导人流,没了。我争取通过我的学习,向大家描述一个概念,建立起一个概念,NIO到底是个啥。
NIO基本概念
要说NIO的基本概念,和IO的对比是绕不过去的,因为与自己固有心智模式的对比,才能理解的更透彻,更有助于理解。
上面这个图,大家应该都很了解了,IO的图,一般来说,IO就是说的输入输出,一般输入指从文件(来自网络、本地磁盘)到程序的流过程,输出是指从程序到文件的流过程。在传输的时候,一般是单向的。底层实际上是一个个的小包进行传输。在生活中的类比可以是自来水流或者电流,不过,一般生活中的流都是从厂流到家里,一般不会从家里流回电厂。
OK,我们现在来看一下NIO的图
上面介绍过的部分,不再介绍,说说没有介绍过的部分。这里有三个词,缓冲区、通道和选择器。分别用生活中的例子来介绍一下。我们回到事情的本身来看,为什么要IO,为什么要输入输出呢?因为程序可能需要文件中的东西,也有可能是程序产生的东西要存在文件中。这个过程就像极了生活中的这个场景。
工厂生产完东西,不能堆在工厂吧?更通常的做法就是,存放到仓库中。那仓库中的东西也不能一辈子放那儿吧?要不就发到实体店再去卖,要不然就快递直接给买家。那如果我们把仓库类比成文件,从工厂到仓库的过程就是输出,从仓库到实体店或者买家的过程就是输入。OK,背景解释清楚了。
那具体的运输过程不就是类比于我们数据传输的过程吗?我们想想咯,在现实生活中,我们总不能说,发货时,当货没有全到目的地,这段过程,我们仓库就啥事儿不干,搁那儿等着吧?生意不做啦?
肯定不是吧?我们一般都是,货物装车,或者,我不管你怎么给我整,我托快递公司帮我整,我一发货我就干其他事儿了。快递公司这个事儿我们以后再说,现在暂时没有它的角色,但是大家都知道快递公司,效率比自己运高。如果自己运呢?整辆车嘛,装车,发货。注意了!这个车 就是我们的缓冲区。通道呢?就是运输的路线,有人解释为铁轨,可以。解释成国道、省道、高速公路我觉得也未尝不可,就是连接于发货地和目的地之间的路径嘛。
最后,选择器呢?也简单,你看哦,我们货物到达目的地之后,总不能又堆那儿吧?实体店怎么做?店长总归要让小李、小王、小张,谁闲着呢?谁去理货。而这些店员就是几条不同的线程,而店长就是选择器。
OK,文字很多,但是我相信如果你真的认真的看一遍,结合个人生活常识,很快就能理解的。
缓冲区
接下来要讲得就是缓冲区的基础概念,你以为我又要画图?
http://blog.csdn.net/abc_key/article/details/29909375
http://www.cnblogs.com/chenpi/p/6475510.html
这部分收工啦~(诶,对,我就是懒)
这里再说两嘴Buffer的直接缓冲区和间接缓冲区吧~
间接缓冲区的建立Buffer方法
ByteBuffer.allocate(capacity);
直接缓冲区建立Buffer方法
ByteBuffer.allocateDirect()
以上的两个方法,我都是以ByteBuffer为例
画图说说有何不同。
好啦好啦,这张图和下张图都不是我画的啦, 一点我的风格都没有。
这是间接缓冲区,为什么间接,因为其实文件从磁盘到内存的过程,其实要在内存的两个地方导一下手。其实可以考虑一下,这一份内存中的复制有没有必要。
那直接缓冲区就简单解释了,就是在内存中只有一份。
这个图嘛,糊是糊了点,毕竟不是我手画的,将就看吧~
直接字节缓冲区可以通过调用此类的allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
白话
直接缓冲的创建和销毁消耗大,虽然快一点,但是慎用。
用法
好像不放几段代码,就完全没有技术博客的意思嘛,用一个简单的例子,复制文件来看看这个东西到底是怎么用的吧~
package com.pochi.nio;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* 这里我是想做一个对比,对比一下,NIO(直接缓冲/间接缓冲) IO的复制效率差别有多大
*/
public class NIOCopy {
public static void main(String[] args) throws Exception {
String filePath="E:/1.zip";
// IO 78821ms
copyByIO(filePath);
// NIO(间接)
copyByNIO(filePath);
// NIO(直接)
copyByNIO2(filePath);
}
private static void copyByNIO2(String filePath) throws Exception {
long start=System.currentTimeMillis();
// 首先要拿到通道,入通道出通道
FileChannel readChannel = FileChannel.open(Paths.get(filePath), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Paths.get("e:/NIO2.zip"), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.READ);
readChannel.transferTo(0,readChannel.size(),writeChannel);
long end=System.currentTimeMillis();
System.out.println("NIO2:::"+(end-start));
}
private static void copyByNIO(String filePath) throws Exception {
long start=System.currentTimeMillis();
// 首先要拿到通道,入通道出通道
ReadableByteChannel inChannel = Channels.newChannel(new FileInputStream(filePath));
WritableByteChannel outChannel = Channels.newChannel(new FileOutputStream("e:/nio.zip"));
ByteBuffer buf = ByteBuffer.allocate(1024);
int len=0;
while((len=inChannel.read(buf))!=-1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
inChannel.close();
outChannel.close();
long end=System.currentTimeMillis();
System.out.println("NIO:::"+(end-start));
}
private static void copyByIO(String filePath) throws IOException {
long start=System.currentTimeMillis();
FileOutputStream fileOutputStream = new FileOutputStream("e:/io.zip");
FileInputStream fileInputStream = new FileInputStream(filePath);
byte [] buf=new byte[1024];
int len=0;
while((len=fileInputStream.read(buf))!=-1){
fileOutputStream.write(buf,0,len);
}
fileInputStream.close();
fileOutputStream.close();
long end=System.currentTimeMillis();
System.out.println("IO:::"+(end-start));
}
}
结果
IO:::63649
NIO:::84639
NIO2:::96589
令人惊讶的一点,就是其实NIO的复制文件速度,并没有比IO快,直接缓冲区,并没有比间接缓冲区快,这我就凌乱了。
分散读取和聚合写入
就是说,我们现在呢,读和写用的都是一个buffer,一个buffer不是慢嘛。
你这一辆车,不够运的,那么就把货物分一分嘛,桌子分成桌子面和桌子腿,分别运输嘛,到底目的地再组合成桌子。
具体怎么做:
http://blog.csdn.net/u013063153/article/details/76562221
别人写的不错,到时候用的时候,再看吧,再说,最后大家也不怎么会这么用了。
非阻塞式Socket操作
看这里,太麻烦了,难怪要有Netty
http://www.cnblogs.com/chengJAVA/p/5715629.html
虽然哦,这个操作复杂不想copy重现,但是!
原理咱们还是要分析一波
public class HelloWorldServer {
// 缓冲区容量
static int BLOCK = 1024;
static String name = "";
protected Selector selector;
protected ByteBuffer clientBuffer = ByteBuffer.allocate(BLOCK);
protected CharsetDecoder decoder;
static CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();
// 初始化
public HelloWorldServer(int port) throws IOException {
selector = this.getSelector(port);
Charset charset = Charset.forName("GB2312");
decoder = charset.newDecoder();
}
// 获取Selector
protected Selector getSelector(int port) throws IOException {
ServerSocketChannel server = ServerSocketChannel.open();
Selector sel = Selector.open();
server.socket().bind(new InetSocketAddress(port));
// 非阻塞式
server.configureBlocking(false);
// 注册选择器
server.register(sel, SelectionKey.OP_ACCEPT);
return sel;
}
// 监听端口
public void listen() {
try {
for (;;) {
selector.select();
Iterator iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
iter.remove();
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 处理事件
protected void process(SelectionKey key) throws IOException {
if (key.isAcceptable()) { // 接收请求
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
//设置非阻塞模式
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 读信息
SocketChannel channel = (SocketChannel) key.channel();
int count = channel.read(clientBuffer);
if (count > 0) {
clientBuffer.flip();
CharBuffer charBuffer = decoder.decode(clientBuffer);
name = charBuffer.toString();
// System.out.println(name);
SelectionKey sKey = channel.register(selector,
SelectionKey.OP_WRITE);
sKey.attach(name);
} else {
channel.close();
}
clientBuffer.clear();
} else if (key.isWritable()) { // 写事件
SocketChannel channel = (SocketChannel) key.channel();
String name = (String) key.attachment();
ByteBuffer block = encoder.encode(CharBuffer
.wrap("Hello !" + name));
channel.write(block);
//channel.close();
}
}
public static void main(String[] args) {
int port = 8888;
try {
HelloWorldServer server = new HelloWorldServer(port);
System.out.println("listening on " + port);
server.listen();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面的代码就是服务器端的一套完整流程, 这套流程呢,我想用一幅灵魂画作来解释
解释一下这张图,图的左边是一堆客户端,可能随时会发各种消息,比如建立连接、发送消息等等。但是无论他什么时候法,发什么,都会有一个selector先判断,就是像上面的代码一样,会有一个无限循环的listen(),如果监听到写操作,它就调用一个或多个线程做写操作,如果是其他操作需求,就做不同的操作需求。这是一个无限的循环的过程。
后记
但是呢,其实这还是有点问题,就是。这样的NIO只能说它是非阻塞的,但是却不能说它是异步的,因为其实不管到底selector返回一个什么样的结果,都要应用程序做一个read或者其他的处理,这个处理是主动的。能不能由操作系统完成呢?能,不过要在NIO2中完成,下次见~