java NIO 深入学习

java NIO概述:

1、在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 类, 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在 Java 堆和 Native 堆中来回复制数据。

2、 NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待

NIO和IO的区别:

1、 NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去读写数据,所以在效率上NIO比IO的效率高很多。

2、 NIO不是和IO一样使用inputStream/outputStream的形式进行操作数据,但是又是基于这种流形式。同时又采用了通道和缓冲区的形式处理数据

3、 还有一点。NIO是双向的,而IO是单向的

4、 NIO的缓冲区(字节数组)还可以进行分片处理。可以建立直接缓冲区、非直接缓冲区、直接缓冲区是为了提高IO的速度。而以一种特殊的方式进行分配内存缓冲区

以上几点可以直接概括为:IO是面向流的,NIO面向缓冲区的

NIO的三个核心:

1、缓冲区(buffer)

在java NIO中负责数据的存取,缓冲区(buffer)本质就是一个数组,用于存储不同的数据类型,根据类型的不同(boolean)除外,提供了相依的类型缓冲区,分别为:

 * ByteBuffer

 * CharBuffer

 * ShortBuffer

 * IntBuffer

 * LongBuffer

 * FloatBuffer

 * DoubleBuffer

其中(ByteBuffer)最为常用。

上述的缓冲区管理的方式几乎一致,通过allocate();

缓冲区存取数据的两个核心方法:

put():存入数据到缓冲区

get():获取缓冲区的数据

     缓冲区中的四个核心属性:

capacity:容量,表示缓冲区最大的存储数量,一旦声明不能改变

limit:界限,表示缓冲区可以操作数据的大小,limit后面的数据不能操作

position:位置,表示缓冲区正在操作数据的位置。

mark:标记,用于记录当前position的位置,可以使用rest()恢复到mark的位置。

其中:capatity >= limit >= position >= mark

2、通道(channel):

  概念:用于源节点与目标节点的连接。在 java NIO中负责缓存区中数据的传输,channel本身不存储数据,因此需要配合缓冲区进去传输。通道(channel)可以

      简单的理解为火车的轨道,火车相当是缓冲区。

     通道的主要类:

 *     java.nio.channels.Channel 接口:

 *         |--FileChannel

 *         |--Sockethannel

 *         |--ServerSocketChannel

 *         |--DatagramChannel

     获取管道:

 * 1、java针对支持通道的类提供了getChannel()方法

 *    本地IO:

 *    FileInputStream/FileOutputStream

 *    RandomAccessFile

 *   

 *    网络IO:

 *    Socket

 *    ServerSocket

 *    DatagramSocket

 * 2、在 JDK 1.7中的NIO.2针对各个通道提供了静态方法open()

 * 3、在 JDK 1.7中的NIO.2的Files 工具类的newByteChannel()

 通道之间的数据传输

 * transferFrom()

 * transfetTo()

 分散(Scatter)与聚集(Gather)

* 分散读取(Scattering Reads) :将通道中的数据分散到多个缓冲区

* 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中

3、选择器(selector)

1、通道(channel)和缓冲区(Buffer)的机制,使得线程无阻塞的等待IO事件的就绪,但是要有人来监管它,这个时候就需要选择器(selector)完成,这就是所谓的同步。

2、选择器(selector)允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便

3、要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件。

Selector中注册的感兴趣事件有:

OP_ACCEPT

OP_CONNECT

OP_READ

OP_WRITE

优化:一种优化方式是:将Selector进一步分解为Reactor,将不同的感兴趣事件分开,每一个Reactor只负责一种感兴趣的事件。这样做的好处是:1、分离阻塞级别,减少了轮询的时间;2、线程无需遍历set以找到自己感兴趣的事件,因为得到的set中仅包含自己感兴趣的事件

NIO缓冲区使用实例:

@Test
    public void test() throws Exception
    {
        String str = "abcd";
        //创建缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        //获取缓冲区的容量 capatity
        System.out.println("缓冲区的容量为:"+buf.capacity());
        //获取缓冲区的界限 limit
        System.out.println("缓冲区的界限为:"+buf.limit());
        //获取正则操作数据的位置 position
        System.out.println("缓冲区操作数据的位置:"+buf.position());
        System.out.println("----------------------------------------------");
        
        //向缓冲区添加数据
        buf.put(str.getBytes());
        //添加数据后注意观察 postion 的变化
        //获取缓冲区的容量 capatity
        System.out.println("缓冲区的容量为:"+buf.capacity());
        //获取缓冲区的界限 limit
        System.out.println("缓冲区的界限为:"+buf.limit());
        //获取正则操作数据的位置 position
        System.out.println("缓冲区操作数据的位置:"+buf.position());  
        
        System.out.println("-----------------------------------------------");
        
        //切换至读取模式     如果想要获取缓冲区中的数据需要切换至读模式
        buf.flip();
        //注意观察切换至读模式后 其 缓冲区的操作界限
        System.out.println("切换至读取模式缓冲区的界限为:"+buf.limit());
        byte [] b = new byte[buf.limit()];
        //使用get获取缓冲区中的数据
        buf.get(b,0,2);
        System.out.println(new String(b,0,2));
        //获取正则操作数据的位置 position
          System.out.println("缓冲区操作数据的位置:"+buf.position());  
        //使用mark标记位置
        buf.mark();
        buf.get(b,2,2);
        System.out.println(new String(b,2,2));
        System.out.println("缓冲区操作数据的位置:"+buf.position()); 
        
        //使用rest重置位置
        buf.reset();
        System.out.println("使用rest重置后缓冲区操作数据的位置:"+buf.position()); 
    }

