JavaIO进阶系列——NIO day1-3

181 篇文章 3 订阅
21 篇文章 1 订阅

NIO

Java NlO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写

注意点(*)

这里我特地提到最上面,这个注意点是一个很容易犯的错误

ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) != -1) {
    ....                  
 }

当你们看到上面这段代码的时候是否感到十分熟悉?
没错,这就是读取字节流最常用的操作,之前我们在写的时候几乎天天写这个,判断条件就是(len = sChannel.read(buffer)) != -1,但在NIO的通道(channel)的读取上这个地方却是错误的!

原因

channel按照缓冲区读取数据:

  1. 有数据,返回数据的长度
  2. 无数据,返回0

没错,就是在无数据的时候,返回的是0,也就是说你前面用来接收的len永远无法!= -1这样就会导致你从程序在读取的时候进行无限循环!

解决

方法1:> 0

ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) > 0) {
    ....                  
 }

方法二:!= 0

ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) != 0) {
    ....                  
 }

NIO三大核心部分

  1. Channel(通道)
  2. Buffer(缓冲区)
  3. Selector(选择器)

NIO非阻塞模式

Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。

NIO和BIO比较

NIOBIO
块方式处理数据流方式处理数据
效率高效率低
非阻塞阻塞
基于Channel,Buffer基于字节流或字符流

NIO中数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中

NIO中Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

NIO三大核心部分详细

每个selector对应多个channel,每个channel都有一个buffer,selector的切换需要事件进行驱使

Channel通道

Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区也支持异步地读写

通道表示IO源与目标打开的连接,本身不能直接访问数据,而是与Buffer进行交互

Channel的特点
  1. 可以同时进行读写
  2. 可以实现异步读写数据
  3. 从缓冲读数据,也可以写数据到缓冲中
Channel类结构

在这里插入图片描述
主要的实现

  1. FileChannel:用于读取、写入、映射和操作文件的通道
  2. DatagramChannel:通过UDP读写网络中的数据通道
  3. SocketChannel:通过TCP读写网络中的数据
  4. ServerSocketChannel:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 【ServerSocketChanne类似 ServerSocket , SocketChannel类似Socket】
FileChannel常用方法

除了熟悉的byte的读、写、关闭操作 通道,这个类定义了以下文件特定的操作:

  1. 文件的一个区域可能是 mapped直接进入内存; 对于大文件,这通常更有效 比调用通常的 read或者 write方法。
  2. 对文件所做的更新可能是 forced out到底层存储设备,确保数据不 在系统崩溃的情况下丢失。
  3. 可以从文件传输字节 to some other channel, 和 vice versa, 以一种可以被许多操作系统优化的方式 直接与文件系统缓存进行非常快速的传输。
  4. 文件的一个区域可能是 locked 防止其他程序访问
方法说明
abstract void force​(boolean metaData)强制将此通道文件的任何更新写入存储 包含它的设备。
FileLock lock()获取此频道文件的排他锁。
abstract MappedByteBuffer map​(FileChannel.MapMode mode, long position, long size)将此通道文件的一个区域直接映射到内存中。
static FileChannel open​(Path path, OpenOption… options)打开或创建文件,返回文件通道以访问文件。
abstract long position()返回此通道的文件位置。
abstract int read​(ByteBuffer dst)从此通道中读取一个字节序列到给定的缓冲区中。
long read​(ByteBuffer[] dsts)从此通道中读取一个字节序列到给定的缓冲区中。
abstract long read​(ByteBuffer[] dsts, int offset, int length)从该通道读取一个字节序列到该通道的子序列中 给定的缓冲区。
abstract long size()返回此通道文件的当前大小。
abstract long transferFrom​(ReadableByteChannel src, long position, long count)将字节从给定的可读字节传输到该通道的文件中 渠道。
abstract long transferTo​(long position, long count, WritableByteChannel target)将此通道文件中的字节传输到给定的可写字节 渠道。
abstract FileChannel truncate​(long size)将此通道的文件截断为给定大小。
abstract int write​(ByteBuffer src)从给定的缓冲区将字节序列写入此通道。
long write​(ByteBuffer[] srcs)从给定的缓冲区将字节序列写入此通道。
abstract long write​(ByteBuffer[] srcs, int offset, int length)将字节序列从 给定的缓冲区。
abstract int write​(ByteBuffer src, long position)从给定的缓冲区将字节序列写入此通道, 从给定的文件位置开始。
FileChannel实例(写数据)
package test.channel;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class ChannelTest {

    public static void main(String[] args) throws IOException {
        FileOutputStream fileOutputStream = new FileOutputStream("file_channel1.txt");
        //获取字节输出流对应的通道
        FileChannel channel = fileOutputStream.getChannel();
        //分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //设置数据
        buffer.put("file_channel_test01".getBytes(StandardCharsets.UTF_8));
        //设置缓冲区为写模式
        buffer.flip();
        //写数据
        channel.write(buffer);
        //关闭通道
        channel.close();
    }
}

