NIO原理剖析与Netty初步----浅谈高性能服务器开发(一)

    除特别注明外,本站全部文章均为原创,转载请注明地址 数据库

    在博主不长的工做经历中,NIO用的并很少,因为使用原生的Java NIO编程的复杂性,大多数时候咱们会选择Netty,mina等开源框架,但理解NIO的原理就不重要了吗?偏偏相反,理解NIO底层机制是理解这一切的基础,由此我总结一下当初学习NIO时的笔记,以便后续复习。编程

     如下是我理解的Java原生NIO开发大体流程:服务器

    

       上图大体描述的是服务端的NIO操做。框架

第一步,绑定一个服务的端口

       这与传统阻塞IO中的ServerSocket相似,没什么好说的socket

第二步,打开通道管理器Selector并在Selector上注册一个事件

      当注册的事件发生时,Selector.select()会返回,不然一直阻塞。这一步颇有意思,也是NIO第一个与传统IO不一样的地方,NIO经过一个Selector线程能够管理大量客户端链接,反之传统IO一个客户端链接进来必须建立一个新的线程为它服务(固然你可使用链接池),咱们知道线程对服务端来讲是十分宝贵的资源,一个服务端进程所包含的线程是有   限的;此外,每一个线程会占用必定的内存空间,过多的线程可能致使内存溢出,这种状况下你可能会到想对虚拟机进行调优,好比经过修改参数-Xss限制单个线程大小,但这又可   能致使StackOverFlow;另外,线程调度须要切换上下文,对于操做系统,它须要经过TCB(线程控制块)来对线程进行调度,过多的上下文切换浪费了CPU时间,下降了系统效     率。学习

第三步,轮循访问Selector,当注册的事件到达时,方法返回

   下面的代码能够看到,方法总体是一个死循环,轮询访问Selector,发生某些已经注册在Selector上的事件时,该方法返回。能够经过selector.selectedKeys获取发生的事件,this

该方法返回的是一个泛型集合Set<SelectionKey>,遍历这个集合与遍历普通集合没有什么不一样,这里咱们经过迭代器迭代的缘由是咱们须要删除已经处理的Key,避免重复处理:spa

public void listen() throws IOException {// 轮询访问selector
        while (true) {
            // 当注册的事件到达时,方法返回;不然,该方法会一直阻塞
            selector.select();
            // 得到selector中选中的项的迭代器,选中的项为注册的事件
            Iterator<?> ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                //删除已选的key,以防重复处理
                ite.remove();
         //这里能够写咱们本身的处理逻辑
                handle(key);
            }
        }
}

   在第二步时,已经在Selector上注册了Accept事件,当这里的selector.select()返回时,表明客户端已经能够链接了,在handle方法里能够处理这个事件:操作系统

public void handle(SelectionKey key) throws IOException {
    // 客户端请求链接事件
    if (key.isAcceptable()){
//从Key里能够很方便的取到注册这个事件的Channel

        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        // 得到和客户端链接的通道
        SocketChannel channel = server.accept();
        // 设置成非阻塞
        channel.configureBlocking(false);线程

        logger.info("客户端已经链接!");

        //客户端链接在通道管理器Selector上注册读事件

        channel.register(this.selector, SelectionKey.OP_READ);

    }
}

    上面的代码很简单,咱们经过key获取到注册它的那个Channel,在这里是ServerSocketChannel,经过server.accept()获取客户端链接,这里一样能够类比到传统的阻塞

IO,在阻塞IO中咱们能够经过ServerSocket.accept获取到socket,惟一不一样的是,阻塞IO中的accept方法是阻塞操做,而NIO中是非阻塞的。

    固然,仅仅是链接到客户端并无什么用处,服务端须要有读写数据的能力,好比你能够用NIO实现一个Http服务器(固然最佳实践使用Netty等框架)。因此咱们须要在Selector

上注册读事件,一样,当读事件发生时,执行咱们本身的业务逻辑。下面是修改后的代码:

public void handle(SelectionKey key) throws IOException {
    if (key.isAcceptable()){
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel channel = server.accept();
        channel.configureBlocking(false);
        logger.info("客户端已经链接!");
        channel.register(this.selector, SelectionKey.OP_READ);
    } else if(key.isReadable()){
        SocketChannel channel = (SocketChannel) key.channel();
        // 建立读取缓冲
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //读取到Buffer中
        int read = channel.read(buffer);
        if(read > 0){
            byte[] data = buffer.array();
            String msg = new String(data).trim();
            logger.info("receive msg: {}",msg)
            
            //回写数据
            ByteBuffer outBuffer = ByteBuffer.wrap("OK".getBytes());
            channel.write(outBuffer);
        }else{
            logger.info("client closed!!!");
            key.cancel();
        }
    }
} 

总结

     本文大体讲述了使用NIO进行服务器端开发的大体流程,但代码显然仍然存在问题,其一是咱们只使用了一个线程执行全部操做,包括接收客户端链接,读取数据,返回数据,对于这个简单的Demo来讲已经足够了,但在实际的服务器开发中,例如你想使用NIO开发本身的HTTP服务器,服务器本地须要作大量操做,包括解析用户请求,根据请求路由到某一个Action执行业务逻辑,这其中又极可能某些数据从数据库读取,渲染模板等操做,十分耗时,这无疑又称为系统的瓶颈,再者,使用单一线程不能充分利用多核CPU提供的计算能力。下一篇中会看到,在基于Reactor模型的Netty中,会使用一个Boss线程接收客户端请求,使用多个Worker线程执行具体的业务逻辑。

相关文章
相关标签/搜索