Java NIO及其常用API

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dxx123446/article/details/78447764

一、初识NIO

 > 在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 类, 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在 Java 堆和 Native 堆中来回复制数据。NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。
   在NIO中有几个比较关键的概念:Buffer(缓冲区),Channel(通道),Selector(选择器)。

Buffer(缓冲区):
在Java NIO中负责数据的存取,缓冲区就是数组,用于存储不同类型的缓冲区。为什么说NIO是基于缓冲区的IO方式呢?因为,当一个链接建立完成后,IO的数据未必会马上到达,为了当数据到达时能够正确完成IO操作,在BIO(阻塞IO)中,等待IO的线程必须被阻塞,以全天候地执行IO操作。为了解决这种IO方式低效的问题,引入了缓冲区的概念,当数据到达时,可以预先被写入缓冲区,再由缓冲区交给线程,因此线程无需阻塞地等待IO。
Channel(通道):

用于源节点与目标节点的连接,在java NIO 中负责缓冲区中的数据传输,Channel本身不存储数据,因此需要缓冲区配合进行传输,通道就相当于铁轨,缓冲区就相当于火车。
引用 Java NIO 中权威的说法:通道是 I/O 传输发生时通过的入口,而缓冲区是这些数 据传输的来源或目标。对于离开缓冲区的传输,您想传递出去的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据放置在您所提供的缓冲区中。

例如 有一个服务器通道 ServerSocketChannel serverChannel,一个客户端通道 SocketChannel clientChannel;服务器缓冲区:serverBuffer,客户端缓冲区:clientBuffer。

当服务器想向客户端发送数据时,需要调用:clientChannel.write(serverBuffer)。当客户端要读时,调用 clientChannel.read(clientBuffer)

当客户端想向服务器发送数据时,需要调用:serverChannel.write(clientBuffer)。当服务器要读时,调用 serverChannel.read(serverBuffer)

Selector(选择器):
通道和缓冲区的机制,使得线程无需阻塞地等待IO事件的就绪,但是总是要有人来监管这些IO事件。这个工作就交给了selector来完成,这就是所谓的同步。Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件。

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

OP_ACCEPT:接收

OP_CONNECT :连接

OP_READ :读

OP_WRITE:写

二、Java NIO 常用API

1.Buffer

package com.jxhtyw;

import java.nio.ByteBuffer;

import org.junit.Test;

/** 
 *缓冲区(buffer):在java NIO中负责数据的存取,缓冲区就是数组,用于存储不同类型的缓冲区
 *缓冲区类型:
 *ByteBuffer
 *CharBuffer
 *IntBuffer
 *ShortBuffer
 *LongBuffer
 *DoubleBuffer
 * 
 * 核心方法:put()和get()
 * 
 * 核心属性:
 * capacity:容量,表示缓冲区中最大数据的存储容量
 * limit:界限,表示缓冲区中可以操作数据的大小(limit后数据不能进行读写)
 * position:位置,表示缓冲区中正在操作数据的位置
 * 
 * mark():标记
 * reset():恢复到mark的位置
 *
 * @author dxx 
 * @version 2017-11-02 下午7:46:38 
 */
public class NioBuffer {
    String str="helloworld";

    @Test
    public void run(){
        //1.分配一个指定大小的缓冲区
        ByteBuffer bb=ByteBuffer.allocate(1024);
        System.out.println(bb.capacity());
        System.out.println(bb.limit());
        System.out.println(bb.position());

        //2.存入数据
        bb.put(str.getBytes());
        System.out.println(bb.capacity());
        System.out.println(bb.limit());
        System.out.println(bb.position());//位置变为10

        //3.切换读取数据模式
        bb.flip();
        System.out.println(bb.capacity());//缓冲区大小仍然为1024
        System.out.println(bb.limit());//可读取数量为10个的字节
        System.out.println(bb.position());//位置切换到0了,可以从0开始读取

        //4.读取数据
        byte[] by=new byte[bb.limit()];
        bb.get(by);//获取到缓冲区可读取的所有数据(也就是10),存放在by数组中
        //System.out.println(by);
        System.out.println(new String(by,0,by.length));
        System.out.println(bb.capacity());
        System.out.println(bb.limit());
        System.out.println(bb.position());
        try {
            bb.get();//再读的话就越界了
        } catch (Exception e) {
            e.printStackTrace();
        }

        //5.rewind() :可重复读数据
        bb.rewind();
        System.out.println(bb.capacity());
        System.out.println(bb.limit());
        System.out.println(bb.position());//位置变为0了,说明又可以读了

        //6.clear():清空缓冲区,但是缓冲区的数据依然存在,但是处于“被遗忘状态”
        bb.clear();
        System.out.println(bb.capacity());
        System.out.println(bb.limit());//指针全部回到最原始状态,不知道有多少数据
        System.out.println(bb.position());
        System.out.println((char)bb.get());
    }