在这里插入图片描述

FileChannel实例(读数据)
package test.channel;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ChannelTest02 {
    public static void main(String[] args) throws IOException {
        //获取目标文件
        FileInputStream fileInputStream = new FileInputStream("file_channel1.txt");
        //获取通道
        FileChannel channel = fileInputStream.getChannel();
        //设置缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //从channel中读取数据到缓冲区中
        channel.read(buffer);
        //设置为写模式
        buffer.flip();
        //读取并输出
        String s = new String(buffer.array(), 0, buffer.remaining());
        System.out.println(s);
    }
}

在这里插入图片描述

FileChannel实例(文件复制)
package test.channel;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ChannelTest03 {
    public static void main(String[] args) throws IOException {
        //定义文件
        String path = "file_channel1.txt";
        String targetPath = "copy_file_channel1.txt";
        //获取字节输出或输入流
        FileInputStream fileInputStream = new FileInputStream(path);
        //设置输出流
        FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
        //获取通道
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        //分配缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true){
            //需要先清空缓冲区
            buffer.clear();
            //写入数据到buffer中
            int read = inChannel.read(buffer);
            //判断数据是否读完
            if (read==-1){
                break;
            }
            buffer.flip();
            //从缓冲区写数据到文件中
            outChannel.write(buffer);
        }
        outChannel.close();
        inChannel.close();

    }
}

在这里插入图片描述

FileChannel实例(分散读取与聚集写入)

即:分配多个缓冲区读取数据并最后将读取的数据聚集起来写到目标中
分散读取:Scatter,指将Channel通道的数据读入多个Buffer中
聚集写入:Gather,指将多个Buffer中的数据聚集到Channel中

package test.channel;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class ChannelTest04 {
    public static void main(String[] args) throws IOException {
        String path = "file_channel1.txt";
        String targetPath = "copy_file_channel2.txt";
        FileInputStream fileInputStream = new FileInputStream(path);
        FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
        //定义多个缓冲区
        ByteBuffer buffer1 = ByteBuffer.allocate(4);
        ByteBuffer buffer2 = ByteBuffer.allocate(10);
        ByteBuffer buffer3 = ByteBuffer.allocate(11);
        //定义数组
        ByteBuffer[] buffers = {buffer1,buffer2,buffer3};
        //获取输入输出流通道
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        //从通道中读取数据分散到各个缓冲区
        inChannel.read(buffers);
        //从每个缓冲区查询是否有数据,进行分散读取
        for (ByteBuffer buffer : buffers) {
            //切换读取模式
            buffer.flip();
            System.out.println(new String(buffer.array(),0,buffer.remaining()));
        }
        //进行聚集写入
        outChannel.write(buffers);
        outChannel.close();
        inChannel.close();
        System.out.println("over");
    }
}

*FileChannel实例(复制文件,使用复制通道数据方法)

用这种方式更加简单,方便
从目标通道中复制原通道数据使用transferFrom()方法
从原通道复制数据到目标通道使用transferTo()方法

