JAVA的BIO、NIO、AIO模式精解(二)

4.JAVA NIO深入剖析

4.1 java NIO基本介绍

  • Java NIO(New IO)即java non-block IO。NIO支持面向缓冲区的,基于通道的IO操作。NIO可理解为非阻塞IO,传统IO只能阻塞读写,而NIO可配置socket为非阻塞式。
  • NIO类在java.nio包下,对原io包进行改写
  • NIO三大组件 《Channel通道》,《Buffer缓冲区》,《Selector选择器
  • NIO非阻塞模式使一个线程从某管道发送请求或读取数据,没有数据不会获取,不会阻塞直至数据可读。该线程可做其他事,非阻塞写也是。
  • NIO可一个线程处理多个操作。1K请求可分配20~80个线程处理,而不是阻塞IO分配1000。

4.2 NIO和NIO比较

  • BIO以流方式处理数据,效率低。NIO以块处理数据,效率高。
  • BIO阻塞,NIO非阻塞
  • BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道到缓冲区,或相反,Seletor用于监听多个通道的事件(链接请求,数据就绪),因此单线程就可监听多个客户端。
NIO BIO
面向缓冲区Buffer 面向流Stream
非阻塞Non Blocking IO 阻塞Blocking IO
选择器Selector

4.3 NIO三大核心原理示意图

Buffer:一块可读写数的内存,被包装成NIO Buffer对象,并提供一组方法来访问。相对数组更易操作管理。
Channel:可读可写的管道,流(inPut,Output)是单向。通道可支持读取写入缓冲区,也支持异步读写。
Selector:可检查多个NIO管道是否就绪。这样一个单独线程可管理多个Channel。
JAVA的BIO、NIO、AIO模式精解(二)_第1张图片

  • 每个Channel都对应一个Buffer
  • 一个线程对应一个Selector,一个Selector对应多个Channel。
  • 程序切换到那个channel由事件决定。selector根据不同事件在各个管道上切换
  • Buffer是内存块,底层数组实现
  • 数据读写由Buffer完成双向,BIO输入输出单向。
  • Java NIO核心:通道表示打开到IO设备负责传输,缓冲区表示存储数据负责存取。

4.4 NIO核心1:缓冲区

缓冲区

一个用于基本数据类型的容器。nio包定义。所有缓冲区都是Buffer抽象类的子类。主要用于与NIO通道进行交互。数据都是从通道读入缓冲区,在写入管道。
Buffer

Buffer类及其子类

Buffer类似数组,保存多个相同类型的数据。根据类型不同,有以下子类:Byte,Char,Short,Int,Long,Float,Double。上述Buffer采取相同的方法管理数,只是数据类型不同。

static XxxBufer allocate(int capacity) : 创建一个容量为capacity的 XxxBuffer对象

缓冲区的基本属性

  • 容量capacity:内存块的固定大小,不能为负,不能修改。
  • 限制limit:缓冲区可操作数据大小,不能为负,不能超容量。写入模式,限制等于Buffer容量,读取模式,等于写入数据量。
  • 位置Position:下个要读取或写入的数据索引。
  • 标记mark与重置reset:标记是索引,通过Buffer中的mark方法指定Buffer中特定的position,之后可调用reset方法恢复position。标记,职位,限制,容量遵循以下不变式:0<=mark<=position<=limit<=limit<=capacity
  • 图示
    JAVA的BIO、NIO、AIO模式精解(二)_第2张图片

缓冲区常见方法

