Java NIO

目录

 

什么是NIO?

NIO的核心一:缓冲区(Buffer)

NIO的核心二:通道(Channel)

NIO的核心三:选择器(Selector)

NIO案例:聊天室


什么是NIO?

首先我们知道传统的IO 是面向流的是阻塞的,而NIO则是面向缓冲区非阻塞的。 NIO核心在于通道(Channel,负责传输)、缓冲区(Buffer,负责存储数据)、选择器(Selector,使一个单独的线程管理多个Channel。Selector 是非阻塞IO 的核心)。

NIO的核心一:缓冲区(Buffer)

缓冲区(Buffer)概念:在 Java NIO 中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据, 根据数据类型不同(boolean 除外),提供了相应类型的缓冲区:ByteBuffer、CharBuffer 、ShortBuffer、 IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。其中还有一个MappedByteBuffer内存映射文件(这里不做详解),首先我们来理解Buffer中四个非常重要的属性:

  • capacity : 容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。
  • limit : 界限,表示缓冲区中可以操作数据的大小。(limit 后数据不能进行读写)
  • position : 位置,表示缓冲区中正在操作数据的位置。
  • mark : 标记,表示记录当前 position 的位置。可以通过 reset() 恢复到 mark 的位置

下图详细的展示了这四个字段的在缓冲区中的四个属性(0 <= mark <= position <= limit <= capacity)

Buffer的核心API如下:

allocate(int capacity)                  创建非直接缓冲区
allocateDirect(int capacity)            创建非直接缓冲区
Bufferclear()                           清空缓冲区并返回对缓冲区的引用
Buffer flip()                           将缓冲区的界限设置为当前位置,并将当前位置充值为0
int capacity()                          返回Buffer 的capacity大小
boolean hasRemaining()                  判断缓冲区中是否还有元素
int limit()                             返回Buffer 的界限(limit) 的位置
Bufferlimit(int n)                      将设置缓冲区界限为n, 并返回一个具有新limit 的缓冲区对象
Buffer mark()                           对缓冲区设置标记
int position()                          返回缓冲区的当前位置position
Buffer position(int n)                  将设置缓冲区的当前位置为n , 并返回修改后的Buffer 对象
int remaining()                         返回position 和limit 之间的元素个数
Buffeset()                              将位置position 转到以前设置的mark 所在的位置
Buffer rewind()                         将位置设为为0,取消设置的mark
byte get()                              读取单个字节
byte[] get(byte[] dst)                  批量读取多个字节到dst 中
byte get(int index)                     读取指定索引位置的字节(不会移动position)
ByteBuffer put(byte b)                  将给定单个字节写入缓冲区的当前位置
ByteBuffer put(byte[] src)              将src 中的字节写入缓冲区的当前位置
ByteBuffer put(int index, byte b)       将指定字节写入缓冲区的索引位置(不会移动position)
   

NIO的核心二:通道(Channel)

通道(Channel):用于源节点与目标节点的连接。在 Java NIO 中负责缓冲区中数据的传输。Channel 本身不存储数据,因此需要配合缓冲区进行传输。通道的主要实现类FileChannel、SocketChannel 、ServerSocketChannel、DatagramChannel ,第一个和文件相关,后三个与网络传输相关,我们先介绍FileChannel。

FileChannel

创建通道:①FileInputStream、FileOutputStream、RandomAccessFile的getChannel()方法,②在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open(),③在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()。

//第一种创建通道
FileInputStream fis = new FileInputStream("test.txt");
FileChannel inChannel = fis.getChannel();
//第二种创建通道
FileChannel inChannel = FileChannel.open(Paths.get("test.txt"), StandardOpenOption.READ);
//第三种创建通道
FileChannel inChannel= Files.newByteChannel(Paths.get("test.txt"),StandardOpenOption.READ);

向FileChannel读取数据

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

向FileChannel写数据

String newData = "Hello Nio" + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    channel.write(buf);
}

通道之间传输数据