transferFrom:
package test.channel;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class ChannelTest05 {
    public static void main(String[] args)throws IOException {
        String path = "file_channel1.txt";
        String targetPath = "copy_file_channel3.txt";
        FileInputStream fileInputStream = new FileInputStream(path);
        FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
        //获取输入输出流通道
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        //从原始通道复制数据
        outChannel.transferFrom(inChannel, inChannel.position(), inChannel.size());
        outChannel.close();
        inChannel.close();
        System.out.println("over");
    }
}

transferTo:
package test.channel;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class ChannelTest06 {
    public static void main(String[] args) throws IOException {
        String path = "file_channel1.txt";
        String targetPath = "copy_file_channel4.txt";
        FileInputStream fileInputStream = new FileInputStream(path);
        FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
        //获取输入输出流通道
        FileChannel inChannel = fileInputStream.getChannel();
        FileChannel outChannel = fileOutputStream.getChannel();
        //从原始通道复制数据
        inChannel.transferTo(inChannel.position(), inChannel.size(), outChannel);
        outChannel.close();
        inChannel.close();
        System.out.println("over");
    }
}

测试

在这里插入图片描述

Buffer缓冲区

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理

存在于Java.nio包下,所有缓冲区是Buffer抽象类的子类,用于NIO通道交互

在这里插入图片描述

Buffer类结构图

在这里插入图片描述
我们可以看到下面有各种原始类型的Buffer

Buffer的四个组成
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
  1. mark:标记,标记其实是个索引,通过Buffer的mark方法指定一个特定的位置(position)为mark,后续我们可以使用reset方法对position进行恢复
  2. position:位置,其实就是下一个要被读取或写入的数据的索引,位置不能为负数或大于限制
  3. limit:限制,表示缓冲区可以操作的数据的大小的最大值,超过最大值的数据无法进行读写,同时limit不为负数,且不能大于缓冲区的容量,在写入模式中,限制等于buffer的容量。读取模式下,limit等于写入的数据量
  4. capacity:容量,表示一个内存的大小,容量必须大于等于0,并且创建之后不能修改容量的大小

即:0 <= mark <= position <= limit <=capacity

当我们需要读数据时需要使用flip方法重置position

Buffer对象中的方法
方法说明
get()读取单个字节
put()写入缓冲区
Buffer clear()清空缓冲区并返回对缓冲区的引用
Buffer flip( )为将缓冲区的界限设置为当前位置,并将当前位置充值为Оint capacity返回 Buffer 的 capacity大小
boolean hasRemaining( )判断缓冲区中是否还有元素int limit(返回 Buffer 的界限(limit)的位置
Buffer limit(int n)将设置缓冲区界限为 n,并返回一个具有新limit的缓冲区对象
Buffer mark()对缓冲区设置标记
int position()返回缓冲区的当前位置 position
Buffer position(int n)将设置缓冲区的当前位置为n ,并返回修改后的Buffer 对象
int remaining()返回 position和 limit 之间的元素个数
Buffer reset()将位置position转到以前设置的 mark所在的位置
Buffer rewind()将位置设为0,取消设置的 mark
Buffer读写数据步骤
  1. 写入数据到Buffer
  2. 使用flip()方法,转换为读取模式
  3. 从Buffer中读取数据
  4. 调用clear()方法或compact()方法清除缓冲区
常用方法实例
package test;


import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

public class Test {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(256);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println(buffer.mark());
        System.out.println("================");
        buffer.put("element1".getBytes(StandardCharsets.UTF_8));
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
        System.out.println(buffer.mark());
        //使用flip
        buffer.flip();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.mark());
        //读取数据,先执行flip方法
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes);
        String s = new String(bytes);
        System.out.println(s);

    }
}

在这里插入图片描述

Selector选择器

Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率

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

选择器可以监测多个注册通道上是否有事件的发生,若事件发生,获取事件然后进行相应处理。以达到单线程管理多通道(管理多个连接和请求),只有在连接通道时有读写事件发生时,才会进行读写,大大减少了系统的开销,并不必要为了每个连接都去创建一个线程,无需维护多个线程,避免了多线程之间的上下文切换导致的开销问题

