zoukankan      html  css  js  c++  java
  • 探索Java NIO

    什么是NIO?

      java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO),NIO提供了与标准IO不同的IO工作方式。

    核心部分:

      • Channels(通道)
      • Buffers(缓冲区)
      • Selectors
      • 除此之外还有组件,像Pipe、FileLock,但这些都是建立在以上三个核心基础之上的。

    与传统IO的区别:

      IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

      传统IO的流是阻塞的,这就意味着当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。NIO的非阻塞模式,不是保持线程阻塞,在数据可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

    Channel

      在标准的IO当中,都是基于字节流/字符流进行操作的,而在NIO中则是是基于Channel和Buffer进行操作,其中的Channel的虽然模拟了流的概念,类似与流,但并不相同。

      

    区别StreamChannel
    支持异步 不支持 支持
    是否可双向传输数据 不能,只能单向 可以,既可以从通道读取数据,也可以向通道写入数据
    是否结合Buffer使用 必须结合Buffer使用
    性能 较低 较高

      Channel必须结合着Buffer使用,不能直接往通道里面读写数据

    Channel的实现类

      • FileChannel(从文件中读写数据)
      • DatagramChannel(通过UDP读写网络传输的数据)
      • SocketChannel(通过TCP读写网络传输的数据)
      • ServerSocketChannel(可以监听CP连接,对每一个新的连接都会创建一个SocketChannel)

        简单的一个读写文件的实例:

        

    RandomAccessFile aFile = new RandomAccessFile("filePath/inFileName", "rw");
    
            RandomAccessFile outFile = new RandomAccessFile("filePath/outFileName", "rw");
            FileChannel inChannel = aFile.getChannel();
            FileChannel outChannel = outFile.getChannel();
         
            ByteBuffer buf = ByteBuffer.allocate(1024);
    
            int bytesRead = inChannel.read(buf);
            while (bytesRead != -1) {
    
                buf.flip();
    
                while(buf.hasRemaining()){
                    outChannel.write(buf);
                }
    
                buf.clear();
                bytesRead = inChannel.read(buf);
            }
    
            aFile.close();
        }

        在读取数据中遇到中文乱码问题

    Charset charset = Charset.forName("UTF-8");// 创建UTF-8字符集,或者 Charset.forName("GBK");
    System.out.print(charset.decode(buf));

    数据传输

    假如有两个Channel,我们可以直接把Channel1的数据传输给Channel2

    主要的方法:

    transferFrom(ReadableByteChannel src, long position, long count)
    
    transferTo(long position, long count, WritableByteChannel target)
    // 从position为0的位置把大小为Channel1.size()的数据写到Channel2中。
    Channel2.transferFrom(Channel1, 0, Channel1.size());
    
    Channel1.transferTo(0, Channel2.size(), Channel2);

     

     

    Buffer

         缓冲区(Buffer)就是在内存中预留指定字节数的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区;在Java NIO中,缓冲区的作用也是用来临时存储数据,可以理解为是I/O操作中数据的中转站。缓冲区直接为通道(Channel)服务,写入数据到通道或从通道读取数据,这样的操利用缓冲区数据来传递就可以达到对数据高效处理的目的。在NIO中主要有八种缓冲区类(其中MappedByteBuffer是专门用于内存映射的一种ByteBuffer)

    Buffer的基本用法

    实际上在Channel的例子中已经体现了Buffer的使用方法:

    1. 创建一个特定长度的Buffer
    2. 读取数据到Buffer
    3. 调用flip()方法,使向Buffer写数据转换为向Buffer读数据
    4. 从Buffer中读取数据
    5. 调用clear()或者compat()方法对缓冲区进行清空

    clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

    Buffer重要的三个属性

    • capacity
    • position
    • limit

    position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

    他们三者在读写时候的关系如图所示:

    capacity

    作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

    position

    当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

    当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

    limit

    在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

    当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

    Buffer的类型

    这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。

    MappedByteBuffer

    在FileChannel上调用map方法 返回一个MappedByteBuffer对象 

    MapMode 有三种Mode:READ_ONLY 、READ_WRITE 、PRIVATE(通过put方法对MappedByteBuffer的修改   不会修改到磁盘文件  只是虚拟内存的修改)

    MappedByteBuffer继承与Buffer,在父类的基础上增加了三个方法:

    1. force缓冲区在READ_WRITE模式下,此方法对缓冲区所做的内容更改强制写入文件
    2. load:将缓冲区的内容载入物理内存,并返回该缓冲区的引用
    3. isLoaded:判断缓冲区的内容是否在物理内存,如果在则返回true,否则返回false

    MappedByteBuffer的用法和ByteBuffer的用法类似,只是创建有所差别

    // 创建一个MappedByteBuffer
    MappedByteBuffer mappedByteBuffer = inChannel.map(MapMode.READ_ONLY, 0, fileChannel.size()); 

    如果要把数据写入磁盘中需调用force()方法,如果不调用只会更新内存中的值。

     Scatter/Gather

     

    分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
    聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

    scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。

     

    scatter样例

    // 定义一个ByteBuffer的数组
    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body   = ByteBuffer.allocate(1024);
    
    ByteBuffer[] bufferArray = { header, body };
    // Buffer从Channel中读取数据
    channel.read(bufferArray);

    数据在读入到buffer中的时候,优先填满第一个buffer,如果在消息传输中,header的字节小于第一个buffer的(capacity)容量大小,那么就会把body的一部分数据写到header中,破坏数据。因此scatter不适合做动态消息。

    gather样例

    // 定义一个Byte Buffer数组
    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body   = ByteBuffer.allocate(1024);
    
    // 把Buffer中的数据写到Channel中
    
    ByteBuffer[] bufferArray = { header, body };
    
    channel.write(bufferArray);

    而这种方式会依次把数组中的ByteBuffer写道Channel中,因此不会打破原始数据。因此gather可用作动态消息的场景中。

    Selector(此处内容部分来自于与小菜fly基于NIO的Socket通信

    选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

    选择器执行过程

    1. 创建一个或者多个可选择的通道
    2. 将这些创建的通道注册到选择器对象中
    3. 选择键会记住开发者关心的通道,它们也会追踪对应的通道是否已经就绪
    4. 开发者调用一个选择器对象的select()方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道
    5. 获取一个键的集合,从而找到当时已经就绪的通道,通过遍历这些键,开发者可以选择出每个从上次调用select()开始直到现在已经就绪的通道

    一个选择器可以被注册多个Channel(通道),选择器可以轮番从迭代器SelectedKeys获取注册的事件。

    服务端和客户端各自维护一个通道调度器(Selector)对象,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

    服务端和客户端的实例

    package cn.nio;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    /**
     *
     * @author*/
    public class NIOServer {
        //通道调度器
        private Selector selector;
    
        /**
         * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
         * @param port  绑定的端口号
         * @throws IOException
         */
        public void initServer(int port) throws IOException {
            // 获得一个ServerSocket通道
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            // 设置通道为非阻塞
            serverChannel.configureBlocking(false);
            // 将该通道对应的ServerSocket绑定到port端口
            serverChannel.socket().bind(new InetSocketAddress(port));
            // 获得一个通道调度器
            this.selector = Selector.open();
            //将通道调度器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
            //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        }
    
        /**
         * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
         * @throws IOException
         */
        @SuppressWarnings("unchecked")
        public void listen() throws IOException {
            System.out.println("server start success");
            // 轮询访问selector
            while (true) {
                //当注册的事件到达时,方法返回;否则,该方法会一直阻塞
                selector.select();
                // 获得selector中选中的项的迭代器,选中的项为注册的事件
                Iterator ite = this.selector.selectedKeys().iterator();
                while (ite.hasNext()) {
                    SelectionKey key = (SelectionKey) ite.next();
                    // 删除已选的key,以防重复处理
                    ite.remove();
                    // 客户端请求连接事件
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key
                                .channel();
                        // 获得和客户端连接的通道
                        SocketChannel channel = server.accept();
                        // 设置成非阻塞
                        channel.configureBlocking(false);
    
                        //在这里可以给客户端发送信息哦
                        channel.write(ByteBuffer.wrap(new String("send a message to client").getBytes()));
                        //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
                        channel.register(this.selector, SelectionKey.OP_READ);
                        
                        // 获得了可读的事件
                    } else if (key.isReadable()) {
                            read(key);
                    }
    
                }
    
            }
        }
        /**
         * 处理读取客户端发来的信息 的事件
         * @param key
         * @throws IOException 
         */
        public void read(SelectionKey key) throws IOException{
            // 服务器可读取消息:得到事件发生的Socket通道
            SocketChannel channel = (SocketChannel) key.channel();
            // 创建读取的缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(10);
            channel.read(buffer);
            byte[] data = buffer.array();
            String msg = new String(data).trim();
            System.out.println("received message from client:"+msg);
            ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
            channel.write(outBuffer);// 将消息回送给客户端
        }
        
        /**
         * 启动服务端测试
         * @throws IOException 
         */
        public static void main(String[] args) throws IOException {
            NIOServer server = new NIOServer();
            server.initServer(8000);
            server.listen();
        }
    
    }
    package com.nio;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    /**
     *
     * @author 14  */
    public class NIOClient {
        //通道调度器
        private Selector selector;
    
        /**
         * 获得一个Socket通道,并对该通道做一些初始化的工作
         * @param ip 连接的服务器的ip
         * @param port  连接的服务器的端口号         
         * @throws IOException
         */
        public void initClient(String ip,int port) throws IOException {
            // 获得一个Socket通道
            SocketChannel channel = SocketChannel.open();
            // 设置通道为非阻塞
            channel.configureBlocking(false);
            // 获得一个通道调度器
            this.selector = Selector.open();
            
            // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
            //用channel.finishConnect();才能完成连接
            channel.connect(new InetSocketAddress(ip,port));
            //将通道调度器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
            channel.register(selector, SelectionKey.OP_CONNECT);
        }
    
        /**
         * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
         * @throws IOException
         */
        @SuppressWarnings("unchecked")
        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();
                    // 连接事件发生
                    if (key.isConnectable()) {
                        SocketChannel channel = (SocketChannel) key
                                .channel();
                        // 如果正在连接,则完成连接
                        if(channel.isConnectionPending()){
                            channel.finishConnect();
                            
                        }
                        // 设置成非阻塞
                        channel.configureBlocking(false);
    
                        //在这里可以给服务端发送信息哦
                        channel.write(ByteBuffer.wrap(new String("send a  message to server").getBytes()));
                        //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                        channel.register(this.selector, SelectionKey.OP_READ);
                        
                        // 获得了可读的事件
                    } else if (key.isReadable()) {
                            read(key);
                    }
    
                }
    
            }
        }
        /**
         * 处理读取服务端发来的信息 的事件
         * @param key
         * @throws IOException 
         */
        public void read(SelectionKey key) throws IOException{
            //和服务端的read方法一样
        }
        
        
        /**
         * 启动客户端测试
         * @throws IOException 
         */
        public static void main(String[] args) throws IOException {
            NIOClient client = new NIOClient();
            client.initClient("localhost",8000);
            client.listen();
        }
    
    }

    非阻塞式服务器

    参考外国友人搭建的非阻塞式服务器,我把GitHub上的项目fork到我自己的仓库里可供大家学习

    地址:https://github.com/yanfzhang/java-nio-server

  • 相关阅读:
    Codefroces 920F SUM and REPLACE(线段树)
    POJ 2155 Matrix (2维树状数组)
    POJ 3067 Japan (树状数组求逆序对)
    Codeforces 919D Substring (拓扑排序+树形dp)
    拓扑排序
    Codeforces 889F Letters Removing(二分 + 线段树 || 树状数组)
    线段树扫描线(2---算矩形的相交面积)
    线段树扫描线(1---算矩形的总面积)
    hdu 6168 Numbers
    Educational Codeforces Round 27 A B C
  • 原文地址:https://www.cnblogs.com/bug-zhang/p/7581447.html
Copyright © 2011-2022 走看看