Java NIO 介绍和基本demo

IO操作主要可分为两阶段
1)把磁盘或者网络数据加载到内核的内存空间
2)把内核的内存空间数据复制到用户进程的内存空间中

阻塞、非阻塞的区别是在于第一阶段,即数据准备阶段。如果在数据准备时,主线程必须等待,就为阻塞;不需要一直等待可以执行其他操作,就是非阻塞。

同步、异步的区别在于第二阶段,如果是用户进程需要主动复制数据到用户内存,则为同步;如果由内核完成数据报复制之后主动返回数据则为异步

前面说到,java中I/O编程,大致可以分为三种,阻塞IO(BIO)、非阻塞IO(NIO)和异步IO(AIO)。

BIO

BIO就是同步阻塞IO。在传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 传统的socket通讯都是阻塞的,服务端接收到客户端请求直到复制完数据都是阻塞的

AIO

异步非阻塞IO,式真正的异步IO,将数据报复制等操作交给内核完成,用户进程可以处理其他事情而不需要干涉
底层过程同 NIO,区别在于,AIO 使用的命令是 epoll ,使用事件驱动的方式来代替轮询的方式,当监听的 I/O 准备好了,采用事件驱动(事件回调)的方式通知进程去获取数据

NIO

同步非阻塞的IO,底层是采用操作系统的IO多路复用模型,通过操作系统的select()/epoll()方法监听多个通道,一旦有一个channel数据报准备好,就通知应用程序去复制数据报。
非阻塞体现:一个 select 处理多个客户应用进程的 I/O,如果第一个 I/O 数据没有准备好,那么就去处理第二个客户端的 I/O,依此类推,客户端之间谁的数据先准备好就先处理谁的,不存在第二个要等第一个处理完才能开始处理的情况;

IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用之上。

提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。两种通道都支持阻塞和非阻塞两种模式,默认采用阻塞的实现方式

1.缓冲区 Buffer

    在 NIO 中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的。所有的缓冲区类型都继承于抽象类 Buffer,比方说:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer.都实现了相同的Buffer接口。
缓冲区本质上是一个数组,并提供了跟踪和记录缓冲区的状态变化的信息。

  其中最重要的属性是下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:
  position:当前操作数据所在的位置,也可以理解做游标,当调用 get()/put()方法读取或者写入缓冲区的时候,position会自    动更新,在新创建一个 Buffer 对象时,position 初始值为0

  limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
  读取缓冲区的数据时,如果limit > position 则认为在缓冲区还有数据可以读取

  capacity:指定可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小
  这三个属性之间满足:0 <=position <= limit <=capacity 的关系

使用缓冲区读取和写入数据通常遵循以下四个步骤:

  •  写数据到缓冲区;
  • 调用buffer.flip()方法;
  • 从缓冲区中读取数据;
  • 调用buffer.clear()

  在向buffer中写入数据时,position会记录下当前数据写入的位置,如果写入完成需要读取数据,那么就需要通过flip()方法将Buffer从写模式切换到读模式,其实就是锁定操作范围,让数据操作范围索引只能在position - limit 之间,源码如下。读取完所有的数据后,就需要清空缓冲区,使得buffer可以再次被写入

//完成两件事:
//1. 把limit 设置为当前的 position 值
//2. 把position 设置为0
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
 }

2.通道 Channel

    我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道区别于流的地方在于通道是双向的,可以用于读、写或者同时读写操作。 NIO中,任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。然后再操作缓冲区中的数据

    操作系统底层的通道一般都是全双工的,所以全双工的Channel比Stream能更好的映射底层操作系统的API。

    Channel主要分两大类:
    SelectableChannel:用户网络读写
    FileChannel:用于文件操作
    后面代码会涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

3.多路复用器 Selector

    Selector是Java  NIO 编程的基础。

    Selector--选择器,顾名思义就是提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,查看是否某个Channel读或者写事件就绪。我们可以通过SelectionKey获取就绪Channel的集合,然后根据其状态进行后续的操作。

    一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

服务端


/**
 * 2020/3/8
 * created by chenpp
 */
public class NioServer {

    private int port;

    private static Selector selector = null;

    /**
     * 指定端口号启动服务
     * */
    public boolean startServer(int port){
        try {
            this.port = port;
            selector = Selector.open();
            //打开监听通道
            ServerSocketChannel server = ServerSocketChannel.open();
            //绑定端口
            server.bind(new InetSocketAddress(this.port));
            //默认configureBlocking为true,如果为 true,此通道将被置于阻塞模式;如果为 false.则此通道将被置于非阻塞模式
            server.configureBlocking(false);
            //创建选择器
            selector = Selector.open();
            //监听客户端连接请求
            server.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务端启动成功,监听端口:" + port);
        }catch (Exception e){
            System.out.println("服务器启动失败");
            return  false;
        }
        return  true;
    }

    public void listen() throws IOException {
        while(true){
            //阻塞方法,轮询注册的channel,当至少一个channel就绪的时候才会继续往下执行
            int keyCount = selector.select();
            System.out.println("当前有:"+keyCount+"channel有事件就绪");
            //获取就绪的SelectionKey
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();
            SelectionKey key = null;
            //迭代就绪的key
            while(it.hasNext()){
                key = it.next();
                it.remove();
                //SelectionKey相当于是一个Channel的表示,标记当前channel处于什么状态
                // 按照channel的不同状态处理数据
                process(key);
            }
        }
    }

