Java NIO 记录(一)

一、NIO简介

Java NIO (New IO , Non Blocking IO)。NIO是一个基于通道,面向缓冲区的,非阻塞的IO操作。

二、NIO的核心

2.1Buffer

2.1.1Buffer概念

Buffer缓冲区,负责NIO中的数据存储。数据可以从Channel中读到Buffer里,也可以从Buffer读到Channel里。

2.1.2Buffer的用法

使用Buffer读取数据的四个步骤
1)写入数据到buffer
2)调用flip()方法
3)从buffer中读取数据
4)调用clear()或者compact()方法

调用clear()或compact()方法能够清空缓冲区,以便让buffer可以再次被写入。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

@Test
public void bufferTest()throws Exception{
    RandomAccessFile inFile = new RandomAccessFile("D:\\1.html", "rw");
    FileChannel channel = inFile.getChannel();
    //为buffer分配大小
    ByteBuffer byteBuffer = ByteBuffer.allocate(48);
    //将数据从channel中写入buffer
    channel.read(byteBuffer);
    while (channel.read(byteBuffer)!=-1){
        //切换成读模式,准备从buffer中读取数据
        byteBuffer.flip();
        System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
        //清空当前缓冲区,为下次写入做好准备
        byteBuffer.clear();
    }
}

2.1.3 Buffer中几个重要的属性

  1. capacity : 表示缓冲区中最大存储数据的容量,一旦声明不能改变。一旦Buffer满了,则不能向其中写入数据,需要通过flip()向外写出数据,或者通过clear()方法清空缓冲区。

  2. limit : 缓冲区中可以进行操作的数据大小。(limit后数据不能进行读写)。
    读模式下,limit为写入时position的值。可以从filp()方法中的源码得知。

    public final Buffer flip() {
         limit = position;
         position = 0;
         mark = -1;
         return this;
    }
    
  3. position : 位置,表示缓冲区中正在操作数据的位置。当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。

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

  4. mark : 标记,表示记录当前 position 的位置。可以通过 reset() 恢复到 mark 的位置 0 <= mark <= position <= limit <= capacity

Java NIO 记录(一)_第1张图片
写模式与读模式下三者的关系

2.1.4 Buffer的种类

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

Buffer的使用,请自行查阅API.

2.2Channel(通道)

2.2.1Channel简介

  • Channel是双向的,即可以从Channel中读取数据,也可以向Channel中写入数据。
  • 通过Channel可以实现异步的读写。
  • Channel的使用要以Buffer为媒介,通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
Java NIO 记录(一)_第2张图片
Channel

2.2.2JAVA NIO中对Channel的实现

  • FileChannel:从文件读取数据的( 文件通道总是阻塞式的 )
  • DatagramChannel:读写UDP网络协议数据
  • SocketChannel:读写TCP网络协议数据
  • ServerSocketChannel:可以监听TCP连接

2.2.3获取Channel的方式

  • FileChannel : 通过 RandomAccessFile,FileInputStream,FileOutputStream
    getChannel()获得。
  • DatagramChannel、SocketChannel、ServerScoketChannel可以通过该类提供的静态方法open()获得。

2.2.4 Channel的使用

  • FileCahnnel使用在上图Buffer使用的例子中已经有所提及。
  • 分散读取和聚集写入 Scatter & Gather
    分散读取(Scatter):是指在进行读操作时将一个Channel里的内容分散读入不同的Buffer里。


    Java NIO 记录(一)_第3张图片
    Scatter

聚集写入(Gather):是指将分散在各个不同Buffer里的数据聚集写入到一个Channel中。


Java NIO 记录(一)_第4张图片
Gather

代码如下:

/*
Scattering Reads 数据从一个Channel读取到多个buffer中
Gatering Writes 数据从多个buffer写入到同一个channel中
 **/
@Test
public void scatterAndGather()  throws Exception{
    /*
    Scattering Reads 最好先知道文件的大小,从而为之分配合适数量的Buffer
     */
    RandomAccessFile inFile = new RandomAccessFile("D:\\1.html", "rw");
    RandomAccessFile outFile = new RandomAccessFile("D:\\2.html", "rw");
    FileChannel inChannel = inFile.getChannel();
    FileChannel outChanner = outFile.getChannel();
    ByteBuffer buf1 = ByteBuffer.allocate(1024);
    ByteBuffer buf2 = ByteBuffer.allocate(1024);
    ByteBuffer buf3 = ByteBuffer.allocate(1024);
    ByteBuffer[] buffArray = {buf1, buf2, buf3};
    while (inChannel.read(buffArray)!= -1){
        buffArray[0].flip();
        buffArray[1].flip();
        buffArray[2].flip();
        while (buffArray[0].hasRemaining() && buffArray[1].hasRemaining()&& buffArray[2].hasRemaining()){
            outChanner.write(buffArray);
        }
        buffArray[0].clear();
        buffArray[1].clear();
        buffArray[2].clear();
    }
    inChannel.read(buffArray);
}

2.2.5通道之间的数据传输

  • TransferFrom

     /*
      FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中
     */
    @Test
    public void transferFrom()throws Exception{
      RandomAccessFile inFile = new RandomAccessFile("E:\\桌面\\desk\\1.jpg","r");
      RandomAccessFile outFile = new RandomAccessFile("F:\\1.jpg", "rw");
      FileChannel inChanner = inFile.getChannel();
      long postion = 0;
      long count = inChanner.size();
      FileChannel outChanner = outFile.getChannel();
      outChanner.transferFrom(inChanner,postion,count);
      outChanner.force(true);
    }
    
  • TransferTo

    /*
    transferTo()方法将数据从FileChannel传输到其他的channel中
    */
    @Test
    public void transferTo()throws Exception{
      RandomAccessFile inFile = new RandomAccessFile("E:\\桌面\\desk\\1.jpg","r");
      RandomAccessFile outFile = new RandomAccessFile("F:\\2.jpg", "rw");
      FileChannel inChanner = inFile.getChannel();
      long postion = 0;
      long count = inChanner.size();
      FileChannel outChanner = outFile.getChannel();
      inChanner.transferTo(postion, count,outChanner);
      outChanner.force(true);
    }
    

