Java IO模型

本文详细介绍了Java NIO(非阻塞I/O)的核心概念,包括BIO、NIO和AIO的区别、NIO的三大核心组件——通道(Channel)、缓存区(Buffer)和选择器(Selector)。通过实例展示了如何使用Buffer进行数据读写,以及如何通过Selector实现单线程管理多个通道的非阻塞IO操作。NIO的优势在于提高了并发性能,尤其适合处理连接数众多且连接时长较长的场景。
摘要由CSDN通过智能技术生成

IO模型

BIO

同步并阻塞(传统型阻塞),服务器实现模式为 一个连接一个线程,即客户端 有链接请求时服务器就需要 启动一个线程进行处理,如果这个连接不做任何事情就会造成不要的线程开销。

BIO模型

NIO

同步非阻塞,服务器实现模式是 为一个 线程处理多个请求,即客户端发送的连接请求都会注册到 多路复用器上,多路复用器 轮询到连接有IO请求就进行处理

NIO模型

AIO

(又称为NIO2.0)异步非阻塞,服务器实现模式为 一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行出出力, 一般适用于连接数较多且连接时长较长的应用

使用场景分析

1、BlO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

2、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。

3、AlO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS(OutputStream)参与并发操作,编程比较复杂,JDK7开始支持。

NIO

JAVA NIO的介绍

JAVA NIO(new IO)也有人称为java non-blocking IO 是从Java 1.4版本开始引入的一个新的IO API,可以替换标准的Java IO,但是使用方式完全不同,NIO支持面向 **缓存区、通道、选择器 **的IO操作。NIO **将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IOread和write只能阻塞进行,线程在读写IO的时候不能干其他的事情 **,比如socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞。而nio可以配置为socket为非阻塞式。

NIO有三大核心类:Channel(通道)、Buffer(缓存区)、Selector(选择器)

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

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

NIO和BIO的比较

BIO以流的方式处理数据,而NIO以数据块的方式处理数据,块IO比流IO快很多

BIO是阻塞的,NIO是非阻塞的

BIO基于字符和字节流进行操作,而NIO基于Channel(通道)和Buffer(缓存区)进行操作,数据总是从通道读取到缓存区,或者从缓存区写入到通道中。Selector(选择器)用来监听多个通道的事件

NIOBIO
面向缓存流(Buffer)面向流(stream)
非阻塞(Non Blocking IO)阻塞IO(Blocking IO)
选择器(Selector)

NIO三大核心原理示意图

NIO 有三大核心部分**:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)**

Buffer缓存区

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

Channel(通道)

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

Selector选择器

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

在这里插入图片描述

NIO核心之一:缓存区

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

img

Buffer类以及其子类

Buffer就像一个数组,可以保存多个相同类型的数据。根据数据类型不同,有了如下子类

//初始化
IntBuffer inBuffer = IntBuffer.allocate(1);
  • ByteBuff
  • CharByte
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

缓存区基本属性

Buffer中的重要概念:

  • **容器(capacity):**作为一个内存块,Buffer具有一定的固定大小,也称为 “容量”,缓存区容量不能为负,并且创建后不能更改。
  • 限制(limit):表示缓存区可以操作数据的大小(limit后数据不能进行读写)。缓存区的限制不能为负,并且不能大于容量。 写入模式,限制等于buffer的容量。读取模式,limit等于写入的数据量
  • 位置(position):下一个要读取或写入数据的索引。缓存区的位置不能为负,并且不能大于其限制
  • **标志(mark)与重置:(reset):**标记是一个索引,通过Buffer的mark()方法指定Buffer的一个特定的position,之后可以通过调用reset()方法恢复到这个position。
  • 标记、位置、限制、容量遵守以下原则:0<=mark<=position<=limit<=capacity

在这里插入图片描述

Buffer常用API

Buffer常见方法
Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
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 所有子类提供了两个用于数据操作的方法:get()put() 方法
取获取 Buffer中的数据
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)

