NIO技术

本文详细介绍了Java的New IO (NIO) 模型,对比了NIO与传统的IO的区别,强调了NIO以块处理数据的特点。内容涵盖NIO的核心组件如Buffer、Channel和Selector,讲解了Buffer的创建、读写操作,以及Channel的使用。同时,还阐述了Selector在多路复用中的作用,展示了如何通过Selector实现服务器的并发处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是NIO

IO回顾

  • IO:Input Output(输入输出)
  • IO技术的作用:解决设备和设备之间的数据传输问题
  • IO的应用场景:图片上传、下载,打印机打印信息表、解析XML…

概念

  • 即Java new IO
  • 是一个全新的、JDK1.4后提供的 IO API
  • Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO

作用

  • NIO和IO有相同的作用和目的,但实现方式不同
  • 可替代标准Java IO的IO API
  • IO是以流的方式处理数据,而NIO是以块的方式处理数据

流与块的比较

  • NIO和IO最大的区别是数据打包和传输方式
  • IO是以流的方式处理数据,而NIO是以块的方式处理数据

面向流:IO是一次一个字节的处理数据,一个输入流产生一个字节,一个输出流就消费一个字节
面向块:IO系统以块的形式处理数据,每一个操作都在一步中产生或消费一个数据块。按块要比按流快得多。

新特性

对比于Java IO,NIO具备的新特征如下:

IONIO
面向流(Stream Oriented)面向缓冲区(Buffer Oriented)
阻塞IO (Blocking IO)非阻塞IO(Non Blocking IO)
选择器(Selectors)

阻塞非阻塞:当进程执行时,需要的数据还未就绪时,是否要处在一个等待状态

  • 可简单人为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
    面向流的I/O系统一次一个字节地处理数据
    一个面向块(缓冲区)的I/O系统以块的形式处理数据

核心组件

Java NIO的核心组件包括:

  • 管道(Channel)
  • 缓冲区(Buffer)
  • 选择器(selector)

在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据
Selector是因为NIO可以使用异步的非阻塞模式才加入的东西

简单理解:

  • Channel管道比作铁路,buffer缓冲区比作火车
    我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区来实现数据的处理
  • 要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区
  • Channel–>运输
    Buffer–>数据
    相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的

Buffer缓冲区

Buffer缓冲区概述

作用:缓冲区,用来存放具体要被传输的数据,比如:文件、socket等。这里将数据装入Buffer再通过通道进行传输
Buffer就是一个数组,用来保存不同数据类型的数据
在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应。

  • ByteBuffer:存储字节数据到缓冲区
  • ShortBuffer:存储字符串数据到缓冲区
  • CharBuffer:存储字符数据到缓冲区
  • IntBuffer:存储整数数据到缓冲区
  • LongBuffer:存储长整型数据到缓冲区
  • DoubleBuffer:存储小数到缓冲区
  • FloatBuffer:存储小数到缓冲区

对于Java中的基本数据类型,都有一个Buffer类型与之对应,最常用的是ByteBuffer类(二进制数据)

ByteBuffer的创建方式

  • 在堆中创建缓冲区:allocate(int capacity)
  • 在系统内存创建缓冲区:allocateDirect(int capacity)
  • 通过普通数组创建缓冲区:wrap(byte[] arr)
import java.nio.ByteBuffer;

public class Demo01Buffer{
    public static void main(String[] args) {
//        在堆中创建缓冲区:allocate(int capacity)
        ByteBuffer buffer1 = ByteBuffer.allocate(10);
//        在系统内存创建缓冲区:allocateDirect(int capacity)
        ByteBuffer buffer2 = ByteBuffer.allocateDirect(10);
//        通过普通数组创建缓冲区:wrap(byte[] arr)
        byte[] arr = {37,98,99};
        ByteBuffer buffer3 = ByteBuffer.wrap(arr);
    }
}

常用方法

读取缓冲区的数据/写数据到缓冲区中
缓冲区的核心方法就是:

  • put(byte b):给数组添加元素
  • get():获取一个元素
import java.nio.ByteBuffer;
import java.util.Arrays;

public class Demo02Buffer{
    public static void main(String[] args) {
        //1.创建出buffer对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);

        //put(byte b):给数组添加元素
        byteBuffer.put((byte) 10);
        byteBuffer.put((byte) 20);
        byteBuffer.put((byte) 30);
        //把缓冲区数组转换成普通数组
        byte[] array = byteBuffer.array();
//        打印
        System.out.println(Arrays.toString(array));
        
//        get():获取一个元素
        byte b = byteBuffer.get(1);
        System.out.println(b);
    }
}

Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息。它们是:

  • 容量Capacity
  • 缓冲区能够容纳的数据元素的最大数量。容量在缓冲区功能区创建时被设定,并且永远不会被改变。(不能被改变的原因也很简单,底层是数组)
    
  • 缓冲区中可以操作数据的大小,代表了当前缓冲区中一共有多少数据(从limit开始后面的位置不能操作)
    
  • 下一个要被读或写的元素位置。Position会自动由相应的get()和put()函数更新
    
  •   以上三个属性值之间有一些相对大小的关系:0 <= position <= limit <= capacity
    
  • 标记Mark
  • 一个备忘位置。用于记录上一次读写的位置
    

