从JDK1.5开始,java引入了java.nio包,nio的含义为非阻塞型IO。这一篇就从使用到源码来简单了解一下NIO吧。
一、基本用法
RandomAccessFile aFile = null;
try {
aFile = new RandomAccessFile(NIOTest.class.getClassLoader().getResource("nio.txt").getPath(), "rw");
FileChannel fileChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buf);
System.out.println(bytesRead);
while (bytesRead != -1) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.compact();
bytesRead = fileChannel.read(buf);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (aFile != null) {
aFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
如上例程,读取一个resource下的nio.txt文件,获取其对应Channel,定义一个ByteBuffer,然后调用read方法。
这里的Channel与传统阻塞型IO中的Stream类似,不同在于,Stream的读写是流的形式的,也就是一个个字节读的,而Channel却是直接传入一个Buffer块,然后就然后那样嗯。
所以我们就可以得出NIO和传统IO的两大区别:1.面向块而非面向流;2.顾名思义,可以非阻塞。
二、非阻塞的好处
既然NIO是非阻塞的,那么非阻塞IO到底有什么好处呢?
既然要思考非阻塞的好处,我们自然要看看阻塞通常被用在哪。最典型的例子就是Socket,而我们这里就使用非阻塞的Socket:SocketChannel来展示下非阻塞IO的作用:
Selector selector = null;
ServerSocketChannel ssc = null;
try {
selector = Selector.open();
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(TIMEOUT) == 0) {
System.out.println("==");
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
if (key.isConnectable()) {
System.out.println("isConnectable = true");
}
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (selector != null) {
selector.close();
}
if (ssc != null) {
ssc.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
这是一个Socket服务端的实现,利用非阻塞服务端ServerSocketChannel和选择器Selector完美展示如何在单一的线程中监听多个客户端的请求。
传统的IO型ServerSocket通常只在一个线程中完成accept,此后的read则是为Socket单独开启一个线程,通过阻塞的方式去监听。而这里,accept、read是平级的,他们都在主线程中完成,而在read操作中,可以从key中获取具体的SocketChannel:
public static void handleRead(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = sc.read(buf);
while (bytesRead > 0) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
if (bytesRead == -1) {
sc.close();
}
}
一个线程,可以完成多个客户端的IO读取,究其原因自然是因为其非阻塞的特性,他虽然也会不断地轮询IO,但是并不因为没有读到数据而阻塞IO端口,而是返回一个null,因此他可以在一个线程中同时监听多个IO端,这给降低并发压力带来可能。而许多框架(如Netty)就是这么去实现的。
三、实现源码
粗略阅读发现,NIO的源码实现相当复杂,方便起见,这里只粗略看看部分的源码,只说明一下其中注意到的细节,暂时不整个流程地去解读源码了。
3.1 ServerSocketChannel的启动
ServerSocket socket;
ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.serverSocket(true);
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
}
public ServerSocket socket() {
Object var1 = this.stateLock;
synchronized(this.stateLock) {
if (this.socket == null) {
this.socket = ServerSocketAdaptor.create(this);
}
return this.socket;
}
}
从源码不难发现,ServerSocketChannel的启动最根本的还是使用了ServerSocket。
3.2 ServerSocketChannel和Selector的绑定
public final SelectionKey register(Selector sel, int ops,
Object att)
throws ClosedChannelException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (blocking)
throw new IllegalBlockingModeException();
SelectionKey k = findKey(sel);
if (k != null) {
k.interestOps(ops);
k.attach(att);
}
if (k == null) {
// New registration
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}
}
从代码中可以看出,register的过程其实是一个双向绑定的过程。
protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
Set var5 = this.publicKeys;
synchronized(this.publicKeys) {
this.implRegister(var4);
}
var4.interestOps(var2);
return var4;
}
}
selector的register将ServerSocketChannel传给了SelectionKey的实现类,然后调用implRegister,这个方法因操作系统而异,在windows中:
protected void implRegister(SelectionKeyImpl var1) {
Object var2 = this.closeLock;
synchronized(this.closeLock) {
if (this.pollWrapper == null) {
throw new ClosedSelectorException();
} else {
this.growIfNeeded();
this.channelArray[this.totalChannels] = var1;
var1.setIndex(this.totalChannels);
this.fdMap.put(var1);
this.keys.add(var1);
this.pollWrapper.addEntry(this.totalChannels, var1);
++this.totalChannels;
}
}
}
可以看到这里把var1传给了一个Wrapper。最终这玩意调用了一个native的方法,具体就不多说了。
3.3 Selector的select
依然看windows下的实现。
protected int doSelect(long var1) throws IOException {
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else {
this.timeout = var1;
this.processDeregisterQueue();
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else {
this.adjustThreadsCount();
this.finishLock.reset();
this.startLock.startThreads();
try {
this.begin();
try {
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
this.finishLock.checkForException();
this.processDeregisterQueue();
int var3 = this.updateSelectedKeys();
this.resetWakeupSocket();
return var3;
}
}
}
private int updateSelectedKeys() {
++this.updateCount;
byte var1 = 0;
int var4 = var1 + this.subSelector.processSelectedKeys(this.updateCount);
WindowsSelectorImpl.SelectThread var3;
for(Iterator var2 = this.threads.iterator(); var2.hasNext(); var4 += var3.subSelector.processSelectedKeys(this.updateCount)) {
var3 = (WindowsSelectorImpl.SelectThread)var2.next();
}
return var4;
}
从这里可以看出,它通过遍历选择器拥有的线程,然后将其转变为SelectThread,调用其subSelector.processSelectedKeys()方法,最终完成了select的操作。
总结
对于NIO的使用,网上的教程非常多,而且也有Netty这样的框架使用它,原本想要从源码角度去对NIO进行解读,但是博客写的并不如意。事实上NIO本身就是操作系统支持的东西,并非单纯JAVA层面的东西,java.nio包中大部分都是接口,具体的实现类都在sun.nio包中,其中的实现代码也是非常的晦涩难懂,变量的命名几乎和代码混淆了一般全是var1var2的命名,但是其基本原理也是不难看出来。所谓Selector,其实无非是把ServerSocketChannel绑定在内部,内部又拥有用于轮训的Thread,可以切换式地将IO放入Thread中读取,其最终调用的也是底层的操作系统开放的接口。
这篇博客写的不如意,NIO的源码比先前想象的复杂多,以后可能会继续写NIO的博客来完善,这篇姑且做个伏笔。
此外最近要找工作,源码的解读先告一段落,接下来可能会根据面试复习的内容去写一写读书笔记(JVM、操作系统、数据库、网络等等)。