放到 入数据到 Buffer 中 中
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()方法清除缓冲区

代码演示:

public class BufferTest {


    @Test
    public void test02(){
        ByteBuffer buffer = ByteBuffer.allocate(10);
        String name ="HIllCheung";
        buffer.put(name.getBytes());
        buffer.flip();//切换成读取模式
        byte [] bt=new byte[buffer.limit()];
        buffer.get(bt,0,2);
        System.out.println(new String(bt));
        System.out.println(buffer.position());
        //标记
        buffer.mark();
        buffer.get(bt,2,2);
        System.out.println(new String(bt));
        System.out.println(buffer.position());
        //reset恢复到标记的位置
        buffer.reset();
        System.out.println(buffer.position());
        if(buffer.hasRemaining()){
            System.out.println(buffer.remaining());
        }
    }
    @Test
    public void test01(){
       //1.分配一个缓存区,容量设置为10
        ByteBuffer allocate = ByteBuffer.allocate(10);
        System.out.println(allocate.position());
        System.out.println(allocate.limit());
        System.out.println(allocate.capacity());
        System.out.println("-------------------");
        //2.put缓冲区中添加数据
        String name ="Cheung";
        allocate.put(name.getBytes());
        System.out.println(allocate.position());
        System.out.println(allocate.limit());
        System.out.println(allocate.capacity());
        System.out.println("-------------------");

        //3.Buffer filp()为缓存区的界限设置为当前的值,并将当前位置设置为0,切换成:可读模式
        allocate.flip();
        System.out.println(allocate.position());
        System.out.println(allocate.limit());
        System.out.println(allocate.capacity());
        System.out.println("-------------------");
        char c = (char) allocate.get();
        System.out.println(c);
        System.out.println(allocate.position());
        System.out.println(allocate.limit());
        System.out.println(allocate.capacity());
    }
}

直接与非直接缓存区

什么是直接内存与非直接内存

根据官方文档的描述:

byte buffer可以是两种类型,**一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。**对于直接内存来说,JVM会将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要操作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理

从数据流的角度,非直接内存是下面这样的作业链:

本地IO -->直接内存–>非直接内存–>直接内存–>本地IO

而直接内存是:

本地IO–>直接内存–>本地IO

很明显,在做IO处理时,比如网络发送大量数据时, 直接内存会具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要 耗费更高的性能,不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以,当有大量的数据需要缓存,并且它的生命周青很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能替身,还是推荐直接使用堆内存。

字节缓存区是直接缓存区还是非直接缓冲区可以通过其isDirect()方法来确定。

使用场景

  • 有很大的数据需要存储,它的生命周期又很长。
  • 适合频繁的IO操作,比如网络并发场景。
//使用方法
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);

NIO核心之二:通道(Channel)

通道Channel概述

通道(Channel):由java.nio.channels包定义的。Channel表示IO源于目标打开的联机。Channel类似于传统的 “流”。只不过Channel 本身不能直接访问数据,Channel只能与Buffer进行交互。

  1. NIO的通道类似于流,但有 区别如下:
    • 通道可以同时进行读写,而流只能读或者只能写
    • 通道可以实现异步读写数据
    • 通道可以缓冲读数据,也可以写到数据到缓冲;
  2. BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO的通道(Channel)是双向的,可以读数据,也可以写数据
  3. Channel在NIO是一个接口
public interface Channel extends Closeable 

常见的Channel实现类

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

FileChannel类

获取通道的一种方式是对支持通道的对象调用getChannel()方法。支持通道的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket

获取通道的其他方式是使用Files类的静态方法 **newByteChannel()获取直接通道。**或者通过通道的静态方法open()打开并返回指定的通道

FileChannel常用方法