buffer代码演示

  • 首先展示一下是:创建缓冲区后,核心变量的值是怎么变化的
public class Demo02 {
    public static void main(String[] args) {
//        1.创建出buffer对象
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        System.out.println("初始化------>capacity---->"+byteBuffer.capacity());
        System.out.println("初始化------>limit---->"+byteBuffer.limit());
        System.out.println("初始化------>position---->"+byteBuffer.position());
        System.out.println("初始化------>mark---->"+byteBuffer.mark());

        System.out.println("--------------------------");
        //添加一些数据到缓冲区
        String s = "JavaEE";
        byteBuffer.put(s.getBytes());
        System.out.println("put后------>capacity------>"+byteBuffer.capacity());
        System.out.println("put后------>limit------>"+byteBuffer.limit());
        System.out.println("put后------>position------>"+byteBuffer.position());
        System.out.println("put后------>mark---->"+byteBuffer.mark());
        }
}

读取缓冲区的数据

//        创建一个limit()大小的字节数组
        byte[] bytes = new byte[byteBuffer.limit()];

//        将读取出来的数据装进字节数组中
        byteBuffer.get(bytes);

//        输出数据
        System.out.println(new String(bytes,0,bytes.length));

Channel通道

Channel通道概述

  • Channel表示IO源与目标打开的连接。类似于传统的“流”
  • 标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中

Channel API

  • 通道(Channel):由java.nio.channels包定义的
  • Java为Channel接口提供的最主要实现类如下:
  •            FileChannel:用于读取、写入、映射和操作文件的通道
    
  •            DatagramChannel:通过UDP读写网络中的数据通道
    
  •            SocketChannel:通过TCP读写网络中的数据
    
  •           ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel
    

FileChannel基本使用

  • 使用FileChannel完成文件的复制
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 FileChannel1 {
   public static void main(String[] args) throws IOException {
//      通道依赖于IO流
//      输入流
      FileInputStream fis = new FileInputStream("C:\\Users\\Administrator\\Desktop\\1.jpg");
//      输出流
      FileOutputStream fos = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\2.jpg");
//      使用流获取通道
      FileChannel f1 = fis.getChannel();
      FileChannel f2 = fos.getChannel();

//      创建缓冲数组
      ByteBuffer buffer = ByteBuffer.allocate(1024);

//      循环
      while (f1.read(buffer) != -1){
//         切换
         buffer.flip();
//         输出
         f2.write(buffer);
//         还原所有指针位置
         buffer.clear();
      }

      fos.close();
      fis.close();
   }
}

网络编程手法信息

  • 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Demo {
    public static void main(String[] args) throws IOException {
//        创建客户端
        SocketChannel sc = SocketChannel.open();
//        指定要连接的服务器ip和端口
        sc.connect(new InetSocketAddress("127.0.0.1",9000));

//        创建缓冲输出
        ByteBuffer buffer = ByteBuffer.allocate(1024);
//        给数组添加数据
        buffer.put("hhh".getBytes());
//        切换
        buffer.flip();
//        输出数据
        sc.write(buffer);
//        关闭资源
        sc.close();
    }
}

  • 服务器端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Demo服务器 {
    public static void main(String[] args) throws IOException {

//        创建服务器端对象,监听对应的端口
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//      绑定要监听的端口
        serverSocketChannel.bind(new InetSocketAddress(9000));

//        连接客户端
        SocketChannel socketChannel = serverSocketChannel.accept();

//        读取数据
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

//        读取到的字节长度
        int len = socketChannel.read(byteBuffer);

//        打印
        System.out.println(new String(byteBuffer.array(),0,len));
    }
}


BIO:同步阻塞
NIO:同步非阻塞(并发支持高)

Selector选择器 多路复用器

多路复用的概念

一个选择器可以同时监听多个服务器端口,帮多个服务器端口同时等待客户端的访问

Selector和Channel的关系

Channel和Buffer比较好理解,联系也比较密切,它们的关系简单来说就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中

选择器和它们的关系又是什么

选择器(Selector)是Channel(通道)的多路复用器,Selector可以同时监控多个通道的IO(输入输出)状况

Selector的作用是什么

负责监听事件和选择事件的对应通道
一个线程对应一个Selector,这个Selector对应3个Channel,每一个Channel下面对应一个Buffer。这个线程可以在3个Channel上来回切换,具体某一个时刻线程切换到哪是由事件决定的。

