NIO基础知识

典型回答

Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。

Java NIO: Channels and Buffers(通道和缓冲区)

标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

Java NIO: Non-blocking IO(非阻塞IO)

Java NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入通道也类似。

Java NIO: Selectors(选择器)

Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。

什么是NIO?

  • IO 阻塞类型 block-IO

  • NIO 非阻塞类型 No-Block-IO 通过Channel+Buffer实现IO操作

  • 标准的IO基于字节和字符流进行操作,而NIO是基于通道和缓冲区进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。

  • JavaNIO可以让你非阻塞的使用IO。例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它,从缓冲区写入通道也类似。

  • JavaNIO引入了选择器的概念。选择器用于监听多个通道的事件(比如:连接打开、数据到达)。因此,单个线程可以监听多个数据通道。

  • 传统IO是单向

  • NIO面向缓冲流,是双向

Buffer的数据存取

  • 一个用于特定基本数据类型的容器,Buffer实质上是一个数组,所有缓冲区都是抽象类Buffer的子类

  • Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入到缓冲区,从缓冲区写入通道中的

  • 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

6

缓冲区理解

  • 在NIO中,缓冲区又分为非直接缓冲区和直接缓冲区。

  • 非直接缓冲区建立在JVM的内存中。

  • 直接缓冲区建立在物理内存中,可以提高效率。

IO/NIO的底层原理

1、BIO

  • Blocking IO ,阻塞IO,同步阻塞IO

  • 阻塞的理解:①accept 阻塞的 ②IO进行操作的时候,要么读,要么写,单线程环境下而言

  • BIO中不可能只有一个客户端进行连接

  • BIO存在的问题

    • 每一个Socket连接服务器端都会立即开启一个线程处理(连接不开启线程,执行IO操作再开启线程)
    • 每个IO操作完成之后,线程就会销毁(好不容易开启的线程不能轻易销毁)

2、NIO

  • New IO 同步非阻塞

  • 非阻塞:客户端连接服务器的时候-------服务器端 连接请求(记录下来,问问你要不要进行IO操作)

  • Selector(Accept、Readable、Writable)

  • Buffer(我是中间的桥梁,所有的IO操作都经过,这样就可以进行读写同时进行)

NIO重点补充

  • IO(BIO)和NIO区别:本质就是阻塞和非阻塞的区别

  • 阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。

  • 非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。

  • IO为同步阻塞形式,NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞通讯模型NIO2.0(AIO)。

  • BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

  • AIO(NIO2.0):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。


1、NIO与IO简介区别

  • NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行读写操作。

  • 传统IO面向流(输入流,输出流),阻塞IO

  • NIO面向缓冲区,非阻塞IO,选择器

2、通道和缓冲区

  • Channel负责传输,Buffer负责存储

  • 应用程序不能直接对Channel进行读写操作,必须通过Buffer来进行。即Channel是通过Buffer来读写数据的。通道可以异步地读写。

  • 通道表示打开到IO设备(文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。

  • 0 <= mark <= position <= limit <= capacity

private int mark = -1;      // 记住目前的position,调用reset()方法可以将position变回之前记住的位置
private int position = 0;  //  用于指明下一个可以被读出的或者写入的缓冲区位置索引。
private int limit;              //  第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可被读,也                                            不可以被写。
private int capacity;       //  缓冲区的容量表示该Buffer的最大数据容量,缓冲区的容量不能为负值,创建后不能改变。

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
 }
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

  • 通道用于源节点与目标节点的连接,负责缓冲区中数据的传输,通道本身不存储数据,因此需要配合缓冲区进行传输

  • java.nio.channels.Channel 接口

    • FileChannel:从文件读取数据
    • SocketChannel:读写TCP网络协议数据
    • ServerSocketChannel:监听TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
    • DatagramChannel:读写UDP网络协议数据

FileChannel

FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。

打开FileChannel

  • 通过使用一个InputStream、OutputStream或RandomAccessFile等IO流来获取一个FileChannel实例。
FileChannel inChannel = new FileInputStream("filename").getChannel();

从FileChannel读取数据

ByteBuffer buffer = ByteBuffer.allocate(capacity);
int bytesRead = inChannel.read(buffer); // read方法返回-1则到达文件末尾

写入数据到FileChannel

  • 获取一个通道:FileChannel outChannel = new FileOutputStream("filename").getChannel();
  • 创建缓冲区,将数据放入缓冲区
ByteBuffer buffer = ByteBuffer.allocate( capacity );
for (int i = 0; i < message.length; ++i) {
    buffer.put( message[i] );
}
buffer.flip();