int read(ByteBuffer dst) 从 从  Channel 到 中读取数据到  ByteBuffer
long  read(ByteBuffer[] dsts) 将 将  Channel 到 中的数据“分散”到  ByteBuffer[]
int  write(ByteBuffer src) 将 将  ByteBuffer 到 中的数据写入到  Channel
long write(ByteBuffer[] srcs) 将 将  ByteBuffer[] 到 中的数据“聚集”到  Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

实例测试

本地写入数据

@Test
public void write() throws Exception {
    FileOutputStream out = new FileOutputStream("data.txt");
    FileChannel channel = out.getChannel(); //根据文件输出流获取通道
    ByteBuffer allocate = ByteBuffer.allocate(1024);//因为通道不能直接写入要接入缓存区
    allocate.put("你好,HIllCheung".getBytes());           //将内容放入缓存区
    allocate.flip();    //切换到写入模式
    channel.write(allocate);

}

本地读取数据

   @Test    public void read() throws  Exception{        FileInputStream in = new FileInputStream("data.txt");        FileChannel channel = in.getChannel();        ByteBuffer allocate = ByteBuffer.allocate(1024);        channel.read(allocate);              String s = new String(allocate.array(), 0, allocate.remaining());        System.out.println(s);    }

本地文件复制

@Testpublic void copy()throws  Exception{    FileInputStream in = new FileInputStream("data.txt");    FileOutputStream out = new FileOutputStream("copy_data.txt");    //获取响应的通道    FileChannel inChannel = in.getChannel();    FileChannel outChannel = out.getChannel();    //分配缓存区    ByteBuffer buffer =ByteBuffer.allocate(1024);    //读取数据    while (true){        //必须先把缓存区清空再写入缓存区        buffer.clear();        if(inChannel.read(buffer)==-1){            //等于-1说明读取完了            break;        }        //数据读取完后,下标落实到最后一位,此时要需要一个复位        buffer.flip();        outChannel.write(buffer);    }    inChannel.close();    outChannel.close();    System.out.println("ok");}

分散(Scatter)和聚焦(Gather)

分散读取(Scatter):是指把Channel通道的数据读入到多个缓存区中去

聚集写入(Gather):是指将多个Buffer中的数据"聚集"到Channel。

在这里插入图片描述

在这里插入图片描述

transferFrom()

从目标通道中去复制原通道数据

@Testpublic void transferTo() throws Exception {    //1.直接输入管道    FileInputStream in = new FileInputStream("1.txt");    FileChannel channel = in.getChannel();    //2.直接输出流管道    FileOutputStream fos = new FileOutputStream("data04.txt");    FileChannel fosChannel = fos.getChannel();    //3.复制    channel.transferTo(channel.position(),channel.size(),fosChannel);    channel.close();    fosChannel.close();}

有点像文件复制

在这里插入图片描述

在这里插入图片描述

总结

在这里插入图片描述

NIO选择三:选择器(Selector)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UpbjpTIH-1627542348311)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20210514102712873.png)]

  • Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
  • Selector能够坚持多个注册的通道上是否 事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有时间发生,便获取事件然后针对每个时间进行相应的处理。这样就可以志勇一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在 连接/通道 真正有读写时间发生时,才会进行读写,就大大地减少了系统开销,并且不必为每哥连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销。

选择器(Selector)的应用

创建Selector:通过调用Selector.open()方法创建一个Selector

Selector selector =Selector.open();

**向选择器注册通道:**selectableChannel.register(Selector sel ,int ops)

 @Test    public void test1() throws Exception {        //1.获取通道        ServerSocketChannel ssChanel = ServerSocketChannel.open();        //2.切换成非阻塞模式        ssChanel.configureBlocking(false);        //3.绑定连接        ssChanel.bind(new InetSocketAddress(9898));        //4.获取选择器        Selector selector = Selector.open();//       5.将通道注册到选择器上,并且指定 监听事件        ssChanel.register(selector, SelectionKey.OP_ACCEPT);    }

当调用register(Selector sel,int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数ops指定。可以监听的时间类型(可使用SelectionKey的四个常量表示):

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

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

Selector示意图和特点说明

**Selector可以实现:**一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的替身。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySkdhZ6w-1614494690634)(F:/typora/tupian/1614438987804.png)]