Buffer clean():清空缓冲区并返回缓冲区的引用
Buffer flip():为将缓冲区的界限设置为当前位置,并将当前位置重置为0
int capacity():返回Buffer的容量
boolean hasRemaining():判断缓冲区还有元素
int limit():返回Buffer界限位置
Buffer limit():设置缓冲区界限为n,并返回一个新的limit的缓冲区对象
Buffer mark():对缓冲区设置标记
int position():返回缓冲区当前位置
Buffer position(int n):设置缓冲区当前位置为n,并返回修改后的Buffer对象
int remaining():返回position和limit之间的元素个数
Buffer reset():将位置position转到以前的设置的mark所在的位置
Buffer rewind():位置设置为0,取消设置的mark

        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println(buffer.position());  //0
        System.out.println(buffer.limit());     //10
        System.out.println(buffer.capacity());  //10
        String name = "xuy";
        buffer.put(name.getBytes());
        System.out.println(buffer.position());  //3
        System.out.println(buffer.limit());     //10
        System.out.println(buffer.capacity());  //10
        buffer.flip();
        System.out.println(buffer.position());  //0
        System.out.println(buffer.limit());     //3
        System.out.println(buffer.capacity());  //10
        char c = (char) buffer.get();
        System.out.println(c);                  //x
        System.out.println(buffer.position());  //1
        System.out.println(buffer.limit());     //3
        System.out.println(buffer.capacity());  //10

        buffer.clear();
        System.out.println(buffer.position());  //0
        System.out.println(buffer.limit());     //10
        System.out.println(buffer.capacity());  //10
        System.out.println(c);                  //x  只有在重复值时生效

        ByteBuffer buf = ByteBuffer.allocate(10);
        String n = "abcdefg";
        buf.put(n.getBytes());
        buf.flip();
        //读取数据
        byte[] b = new byte[2];
        buf.get(b);
        String rs = new String(b);
        System.out.println(rs);              //ab
        System.out.println(buf.position());  //2
        System.out.println(buf.limit());     //7
        System.out.println(buf.capacity());  //10
        buf.mark();                          //标记此刻位置:2
        byte[] b2 = new byte[3];
        buf.get(b2);
        System.out.println(new String(b2));  //cde
        System.out.println(buf.position());  //5
        System.out.println(buf.limit());     //7
        System.out.println(buf.capacity());  //10
        buf.reset();                         //回到标记位置
        if(buf.hasRemaining()) {
            System.out.println(buf.remaining());  //5
        }

缓冲区数据操作

Buffer提供两种用于数据操作方法:get,put获取Buffer中数据。
get():获取单个字节
get(byte[] dst):批量读取多个字节到dst中
get(int index):读取指定索引位置的字节(不移动position)
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将src中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不移动position)

使用Buffer读写数据一般遵循以下四个步骤

  1. 写入数据到Buffer
  2. 调用flip方法,转换为读取模式
  3. 从Buffer中读取数据
  4. 调用buffer.clear()方法或buffer.compact()方法清楚缓冲区

直接与非直接缓冲区

buteBuffer分为基于直接内存(非堆内存),直接作用于本地IO操作,高效;另一种是非直接内存(堆内存),IO操作时要先从本进程内存复制到直接内存,再利用本地IO处理。
可使用isDriect()方法判断
非直接内存作用链:本地IO - 直接内存 - 非直接内存 - 直接内存 - 本地IO
直接内存调用链:本地IO - 直接内存 - 本地IO
结论:发送大量数据,生命周期长,直接内存效率高。直接使用allocateDirect创建,耗费性能。不过数据在JVM外存储不占用应用内存。

4.5 NIO核心2:通道Channel

通道概述

java.nio.channels包定义,表示IO源与目标打开的连接。Channel类似流,但不能直接访问数据,Channel只能与Buffer进行交互。

  1. NIO通道类似流,可异步读写数据,流只能单向。
  2. Channel是在NIO中的一个接口:public interface Channel extends Closeable{}

常用的Channel实现类

FIleChannel:用于读写,映射和操作文件
DatagramChannel:通过UDP读写网络中的数据
SocketChannel:通过TCP读写网络中的数据
ServerSocketChannel:可监听新进来的TCP连接,对每一个连接创建SocketChannel。

FileChannel

