栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java NIO --- 拉勾教育Java课程 学习笔记

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java NIO --- 拉勾教育Java课程 学习笔记

对 拉勾教育Java学习的 学习笔记(里面有很多都是拉勾课件里的内容,我在听课的时候添加了些东西。有很多内容都在代码中,导致代码上很乱。。慢慢改吧)

Java NIO学习
  • 1 什么是NIO
    • 1.1 标准IO回顾
    • 1.2 NIO
    • 1.3 NIO的作用
    • 1.4 流 和 块
    • 1.5 NIO新特性
      • 1.5.1 面向流(Stream Oriented)和面向缓冲区(Buff Oriented)
      • 1.5.2 阻塞(Blocking IO)和非阻塞(Non-Blocking IO)
      • 1.5.3 选择器(Selector)
    • 1.6 NIO的核心组件
  • 2 核心组件
    • 2.1 缓冲区(buffer)
      • 2.1.1 概念
      • 2.1.2 创建缓冲区的三种方式
      • 2.1.3 缓冲区常用方法
      • 2.1.4 核心属性
    • 2.2 Channel管道
      • 2.2.1 概念
      • 2.2.2 Channel API
      • 2.2.3 FIleChannel的基本使用
      • 2.2.4 网络编程中NIO Channel管道的使用
      • 2.2.5 accept阻塞问题的解决
  • 4 Selector选择器
      • 4.1 多路复用器的概念
      • 4.2 Selector与Channel的关系
      • 4.3 可选择通道(SelectableChannel)
      • 4.4 Channel如何注册到Selector
      • 4.5 选择键(SelectionKey)
      • 4.6 Selector的使用流程
        • 4.6.1 创建Selector
        • 4.6.2 将Channel注册到Selector
        • 4.6.3 轮询查询就绪操作
      • 5 拉勾教育课件里的代码 ---- NIO编程实例

1 什么是NIO 1.1 标准IO回顾

什么是IO?

  • IO:Input OutPut(输入 输出)
  • IO技术的作用:解决设备和设备之间数据传输的问题
  • IO使用在上传 下载 XML的解析…对数据的读写操作

Java程序中,对于数据的输入/输出操作 都是以“流”的方式进行的。java.io包下提供了各种“流”类的接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

1.2 NIO

Java New IO,JDK1.4后提供的新的 IO API,在Java API中提供了两套NIO:
一套是针对标准输入输出的NIO,另一套是网络编程的NIO。
NIO对于文件的读写速度快于标准IO,包括在大数据量涌入的情况下,NIO相对于标准IO,对于并发的支持要更好一些,NIO也是很多底层框架的实现。

1.3 NIO的作用

NIO与IO有这共同的作用和目的,都是对数据的处理,但是它们的实现方式不同:

  • IO是以 流 的方式处理数据
  • NIO是以 块 (缓冲区)的方式处理数据
1.4 流 和 块

面向流的IO是一个字节一个字节的处理数据,对于一个输入一次产生一个一个字节,那么一个输出流就一次消费一个字节
面向块的IO系统是以块的形式处理数据,每一个操作都在一步中产生消费一个数据块。

这就解释了为什么IO比NIO处理数据库的速度要快。标准IO在使用时就像是一个水龙头,一滴一滴的滴水,每次滴一滴,就处理一滴;而NIO则是我打开水龙头,“块(缓冲区)”就像个水桶,我快速的接满一桶水,然后对这这桶水进行处理。

1.5 NIO新特性

Blocking IO、Non Blocking IO、Selectors 是对网络编程的IO来说的

1.5.1 面向流(Stream Oriented)和面向缓冲区(Buff Oriented) 1.5.2 阻塞(Blocking IO)和非阻塞(Non-Blocking IO)

当进程执行时,需要的数据还未就绪时,是否要处在等待状态。如果是一直在等待数据,数据不就绪,我就一直等,那么这就是阻塞(Blocking IO);如果进程不是等待,还是在数据准备的这段时间去忙别的去了,那么就是非阻塞(Non-Blocking IO)。
例如:

1.5.3 选择器(Selector)

NIO可以使用异步非阻塞的模式,所以加入了Selector。下面单独讲

1.6 NIO的核心组件
  • 管道(Channel)
  • 缓冲区(Buffer)
  • 选择器(Selector):多路复用器

在上面我们说到NIO不是使用流的方式处理数据,而是以buffer缓冲区和Channel管道配合使用来处理数据。
Selector选择器则是因为NIO可以使用异步的非阻塞模式才加入的东西

buffer缓冲区和Channel管道是怎么配合使用并且完成处理的呢?与面向流的操作有什么区别呢?

1.面向流的IO,程序读取文件首先创建一个输入流(new FileInputSteam(“d://你好.jpg”)),然后就是对流进行一个处理,写入文件首先会创建一个输出流(new FileOutPutSteam(“d://你不好.jph”)),然后在进行写入。在面向流的操作中,就是一个单向的操作,要么去读,要么去写。

2.面向缓冲区的NIO,在对数据做处理时,就像是在中国的魔都上海和京都北京之间输送货物,我们把数据比作货物,把buffer缓冲区比作拉货的火车,Channel管道就是火车轨道了,我们把要运输的货物(数据)放在火车(buffer缓冲区)里,通过连接京都和魔都的火车轨道(Channel管道)进行运输,这样的话火车(buffer缓冲区)是可以在火车轨道(Channel管道)中来回走的,所以我们说这是一个双向的操作。

总结:

  • NIO中,对数据的处理,是通过以buffer缓冲区和Channel管道配合使用来处理数据。
  • Channel管道:不与数据进行交互,作用就是负责运算buffer缓冲区。搞运输的
  • buffer缓冲区:与数据直接进行交互。所有对数据的存取操作都是在对buffer缓冲区进行操作。搞数据的

相对于标准IO的流来说,对数据的操作就是单向的,要么读要么写。在NIO中基于Channel管道这么一个概念,对数据的读写就都是双向的。

2 核心组件 2.1 缓冲区(buffer) 2.1.1 概念

在上面提到的运输货物的例子中,缓冲区(buffer)被比作是火车,也就是装货物的。在Java里,buffef就是用来存放具体要被传输的数据,比如文件、socket等,将数据放入buffer内,然后在放入channel管道中运输。
在Java和各种开发语言中,一般都是用列表、数组等存放数据。Buffer缓冲区就是一个数组,它用来存放各类的数据。根据Java中的八种基本类型(byte、short、int、long、float、double、char、String、boolean),除了boolean类型除外(boolean只是存true和false),其他的都有相应的缓冲区,它们都继承自Buffer抽象类

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

其中用的最多的就是ByteBuffer类(二进制数据)

以下对于Buffer的方法和属性,都使用ByteBuffer类来进行举例

2.1.2 创建缓冲区的三种方式

代码演示:

import java.nio.ByteBuffer;
public class CreateBufferTest {
    public static void main(String[] args) {

        // 1.(常用,建议)在堆中创建缓冲区:allocate(int capacity)    capacity:容量
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);

        // 2.(了解)在系统内存中创建缓冲区:allocateDirect(int capacity)
        ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(10);

        // 3.(了解)通过普通数组创建缓冲区:wrap(byte[] arr)
        byte[] arr = {1,2,3};
        ByteBuffer byteBuffer3 = ByteBuffer.wrap(arr);

    }
}
2.1.3 缓冲区常用方法

核心方法有两个
put(byte b):给缓冲区数组添加元素
get(index) or get(byte[] dst):根据索引获取缓冲区数组中的元素

import java.nio.ByteBuffer;
import java.util.Arrays;
public class BufferMethodsTest {
    public static void main(String[] args) {
        // 1.创建一个ByteBuffer缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        // 2.向缓冲区里添加元素  put(byte b)
        byteBuffer.put((byte)1);
        byteBuffer.put((byte)2);
        byteBuffer.put((byte)3);

        // 3.打印ByteBuffer缓冲区内所有数据 byte[] byteArr = byteBuffer.array()
        System.out.println(Arrays.toString(byteBuffer.array()));

        // 4.通过下标索引获取缓冲区内的数据   get(index) 
        byte b  = byteBuffer.get(1);
        System.out.println("byteBuffer[1] = " + b);
    }
}

输出结果:

[1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
byteBuffer[1] = 2

2.1.4 核心属性

在Buffer抽象类中有四个属性变量,我们进去Buffer的代码中可以看到,这4个核心变量属性提供了关于其所包含的数组的信息

  • capacity: 容量,也就是缓冲区能够容纳数据元素的最大数量,容量在缓冲区创建时被设定(allocate(int capacity)),并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
  • limit: 界限(限制),它规定了缓冲区内可以被操作的数据的大小,代表着当前缓冲区内有多少数据是可以被操作的,从limit开始,后面的数据无法被操作。
  • position: 位置,下一个要被读或写的位置。该属性会随着get()和put()方法的使用而改变
  • mark: 标记,用于记录上一次被读写的位置

接下来我们从代码中来学习属性中的一些问题:(这里主要介绍了当对缓冲区进行操作时,缓冲区属性值的变换,以及 flip() 方法和 clear() 方法的使用,对缓冲区读还是写状态的切换。)

注:为了方便自己学习,代码中写了很多注释,基本的知识点都在里面了,但是看起来太多了就很烦,所以在看的时候,主要是看属性值的改变,以及flip() 和 clear()的使用

import java.nio.ByteBuffer;
public class BufferAttributeTest {
    public static void main(String[] args) {
        // 1.创建一个ByteBuffer缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        // 2.输出查看buffer属性值
        System.out.println("初始--容量capacity  = " + byteBuffer.capacity());  // 10
        System.out.println("初始--界限limit  = " + byteBuffer.limit());     // 10
        System.out.println("初始--位置position  = " + byteBuffer.position());  // 0
        System.out.println("初始--标记mark  = " + byteBuffer.mark());      // java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
        System.out.println("=====================================================================");
        // 3.问题一:当我们进行put操作时,buffer属性值有什么变化
        // // 使用put添加一些数据元素
        String str = "puffHub";
        byteBuffer.put(str.getBytes());
        // // 输出查看使用put操作后的属性值,发现 position 和 mark 的值发生改变
        // // limit的值并没有发生改变,但这并不意味着其他的方法在操作时limit的值不会发生改变  它也是会改变的
        System.out.println("put后--容量capacity  = " + byteBuffer.capacity());  // 10
        System.out.println("put后--界限limit  = " + byteBuffer.limit());     // 10
        System.out.println("put后--位置position  = " + byteBuffer.position());  // 7
        System.out.println("put后--标记mark  = " + byteBuffer.mark());      // java.nio.HeapByteBuffer[pos=7 lim=10 cap=10]
        System.out.println("=====================================================================");


        // 4.问题二:上面的操作是写入操作,且position的值已经为 7 了,也就是说下一次操作无论是读还是写,
        // // 都是要从 7 这个位置开始了,但是我们并不想这样  因为 7 以后是没有值的,我们想从 0 的位置开始读,
        // // 并且读取的内容是"puffHub",该怎么去读呢?
        // // 从上面的叙述中可以得出对缓冲区的操作都是从position到limit,所以我们只需要改变position和limit的值就好了。
        // // 这里我们用到 NIO 中给的flip()方法
        byteBuffer.flip();  // 切换成“读”模式
        // // flip之后,缓冲区中limit和position的值发生了相应的改变
        // // 变化一:limit变成position的值  变化二:position的值变成 0
        System.out.println("flip后--容量capacity  = " + byteBuffer.capacity());  // 10
        System.out.println("flip后--界限limit  = " + byteBuffer.limit());     // 7
        System.out.println("flip后--位置position  = " + byteBuffer.position());  // 0
        System.out.println("flip后--标记mark  = " + byteBuffer.mark());      // java.nio.HeapByteBuffer[pos=0 lim=7 cap=10]

        // 5.数据的读取
        // // 创建一个byte数组,长度为limit(因为我要在缓冲区里读的数据就是这么长,所以就用这个)
        byte[] bytes = new byte[byteBuffer.limit()];
        // // 将读出的数据放在字节数组中 get(byte[] dst)
        byteBuffer.get(bytes);
        // // 打印
        System.out.println(new String(bytes, 0, bytes.length));
        System.out.println("=====================================================================");
        // // get方法也会改变position的值
        System.out.println("get后--容量capacity  = " + byteBuffer.capacity());  // 10
        System.out.println("get后--界限limit  = " + byteBuffer.limit());     // 7
        System.out.println("get后--位置position  = " + byteBuffer.position());  // 7
        System.out.println("get后--标记mark  = " + byteBuffer.mark());      // java.nio.HeapByteBuffer[pos=7 lim=7 cap=10]
        System.out.println("=====================================================================");
        // 6.读取到数据后,如果还想要返回一个写的状态,我们可以调用clear()方法,清空缓冲区
        // // clear()方法的作用是重置缓冲区,核心属性回归到写的模式,重置核心属性回到创建时的值
        // // !!而且!!缓冲区内的值还是存在的!!但是是被遗忘的,就是说无法通过get方法读取到。
        byteBuffer.clear();
        System.out.println("clear后--容量capacity  = " + byteBuffer.capacity());  // 10
        System.out.println("clear后--界限limit  = " + byteBuffer.limit());     // 10
        System.out.println("clear后--位置position  = " + byteBuffer.position());  // 0
        System.out.println("clear后--标记mark  = " + byteBuffer.mark());      // java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]

        // // 这里我们可以确定  原有的值确实是存在的,只不过读取出来的是原有值的ASCII码
        byte b = byteBuffer.get(2);
        System.out.println(b);  // 102 ==> f
    }
}
2.2 Channel管道 2.2.1 概念

由 java.nio.channels 包定义的 Channel管道,类似传统IO中的“流(Stream)”,但又和流有很大的区别。
标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中(白话: 就是数据传输用的通道,作用是打开到IO设备的连接,文件、套接字都行)

区别Channelstream
是否支持异步支持不支持
是否可双向传输数据可以,既可以从通道读取数据,也可以向通道写入数据不能,只能单向
是否结合Buffer使用必须结合Buffer使用不需要
性能较高较低
2.2.2 Channel API

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

FileChannel主要是对本地资源的处理,其余三个 是对网络资源的处理

2.2.3 FIleChannel的基本使用
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTest {
    public static void main(String[] args) throws IOException {

        // 结合标准IO,通道依赖于IO流.创建文件输入输出流
        FileInputStream fileInputStream = new FileInputStream("D:\img\image-20210304173004802.png");
        FileOutputStream fileOutputStream = new FileOutputStream("D:\\img\副本.png");

        // 通过IO流获取Channel通道
        FileChannel fileChannel1 = fileInputStream.getChannel();
        FileChannel fileChannel2 = fileOutputStream.getChannel();

        // 创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        while (fileChannel1.read(byteBuffer) != -1){
            // 切换读写状态(更改buffer核心元素数据,指针位置)
            byteBuffer.flip();
            // 输出(这里存在两个问题)
            // 第一个:输出一次后,position位置会移到最后,我们知道,无论读还是写,操作的都是从position到limit中间的数据元素
            // // 循环第一次输出后position位置移到最后,那接下来的循环就无法读取到数据,且条件永远不会等于 -1 就会造成死循环
            // // 所以我们需要通过clear方法重置position的位置。
            // 第二个:在我们最后一次输出数据的时候,因为我们给定缓冲区的大小是固定的,但是文件的大小有区别,所以当最后一次输出时
            // // 1024个字节的大小不一定会刚刚好全部占满,这时候,只是用Clear重置位置会导致最后一次输出的时候,输出一些空的字节
            // // 所以我们需要在每次输出缓冲区内的数据前,调用flip()方法(前面的代码讲到了该方法的作用以及对核心元素的影响)
            fileChannel2.write(byteBuffer);
            // 还原指针位置(更改buffer核心元素数据,指针位置)
            byteBuffer.clear();
        }
        // 关闭流
        fileOutputStream.close();
        fileInputStream.close();
    }
}
2.2.4 网络编程中NIO Channel管道的使用
  • 客户端
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws IOException {

        // 1.创建对象
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",9999));
        // 2.创建字节缓冲区 并 设置数据 输出
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("你好,世界".getBytes());
        // 一定要重视flip()方法的使用,这里如果没有该方法,服务器端就会读取到很多空字节
        byteBuffer.flip();
        socketChannel.write(byteBuffer);

        socketChannel.close();

        
    }
}
  • 服务端
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NIOServer {
    public static void main(String[] args) throws IOException {

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

        // 2.连接客户端,目前还是阻塞状态
        SocketChannel socketChannel = serverSocketChannel.accept();

        // 3.读取数据
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int len = socketChannel.read(byteBuffer);
        // 这里再服务端进行读取的时候,并没有用到flip()方法或者是clear()方法,但是却没有读取到空字节
        // 原因是,len 是 读取到的数据长度   客户端发送多少,len就是多少
        System.out.println(new String(byteBuffer.array(),0,len));


        
    }
}
2.2.5 accept阻塞问题的解决

