Java NIO

概述

一、NIO简介

  • NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  • NIO基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
  • Selector用于监听多个通道的事件(比如:连接打开,数据到达),单个线程可以监听多个数据通道

二、NIO VS 传统IO

  • IO是面向流的,NIO是面向缓冲区的:NIO中的缓冲区的存在使我们可以在其中对数据进行操作,增加了灵活性
  • IO的各种流是阻塞的,NIO是非阻塞模式:Selector用于监听多个通道的事件,从而实现一个线程管理多个输入和输出通道

三、Channel & Buffer & Selector

1、channel
  • 传统IO中的stream是单向的,而 NIO 中的channel是双向的,既可读又可写;
  • NIO中的Channel的主要实现有:这几个实现分别对应 IO、UDP、TCP
    • FileChannel
    • DatagramChannel
    • SocketChannel
    • ServerSocketChannel
2、buffer
  • NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应几种基本的数据类型
  • NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等
3、selector
  • 要使用Selector, 得向Selector注册Channel,然后调用select()方法,这个方法会一直阻塞到某个注册的通道有事件就绪
  • 事件就绪后这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等

 

FileChannel

一、NIO的实例

public static void method1(){
        RandomAccessFile aFile = null;
        try{
            aFile = new RandomAccessFile("d:\\123.txt","rw");
            FileChannel fileChannel = aFile.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(1024);
            int bytesRead = fileChannel.read(buf);
            System.out.println(bytesRead);
            while(bytesRead != -1)
            {
                buf.flip();
                while(buf.hasRemaining())
                {
                    System.out.print((char)buf.get());
                }
                buf.compact();
                bytesRead = fileChannel.read(buf);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(aFile != null){
                    aFile.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

 

二、buffer 使用

1、基本原理

  • buffer 的本质是一个容器,是一个连续的数组;
  • NIO 数据传递的基本过程如图:

  • 向Buffer中写数据:
    • 从Channel写到Buffer (fileChannel.read(buf))
    • 通过Buffer的put()方法 (buf.put(…))
  • 从Buffer中读数据:
    • 从Buffer读取到Channel (channel.write(buf))
    • 使用get()方法从Buffer中读取数据 (buf.get())

2、详细使用步骤描述

  • buffer的几个参数
    • capacity:指定了可以存储在缓冲区中的最大数据容量
    • position:指的是下一个要被读写的元素的数组下标索引,该值会随get()和put()的调用自动更新
    • limit:指的是缓冲区中第一个不能读写的元素的数组下标索引,也可以认为是缓冲区中实际元素的数量
    • mark:一个备忘位置,调用mark()方法的话,mark值将存储当前position的值,等下次调用reset()方法时,会设定position的值为之前的标记值
    • 四个参数之间的大小关系为:0 <= mark <= position <= limit <= capacity
  • buffer的实际使用过程
    • 创建一个容量大小为10的字符缓冲区:
      ByteBuffer bf = ByteBuffer.allocate(10);

    • 往缓冲区中put()五个字节:
      bf.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'0');

    • 调用flip()方法,切换为读就绪状态:bf.flip()

    • 读取两个元素:
      System.out.println("" + (char) bf.get() + (char) bf.get());

    • 标记此时的position位置:bf.mark()

    • 读取两个元素后,恢复到之前mark的位置处:
      System.out.println("" + (char) bf.get() + (char) bf.get());
      bf.reset();

 

    • 调用compact()方法,释放已读数据的空间,准备重新填充缓存区:bf.compact() ; 这里要是调用 clear 方法的话,position将被设回0,limit设置成capacity

 

SocketChannel

一、概述

  • NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道
    channel.configureBlocking(false)

二、TCP示例

  • client 使用 NIO
      public static void client(){
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            SocketChannel socketChannel = null;
            try
            {
                socketChannel = SocketChannel.open();
                socketChannel.configureBlocking(false);
                socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
                if(socketChannel.finishConnect())
                {
                    int i=0;
                    while(true)
                    {
                        TimeUnit.SECONDS.sleep(1);
                        String info = "I'm "+i+++"-th information from client";
                        buffer.clear();
                        buffer.put(info.getBytes());
                        buffer.flip();
                        while(buffer.hasRemaining()){
                            System.out.println(buffer);
                            socketChannel.write(buffer);
                        }
                    }
                }
            }
            catch (IOException | InterruptedException e)
            {
                e.printStackTrace();
            }
            finally{
                try{
                    if(socketChannel!=null){
                        socketChannel.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }
  • server 使用 BIO
        public static void server(){
            ServerSocket serverSocket = null;
            InputStream in = null;
            try
            {
                serverSocket = new ServerSocket(8080);
                int recvMsgSize = 0;
                byte[] recvBuf = new byte[1024];
                while(true){
                    Socket clntSocket = serverSocket.accept();
                    SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
                    System.out.println("Handling client at "+clientAddress);
                    in = clntSocket.getInputStream();
                    while((recvMsgSize=in.read(recvBuf))!=-1){
                        byte[] temp = new byte[recvMsgSize];
                        System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
                        System.out.println(new String(temp));
                    }
                }
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            finally{
                try{
                    if(serverSocket!=null){
                        serverSocket.close();
                    }
                    if(in!=null){
                        in.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }

     

  • 示例说明:channel 的 write()方法无法保证能写多少字节到SocketChannel,所以在死循环里重复调用write()直到Buffer没有要写的字节为止

 

TCP服务端NIO写法

一、TCP服务端 NIO 代码

public class ServerConnect
{
    private static final int BUF_SIZE=1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;
    public static void main(String[] args)
    {
        selector();
    }
    public static void handleAccept(SelectionKey key) throws IOException{
        ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
        SocketChannel sc = ssChannel.accept();
        sc.configureBlocking(false);
        sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));
    }
    public static void handleRead(SelectionKey key) throws IOException{
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buf = (ByteBuffer)key.attachment();
        long bytesRead = sc.read(buf);
        while(bytesRead>0){
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if(bytesRead == -1){
            sc.close();
        }
    }
    public static void handleWrite(SelectionKey key) throws IOException{
        ByteBuffer buf = (ByteBuffer)key.attachment();
        buf.flip();
        SocketChannel sc = (SocketChannel) key.channel();
        while(buf.hasRemaining()){
            sc.write(buf);
        }
        buf.compact();
    }
    public static void selector() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try{
            selector = Selector.open();
            ssc= ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress(PORT));
            ssc.configureBlocking(false);
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            while(true){
                if(selector.select(TIMEOUT) == 0){
                    System.out.println("==");
                    continue;
                }
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while(iter.hasNext()){
                    SelectionKey key = iter.next();
                    if(key.isAcceptable()){
                        handleAccept(key);
                    }
                    if(key.isReadable()){
                        handleRead(key);
                    }
                    if(key.isWritable() && key.isValid()){
                        handleWrite(key);
                    }
                    if(key.isConnectable()){
                        System.out.println("isConnectable = true");
                    }
                    iter.remove();
                }
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(selector!=null){
                    selector.close();
                }
                if(ssc!=null){
                    ssc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

二、示例代码说明

1、ServerSocketChannel

  • ServerSocketChannel 的打开、关闭和监听(open、close、accept)
  • 在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null,因此需要进行非空检查

2、selector

  • 管理多信道的机制(主动通知)
    • Selector 在内部可以同时管理多个channel,当一个信道有I/O操作的时候,他会通知Selector,Selector就知道这个信道有I/O操作以及具体是何种操作;
    • Selector 的 select 方法返回的结果为 0,代表在你调用的时刻没有任何客户端需要I/O操作,不为 0 代表有客户端准备就绪了,返回的就是可操作的信道数量;
  • Channel和Selector配合使用
    • 二者配合使用必须将Channel注册到Selector上,通过SelectableChannel.register()方法来实现,注册 channel 的时候可以指定监听事件的类型;
    • 监听事件的类型无外乎就四种(连接、接受、读、写)这四种事件用SelectionKey的四个常量来表示:
      1. SelectionKey.OP_CONNECT
      2. SelectionKey.OP_ACCEPT
      3. SelectionKey.OP_READ
      4. SelectionKey.OP_WRITE
    • selecet 方法返回非 0 ,通过调用如下的方法可以返回一个 SelectionKey 的集合,每一个SK对象都代表着注册到 selector 上的信道,遍历此集合获得SK对象后即可进行后续处理

      Set selectedKeys = selector.selectedKeys();
    • Selector不会自己从已选择键集中移除SelectionKey实例,所以每次迭代末尾都要keyIterator.remove()

 

3、SelectionKey

  • ServerSocketChannel 的 register()方法会返回一个SelectionKey对象,这个对象包含如下属性:
    • interest集合:监听事件的集合,SK对象的 interestOps 方法可以得到这个集合
    • ready集合:是通道已经准备就绪的操作的集合,readyOps方法可以得到这个集合
      selectionKey.isAcceptable();
      selectionKey.isConnectable();
      selectionKey.isReadable();
      selectionKey.isWritable();
    • Channel 和 Selector:通过 channel 和 selector 方法即可获得
    • 附加的对象(可选):有两种添加附加信息的方法
      selectionKey.attach(theObject);
      Object attachedObj = selectionKey.attachment();
      -----------------------------------------------
      SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

 

内存映射文件

一、byteBuffer的两种模式

  • 间接模式:就是HeapByteBuffer,即操作堆内存 (byte[]),但是如果文件很大的话可能会出现内存溢出的情况;
  • 直接模式:MappedByteBuffer 将文件直接映射到虚拟内存,这种模式读写性能很高;

二、示例代码

 public static void method4(){
        RandomAccessFile aFile = null;
        FileChannel fc = null;
        try{
            aFile = new RandomAccessFile("src/1.ppt","rw");
            fc = aFile.getChannel();
            long timeBegin = System.currentTimeMillis();
            ByteBuffer buff = ByteBuffer.allocate((int) aFile.length());
            buff.clear();
            fc.read(buff);
            //System.out.println((char)buff.get((int)(aFile.length()/2-1)));
            //System.out.println((char)buff.get((int)(aFile.length()/2)));
            //System.out.println((char)buff.get((int)(aFile.length()/2)+1));
            long timeEnd = System.currentTimeMillis();
            System.out.println("Read time: "+(timeEnd-timeBegin)+"ms");
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(aFile!=null){
                    aFile.close();
                }
                if(fc!=null){
                    fc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
    public static void method3(){
        RandomAccessFile aFile = null;
        FileChannel fc = null;
        try{
            aFile = new RandomAccessFile("src/1.ppt","rw");
            fc = aFile.getChannel();
            long timeBegin = System.currentTimeMillis();
            MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, aFile.length());
            // System.out.println((char)mbb.get((int)(aFile.length()/2-1)));
            // System.out.println((char)mbb.get((int)(aFile.length()/2)));
            //System.out.println((char)mbb.get((int)(aFile.length()/2)+1));
            long timeEnd = System.currentTimeMillis();
            System.out.println("Read time: "+(timeEnd-timeBegin)+"ms");
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(aFile!=null){
                    aFile.close();
                }
                if(fc!=null){
                    fc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

1、示例说明

  • 示例代码通过 FileChannel 的 map 方法实现,这个方法有三个参数(模式,position,size)
  • map方法的模式参数有三个选项:READ_ONLY(只读)、READ_WRITE(读/写:缓冲区修改会传播到文件)、PRIVATE(专用:修改不会传播到文件,会建立一个专用副本)

2、MappedByteBuffer的特有方法

  • force():缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件;
  • load():将缓冲区的内容载入内存,并返回该缓冲区的引用;
  • isLoaded():如果缓冲区的内容在物理内存中,则返回真,否则返回假;

 

知识点补充

一、scatter & gatter

1、简介
  • 分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中
  • 聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel
2、示例代码
  • 这是一个 gather 的示例代码,实际上是通过 buffer 数组来实现的聚集,channel 调用 write 方法直接使用 buffer 数组作为参数
    public class ScattingAndGather
    {
        public static void main(String args[]){
            gather();
        }
        public static void gather()
        {
            ByteBuffer header = ByteBuffer.allocate(10);
            ByteBuffer body = ByteBuffer.allocate(10);
            byte [] b1 = {'0', '1'};
            byte [] b2 = {'2', '3'};
            header.put(b1);
            body.put(b2);
            ByteBuffer [] buffs = {header, body};
            try
            {
                FileOutputStream os = new FileOutputStream("src/scattingAndGather.txt");
                FileChannel channel = os.getChannel();
                channel.write(buffs);
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

二、transferFrom & transferTo

1、transferFrom 
  • FileChannel的transferFrom()方法可以将数据从其他通道传输到FileChannel中;
  • SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中,只会传输此刻准备好的数据(可能不足count字节);
      public static void method1(){
            RandomAccessFile fromFile = null;
            RandomAccessFile toFile = null;
            try
            {
                fromFile = new RandomAccessFile("src/fromFile.xml","rw");
                FileChannel fromChannel = fromFile.getChannel();
                toFile = new RandomAccessFile("src/toFile.txt","rw");
                FileChannel toChannel = toFile.getChannel();
                long position = 0;
                long count = fromChannel.size();
                System.out.println(count);
                toChannel.transferFrom(fromChannel, position, count);
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            finally{
                try{
                    if(fromFile != null){
                        fromFile.close();
                    }
                    if(toFile != null){
                        toFile.close();
                    }
                }
                catch(IOException e){
                    e.printStackTrace();
                }
            }
        }
2、transferTo
  • transferTo()方法将数据从FileChannel传输到其他的channel中
    public static void method2()
        {
            RandomAccessFile fromFile = null;
            RandomAccessFile toFile = null;
            try
            {
                fromFile = new RandomAccessFile("src/fromFile.txt","rw");
                FileChannel fromChannel = fromFile.getChannel();
                toFile = new RandomAccessFile("src/toFile.txt","rw");
                FileChannel toChannel = toFile.getChannel();
                long position = 0;
                long count = fromChannel.size();
                System.out.println(count);
                fromChannel.transferTo(position, count,toChannel);
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            finally{
                try{
                    if(fromFile != null){
                        fromFile.close();
                    }
                    if(toFile != null){
                        toFile.close();
                    }
                }
                catch(IOException e){
                    e.printStackTrace();
                }
            }
        }

三、pipe

1、示例代码
  • Java NIO 管道是2个线程之间的单向数据连接,Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取
        public static void method1(){
            Pipe pipe = null;
            ExecutorService exec = Executors.newFixedThreadPool(2);
            try{
                pipe = Pipe.open();
                final Pipe pipeTemp = pipe;
                exec.submit(new Callable<Object>(){
                    @Override
                    public Object call() throws Exception
                    {
                        Pipe.SinkChannel sinkChannel = pipeTemp.sink();//向通道中写数据
                        while(true){
                            TimeUnit.SECONDS.sleep(1);
                            String newData = "Pipe Test At Time "+System.currentTimeMillis();
                            ByteBuffer buf = ByteBuffer.allocate(1024);
                            buf.clear();
                            buf.put(newData.getBytes());
                            buf.flip();
                            while(buf.hasRemaining()){
                                System.out.println(buf);
                                sinkChannel.write(buf);
                            }
                        }
                    }
                });
                exec.submit(new Callable<Object>(){
                    @Override
                    public Object call() throws Exception
                    {
                        Pipe.SourceChannel sourceChannel = pipeTemp.source();//向通道中读数据
                        while(true){
                            TimeUnit.SECONDS.sleep(1);
                            ByteBuffer buf = ByteBuffer.allocate(1024);
                            buf.clear();
                            int bytesRead = sourceChannel.read(buf);
                            System.out.println("bytesRead="+bytesRead);
                            while(bytesRead >0 ){
                                buf.flip();
                                byte b[] = new byte[bytesRead];
                                int i=0;
                                while(buf.hasRemaining()){
                                    b[i]=buf.get();
                                    System.out.printf("%X",b[i]);
                                    i++;
                                }
                                String s = new String(b);
                                System.out.println("=================||"+s);
                                bytesRead = sourceChannel.read(buf);
                            }
                        }
                    }
                });
            }catch(IOException e){
                e.printStackTrace();
            }finally{
                exec.shutdown();
            }
        }

四、datagramChannel

1、示例代码
  • Java NIO中的DatagramChannel是一个能收发UDP包的通道
  • 因为UDP是无连接,所以不能像其它通道那样读取和写入,它发送和接收的是数据包
      public static void  reveive(){
            DatagramChannel channel = null;
            try{
                channel = DatagramChannel.open();
                channel.socket().bind(new InetSocketAddress(8888));
                ByteBuffer buf = ByteBuffer.allocate(1024);
                buf.clear();
                channel.receive(buf);
                buf.flip();
                while(buf.hasRemaining()){
                    System.out.print((char)buf.get());
                }
                System.out.println();
            }catch(IOException e){
                e.printStackTrace();
            }finally{
                try{
                    if(channel!=null){
                        channel.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }
        public static void send(){
            DatagramChannel channel = null;
            try{
                channel = DatagramChannel.open();
                String info = "I'm the Sender!";
                ByteBuffer buf = ByteBuffer.allocate(1024);
                buf.clear();
                buf.put(info.getBytes());
                buf.flip();
                int bytesSent = channel.send(buf, new InetSocketAddress("10.10.195.115",8888));
                System.out.println(bytesSent);
            }catch(IOException e){
                e.printStackTrace();
            }finally{
                try{
                    if(channel!=null){
                        channel.close();
                    }
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }

     

*******************参考**********************

 

转载于:https://www.cnblogs.com/stanwuc/p/10869466.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值