//第一种通过inChannel获取数据,然后写入到outChannel中
 FileInputStream fis = null;
        FileOutputStream fos = null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;
        try {
            fis = new FileInputStream("test.txt");
            fos = new FileOutputStream("1.txt");
            inChannel = fis.getChannel();
            outChannel = fos.getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            while (inChannel.read(byteBuffer) != -1) {
                byteBuffer.flip();
                outChannel.write(byteBuffer);//向outChannel通道写数据
                byteBuffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null)
                    fis.close();
                if (fis != null)
                    fos.close();
                if (inChannel != null)
                    inChannel.close();
                if (outChannel != null)
                    outChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
//第二种 通过transferTo 或者transferFrom API来操作
inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel,0,inChannel.size());

SocketChannel 、ServerSocketChannel、DatagramChannel 通过下面的一个模拟客户端向服务端发生数据的案例来做简单介绍,具体的使用结合Selector实现非阻塞IO。


    @Test
    public void cilent() throws IOException {
        //获取通道
        SocketChannel socketChannel = SocketChannel.open();
        //连接服务器
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
        FileChannel inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
        //分配指定大小的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        buf.put("hello wpf".getBytes());
        buf.flip();
        //向通道写数据
        socketChannel.write(buf);
        //关闭通道
        inChannel.close();
        socketChannel.close();
    }
    @Test
    public void server() throws IOException {
        //获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //bind端口
        serverSocketChannel.bind(new InetSocketAddress(8080));
        //获取连接
        SocketChannel socketChannel = serverSocketChannel.accept();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (socketChannel.read(byteBuffer)!=-1){//获取数据
            byteBuffer.flip();
            byte[] bytes =new byte[byteBuffer.limit()];
            byteBuffer.get(bytes);
            System.out.println(new String(bytes));
            byteBuffer.clear();
        }
        socketChannel.close();
        serverSocketChannel.close();
    }

NIO的核心三:选择器(Selector)

选择器(Selector)也叫多路复用器,Selector 可以同时监控多个SelectableChannel 的IO 状况,也就是说,利用Selector 可使一个单独的线程管理多个Channel。Selector 是非阻塞IO 的核心。

为什么使用Selector? 仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用Selector能够处理多个通道就足够了。

创建:selector:Selector selector = Selector.open();

注册通道:(注:与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。)

//设置为非阻塞
serverChannel.configureBlocking(false);
//将ServerSocketChannel注册到选择器,指定其行为为"等待接受连接"
serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);

注:通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops 指定(SelectionKey :OP_READ、 OP_WRITE 、OP_CONNECT 、OP_ACCEPT)如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来:
int interestSet=SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey:表示SelectableChannel 和Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作:
int interestOps()                         获取感兴趣事件集合
int readyOps()                            获取通道已经准备就绪的操作的集合
SelectableChannel channel()    获取注册通道
Selector selector()                     返回选择器
boolean isReadable()                检测Channal 中读事件是否就绪
boolean isWritable()                  检测Channal 中写事件是否就绪
booleanisConnectable()            检测Channel 中连接是否就绪
booleanisAcceptable()              检测Channel 中接收是否就绪

选择器的一个完整示例

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
  int readyChannels = selector.select();
  if(readyChannels == 0) continue;
  Set selectedKeys = selector.selectedKeys();
  Iterator keyIterator = selectedKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
  }
}

NIO案例:聊天室

