JAVA网络I/O-闲话NIO

引言

对于很多童鞋来说,JAVA NIO可能是一个让人感到既熟悉又陌生的字眼。很多人可能都是听过名字而没有实际用过。

那么,NIO和普通IO(BIO)有什么区别呢?且听我从头说起。

一、C10K问题

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里面。

显而易见的,这样通过一个、或者少量线程就可以管理多个连接,从而减少了线程切换的开销。

这就是所谓的“多路复用”。

三、epoll:魔法的关键所在

细心的你可能要问了:selector是怎么一下子获取到所有的io事件的呢?JAVA网络I/O-闲话NIO_第1张图片
我们跟着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。

未完待续

你可能感兴趣的:(java通信)