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
在這裏插入圖片描述
它與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()方法創建,套接字通道使用open()方法直接打開
  • isOpen():Channel自帶的方法,告訴這個通道是否打開
  • close: Channel自帶的方法,關閉通道
  • read() : Channel大部分子類擁有的方法,從通道讀取數據到緩衝區,不同的參數有不同的作用,FileChannel有四種read方法
    在這裏插入圖片描述
  • write():Channel大部分子類擁有的方法,從緩衝區寫入數據到通道,FileChannel有四種write方法
    在這裏插入圖片描述

緩衝區與通道:分散、聚集

前面知道了通道類似與流,緩衝區暫時保存數據
那麼程序與實體間數據交流就是通過緩衝區與通道的分散讀取、聚集寫入

分散讀取:將數據從通道中讀取到多個緩衝區(read方法)
在這裏插入圖片描述
聚集寫入:將多個緩衝區的數據寫入到單個通道中(write方法)
在這裏插入圖片描述

案例

聚集寫入文件,分散讀出文件

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();
    }
}

在這裏插入圖片描述
在這裏插入圖片描述

這裏通過選擇器實現了服務器,當然客戶端沒有使用選擇器,即這個聊天系統只能客戶端寫入,服務器讀

總結

  1. 大致瞭解了Java IO的發展
  2. BIO:阻塞式IO;僞異步IO:通過線程池完成BIO;NIO:非阻塞式IO;AIO:異步非阻塞式IO
  3. NIO有三個重要的部件:緩衝區、通道、選擇器
  4. 緩衝區是一個數組,保存要輸入輸出的數據
  5. 通道與流類似,是在實體和字節緩衝區之間有效傳輸數據的媒介,可以雙向傳輸
  6. 選擇器讓一個線程能夠處理多個通道,只能管理非阻塞的通道
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章