    @Test
    public void run2(){
        ByteBuffer bb=ByteBuffer.allocate(1024);
        bb.put(str.getBytes());
        bb.flip();
        byte[] by=new byte[bb.limit()];
        bb.get(by,0,2);
        System.out.println(new String(by,0,2));
        System.out.println(bb.position());//到第二个字节了

        //标记
        bb.mark();

        bb.get(by,2,3);
        System.out.println(new String(by,2,3));
        System.out.println(bb.position());//到第二个字节了

        //重置
        bb.reset();
        System.out.println(bb.position());//位置又回到标记处
    }

    @Test
    public void run3(){
        //非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中
        ByteBuffer bb=ByteBuffer.allocate(1024);
        //直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率
        ByteBuffer bb2=ByteBuffer.allocateDirect(1024);
    }
}

2.Channel

package com.jxhtyw;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import org.junit.Test;

/** 
 *通道(Channel):用于源节点与目标节点的连接,在java NIO 中负责缓冲区中的数据传输,
 *Channel本身不存储数据,因此需要缓冲区配合进行传输,通道就相当于铁轨,缓冲区就相当于火车
 *
 *JDK 1.7中的NIO 针对各个通道提供了静态方法open()
 *
 *通道之间的数据传输
 *transferTo()
 *transferFrom()
 *
 *
 *分散与聚集
 *分散读取:将通道中的数据分散到多个缓冲区中去
 *聚集写入:将多个缓冲区的数据聚集到通道中去
 *
 * @author dxx
 * @version 2017-11-02 下午8:33:05 
 */
public class NioChannel {

    //4.分散与聚集
    @Test
    public void run4() throws IOException{
        //1.分散读取
        RandomAccessFile raf1=new RandomAccessFile("1.txt", "rw");
        //获取通道
        FileChannel channel1 = raf1.getChannel();
        //分配两个指定大小的缓冲区
        ByteBuffer buf1 = ByteBuffer.allocate(100);     
        ByteBuffer buf2 = ByteBuffer.allocate(1024);    
        //构建缓冲区数组
        ByteBuffer[] bufArr={buf1,buf2};
        //通道读取
        channel1.read(bufArr);
        //切换缓冲区为写模式
        for (ByteBuffer byteBuffer : bufArr) {
            byteBuffer.flip();
        }
        System.out.println(new String(bufArr[0].array(), 0, bufArr[0].limit()));
        System.out.println("--------------------------");
        System.out.println(new String(bufArr[1].array(), 0, bufArr[1].limit()));

        //2.聚集写入
        //聚集写入到2.txt中
        RandomAccessFile raf2=new RandomAccessFile("2.txt", "rw");
        FileChannel channel2 = raf2.getChannel();
        //将缓冲区数组写入通道中
        channel2.write(bufArr);
    }