NIO管道(channel)实例(利用通道完成文件的复制(使用非直接缓冲区)

@Test
    public void test01()   //1261   1244
    {
        long start = System.currentTimeMillis();
        FileInputStream ins = null;
        FileOutputStream out = null;
        
        //1、获取通道
        FileChannel inChannel = null;
        FileChannel outChannel= null;
        try
        {
            ins = new FileInputStream("C:\\temp\\test\\1.zip");
            out = new FileOutputStream("C:\\temp\\test\\2.zip");
            inChannel = ins.getChannel();
            outChannel = out.getChannel();
            
            //2、分配指定大小的缓冲区
            ByteBuffer buf = ByteBuffer.allocate(1024);
            //3、将通道中的数据存入缓冲区
            while(inChannel.read(buf) != -1)
            {
                //切换读取数据模式
                buf.flip();
                //将缓冲区的数据写入通道
                outChannel.write(buf);
                //清空缓冲区
                buf.clear();
            }
        }
        catch(Exception  e)
        {
            e.printStackTrace();
        }
        finally
        {
            if(outChannel != null)
            {
                try {
                    outChannel.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                }
            }
            
            if(inChannel != null)
            {
                try {
                    inChannel.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            
            if(ins != null)
            {
                try {
                    ins.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            
            if(out != null)
            {
                try {
                    out.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end - start));
    }

NIO管道(channel)实例(利用通道完成文件的复制(使用直接缓冲区)

@Test
    public void test02() throws Exception  
    {
        long start = System.currentTimeMillis();
        
        FileChannel inChannel = FileChannel.open(Paths.get("C:\\temp\\test\\1.zip"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("C:\\temp\\test\\2.zip"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
        
        //内存映射文件
        MappedByteBuffer inMapperBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMapperBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
        
        //直接对缓冲区进行数据操作
        byte [] dst = new byte[inMapperBuf.limit()];
        //获取直接缓冲区的数据
        inMapperBuf.get(dst);
        //将获取到的数据写入内存映射文件中
        outMapperBuf.put(dst);
        
        outChannel.close();
        inChannel.close();
        
        long end = System.currentTimeMillis();
        System.out.println("耗时:"+(end - start));
    }

总结:使用直接缓冲区完成文件复制的时间比非直接缓冲区快。

NIO通道(channel)实例(使用分散读取,聚集写入)

@Test
    public void test04() throws IOException
    {
        RandomAccessFile inRaf = new RandomAccessFile("C:\\temp\\test\\1.zip", "rw");

        //获取通道   51845754   155537264
        FileChannel channel = inRaf.getChannel();
        int l = (int) (channel.size() % 3);
        int len = (int) Math.floor((channel.size() * 1.0) / 3.0);
        ByteBuffer [] bytes = new ByteBuffer[3];
        for(int i = 0; i < bytes.length; i++)
        {
            if(i == bytes.length+1)
                len = len + l;
            bytes[i] = ByteBuffer.allocate(len);        
        }
        
        //分散读取
        channel.read(bytes);
        
        //切换数据读取模式
        for(ByteBuffer byteBuffer:bytes)
        {
            byteBuffer.flip();
        }
        
        //聚集写入
        RandomAccessFile outRaf = new RandomAccessFile("C:\\temp\\test\\2.zip", "rw");
        FileChannel outChannel = outRaf.getChannel();
        outChannel.write(bytes);
        
        outRaf.close();
        inRaf.close();
        
    }

NIO通道(channel)实例(SocketChannel和ServerSocketChannel)的使用:

//客户端
    @Test
    public void client() throws Exception
    {
        //1、获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
        FileChannel inChannel = FileChannel.open(Paths.get("C:\\temp\\test\\1.zip"), StandardOpenOption.READ);
        //2、分配指定大小的缓冲区
        ByteBuffer dst = ByteBuffer.allocate(1024);
        
        //读取本地文件,并发送服务端
        while(inChannel.read(dst) > -1)
        {
            dst.flip();   //切换读取模式
            sChannel.write(dst);
            dst.clear();
        }
        
        //关闭通道
        inChannel.close();
        sChannel.close();
    }


//服务端
    @Test
    public void server() throws Exception
    {
        //1、获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        FileChannel outChannel = FileChannel.open(Paths.get("C:\\temp\\test\\2.zip"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        
        //绑定连接
        ssChannel.bind(new InetSocketAddress(9898));
        
        //获取客户端连接通道
        SocketChannel sChannel = ssChannel.accept();
        
        //分配指定的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        //接收客户端的数据,并保存到本地
        while(sChannel.read(buf) > -1)
        {
            buf.flip();
            outChannel.write(buf);
            buf.clear();
        }
        
        //关闭通道
        sChannel.close();
        ssChannel.close();
        outChannel.close();
    }

NIO选择器(selector)实例

//客户端
    @Test
    public void client() throws Exception
    {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
        
        //设置非阻塞模式
        sChannel.configureBlocking(false);
        
        //设置缓冲区大小
        ByteBuffer buf = ByteBuffer.allocate(1024);
        Scanner scan = new Scanner(System.in);
        while(scan.hasNext())
        {
            String str = scan.next();
            //System.out.println(str);
            buf.put((sdf.format(new Date())+"\n"+str).getBytes());
            //buf.put(str.getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
        sChannel.close();
        //scan.close();
    }

//服务端
    @Test
    public void server() throws Exception
    {
        //1、获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();        
        //2、设置非阻塞模式
        ssChannel.configureBlocking(false);        
        //3、绑定连接
        ssChannel.bind(new InetSocketAddress("127.0.0.1",9898));        
        //4、获取选择器
        Selector selector = Selector.open();
        //5、将通道注册到选择器上,并指定“监听接收事件”
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6、轮询获取选择器上已经准备就绪的事件
        while(selector.select() > 0)
        {
            //7、获取当前选择器中所有已注册的“选择键” (已就绪的监听事件)
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while(it.hasNext())
            {
                //8、获取已就绪的事件
                SelectionKey sk = it.next();
                //9、判断具体是什么事件准备就绪了
                if(sk.isAcceptable())
                {
                    //10、若“接收就绪”,获取客户端连接
                    SocketChannel sChannel = ssChannel.accept();
                    //11、设置非阻塞模式
                    sChannel.configureBlocking(false);
                    //12、将通道注册到选择器上
                    sChannel.register(selector, SelectionKey.OP_READ);
                }
                else if(sk.isReadable())
                {
                    //13、获取当前选择器上“读就绪”状态的通道
                    SocketChannel sChannel = (SocketChannel)sk.channel();
                    //14、读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    while((len = sChannel.read(buf)) > 0)
                    {
                        buf.flip();
                        System.out.println(new String(buf.array(),0,len));
                        buf.clear();
                    }
                }
                //15、取消选择键SelectorKey
                it.remove();
            }    
        }
    }

NIO实例 DatagrampChannel类的使用:

在java NIO 中DatagramChannel 是一个只能收发UDP包的通道。因为UDP是一个无连接的网络协议,所以不能像其他通道一样读取和写入它发送和接收的是数据包

代码示例:

//发送数据包
    @Test
    public void send() throws Exception
    {
        DatagramChannel dc = DatagramChannel.open();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //设置无阻塞模式
        dc.configureBlocking(false);
        ByteBuffer buf = ByteBuffer.allocate(1024);
        
        Scanner scan = new Scanner(System.in);
        while(scan.hasNext())
        {
            String str = scan.next();
            buf.put((sdf.format(new Date())+"\n"+str).getBytes());
            buf.flip();
            dc.send(buf, new InetSocketAddress("127.0.0.1",9898));
            buf.clear();
        }
        dc.close();
    }
    
    //接收数据包
    @Test
    public void receive() throws Exception
    {
        DatagramChannel dc = DatagramChannel.open();
        //设置非阻塞模式
        dc.configureBlocking(false);
        dc.bind(new InetSocketAddress(9898));
        
        Selector selector = Selector.open();
        dc.register(selector, SelectionKey.OP_READ);
        while(selector.select() > 0)
        {
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while(it.hasNext())
            {
                SelectionKey sk = it.next();
                if(sk.isReadable())
                {
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    //接收数据包
                    dc.receive(buf);
                    //切换至读模式
                    buf.flip();
                    System.out.println(new String(buf.array(),0,buf.limit()));
                    buf.clear();
                }
            }
        }
    }
上一篇:java – 为什么DirectByteBuffer.array()有额外的大小?


下一篇:java – 从ByteBuffer读取字符串而不进行双缓冲