使用场景

当NIO中使用单线程但处理多个客户端连接时,就会使用Selector选择器了

创建选择器

使用open方法进行创建

Selector open = Selector.open();
将通道注册到选择器中
package test.channel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;

public class SelectorDemo1 {
    public static void main(String[] args) throws IOException {
        //创建通道
        ServerSocketChannel sChannel = ServerSocketChannel.open();
        //切换为非阻塞模式
        sChannel.configureBlocking(false);
        //绑定连接,设置端口
        sChannel.bind(new InetSocketAddress(9999));
        //获取选择器
        Selector selector = Selector.open();
        //通道注册
        sChannel.register(selector, SelectionKey.OP_ACCEPT);
			//接下来就可以开启轮询获取事件了
    }
}

关于SelectionKey

是监听事件类型

  1. OP_READ = 1 << 0:表示1,读操作
  2. OP_WRITE = 1 << 2:表示4,写操作
  3. OP_CONNECT = 1 << 3:表示8,连接操作
  4. OP_ACCEPT = 1 << 4:表示16,接收操作
Selector实例
Server
package test.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * NIO非阻塞通信服务端
 */
public class Server {
    public static void main(String[] args) throws Exception {
        //获取通道
        ServerSocketChannel channel = ServerSocketChannel.open();
        //切换非阻塞模式
        channel.configureBlocking(false);
        //提供端口进行绑定,开放给客户端
        channel.bind(new InetSocketAddress(9999));
        //获取选择器
        Selector selector = Selector.open();
        //将通道注册到选择器中,指定监听事件
        channel.register(selector, SelectionKey.OP_ACCEPT);
        //使用选择器进行轮询准备好的事件,使用select函数,大于0表示有事件
        try{
            while (selector.select() > 0) {
                //获取选择器中所有注册通道的就绪好的事件
                //使用迭代器进行获取,选择器的selectedKeys().iterator()
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                //遍历事件
                while (it.hasNext()) {
                    //提取当前事件
                    SelectionKey selectionKey = it.next();
                    //判断事件类型
                    //判断是否为接收事件
                    if (selectionKey.isAcceptable()) {
                        //获取当前接入的客户端通道
                        SocketChannel sChannel = channel.accept();
                        //切换为非阻塞模式
                        sChannel.configureBlocking(false);
                        //将客户端通道注册到选择器中,注册读事件(客户端是写,服务端是读)
                        sChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
                        //若是读事件,获取客户端通道
                        SocketChannel sChannel = (SocketChannel) selectionKey.channel();
                        //读取数据
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int len = 0;
                        while ((len = sChannel.read(buffer)) > 0) {
                            //切换读取模式
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, len));
                            //归位
                            buffer.clear();
                        }
                    }
                    //处理完毕,进行移除,否则会重复进行处理
                    it.remove();
                }
            }
        }catch (Exception e){
            System.out.println("connect end");
        }
    }
}

Client
package test.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
 * NIO非阻塞客户端
 */
public class Client {
    public static void main(String[] args) throws Exception {
        //获取服务端提供的通道进行连接,对应IP地址以及端口号
        SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
        //切换非阻塞模式
        channel.configureBlocking(false);
        //分配缓冲区大小
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //发送数据到服务端
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.print("client:");
            String str = scanner.nextLine();
            buffer.put(("client:"+str).getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            //写到缓冲区中
            channel.write(buffer);
            //清除,归位
            buffer.clear();
            //指定退出
            if ("bye".equals(str)){
                break;
            }
        }

    }
}

测试

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

直接缓冲区与非直接缓冲区

byte ,buffer可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。
从数据流的角度,非直接内存是下面这样的作用链:
在这里插入图片描述
直接内存:
在这里插入图片描述
很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。
直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。
所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。

使用直接内存

ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024);
//isDirect()判断是否为直接内存
System.out.println(buffer1.isDirect());
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值