对于很多童鞋来说,JAVA NIO可能是一个让人感到既熟悉又陌生的字眼。很多人可能都是听过名字而没有实际用过。
那么,NIO和普通IO(BIO)有什么区别呢?且听我从头说起。
c10k问题,即如何让一台机器同时处理10k个网络连接。
随着互联网的发展,这个问题其实已经非常普遍。并且产生了c100K、c1000K等问题。像支付宝、QQ、微信,甚至面对的是数十亿计的连接数(当然肯定有集群)。
玩过Java网络编程的肯定知道,传统的I/O技术,会对每个连接创建一个套接字(socket),同时创建一个线程来对这个socket进行read/write操作。
对于连接数较少的应用,几十个,几百个连接都没有问题。
但是,当连接数增加到一万个的时候,问题就出来了:在操作系统中,线程切换是一件很耗费cpu时间的事情。当线程数达到一万个的时候,系统资源大量被消耗,就会变得很卡。
所以,为了解决这样的问题,NIO技术应运而生。
感叹号代表重要。
重复代表重要。
前面说到BIO会对每个连接新建一个线程,因此当连接数很大的时候,系统就会因为线程切换问题导致资源占用过多。
那么,一个容易想到的点就是:能不能用更少的线程数来管理成千上万个连接?
答案是可以的。
JAVA NIO对传统BIO优势的关键就在于线程。
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程就可以监听多个数据通道。
下面是一段selector的服务端示例代码:
public class Main {
private static final int BUF_SIZE=1024;
private static final int PORT = 8080;
private static final int TIMEOUT = 3000;
public static void main(String[] args)
{
selector();
}
public static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(BUF_SIZE));
}
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();
}
}
public static void handleWrite(SelectionKey key) throws IOException{
ByteBuffer buf = (ByteBuffer)key.attachment();
buf.flip();
SocketChannel sc = (SocketChannel) key.channel();
while(buf.hasRemaining()){
sc.write(buf);
}
buf.compact();
}
public static void selector() {
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.isWritable() && key.isValid()){
handleWrite(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();
}
}
}
}
可以看出,服务端不断调用selector.select方法,当返回结果不为0时,通过代码段:
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
获取所有的I/O事件。继而对accept、read、write事件分别做相应的处理。
当有新的连接产生时,服务端并没有新建线程,而只是把channel注册到selector里面。
显而易见的,这样通过一个、或者少量线程就可以管理多个连接,从而减少了线程切换的开销。
这就是所谓的“多路复用”。
细心的你可能要问了:selector是怎么一下子获取到所有的io事件的呢?
我们跟着selector.select往下走,走到最底层,发现调用了一个内部类SubSelect类的native方法poll0。
后来笔者发现,在sun.nio.ch这个包下面的类也会随着操作系统的不同而不同,由于笔者现在用的电脑是windows, 使用的SelectProvider是WindowsSelectorProvider,最终调的是poll0;如果是mac os,会最终使用kqueue;如果是Linux, 就会使用epoll。所以epoll其实是只有在linux系统才存在的;当然mac和windows也有类似的替代方案,但不叫epoll罢了。
这里先打个TODO,等我有了相应系统的电脑再做研究。
引用一篇文章里面的一句描述:
在早期的JDK1.4和1.5 update10版本之前,Selector基于select/poll模型实现,是基于IO复用技术的非阻塞IO,不是异步IO。在JDK1.5 update10和linux core2.6以上版本,sun优化了Selctor的实现,底层使用epoll替换了select/poll。