设置非阻塞的方法 ServerSocketChannel.configureBlocking(false)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class NonBlockingIOServer {
    public static void main(String[] args) throws IOException, InterruptedException {

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

        // 设置非阻塞
        serverSocketChannel.configureBlocking(false);

        while (true){
            // 2.连接客户端 , accept不会处于阻塞状态
            // 如果连接成功就是sc对象,如果没有连接就是sc = null( 解决空指针异常 )
            SocketChannel socketChannel = serverSocketChannel.accept();
            if(serverSocketChannel != null){
                // 3.读取数据
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                int len = socketChannel.read(byteBuffer);
                System.out.println(new String(byteBuffer.array(),0,len));
                break;
            } else {
                // 没有客户访问
                System.out.println("因为处于非阻塞状态,此时可以去忙别的事去了");
                Thread.sleep(3000);
            }
        }
    }
}
4 Selector选择器

Selector选择器,又被称为多路复用器

4.1 多路复用器的概念

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

非多路复用: 服务器如果想同时监听多个客户端,需要开启相同数量的线程去监听每一个客户端。比较占用系统资源,线程之间的切换会对操作系统造成很高的代价。
多路复用:一个线程可以同时监听多个端口(通过Select多路复用器来实现)。节省系统资源。

4.2 Selector与Channel的关系

