JavaNIO(3)、JavaNIO介绍

一、概述

  1. NIO主要有三大核心部分:
    • Channel(通道)
    • Buffer(缓冲区)
    • Selector(选择器)
  2. 传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作。数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
  3. Selector(选择器)用于监听多个通道的事件,比如:连接打开,数据到达。因此,单个线程可以监听多个数据通道。
  4. NIO与传统IO之间的最大区别是,传统IO是面向流的,NIO是面向缓冲区的。
  5. 传统IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,这些字节没有被缓存在任何地方。并且,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要在程序中自行维护一个缓冲区。
  6. NIO的缓冲区不需要程序自行维护,但是在获取数据时需要检查该缓冲区中是否包含你需要处理的数据。而且,需要确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
  7. 传统IO的各种流都是阻塞的。这意味着,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取或者数据完全写入。该线程在此期间不能再干任何事情。
  8. NIO是非阻塞模式的。在一个线程中从某通道发送请求读取数据,它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都读不到,该操作不会阻塞线程。写操作也是如此,不需要等待全部写入。所以一个单独的线程可以管理多个输入和输出通道。

二、Channel

  1. Channel为通道的意思
  2. Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,如InputStream,OutputStream。而Channel是双向的,即可以用来进行读,又可以用来写。
  3. NIO中的Channel的主要实现有:
    • FileChannel(文件)
    • DatagramChannel(UDP)
    • SocketChannel(TCP Client)
    • ServerSocketChannel(TCP Server)

三、Buffer

NIO中的关键Buffer实现有:

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

分别对应基本数据类型:byte,char,double,float,int,long,short

NIO中还有MappedByteBuffer,HeapByteBuffer,DirectByteBuffer等。

四、Selector

Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。

例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

五、例子

  1. FileChannel的使用

    首先,使用传统IO FileInputStream读取文件内容

    public class FileInputStreamTest {
        public static void main(String[] args) {
            InputStream in = null;
    
            try {
                in = new BufferedInputStream(new FileInputStream("D:\\data\\tss.txt"));
                byte[] buf = new byte[1024];
                int bytesRead = in.read(buf);
                while(bytesRead != -1){
                    for (int i = 0;i < bytesRead;i++)
                        System.out.print((char)buf[i]);
                    bytesRead = in.read(buf);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            finally {
                try{
                    if(in != null){
                        in.close();
                    }
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }
    

    使用RandomAccessFile 读取文件

    public class RandomAccessFileTest {
        public static void main(String[] args) {
            RandomAccessFile file = null;
    
            try {
                file = new RandomAccessFile("D:\\data\\tss.txt","rw");
                FileChannel fileChannel = file.getChannel();
                //分配空间
                ByteBuffer buf = ByteBuffer.allocate(1024);
                //写入数据
                int bytesRead = fileChannel.read(buf);
                System.out.println(bytesRead);
                while (bytesRead != -1){
                    //调用flip方法
                    buf.flip();
                    while (buf.hasRemaining()){
                        //读取数据
                        System.out.print((char)buf.get());
                    }
                    //调用compact()方法
                    buf.compact();
                    bytesRead = fileChannel.read(buf);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            finally {
                try{
                    if(file != null){
                        file.close();
                    }
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }
    
  2. Buffer的使用

    从上面的RandomAccessFileTest类中可以总结出使用Buffer一般遵循的几个步骤:

    • 分配空间(ByteBuffer buf = ByteBuffer.allocate(1024); 还可以使用allocateDirector)
    • 写入数据到Buffer(int bytesRead = fileChannel.read(buf);)
    • 调用flip()方法
    • 从Buffer中读取数据(System.out.print((char)buf.get());)
    • 调用clear()方法或者compact()方法

    向Buffer中写数据:

    • 从Channel写到Buffer(fileChannel.read(buf))
    • 通过Buffer的put()方法(buf.put())

    从Buffer中读取数据:

    • 从Buffer读取到Channel(channel.write(buf))
    • 使用get()方法从Buffer中读取数据(buf.get())

    可以把Buffer简单地理解为一组基本数据类型的元素列表。它通过几个变量来保存这个列表的当前位置状态:

    状态说明
    capacity缓冲区数组的总长度
    position下一个要操作的数据元素的位置
    limit缓冲区数组中不可操作的下一个元素的位置:limit<=capacity
    mark用于记录当前position的前一个位置或者默认为-1

    以ByteBuffer.allocate(11)为例创建一个11个byte的缓冲区,初始状态如下图:

在这里插入图片描述

position的位置为0,capacity和limit默认都是数组长度。

这时调用fileChannel.read(buf)向缓冲区中写入5个数据,状态如下:

在这里插入图片描述

position=5,其他不变

在读取缓冲区的数据之前,调用buf.flip()方法,此时的状态如下:

在这里插入图片描述

此时,limit=position=5,position=0。表示从position到limit之间的数据是可以操作的。

后面每调用一次buf.get(),position的值加1,直到position=5为止,这时调用  buf.hasRemaining()返回false。

数据读取完成后,调用buf.compact()重置为初始状态,mark=-1,position=0,limit=11,capacity=11。

clear()方法和compact()方法的区别如下:

clear()方法直接把position置为0,limit置为capacity。相当于Buffer被清空,不管其中的数据是否已经处理过。注意这里说的清空是指把缓冲区的状态置为初始化了,其中的数据还保存在Buffer中,只是已经不能再引用到

compact()方法将所有未读的数据拷贝到Buffer的起始处,然后将position设到最后一个未读元素的后面,limit同样设置为capacity。这样再往Buffer中写数据时,不会覆盖未读的数据。

比如上面的例子,在get()两次数据后position=2,limit=5。这时调用compact(),position=(limit-position)=3,limit=capacity=11。

由以上的描述可以看出:

  • 在写入操作前,调用compact()或clear()方法,position到limit的位置表示可以写入的位置
  • 在读取操作前,调用flip()方法,position到limit的位置表示可以读取的位置
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值