2.3 Selector(选择器)

Java NIO中通过Selector(选择器)来监测一到多个NIO通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。


Java NIO 记录(一)_第5张图片
Selector

2.3.1Selector的使用

1.创建Selector 通过调用Selector.open()方法创建一个Selector.

2.向Selector注册通道。与Selector一起使用时,Channel必须处于非阻塞模式下(register方法中有做限定,阅读源码可知)。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以通过channel.configureBlocking(false)切换成非阻塞模式。

channel.configureBlocking(false);  
SelectionKey key = channel.register(selector,  Selectionkey.OP_READ);

register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:

 Selectionkey.OP_READ
 Selectionkey.OP_CONNECT
 Selectionkey.OP_ACCEPT
 Selectionkey.OP_WRITE

如果你想要监听多个事件,则可以通过“位或"操作将常量连接起来。如:

int listener =  Selectionkey.OP_READ | Selectionkey.OP_CONNECT

注:ServerSocketChannel中只可以注册SelectionKey.OP_ACCEPT事件

3.返回值SelectionKey包含的属性

  • interest集合
用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的   事件是否在interest 集合中。
int interestSet = selectionKey.interes();  

boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
  • ready集合
    ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。你可以通过以下四个方法来判断channel中什么事件或操作已经就绪。
selectionKey.isAcceptable();  
selectionKey.isConnectable();  
selectionKey.isReadable();  
selectionKey.isWritable();
  • Channel
通过SelectionKey来获得Channel
SelectableChannel channel  = selectionKey.channel();
  • Selector
通过SelectionKey来获得Selector
Selector selector = selectionKey.selector();
  • 附加的对象(可选)

三、Selector与SocketChannel的使用实例

3.1基于TCP协议ServerSocketChannel 服务端的创建

@Test
public void server() throws Exception {
    //获得一个ServerSocketChannel
    ServerSocketChannel channel = ServerSocketChannel.open();
    //将Channel注册成为非阻塞式   与selector一起使用时,channel必须处于非阻塞模式下。
    channel.configureBlocking(false);
    //为Channel绑定一个端口
    channel.bind(new InetSocketAddress(8023));
    //获得选择器
    Selector selector = Selector.open();
    //注册监听事件     因为是服务端,所以只能注册SelectionKey.OP_ACCEPT这个事件
    channel.register(selector, SelectionKey.OP_ACCEPT);
    while (selector.select()>0) {
        Set selectedKeys = selector.selectedKeys();
        Iterator keyIterator = selectedKeys.iterator();
        while (keyIterator.hasNext()) {
            SelectionKey key1 = keyIterator.next();
            if (key1.isAcceptable()) {
                //当一个连接 连接到服务端的时刻获得这个连接的通道
                SocketChannel channel1 = channel.accept();
                channel1.configureBlocking(false);
                //为客户端连接注册读就绪的监听事件
                channel1.register(selector, SelectionKey.OP_READ );
            } else if (key1.isReadable()) {
                //读事件准备就绪时    获得通道   读取客户端发来的消息
                SocketChannel acceptChannel = (SocketChannel)key1.channel();
                //创建缓冲区
                ByteBuffer readBuff = ByteBuffer.allocate(48);
                int len = 0;
                while ((len = acceptChannel.read(readBuff))!= -1){
                     System.out.println(new String(readBuff.array()));
                    //清空缓冲区,为下次数据写入做准备
                    readBuff.clear();
                }
                //关闭连接
                acceptChannel.close();
            }
            //由于selector不会自动移除已经准备就绪的key,所以需手动移除。key下次准备就绪时,会被再次放入SelectionKey中
            keyIterator.remove();
        }
    }
}

3.2基于SocketChannel 客户端的创建

@Test
public void client () throws Exception{
    //获得SocketChannel通道
    SocketChannel channel = SocketChannel.open();
    //将通道设置为非阻塞模式
    channel.configureBlocking(false);  //将通道设置为非阻塞式的时候要通过调用  channel.finishConnect();来完成连接,否则会报NotYetConnectedException异常
    channel.connect(new InetSocketAddress("127.0.0.1",8023));
    channel.finishConnect();
    //获得选择器
    Selector selector = Selector.open();
    //为通道注册连接就绪事件
    channel.register(selector, SelectionKey.OP_CONNECT);
    while (selector.select()>0){
        Set selectionKeys = selector.selectedKeys();
        Iterator iterator = selectionKeys.iterator();
        while (iterator.hasNext()){
            SelectionKey key = iterator.next();
            if(key.isConnectable()){
                //通过key来获得当前的通道
                SocketChannel connectiChannerl = (SocketChannel) key.channel();
                //判断是否是正在连接
                if (connectiChannerl.isConnectionPending()){
                    //如果正在连接,则手动完成连接
                    connectiChannerl.finishConnect();
                }
            }
            iterator.remove();
        }
    }
    //向服务端发送消息
    String str = "Hello ! How  are  you ?";
    ByteBuffer buffer= ByteBuffer.allocate(48);
    buffer.put(str.getBytes());
    //切换模式 调用此方法为一系列通道写入或相对获取 操作做好准备
    buffer.flip();
    channel.write(buffer);
    buffer.clear();
    channel.close();
}

你可能感兴趣的:(Java NIO 记录(一))