获取通道一种方式是对支持通道对象调用getChannel()方法,支持通道类型:FileInputStream,FileOutputStream,RendomAccessFile,DatagramSocket,Socket,ServerSocket。
获取管道的其他方式是使用File类的静态方法newByteChannel()获取字节通道。open()打开并返回指定通道。

        //写测试
        try {
            //1.从字节输出流写目标文件
            FileOutputStream fos = new FileOutputStream("data.txt");
            //2.得到字节输出流对应的Channel
            FileChannel channel = fos.getChannel();
            //3.分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("xuyu".getBytes());
            //4.缓冲区改为写模式
            buffer.flip();
            channel.write(buffer);
            channel.close();
            System.out.println("write ok!");
        }catch (Exception e){
            e.printStackTrace();
        }

        //读测试
        try {
            //1.定义一个文件字节输入流说源文件联通
            FileInputStream is = new FileInputStream("data.txt");
            //2.需要得到文件字节输入流管道
            FileChannel channel = is.getChannel();
            //3.定义一个缓冲区
            ByteBuffer buffer1 = ByteBuffer.allocate(1024);
            //4.读取数据到缓冲区
            channel.read(buffer1);
            buffer1.flip();
            //5.读取缓冲区中的数据并输出
            String rs = new String(buffer1.array(), 0,buffer1.remaining());  //回到首位读取
            System.out.println(rs);
        } catch (Exception e){
            e.printStackTrace();
        }
        
        //文件复制
        try{
            //源文件
            File srcFile = new File("C:\\Desktop\\Test.jpg");
            File desFile = new File("C:\\Desktop\\Test.jpg");
            //得到一个字节输出流和字节输入流
            FileInputStream fis = new FileInputStream(srcFile);
            //得到一个字节输出流
            FileOutputStream fos = new FileOutputStream(desFile);
            //得到文件通道
            FileChannel isChannel = fis.getChannel();
            FileChannel osChannel = fos.getChannel();
            
        //分散与聚集
        try{
            //1.字节输入管道
            FileInputStream is = new FileInputStream("data.txt");
            FileChannel isChannel = is.getChannel();
            //2.字节输出流管道
            FileOutputStream fos = new FileOutputStream("data2.txt");
            FileChannel fosChannel = fos.getChannel();
            //3.定义多个缓冲区做数据分散
            ByteBuffer buffer1 = ByteBuffer.allocate(4);
            ByteBuffer buffer2 = ByteBuffer.allocate(1024);
            ByteBuffer[] buffers  = {buffer1, buffer2};
            //4.从通道中读取数据分散到各个缓冲区
            isChannel.read(buffers);
            //5.从每个缓冲区中查询是否有数据读取到了
            for (ByteBuffer buffer : buffers) {
                buffer.flip();  //切换到读模式
                System.out.println(new String(buffer.array(), 0, buffer.remaining()));
            }
            //6.聚集写入
            fosChannel.write(buffers);
            isChannel.close();
            fosChannel.close();
            System.out.println("文件复制完成");
        }catch (Exception e){
            e.printStackTrace();
        }

        //从目标通道复制原通道数据
        try{
            //1.字节输入管道
            FileInputStream fis = new FileInputStream("data.txt");
            FileChannel fisChannel = fis.getChannel();
            //2.字节输出流管道
            FileOutputStream fos = new FileOutputStream("data1.txt");
            FileChannel fosChannel = fos.getChannel();
            //3.复制
            fosChannel.transferFrom(fisChannel,fosChannel.position(),fisChannel.size());
            fisChannel.close();
            fosChannel.close();
        }catch (Exception e){
            e.printStackTrace();
        }

        //从原通道数据复制到目标通道
        try{
            //1.字节输入管道
            FileInputStream is = new FileInputStream("data.txt");
            FileChannel isChannel = is.getChannel();
            //2.字节输出管道流
            FileOutputStream fos = new FileOutputStream("data1.txt");
            FileChannel osChannel = fos.getChannel();
            //3.复制
            isChannel.transferTo(isChannel.position(), isChannel.size(),osChannel);
            isChannel.close();
            osChannel.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

NIO核心3:选择器

Selector是SelectableChannel对象的多路复用器,Selector可同时监听多个SelectableChannel的IO状况。利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。
JAVA的BIO、NIO、AIO模式精解(二)_第3张图片

  • JavaNIO用非阻塞IO方式。用一个线程处理多个客户端连接,就会用到Selector选择器
  • Selector能检测多个注册的通道上是否有事件发生(注:多个Channel以事件的方式可注册到同一个Selector),如果有事件发生,便获取事件然后对每个事件进行相应的处理。这样就可以只用一个线程去管理多个通道。即多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大较少了系统开销,不必为每个连接创建线程。
  • 避免多线程之间的上下文切换。

Selector选择器应用

创建Selector:调用Selector.open()
向选择器注册通道:SelectableChannel.register(Selector sel, int ops);

   //获取通道
   ServerSocketChannel ssChannel = ServerSocketChannel.open();
   //切换非阻塞模式
   ssChannel.configureBlocking(false);
   //绑定连接
   ssChannel.bind(new InetSocketAddress(9898));
   //获取选择器
   Selector selector = Selector.open();
   //将通道注册到选择器上,并且指定“监听接收事件”
   ssChannel.register(selector, SelectionKey.OP_ACCEPT);

当调用register()将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可监听二点事件类型:

  • 读:SelectionKey.OP_READ (1)
  • 写:SelectionKey.OP_WRITE (4)
  • 连接:SelectionKey.OP_CONNECT (8)
  • 接收:SelectionKey.OP_ACCEPT (16)
  • 若注册时不止监听一个事件,则可使用”为或“操作符连接。

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE

4.7 NIO非阻塞式网络通信原理分析

Selector示意图和特点说明

selector可以实现:一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能,弹性伸缩能力和可靠性都得到了极大的提升。
JAVA的BIO、NIO、AIO模式精解(二)_第4张图片

服务端流程

  • 1.当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel获取通道
ServerSocketChannel = ssChannel = ServerSocketChannel.open();
  • 2.切换非阻塞模式
ssChannel.configureBlocking(false);
  • 3.绑定连接
ssChannel.bind(new InetSocketAddress(9999));
  • 4.获取选择器
ssChannel.bind(new InetSocketAddress(9999));
  • 5.将通道注册到选择器上,并且指定“监听接收事件”
ssChannel.register(selector, selectionKey.OP_ACCEPT);
  • 6.轮询式的获取选择器上一已经“准备就绪”的事件
while(selector.select() > 0) {
	sout("第一轮");
	//获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
	Iterator<SelectionKey> it = selector.selectedKeys().iterator();
	while(it.hasNext()) {
		//获取准备“就绪”的事件
		SelectionKey sk = it.next();
		//判断具体是什么事件准备就绪
		if(sk.isAcceptable()) {
			//若就绪,获取客户端连接
			SocketChannel sChannel = ssChannel.accpet();
			//切换非阻塞模式
			sChannel.configureBlocking(false);
			//将该通道注册到选择器上
			sChannel.register(selector, SelectionKey.OP_READ);
		} else if (sk.isReadable()) {
			//获取当前选择器上“读就绪”状态的通道
			SocketCahnnel sChannel = (SocketChannel) sk.channel();
			//读取数据
			ByteBuffer buf = ByteBuffer.alocat(1024);
			int len = 0;
			while((len = sChannel.read(buf)) > 0) {
				buf.flip();
				sout(new String(buf.array(), 0 ,len));
				buf.clear();
			}
		}
		//取消选择键 SelectionKey
		it.remove();
	}
}

客户端流程

  • 1.获取通道
SocketChannel sChannel = socketChannel.opan(new InetSocketAddress("127.0.0.1", 9999));
  • 2.切换非阻塞模式
sChannel.configureBlocking(false);
  • 3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
  • 4.发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()) {
	String str = scan.nextLine();
	buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis()) + "\n" + str).getBytes());
	buf.filp();
	sChannel().wriet(buf);
	buf.clear();
}