把缓冲区数据写入通道中:outChannel.write(buffer);
Channel与Buffer的例子

public class ReadFile {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("ReadFile.java");
        FileChannel fileChannel = fileInputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(256);

        while (fileChannel.read(byteBuffer) != -1) {
            byteBuffer.flip();
            Charset charset = Charset.forName("UTF-8");
            CharsetDecoder decoder = charset.newDecoder();
            CharBuffer charBuffer = decoder.decode(byteBuffer);
            System.out.println(charBuffer);
            byteBuffer.clear();
        }

        fileChannel.close();
        fileInputStream.close();
    }
}

SocketChannel

打开SocketChannel

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

从SocketChannel读取数据

ByteBuffer buf = ByteBuffer.allocate(256);
int bytesRead = socketChannel.read(buf);

写入数据到SocketChannel

String newData = "New String to write to file...";

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    channel.write(buf);
}

非阻塞模式

  • 可以设置 SocketChannel 为非阻塞模式(non-blocking mode)。设置之后,就可以在异步模式下调用connect(),read() 和write()了。

connect()

  • 如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
while (!socketChannel.finishConnect()) {
    // wait, or do something else
}

write()

  • 非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。

read()

  • 非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。

ServerSocketChannel

 打开ServerSocketChannel

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
while (true) {
    // 监听连接
    SocketChannel socketChannel = serverSocketChannel.accept();
    //do something with socketChannel...
}

非阻塞模式

  • 在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的ull将是n
  • 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);
while (true) {
    // 监听连接
    SocketChannel socketChannel = serverSocketChannel.accept();
    if (socketChannel != null) {
        //do something with socketChannel...
    }
}

DatagramChannel

因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

打开DatagramChannel

DatagramChannel channel = DatagramChannel.open();
// 可以在UDP端口上接收数据包
channel.socket().bind(new InetSocketAddress(8000));

接收数据

ByteBuffer buf = ByteBuffer.allocate(256);
buf.clear();
// 通过receive()方法从DatagramChannel接收数据
// receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。
channel.receive(buf);

发送数据

String newData = "New String to write to file...";

ByteBuffer buf = ByteBuffer.allocate(256);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
// 通过send()方法从DatagramChannel发送数据
int bytesSent = channel.send(buf, new InetSocketAddress("127.0.0.1", 8000));
  • 这个例子发送一串字符到”127.0.0.1”服务器的UDP端口8000。 因为服务端并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证。

3、获取通道的三种方法

  • 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()

4、分散读取和聚集写入

  • 通道之间的数据传输

    • transferFrom()
    • transferTo()
  • 分散读取:指从Channel中读取的数据分散到多个Buffer中(按照缓冲区的顺序,从Channel中读取的数据依次将Buffer填满)

Java NIO: Scattering Read

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);
  1. buffer首先被插入到数组,然后再将数组作为channel.read() 的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
  2. Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。
  • 聚集写入:将多个缓冲区的数据聚集到通道中

Java NIO: Gathering Write

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);
  • buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。

5、字符集:Charset

  • 编码:字符串-->字节数组

  • 解码:字节数组-->字符串

