Java网络编程(4)NIO的理解与NIO的三个组件

前言

前面通过Socket实现了一个简单的聊天系统,且对Socket进行了一定的了解
Java网络编程(3)Socket实现一个简单的聊天系统
而前面的Socket都是通过IO实现的
现在来系统的了解IO与NIO

目录

  1. Java的IO演变
    1.1. BIO
    1.2. 伪异步IO
    1.3. NIO
    1.4. AIO
  2. NIO结构
    2.1. 缓冲区Buffer
    2.2. 通道Channel
    2.3. 多复用选择器Selector
  3. 缓冲区操作
    3.1. ByteBuffer
  4. 通道Channel
    4.1. 常用操作
  5. 缓冲区与通道:分散、聚集
    5.1. 案例
  6. 选择器Selector
    6.1. 常用方法
    6.2. Selector的使用
    6.3. Selector案例
  7. 总结

Java的IO演变

BIO

在jdk1.4之前,Java的Socket通信都是通过同步阻塞模式BIO(block-IO)

同步阻塞式模式在应用时性能和可靠性是非常差的
在前面的应用也可以看出:因为是阻塞式,一个线程只能实现一个通信,在高并发会消耗太多资源
在这里插入图片描述
一客户端一线程形式

伪异步IO

前面使用线程池完成多客户端连接服务器,就是这种伪异步IO

		//创建线程池:限定最多50个线程
        ExecutorService executor= Executors.newFixedThreadPool(50);
        while (true) {
            //接受连接,创建socket
            Socket socket = serverSocket.accept();

            System.out.println("IP地址:" + socket.getLocalAddress());
            //线程
            Runnable runnable=()->{
                try {
                    BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
                    String str=null;
                    while ((str=reader.readLine())!=null){


                        System.out.println(str);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            };

            executor.submit(runnable);

        }

在这里插入图片描述
JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理,有一定的效率但本质上还是BIO

线程池的运用有一定的弊端:当网络传输慢会阻塞线程,阻塞的线程过多会影响线程池效率甚至崩溃

NIO

为了解决网络通信问题,jdk1.4推出了非阻塞模式NIO

NIO可以称New IO,也可以称Non-block IO

NIO在Java代码提供了高速的、面向块的IO,提供了很多API和类库

在这里插入图片描述

AIO

jdk1.7提供了异步非阻塞IO - AIO,支持文件的异步IO操作和针对网络套接字的异步操作等等

在这里插入图片描述
一步步来,慢慢理解

NIO结构

NIO:非阻塞式IO,可以称为New IO,或者Non-block IO
NIO是封装了IO,是IO的加强版
在这里插入图片描述
在这里插入图片描述
它与BIO不同在与通道Channel与缓冲区Buffer、多复用选择器Selector三个重要组件

缓冲区Buffer

缓冲区Buffer是一个对象,包含了一些要写入读出的数据

以前的IO是面向流,通过直接流读写数据
在这里插入图片描述

Java程序直接读出或写入流就可以通信了

当然现在的IO也有缓冲,例如BufferedReader等,这些都是NIO重新实现过了

在NIO库中,所有的数据都是缓冲区处理,缓冲区实质上是一个数组,常用的是字节数组ByteBuffer

所有的缓冲区类型都继承于抽象类Buffer,对于Java中的基本类型,都有一个具体Buffer类型与之相对应(除了Boolean类型)
在这里插入图片描述

通道Channel

在Java NIO中,通道是在实体和字节缓冲区之间有效传输数据的媒介
在这里插入图片描述

通道在实体与缓冲区之间,通过通道来读取、写入数据

通道的作用于流相似,但不同的是通道是双工的,可以同时进行读、写

和传统IO分为 File IO与Stream IO类似,NIO有两种类型的通道:文件通道(file)和套接字通道(socket)

多复用选择器Selector

多复用选择器Selector是NIO编程的重点
选择器用于使用单个线程处理多个通道,它会轮询注册在其上的通道,确定哪个通道准备好通信,通过SelectionKey获得就绪Channel的集合,然后进行IO操作
选择器只能管理非阻塞的通道

在这里插入图片描述
这就比伪异步IO的线程池方便多了,通过选择器单线程即可处理多个Channel

缓冲区操作

在这里插入图片描述
所有缓冲区类型继承抽象类Buffer,大部分缓冲区类型的操作都类似,仅学习一下最常用的ByteBuffer的操作(能与channel交互的只有ByteBuffer

ByteBuffer

实例化:
Buffer、ByteBuffer等类都是抽象类
在这里插入图片描述
抽象类无法实例化
ByteBuffer提供了四个静态工厂方法得到ByteBuffer实例
在这里插入图片描述
在这里插入图片描述
这四个方法:

  • allocate(int capacity)
    堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器(HeapByteBuffer实例)

  • allocateDirect(int capacity)
    是不使用JVM堆栈而是通过操作系统来创建内存块用作缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并长期存在,或者需要经常重用时,才使用这种缓冲区

  • wrap(byte[] array)
    这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方。其实ByteBuffer底层本来就有一个bytes数组负责来保存buffer缓冲区中的数据,通过allocate方法系统会帮你构造一个byte数组(本质也是HeapByteBuffer实例)

  • wrap(byte[] array, int offset, int length)
    上一个方法的基础上可以指定偏移量和长度,这个offset也就是包装后byteBuffer的position,而length呢就是limit-position的大小,从而我们可以得到limit的位置为length+position(offset)

 ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
 ByteBuffer byteBuffer1=ByteBuffer.allocateDirect(1024);
 ByteBuffer byteBuffer2=ByteBuffer.wrap(new byte[]{});
 ByteBuffer byteBuffer3=ByteBuffer.wrap(new byte[]{},0,100);

get方法:
四种参数四种get方法:

  • get():相对方法,读取当前位置的byte,然后position +1
  • get(byte[] dst):相对体积方法,将此缓冲区传输到给定的目标数组中的字节数
  • get(byte[] dst, int offset, int length) :从当前位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域
  • get(int index) : 绝对方法,从指定下标开始读取
    在这里插入图片描述

其他方法:

asIntBuffer()等:输入的数据可能是其他类型,可以使用这类方法将ByteBuffer转化成想要的类型
flip():翻转
put():放置
等等

通道Channel

channel类继承结构:
在这里插入图片描述
有很多种channel,分为两种类型:文件通道、套接字通道
常用的有:

  • FileChannel:用于读取、写入、映射和操作文件的通道
  • DatagramChannel:读写UDP通信的数据,对应DatagramSocket类
  • SocketChannel:读写TCP通信的数据,对应Socket类
  • ServerSocketChannel:监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类(服务器)
  • ScatteringByteChannel和GatheringByteChannel:分散聚集通道,由操作系统完成
  • WritableByteChannel和ReadableByteChannel:接口提供读写API

常用操作:

  • 实例化:通道可以使用流的getChannel()方法创建,JDK1.7 中的NIO2针对各个通道提供了一个静态的方法open(),JDK1.7 中的NIO2的Files工具类的newByteChannel()
  • isOpen():Channel自带的方法,告诉这个通道是否打开
  • close: Channel自带的方法,关闭通道
  • read() : Channel大部分子类拥有的方法,从通道读取数据到缓冲区,不同的参数有不同的作用,FileChannel有四种read方法
    在这里插入图片描述
  • write():Channel大部分子类拥有的方法,从缓冲区写入数据到通道,FileChannel有四种write方法
    在这里插入图片描述

缓冲区与通道:分散、聚集

前面知道了通道类似与流,缓冲区暂时保存数据
那么程序与实体间数据交流就是通过缓冲区与通道的分散读取、聚集写入

分散读取:将数据从通道中读取到多个缓冲区(read方法)
在这里插入图片描述
聚集写入:将多个缓冲区的数据写入到单个通道中(write方法)
在这里插入图片描述有两个专门的接口就是实现聚集写入与分散读出:ScatteringByteChannel和GatheringByteChannel

FileChannel实现了这两个接口
在这里插入图片描述

案例

聚集写入文件,分散读出文件

package com.company.ScatterGather;

import java.io.FileInputStream;
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.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;

public class ScatterGatherIO {
    //聚集写入
    public static void Gather(String data) throws FileNotFoundException {
        //创建两个ByteBuffer存数据
        ByteBuffer byteBuffer1=ByteBuffer.allocate(20);
        ByteBuffer byteBuffer2=ByteBuffer.allocate(400);
        //把整数放入byteBuffer1
        byteBuffer1.asIntBuffer().put(1024);
        //把输入的String变量放入byteBuffer2
        byteBuffer2.asCharBuffer().put(data);
        //GatheringByteChannel接口允许委托操作系统完成任务
        //CreatChanner使用文件写入流
        GatheringByteChannel gatherChannel=CreatChanner("TestOut.txt",true);
        //聚集写入通道
        try {
            //write只允许一个ByteBuffer
            gatherChannel.write(new ByteBuffer[]{byteBuffer1,byteBuffer2});
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    //分散写出
    public static void Scatter() throws FileNotFoundException {
        //创建两个ByteBuffer存数据
        ByteBuffer byteBuffer1=ByteBuffer.allocate(20);
        ByteBuffer byteBuffer2=ByteBuffer.allocate(400);
        //读取文件通道
        ScatteringByteChannel scatterChannel=CreatChanner("TestOut.txt",false);

        try {
            scatterChannel.read(new ByteBuffer[]{byteBuffer1,byteBuffer2});
        } catch (IOException e) {
            e.printStackTrace();
        }
        //buffer位置置0
        byteBuffer1.rewind();
        byteBuffer2.rewind();

        System.out.println(byteBuffer1.asIntBuffer().get());
        System.out.println(byteBuffer2.asCharBuffer().toString());
    }



    //输入文件地址和输入方向,决定通道方向
    public static FileChannel CreatChanner(String fileUrl,boolean out) throws FileNotFoundException {
        FileChannel fileChannel=null;
        if (out){
            fileChannel=new FileOutputStream(fileUrl).getChannel();
        }
        else
            fileChannel=new FileInputStream(fileUrl).getChannel();

        return fileChannel;
    }

    public static void main(String[] args) throws FileNotFoundException {
        String data="hello,welcome to ScatterGatherIO";
        Gather(data);
        Scatter();
    }
}

在这里插入图片描述

上面展示将通道与缓冲区的使用

选择器Selector

在这里插入图片描述
选择器让一个线程能够处理多个通道,选择器轮询注册在其上的通道,Selector只能管理非阻塞的通道,文件通道(FileChannel等等)是阻塞的,无法管理

常用方法

在这里插入图片描述

  • open():Selector是抽象类,实例化要通过Selector.open()方法
    在这里插入图片描述
  • select():选择一组键,该通道为IO操作准备,这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件,当这个方法返回后, 当前线程就可以处理 Channel 的事件(返回int型数据,大于0即有多少个通道就绪)
  • selectedKeys():返回准备好的通道集合,返回值是Set< SelectionKey>集合型,SelectionKey是就绪通道的标识
  • wakeup():唤醒在select()方法中阻塞的线程

Selector的使用

在这里插入图片描述

案例

服务器:

package com.company.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;

public class Server {
    public static void main(String[] args) throws IOException {
        //打开ServerSocketChannel通道,等待连接
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置非阻塞
        serverSocketChannel.configureBlocking(false);
        //绑定端口号
        serverSocketChannel.bind(new InetSocketAddress(8080));
        //打开选择器
        Selector selector = Selector.open();
        //将通道注册到选择器上,监听接收事件

        serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
        //轮询式的获取选择器上已经‘准备就绪’的事件
        while (selector.select()>0){
            //获取当前选择器中所有注册的"选择健(已就绪的监听事件)"
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                //SelectionKey表示注册的标识
                SelectionKey selectionKey = iterator.next();
                //判断具体事件,就绪
                if (selectionKey.isAcceptable()){
                    //serverSocketChannel接受客户端连接,返回SocketChannel通道
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //设置非阻塞
                    socketChannel.configureBlocking(false);
                    //将客户端通道注册到选择器上
                    //OP_READ表示通道可读
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else if (selectionKey.isReadable()){
                    //获取当前选择器上“读就绪”状态的通道
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    //读取客户端传过来的数据
                    int len = 0;
                    while ((len = socketChannel.read(buffer))>0){
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,len));
                        buffer.clear();
                    }
                }
                //取消选择键selectionKey
                iterator.remove();
            }
        }

    }
}

客户端:

package com.company.Selector;



import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        //打开客户端通道SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
        //设置为非堵塞模式
        socketChannel.configureBlocking(false);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //发送数据给服务端
        //控制台输入数据
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String msg = scanner.next();
            //写入缓存
            byteBuffer.put(msg.getBytes());
            //byteBuffer切换模式:读模式
            byteBuffer.flip();
            //读取byteBuffer的数据
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        //关闭连接
        socketChannel.close();
    }
}

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

这里通过选择器实现了服务器,客户端,客户端可以给服务器发送信息,在服务器实现选择器,对于SelectionKey的事件做不同的处理
其实上面的服务器已经实现了多播
同样的代码实现客户端ClientA:

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
这就是选择器的作用

总结

  1. 大致了解了Java IO的发展
  2. BIO:阻塞式IO;伪异步IO:通过线程池完成BIO;NIO:非阻塞式IO;AIO:异步非阻塞式IO
  3. NIO有三个重要的部件:缓冲区、通道、选择器
  4. 缓冲区是一个数组,保存要输入输出的数据
  5. 通道与流类似,是在实体和字节缓冲区之间有效传输数据的媒介,可以双向传输
  6. 选择器让一个线程能够处理多个通道,只能管理非阻塞的通道

已完成三个部件的详解
Java网络编程(5)NIO - Buffer详解
Java网络编程(6)NIO - Channel详解
Java网络编程(8)NIO - Selector详解

发布了95 篇原创文章 · 获赞 25 · 访问量 4195

猜你喜欢

转载自blog.csdn.net/key_768/article/details/104577022