服务端流程(*)

  • 1. 当客户端连接服务端时,服务端会通过ServerSocketChannel得到SocketChannel:1.获取通道
ServerSocketChannel ssChanel = ServerSocketChannel.open();
  • **2.**切换为非阻塞模式

    //2.切换成非阻塞模式ssChanel.configureBlocking(false);
    
  • **3.**绑定连接

    //3.绑定连接ssChanel.bind(new InetSocketAddress(9898));
    
  • **4.**获取选择器

    //4.获取选择器Selector selector = Selector.open();
    
  • **5.**将通道注册到选择器上,并且指定"监听接收事件"

ssChanel.register(selector, SelectionKey.OP_ACCEPT);
  • **6.**轮询式的获取选择器上已经"准备就绪"的事件
   while (selector.select()>0){            System.out.println("轮一轮");//            获取当前选择器所有注册的“选择键”            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();            while (iterator.hasNext()){                SelectionKey sk = iterator.next();                if(sk.isAcceptable()){                    //若接受就绪,获取客户端连接                    SocketChannel sChannel = socketChannel.accept();                    sChannel.configureBlocking(false);                    sChannel.register(selector,SelectionKey.OP_READ);                }else if(sk.isReadable()){                    SocketChannel channel = (SocketChannel) sk.channel();                    //14.读取数据                    ByteBuffer buf = ByteBuffer.allocate(1024);                    int len =0;                    while ( (len=channel.read(buf))>0){                        buf.flip();                        System.out.println(new String(buf.array(),0,len));                        buf.clear();                    }                }                iterator.remove();            }        }

例子

需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。

public class NIO_Client {    public static void main(String[] args) throws Exception {        //获取通道        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));        //切换到非阻塞模式        sChannel.configureBlocking(false);        //3.分配指定大小的缓冲区        ByteBuffer buf = ByteBuffer.allocate(1024);        //4.发送数据给服务端        Scanner sc =new Scanner(System.in);        while (sc.hasNext()){            //要发送的内容            System.out.println(1);            String str = sc.nextLine();            buf.put(                    ((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis()))                    +"\n"+str).getBytes()            );            buf.flip();            sChannel.write(buf);            buf.clear();        }        sChannel.close();        sc.close();    }}
public class NIO_Server {
    public static void main(String[] args) throws Exception {
        //获取通道
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        //切换成非阻塞式
        socketChannel.configureBlocking(false);
        //绑定连接
        socketChannel.bind(new InetSocketAddress(9999));
        //获取选择器
        Selector selector =Selector.open();
        // 将通道注册到选择器上,并且“监听接收事件”
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //轮询式的获取选择器上已经 “准备就绪”的时间
        while (selector.select()>0){
            System.out.println("轮一轮");
//            获取当前选择器所有注册的“选择键”
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){

                SelectionKey sk = iterator.next();
                if(sk.isAcceptable()){
                    //若接受就绪,获取客户端连接
                    SocketChannel sChannel = socketChannel.accept();

                    sChannel.configureBlocking(false);
                    sChannel.register(selector,SelectionKey.OP_READ);
                }else if(sk.isReadable()){
                    SocketChannel channel = (SocketChannel) sk.channel();
                    //14.读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len =0;
                    while ( (len=channel.read(buf))>0){
                        buf.flip();
                        System.out.println(new String(buf.array(),0,len));
                        buf.clear();
                    }
                }
                iterator.remove();
            }
        }
    }
}

群聊系统

目标
需求:进一步理解 NIO 非阻塞网络编程机制,实现多人群聊

  • 编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
  • 服务器端:可以监测用户上线,离线,并实现消息转发功能
  • 客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值