    //3.通道之间的数据传输,直接缓冲区
    @Test
    public void run3() throws IOException{
        FileChannel inChannel = FileChannel.open(Paths.get("e:/01.avi"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("e:/02.avi"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
//      inChannel.transferTo(0, inChannel.size(), outChannel);
        outChannel.transferFrom(inChannel, 0, inChannel.size());
    }

    //2.利用直接缓冲区完成文件的复制
    @Test
    public void run2() throws IOException{
        long start=System.currentTimeMillis();
        //获取通道
        //读模式
        FileChannel inChannel = FileChannel.open(Paths.get("e:/01.avi"), StandardOpenOption.READ);
        //读写模式
        FileChannel outChannel = FileChannel.open(Paths.get("e:/02.avi"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);

        //内存映射文件
        MappedByteBuffer inBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outBuf = outChannel.map(MapMode.READ_WRITE,0,inChannel.size());

        //对缓冲区的数据进行读写操作
        byte[] by=new byte[inBuf.limit()];
        inBuf.get(by);
        outBuf.put(by);
        inChannel.close();
        outChannel.close();
        long end=System.currentTimeMillis();
        System.out.println("耗费时间:"+(end-start));
    }

    //1.利用通道完成文件的复制(非直接缓冲区)
    @Test
    public void run() throws IOException{
        long start=System.currentTimeMillis();
        FileInputStream fis=null;
        FileOutputStream fos=null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            fis=new FileInputStream("e:/01.avi");
            fos=new FileOutputStream("e:/02.avi");
            inChannel = fis.getChannel();
            outChannel = fos.getChannel();
            //分配指定大小的缓冲区
            ByteBuffer bb = ByteBuffer.allocate(1024);
            //将通道中的数据存入缓冲区,这个时候的缓冲区是写模式
            while(inChannel.read(bb)!=-1){
                //将缓冲区切换为读模式
                bb.flip();
                outChannel.write(bb);
                bb.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            fis.close();
            fos.close();
            inChannel.close();
            outChannel.close();
        }
        long end=System.currentTimeMillis();
        System.out.println("耗费时间:"+(end-start));
    }
}

3.阻塞NIO

package com.jxhtyw;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import org.junit.Test;

/** 
 * 阻塞NIO
 *
 * @author dxx
 * @version 2017-11-02 下午9:36:08 
 */
public class NioBlock2 {
    //1.网络通信客户端
    @Test
    public void client() throws IOException{
        //获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
        FileChannel fChannel = FileChannel.open(Paths.get("1.png"), StandardOpenOption.READ);
        //分配指定大小的缓冲区
        ByteBuffer bb = ByteBuffer.allocate(1024);
        //读取本地文件,并发送到客户端
        while(fChannel.read(bb)!=-1){
            //将缓冲区转换为写模式
            bb.flip();
            socketChannel.write(bb);
            bb.clear();
        }
        //当接收不到缓冲区的数据时,强制关闭通道(阻塞式体现在这里)
        socketChannel.shutdownOutput();
        //接受服务器端的反馈
        int len=0;
        while((len=socketChannel.read(bb))!=-1){
            bb.flip();
            System.out.println(new String(bb.array(),0,len));
            bb.clear();
        }
        //关闭通道
        socketChannel.close();
        fChannel.close();
    }

    //2.服务端
    @Test
    public void server() throws IOException{
        //获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        FileChannel fChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        //绑定连接
        ssChannel.bind(new InetSocketAddress(8888));
        //获取客户端连接的通道
        SocketChannel accept = ssChannel.accept();
        //分配指定大小的缓冲区
        ByteBuffer bb = ByteBuffer.allocate(1024);
        //读取本地文件,并发送到客户端
        while(accept.read(bb)!=-1){
            //将缓冲区转换为写模式
            bb.flip();
            fChannel.write(bb);
            bb.clear();
        }
        //发送反馈给客户端
        bb.put("接收数据成功".getBytes());
        bb.flip();
        accept.write(bb);

        //关闭通道
        ssChannel.close();
        fChannel.close();
    }
}

运行结果:
**这里写图片描述**
3.非阻塞NIO——简单聊天室的建立

package com.jxhtyw;

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.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Random;
import java.util.Scanner;
import org.junit.Test;

/** 
 * 非阻塞NIO
 * 
 *一、使用NIO完成网络通信
 *1.通道(Channel):负责连接
 *2.缓冲区(Buffer):负责数据的存取
 *3.选择器(Selector):用于监控IO情况
 *
 * @author dxx 
 * @version 2017-11-05 上午10:42:18 
 */
public class NioNotBlock {
    //客户端
    @Test
    public void client() throws IOException{
        //1.获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
        //2.切换为非阻塞模式
        sChannel.configureBlocking(false);
        //3.分配指定大小的缓冲区
        ByteBuffer bb = ByteBuffer.allocate(1024);
        //4.发送数据到客户端(实时时间+键盘录入)
        Scanner sc=new Scanner(System.in);
        String[] nameArr={"Kobe","James","Iverson","Duncan","Curry","Harden"};
        Random rand = new Random();
        int i = rand.nextInt(5);
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date = sdf.format(new Date());
        while(sc.hasNext()){
            String str = sc.next();
            bb.put((nameArr[i]+"  "+date+"\n"+str).getBytes());
            bb.flip();
            sChannel.write(bb);
            bb.clear();
        }
        //5.关闭通道
        sChannel.close();
    }

    //服务端
    @Test
    public void server() throws IOException{
        //1.获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        //2.切换为非阻塞模式
        ssChannel.configureBlocking(false);
        //3.绑定连接
        ssChannel.bind(new InetSocketAddress(8888));
        //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 clientCh = ssChannel.accept();
                    //11.切换非阻塞模式
                    clientCh.configureBlocking(false);
                    //12.将该通道注册到选择器
                    clientCh.register(selector,SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    //13.获取当前选择器上“读就绪”的通道
                    SocketChannel rChannel=(SocketChannel) sk.channel();
                    //14.读取数据
                    ByteBuffer bb = ByteBuffer.allocate(1024);
                    int len=0;
                    while((len=rChannel.read(bb))>0){
                        bb.flip();
                        System.out.println(new String(bb.array(),0,len));
                        bb.clear();
                    }
                }
                //15.取消选择键 SelectionKey
                it.remove();
            }
        }
    }
}

聊天室效果:
这里写图片描述

猜你喜欢

转载自blog.csdn.net/dxx123446/article/details/78447764