Selector选择器 是 channel通道 的 多路复用器,可以通过Selector同时监听多个通道IO(输入输出)的情况
他们之间是 注册 的关系

Selector的作用是什么?

负责监听事件和选择事件的对应通道。

选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。可以只用一个线程来回切换,处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

4.3 可选择通道(SelectableChannel)

注意:并不是所有的通道Channel都会被Select复用,比如FileChannel。
Java定义了一个SelectableChannel的抽象类,只有继承了这个抽象类的Channel,才会被Select复用
SelectableChannel抽象类,提供了实现通道的可选择性所需要的公共方法

在这里要注意的是,通道和选择器并不是绑定一 一对应的,他们并不是一对一的关系,同一个通道可以被多个选择器选择,但对于选择器而言,同一个选择器只能被注册一次。

通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。

4.4 Channel如何注册到Selector

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

第一个参数Selector sel:指定通道要注册的选择器是谁(一个Channel可以注册到多个选择器上,所以要指定选择器)

第二个参数int ops:指定选择器需要查询的通道操作 (选择器对通道的那些就绪行为感兴趣)

可以供选择器查询的通道操作,从类型来分,包括以下四种:

(1)可读 : SelectionKey.OP_READ
(读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了)

(2)可写 : SelectionKey.OP_WRITE
(写就绪事件,表示已经可以向通道写数据了)