4.8 NIO非阻塞式网络通信案例

需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
地址:https://gitee.com/xuyu294636185/JAVA_NIO_DEMO.git

4.9 NIO网络编程-群聊

需求:NIO非阻塞网路编程实现多人群聊

  • 编写一个NIO群聊,实现客户端与客户端通信(非阻塞)。
  • 服务端:可检测用户上线,离线,并实现消息转发。
  • 客户端:通过channel可无阻塞发送消息给所有客户端用户,同时接收其他客户端通过服务端转发来的消息。
  • 代码地址:https://gitee.com/xuyu294636185/JAVA_NIO_DEMO.git

5.JAVA AIO深度剖析

5.1 AIO编程

  • java AIO(NIO2):异步非阻塞,服务器实现为一个有效请求一个线程,客户端的I/O请求都是由OS先完成通知服务器应用去启动线程进行处理。
BIO NIO NIO
Socket SocketChannel AsynChronousScoketChannel
ServerSocket ServerSocketChannel AsynchronousServerSocketChannel
与NIO不同,当进行读写操作时,只须直接调用API的read或write异步方法,当有流可读时,操作系统会将可读流传入read缓冲区,当write方法传递的流写完,操作系统主动通知应用程序。

你可能感兴趣的:(BIO,NIO,AIO,java,nio,jvm)