从底层来看,Selector允许单个线程处理多个Channel。仅用单个线程来处理多个Channel的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

可选择通道(Selectable Channel)

  • 注意:并不是所有的Channel,都是可以被Selector复用的。比方说:FileChannel就不能被选择器复用。
  • 判断一个Channel能被Selector复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能
  • SelectableChannel的结构如下图:
    在这里插入图片描述SelectableChannel类提供了实现通道的可选择性所需要的公共方法,是所有支持就绪检查的通道类的父类
通道和选择器注册之后,它们是绑定关系吗?
  • 不是一对一关系,一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次
  • 通道和选择器之间的关系,使用注册的方式完成。SelectableChannel乐意被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。
  • 在这里插入图片描述

Channel注册到Selector

  • 使用Channel.register(Selector sel , int ops)方法,将一个通道注册到一个选择器时。

     第一个参数:指定通道要注册的选择器是谁
     第二个参数:指定选择器需要查询的通道操作
    
  • 可以供选择器查询的通道操作,从类型来分,包括以下四种:

  •     (1)接收:SelectionKey.OP_ACCEPT(接收连接进行事件,表示服务器监听到了客户连接,那么服务器可以接收这个连接了)
        (2)连接:SelectionKey.OP_CONNECT(连接就绪事件,表示客户与服务器的连接已经建立成功)
        (3)可写:SelectionKey.OP_WRITE(写就绪事件,表示已经可以向通道写数据了)
        (4)可读:SelectionKey.OP_READ(读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了)
    

Selector对通道的多操作可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

Selector的使用流程

创建Selector

Selector对象是通过调用静态工厂方法open()来实例化的,如下

//获取Selector选择器
Selector selector = Selector.open();

将Channel注册到Selector

要实现Selector管理Channel,需要将Channel注册到相应的Selector上,如下:

//        获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9999));
//        设置为非阻塞**(与selector一起使用时,Channel必须要处于非阻塞模式下,如果是阻塞的,会抛出异常)
        serverSocketChannel.configureBlocking(false);

//        将通道注册到选择器上,制定监听时间为'接收'事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

上面通过调用通道的register()方法会将它注册到一个选择器上,首先需要注意的是:
与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException

轮训查询就绪操作

查询就绪操作:

  • 通过Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,保存在一个元素时SelectionKey对象的Set集合中
  • select()方法返回的int值,表示有多少通道已经就绪
  • 而一旦调用select()方法,并且返回值不为0时,通过调用Selector的selectedKeys()方法来访问已经选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作

NIO编程实例

服务器端

package selector;

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SelectorServer {
    public static void main(String[] args) throws IOException {
//        获取Selector选择器
        Selector s = Selector.open();
        
//        获取通道
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        ServerSocketChannel ssc3 = ServerSocketChannel.open();

        //        设置非阻塞      
        ssc1.configureBlocking(false);
        ssc2.configureBlocking(false);
        ssc3.configureBlocking(false);
//        绑定连接
        ssc1.bind(new InetSocketAddress(8000));
        ssc2.bind(new InetSocketAddress(9000));       
        ssc3.bind(new InetSocketAddress(10001));
        
        
//        将通道注册到选择器上,并注册的操作为:接收操作
        ssc1.register(s, SelectionKey.OP_ACCEPT);
        ssc2.register(s, SelectionKey.OP_ACCEPT);
        ssc3.register(s, SelectionKey.OP_ACCEPT);
        
        
//        采用轮询的方式,查询获取“准备就绪”的注册过的操作
        while (s.select() > 0){
//            获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
            Iterator<SelectionKey> selectionKeys = s.selectedKeys().iterator();
            while (selectionKeys.hasNext()){
                
//                获取“准备就绪”的事件
                SelectionKey selectionKey = selectionKeys.next();
                
//                获取ServerSocketChannel
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                
//                接收客户端发来的数据
                SocketChannel socketChannel = serverSocketChannel.accept();
                
//                读取数据
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int length = 0 ;
                while ((length = socketChannel.read(byteBuffer)) != -1){
                    byteBuffer.flip();
                    System.out.println(new String(byteBuffer.array(),0,length));
                    byteBuffer.clear();
                }
                socketChannel.close();
                
            }
        }
        
    }
}

客户端

package selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class SelectorClient {
    public static void main(String[] args) throws IOException {
//        创建客户端
        SocketChannel sc = SocketChannel.open();
//        指定要连接的服务器IP和端口
        sc.connect(new InetSocketAddress("127.0.0.1",9000));
//        创建缓冲输出
        ByteBuffer buffer = ByteBuffer.allocate(1024);

//        给数组添加数据
        buffer.put("hhhh".getBytes());

//        切换
        buffer.flip();
//        输出数据
        sc.write(buffer);
//        关闭资源
        sc.close();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值