(3)连接 : SelectionKey.OP_CONNECT
(连接就绪事件,表示客户端与服务器的连接已经建立成功)

(4)接收 : SelectionKey.OP_ACCEPT
(接收连接进行事件,表示服务器监听到了客户连接,服务器就可以接收这个连接了)

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

在调用register方法的时候,可以做到注册到Selector选择器,也可以指定通道操作,
但是对于注册的这个selector选择器来说,它是怎么知道我们指定的事件什么时候会处于就绪状态呢?

这里有个问题是,虽然我们指定了监听事件,但这并不意味着selector选择器就可以自动的监听了,也就是说
selector选择器并不会去自动的监听指定的事件,而是selector要去调用方法去查询它感兴趣的事件有没有发生。
4.5 选择键(SelectionKey)

Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。

Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。

select()			:选择器等待客户端连接的方法
					阻塞问题:
						1.在开始没有客户访问的时候是阻塞的
						2.在有客户来访问的时候方法会变成非阻塞的
						3.如果客户的访问被处理结束之后,又会恢复成阻塞的
						
selectedKeys()		:选择器会把被连接的服务端对象放在Set集合中,这个方法就是返回一个Set集合
4.6 Selector的使用流程 4.6.1 创建Selector

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

 		 // 1、获取Selector选择器
            Selector selector = Selector.open();