    private void process(SelectionKey key) throws IOException {
        //该channel已就绪,可接收消息
        if(key.isAcceptable()){
            System.out.println("accept事件就绪...");
            doAccept(key);
        }else if(key.isReadable()){
            System.out.println("read事件就绪...");
            doRead(key);
        }else if(key.isWritable()){
            System.out.println("write事件就绪...");
            doWrite(key);
        }
    }

    private void doWrite(SelectionKey key) throws IOException {
        //获取对应的socket
        SocketChannel socket = (SocketChannel)key.channel();
        //获取key上的附件
        String content = (String)key.attachment();
        socket.write(ByteBuffer.wrap(content.getBytes()));
        socket.close();
    }

    private void doRead(SelectionKey key) throws IOException {
        //获取对应的socket
        SocketChannel socket = (SocketChannel)key.channel();
        //设置一个读取数据的Buffer 大小为1024
        ByteBuffer buff = ByteBuffer.allocate(1024);
        StringBuilder content = new StringBuilder();

        while(socket.read(buff) > 0) {
            buff.flip();
            content.append(new String(buff.array(),"utf-8"));
        }
        //注册selector,并设置为可写模式
        key = socket.register(selector,SelectionKey.OP_WRITE);
        //在key上携带一个附件(附近就是之后要写的内容)
        key.attach("服务端已收到:"+content);
        System.out.println("读取内容:" + content);
    }

    private void doAccept(SelectionKey key) throws IOException {
        //获取对应的channel
        ServerSocketChannel server = (ServerSocketChannel)key.channel();
        //从channel中获取socket信息
        SocketChannel socket = server.accept();
        //设置为非阻塞模式
        socket.configureBlocking(false);
        //注册selector,并设置为可读模式
        socket.register(selector, SelectionKey.OP_READ);
    }

}
/**
 * 2020/3/8
 * created by chenpp
 */
public class NioServerStarter {
    public static void main(String[] args) throws IOException {
        NioServer nioServer = new NioServer();
        nioServer.startServer(8080);
        nioServer.listen();
    }
}

客户端

package com.chenpp.nio;

import javax.sound.midi.SoundbankResource;
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;
import java.util.Set;
import java.util.UUID;

/**
 * 2020/3/8
 * created by chenpp
 */
public class NioClient {


    private static Selector selector = null;


    public void start(String ip, int port) throws IOException {
        //创建选择器
        selector = Selector.open();
        //打开监听通道
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        //连接对应的服务器 ip , port
        socketChannel.connect(new InetSocketAddress(ip, port));
        //注册select为连接状态
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        System.out.println("客户端,启动成功...");
    }

    public void listen() throws IOException {
        while (true) {
            //阻塞方法,轮询注册的channel,当至少一个channel就绪的时候才会继续往下执行
            selector.select();
            //获取就绪的SelectionKey
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();
            SelectionKey key = null;
            //迭代就绪的key
            while (it.hasNext()) {
                key = it.next();
                it.remove();
                //SelectionKey相当于是一个Channel的表示,标记当前channel处于什么状态
                // 按照channel的不同状态处理数据
                process(key);

            }
        }
    }

    private void process(SelectionKey key) throws IOException {
        //channel处于可连接状态,发送消息给服务端

        if (key.isConnectable()) {
            System.out.println("connect事件就绪 ....");
            SocketChannel clientChannel = (SocketChannel) key.channel();
            if (clientChannel.isConnectionPending()) {
                clientChannel.finishConnect();
            }
            clientChannel.configureBlocking(false);
            String name = UUID.randomUUID().toString();
            System.out.println("客户端发送数据:{}" + name);
            ByteBuffer buffer = ByteBuffer.wrap(name.getBytes());
            clientChannel.write(buffer);
            clientChannel.register(key.selector(), SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            //获取对应的socket
            System.out.println("read事件就绪 ....");
            SocketChannel socket = (SocketChannel) key.channel();
            //设置一个读取数据的Buffer 大小为1024
            ByteBuffer buff = ByteBuffer.allocate(1024);
            StringBuilder content = new StringBuilder();
            int len = socket.read(buff);
            if (len > 0) {
                buff.flip();
                content.append(new String(buff.array(), "utf-8"));
                //让客户端读取下一次read
                System.out.println("客户端收到反馈:" + content);
                key.interestOps(SelectionKey.OP_READ);
            }else if(len <= 0){
                key.cancel();
                socket.close();
            }

        }
    }


}
/**
 * 2020/3/8
 * created by chenpp
 */
public class NioClientStarter {
    public static void main(String[] args) throws IOException {
        NioClient client = new NioClient();
        client.start("localhost",8080);
        client.listen();

    }
}

NIO工作原理:

  • 由一个专门的线程来处理所有的 IO 事件,并负责分发。
  • 事件驱动机制:事件到达的时候触发,而不是同步的去监视事件。


参考:

https://blog.csdn.net/anxpp/article/details/51512200

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值