//server端
public class Server implements Runnable {
    //1 多路复用器
    private Selector selector;
    //2 建立缓冲区
    private ByteBuffer readBuf=ByteBuffer.allocate(1024);
    private ByteBuffer writeBuf=ByteBuffer.allocate(1024);
    //构造函数
    public Server(int port){
        try {
            //1 打开多路复用器
            this.selector=Selector.open();
            //2 打开服务器通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //3 设置服务器通道为非阻塞方式
            ssc.configureBlocking(false);
            //4 绑定ip
            ssc.bind(new InetSocketAddress(port));
            //5 把服务器通道注册到多路复用器上,只有非阻塞信道才可以注册选择器.并在注册过程中指出该信道可以进行Accept操作
            ssc.register(this.selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器已经启动.....");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        while(true){//一直循环
            try {
                this.selector.select();//多路复用器开始监听
                //获取已经注册在多了复用器上的key通道集
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                //遍历
                while (keys.hasNext()) {
                    SelectionKey key = keys.next();//获取key
                    //如果是有效的
                    if(key.isValid()){
                        // 如果为阻塞状态,一般是服务端通道
                        if(key.isAcceptable()){
                            this.accept(key);
                        }
                        // 如果为可读状态,一般是客户端通道
                        if(key.isReadable()){
                            this.read(key);
                        }
                    }
                    //从容器中移除处理过的key
                    keys.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
    //从客户端通道获取数据并进行处理
    private void read(SelectionKey key) {
        try {
            //1 清空缓冲区旧的数据
            this.readBuf.clear();
            //2 获取之前注册的socket通道对象
            SocketChannel sc = (SocketChannel) key.channel();
            //3 读取数据
            int count = sc.read(this.readBuf);
            //4 如果没有数据
            if(count == -1){
                key.channel().close();
                key.cancel();
                return;
            }
            //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
            this.readBuf.flip();
            //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
            byte[] bytes = new byte[this.readBuf.remaining()];
            //7 接收缓冲区数据
            this.readBuf.get(bytes);
            //8 打印结果
            String body = new String(bytes).trim();
            System.out.println("服务端接受到客户端请求的数据: " + body);
            //9 告诉客户端已收到数据
            writeBuf.put("你好,客户端,我已收到数据".getBytes());
            //对缓冲区进行复位
            writeBuf.flip();
            //写出数据到服务端
            sc.write(writeBuf);
            //清空缓冲区数据
            writeBuf.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //接受一个客户端socket进行处理
    private void accept(SelectionKey key) {
        try {
            //1 获取服务通道
            ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
            //2 执行阻塞方法,当有客户端请求时,返回客户端通信通道
            SocketChannel sc = ssc.accept();
            //3 设置阻塞模式
            sc.configureBlocking(false);
            //4 注册到多路复用器上,并设置可读标识
            sc.register(this.selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        //启动服务器
        new Thread(new Server(9527)).start();
    }
}
//客户端
public class Client {
    //客户端信道选择器,轮询读取服务端返回数据
    private Selector selector;
    //连接信道
    private SocketChannel sc;
    public Client(){
        try {
            this.sc=SocketChannel.open();//打开信道
            sc.connect(new InetSocketAddress("127.0.0.1",9527));连接服务端
            sc.configureBlocking(false);//设置非阻塞
            selector = Selector.open();//必须打开
            //将当前客户端注册到多路复用器上,并设置为可读状态
            sc.register(this.selector, SelectionKey.OP_READ);
            //开启线程,一直轮询
            new Thread(()->{
                while(true){//一直循环
                    try {
                        this.selector.select();//多路复用器开始监听
                        //获取已经注册在多了复用器上的key通道集
                        Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                        //遍历
                        while (keys.hasNext()) {
                            SelectionKey key = keys.next();//获取key
                            //如果是有效的
                            if(key.isValid()){
                                // 如果为可读状态,读取服务端返回的数据
                                if(key.isReadable()){
                                    this.read(key);
                                }
                            }
                            //从容器中移除处理过的key
                            keys.remove();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //客户端获取服务端返回的数据
    private void read(SelectionKey key) {
        try {
            //建立写缓冲区
            ByteBuffer readBuf = ByteBuffer.allocate(1024);
            //2 获取之前注册的socket通道对象
            SocketChannel sc = (SocketChannel) key.channel();
            //3 读取数据
            int count = sc.read(readBuf);
            //4 如果没有数据
            if(count == -1){
                key.channel().close();
                key.cancel();
                return;
            }
            //5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
            readBuf.flip();
            //6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
            byte[] bytes = new byte[readBuf.remaining()];
            //7 接收缓冲区数据
            readBuf.get(bytes);
            //8 打印结果
            String body = new String(bytes).trim();
            System.out.println("客户端已接受到服务端返回的数据: " + body);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //建立写缓冲区
        ByteBuffer writebuf = ByteBuffer.allocate(1024);
        Client client = new Client();
        try {
            while(true){
                //定义一个字节数组,然后使用系统录入功能:
                byte[] bytes = new byte[1024];
                System.in.read(bytes);
                //把数据放到缓冲区中
                writebuf.put(bytes);
                //对缓冲区进行复位
                writebuf.flip();
                //写出数据到服务端
                client.sc.write(writebuf);
                //清空缓冲区数据
                writebuf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(client.sc != null){
                try {
                    client.sc.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

指挥官飞飞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值