4.6.2 将Channel注册到Selector

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

     		 // 2、获取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            // 3.设置为非阻塞
            serverSocketChannel.configureBlocking(false);

            // 4、绑定连接
            serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

            // 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
            serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

上面通过调用通道的register()方法会将它注册到一个选择器上。

首先需要注意的是:

与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException

4.6.3 轮询查询就绪操作

万事俱备,下一步是查询就绪的操作。

通过Selector的 select() 方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。

select()方法返回的int值,表示有多少通道已经就绪

而一旦调用select()方法,并且返回值不为0时,下一步该干啥?

通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作:

整个代码的使用(乱糟糟的 不推荐看,可以看再看后面的代码):

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

public class SelectorServer {
    public static void main(String[] args) throws IOException {

        // 小目标:通道注册到选择器上

        // 1. 获取selector选择器
        Selector selector = Selector.open();
        // 2.获取通道对象
        ServerSocketChannel serverSocketChannel1 = ServerSocketChannel.open();
        ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open();
        ServerSocketChannel serverSocketChannel3 = ServerSocketChannel.open();

        serverSocketChannel1.bind(new InetSocketAddress(9999));
        serverSocketChannel2.bind(new InetSocketAddress(8888));
        serverSocketChannel3.bind(new InetSocketAddress(7777));
        // 3.※设置为非阻塞,(与selector同时使用时,channel必须处在非阻塞状态下,不然会抛出异常)
        serverSocketChannel1.configureBlocking(false);
        serverSocketChannel2.configureBlocking(false);
        serverSocketChannel3.configureBlocking(false);
        // 4.完成注册操作;并且指定选择器需要查询的通道操作,也就是制定 监听事件 为 “接收事件”
        serverSocketChannel1.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannel2.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannel3.register(selector, SelectionKey.OP_ACCEPT);

        // 5.select()  查询已经就绪的通道操作  返回值:表示有多少通道已经就绪  0:就是没有就绪的通道操作
        // 调用后会进入阻塞状态,至少有一个通道上的操作就绪了,才会解除阻塞状态
//        int select = selector.select();
//        // 没有就绪的通道,就会处于阻塞状态,下面的代码就不会执行
//        System.out.println(select);
        // 6.通过轮询查询已经就绪的通道操作(SelectionKey)
        while (selector.select() > 0){
            // 集合中就是所有已经准备就绪的操作(事件)
            Set keySet = selector.selectedKeys();
            // 遍历上面集合,去判断SelectionKey的类型是哪一种(READ WRITE ACCEPT CONNECT),然后分别进行处理。
            Iterator selectionKeys = keySet.iterator();
            while (selectionKeys.hasNext()){
                // 已经准备就绪的事件
                SelectionKey selectionKey = selectionKeys.next();

                // 判断SelectorKey的类型
                if(selectionKey.isAcceptable()){
                    // 如果为ACCEPT类型
                    // SelectableChannel channel = selectionKey.channel();
                    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len = socketChannel.read(byteBuffer);
                    System.out.println(new String(byteBuffer.array(),0,len));

                    // 资源的关闭
                    socketChannel.close();

                }else if(selectionKey.isConnectable()){
                    return;
                }else if(selectionKey.isReadable()){
                    return;
                }else {//selectionKey.isWritable()
                    return;
                }
            }
            // 移除选择键
            selectionKeys.remove();

        }
        serverSocketChannel3.close();
        serverSocketChannel2.close();
        serverSocketChannel1.close();
    }
}
5 拉勾教育课件里的代码 ---- NIO编程实例