6、NIO的非阻塞式网络通信

  • SelectionKey: 表示SelectionChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。

  • 当调用register(Selector sel, int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。

  • 可以监听的事件类型(可使用SelectionKey的四个常量表示):

    • 读:SelectionKey.OP_READ
    • 写:SelectionKey.OP_WRITE
    • 连接:SelectionKey.OP_CONNECT
    • 接收:SelectionKey.OP_ACCEPT
  • 若注册时不止监听一个事件,则可以使用位或操作符连接

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
package selector;

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.util.Iterator;
import java.util.Set;

public class WebServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
            ssc.configureBlocking(false);

            Selector selector = Selector.open();
            // 注册 channel,并且指定感兴趣的事件是 Accept
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            ByteBuffer readBuff = ByteBuffer.allocate(1024);
            ByteBuffer writeBuff = ByteBuffer.allocate(128);
            writeBuff.put("received".getBytes());
            writeBuff.flip();

            while (true) {
                int nReady = selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();

                while (it.hasNext()) {
                    SelectionKey key = it.next();
              
                    if (key.isAcceptable()) {
                        // 创建新的连接,并且把连接注册到selector上,而且,
                        // 声明这个channel只对读操作感兴趣。
                        SocketChannel socketChannel = ssc.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    else if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        readBuff.clear();
                        socketChannel.read(readBuff);

                        readBuff.flip();
                        System.out.println("received : " + new String(readBuff.array()));
                        key.interestOps(SelectionKey.OP_WRITE);
                    }
                    else if (key.isWritable()) {
                        writeBuff.rewind();
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        socketChannel.write(writeBuff);
                        key.interestOps(SelectionKey.OP_READ);
                    }
                   it.remove();

                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7、管道(Pipe)

  • Java NIO 管道是2个线程之间的单向数据连接
  • Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

public void test()throws IOException{
    //1.获取管道
    Pipe pipe = Pipe.open();
   
    //2.将缓冲区中的数据写入通道
    ByteBuffer buf = ByteBuffer.allocate(1024);
    Pipe.SinkChannel sinkChannel = pipe.sink();
    buf.put("通向单向管道发送数据".getBytes());
    buf.flip();
    sinkChannel.write(buf);
    
    //3.读取缓冲区中的数据
    Pipe.SourceChannel sourceChannel = pipe.source();
    buf.flip();
    int len = sourceChannel.read(buf);
    System.out.println(new String(buf.array(),0,len));
    
    sourceChannel.close();
    sinkChannel.close();
}

完整NIO例子

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.util.Iterator;

/**
 * NIO服务端
 */
public class NIOServer {
    //通道管理器
    private Selector selector;

    /**
     * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
     * @param port 绑定的端口号
     * @exception IOException
     */
    public void initServer(int port) throws IOException {
        // 获得一个ServerSocket通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置通道为非阻塞
        serverChannel.configureBlocking(false);
        // 将该通道对应的ServerSocket绑定到port端口
        serverChannel.socket().bind(new InetSocketAddress(port));
        // 获得一个通道管理器
        this.selector = Selector.open();
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
        //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     * @exception IOException
     */
    @SuppressWarnings("unchecked")
    public void listen() throws IOException {
        System.out.println("服务端启动成功!");
        // 轮询访问selector
        while (true) {
            //当注册的事件到达时,方法返回;否则,该方法会一直阻塞
            selector.select();
            // 获得selector中选中的项的迭代器,选中的项为注册的事件
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                // 客户端请求连接事件
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key
                            .channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    //在这里可以给客户端发送信息哦
                    channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
                    //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);

                    // 获得了可读的事件
                } else if (key.isReadable()) {
                    read(key);
                }

            }

        }
    }

    /**
     * 处理读取客户端发来的信息 的事件
     * @param key
     * @exception IOException
     */
    public void read(SelectionKey key) throws IOException {
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(10);
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("服务端收到信息:" + msg);
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
        channel.write(outBuffer);// 将消息回送给客户端
    }

    /**
     * 启动服务端测试
     * @exception IOException
     */
    public static void main(String[] args) throws IOException {
        NIOServer server = new NIOServer();
        server.initServer(8000);
        server.listen();
    }

}
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.SocketChannel;
import java.util.Iterator;

/**
 * NIO客户端
 */
public class NIOClient {
    //通道管理器
    private Selector selector;

    /**
     * 获得一个Socket通道,并对该通道做一些初始化的工作
     * @param ip 连接的服务器的ip
     * @param port  连接的服务器的端口号
     * @throws IOException
     */
    public void initClient(String ip,int port) throws IOException {
        // 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        // 设置通道为非阻塞
        channel.configureBlocking(false);
        // 获得一个通道管理器
        this.selector = Selector.open();

        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
        //用channel.finishConnect();才能完成连接
        channel.connect(new InetSocketAddress(ip,port));
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    public void listen() throws IOException {
        // 轮询访问selector
        while (true) {
            selector.select();
            // 获得selector中选中的项的迭代器
            Iterator ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                // 连接事件发生
                if (key.isConnectable()) {
                    SocketChannel channel = (SocketChannel) key
                            .channel();
                    // 如果正在连接,则完成连接
                    if(channel.isConnectionPending()){
                        channel.finishConnect();

                    }
                    // 设置成非阻塞
                    channel.configureBlocking(false);

                    //在这里可以给服务端发送信息哦
                    channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
                    //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);

                    // 获得了可读的事件
                } else if (key.isReadable()) {
                    read(key);
                }

            }

        }
    }
    /**
     * 处理读取服务端发来的信息 的事件
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key) throws IOException{
        //和服务端的read方法一样
    }


    /**
     * 启动客户端测试
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        NIOClient client = new NIOClient();
        client.initClient("localhost",8000);
        client.listen();
    }

}

参考链接 http://ifeve.com/java-nio-all/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值