代码不要看上边那个 还是看这个吧,这个更清楚一些

客户端:

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("拉勾教育".getBytes());

        //切换
        buffer.flip();

        //输出数据
        sc.write(buffer);

        //关闭资源
        sc.close();
    }

服务端

package com.lagou.selector;

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

public class Demo服务端 {
    public static void main(String[] args) throws IOException {
        //创建服务端对象
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        ssc1.bind(new InetSocketAddress(8000));
        //设置非阻塞
        ssc1.configureBlocking(false);

        //创建服务端对象
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        ssc2.bind(new InetSocketAddress(9000));
        ssc2.configureBlocking(false);

        //创建服务端对象
        ServerSocketChannel ssc3 = ServerSocketChannel.open();
        ssc3.bind(new InetSocketAddress(10001));
        ssc3.configureBlocking(false);

        //创建选择器对象
        Selector s = Selector.open();

        //两个服务器都要交给选择器来管理
        ssc1.register(s, SelectionKey.OP_ACCEPT);
        ssc2.register(s, SelectionKey.OP_ACCEPT);
        ssc3.register(s, SelectionKey.OP_ACCEPT);

        //获取集合
        //selectedKeys() :返回集合,集合作用存放的是被连接的服务对象的key
        Set set = s.selectedKeys();

        System.out.println("集合中元素的个数: " + set.size());  //0(没有服务端被访问的时候显示0)

        //select():这是选择器连接客户端的方法
        s.select();

        System.out.println("集合中元素的个数: " + set.size());  //1(有一个服务端被访问的时候显示1)
    }
}
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;

public class Selector服务端 {

    public static void main(String[] args) throws IOException {

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

        // 2、获取通道
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        ServerSocketChannel ssc3 = ServerSocketChannel.open();

        // 3.设置为非阻塞
        ssc1.configureBlocking(false);
        ssc2.configureBlocking(false);
        ssc3.configureBlocking(false);

        // 4、绑定连接
        ssc1.bind(new InetSocketAddress(8000));
        ssc2.bind(new InetSocketAddress(9000));
        ssc3.bind(new InetSocketAddress(10000));


        // 5、将通道注册到选择器上,并注册的操作为:"接收"操作
        ssc1.register(selector, SelectionKey.OP_ACCEPT);
        ssc2.register(selector, SelectionKey.OP_ACCEPT);
        ssc3.register(selector, SelectionKey.OP_ACCEPT);


        // 6、采用轮询的方式,查询获取"准备就绪"的注册过的操作
        while (selector.select() > 0) {
            // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
            Iterator selectedKeys = selector.selectedKeys().iterator();
            while (selectedKeys.hasNext()) {

                // 8、获取"准备就绪"的事件
                SelectionKey selectedKey = selectedKeys.next();

                    // 9、获取ServerSocketChannel
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectedKey.channel();
                // 10、接受客户端发来的数据
                SocketChannel socketChannel = serverSocketChannel.accept();

                // 11、读取数据
                    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();
                }
                // 12、移除选择键
                selectedKeys.remove();
            }
        // 13、关闭连接
       	 ssc1.close();
       	 ssc2.close();
      	 ssc3.close();
        }

}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/532274.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号