BIO、NIO、AIO面試題(總結最全面的面試題!!!)

文章目錄

BIO、NIO、AIO、Netty

什麼是IO

  • Java中I/O是以流爲基礎進行數據的輸入輸出的,所有數據被串行化(所謂串行化就是數據要按順序進行輸入輸出)寫入輸出流。簡單來說就是java通過io流方式和外部設備進行交互。

  • 在Java類庫中,IO部分的內容是很龐大的,因爲它涉及的領域很廣泛:標準輸入輸出,文件的操作,網絡上的數據傳輸流,字符串流,對象流等等等。
    在這裏插入圖片描述

  • 比如程序從服務器上下載圖片,就是通過流的方式從網絡上以流的方式到程序中,在到硬盤中

在瞭解不同的IO之前先了解:同步與異步,阻塞與非阻塞的區別

  • 同步,一個任務的完成之前不能做其他操作,必須等待(等於在打電話)
  • 異步,一個任務的完成之前,可以進行其他操作(等於在聊QQ)
  • 阻塞,是相對於CPU來說的, 掛起當前線程,不能做其他操作只能等待
  • 非阻塞,,無須掛起當前線程,可以去執行其他操作

什麼是BIO

  • BIO:同步並阻塞,服務器實現一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,沒處理完之前此線程不能做其他操作(如果是單線程的情況下,我傳輸的文件很大呢?),當然可以通過線程池機制改善。BIO方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。

什麼是NIO

  • NIO:同步非阻塞,服務器實現一個連接一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時才啓動一個線程進行處理。NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,併發侷限於應用中,編程比較複雜,JDK1.4之後開始支持。

什麼是AIO

  • AIO:異步非阻塞,服務器實現模式爲一個有效請求一個線程,客戶端的I/O請求都是由操作系統先完成了再通知服務器應用去啓動線程進行處理,AIO方式使用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用操作系統參與併發操作,編程比較複雜,JDK1.7之後開始支持。.

  • AIO屬於NIO包中的類實現,其實IO主要分爲BIO和NIO,AIO只是附加品,解決IO不能異步的實現

  • 在以前很少有Linux系統支持AIO,Windows的IOCP就是該AIO模型。但是現在的服務器一般都是支持AIO操作

什麼Netty

  • Netty是由JBOSS提供的一個Java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

  • Netty 是一個基於NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。Netty相當簡化和流線化了網絡應用的編程開發過程,例如,TCP和UDP的socket服務開發。
    在這裏插入圖片描述
    Netty是由NIO演進而來,使用過NIO編程的用戶就知道NIO編程非常繁重,Netty是能夠能跟好的使用NIO

BIO和NIO、AIO的區別

  • BIO是阻塞的,NIO是非阻塞的.
  • BIO是面向流的,只能單向讀寫,NIO是面向緩衝的, 可以雙向讀寫
  • 使用BIO做Socket連接時,由於單向讀寫,當沒有數據時,會掛起當前線程,阻塞等待,爲防止影響其它連接,,需要爲每個連接新建線程處理.,然而系統資源是有限的,,不能過多的新建線程,線程過多帶來線程上下文的切換,從來帶來更大的性能損耗,因此需要使用NIO進行BIO多路複用,使用一個線程來監聽所有Socket連接,使用本線程或者其他線程處理連接
  • AIO是非阻塞 以異步方式發起 I/O 操作。當 I/O 操作進行時可以去做其他操作,由操作系統內核空間提醒IO操作已完成(不懂的可以往下看)

IO流的分類

在這裏插入圖片描述
按照讀寫的單位大小來分:

  • 字符流:以字符爲單位,每次次讀入或讀出是16位數據。其只能讀取字符類型數據。
    (Java代碼接收數據爲一般爲char數組,也可以是別的)
  • 字節流:以字節爲單位,每次次讀入或讀出是8位數據。可以讀任何類型數據,圖片、文件、音樂視頻等。
    (Java代碼接收數據只能爲byte數組)

按照實際IO操作來分:

  • 輸出流:從內存讀出到文件。只能進行寫操作。
  • 輸入流:從文件讀入到內存。只能進行讀操作。
  • 注意:輸出流可以幫助我們創建文件,而輸入流不會。

按照讀寫時是否直接與硬盤,內存等節點連接分:

  • 節點流:直接與數據源相連,讀入或讀出。
  • 處理流:也叫包裝流,是對一個對於已存在的流的連接進行封裝,通過所封裝的流的功能調用實現數據讀寫。如添加個Buffering緩衝區。(意思就是有個緩存區,等於軟件和mysql中的redis)
  • 注意:爲什麼要有處理流?主要作用是在讀入或寫出時,對數據進行緩存,以減少I/O的次數,以便下次更好更快的讀寫文件,纔有了處理流。

什麼是內核空間

  • 我們的應用程序是不能直接訪問硬盤的,我們程序沒有權限直接訪問,但是操作系統(Windows、Linux…)會給我們一部分權限較高的內存空間,他叫內核空間,和我們的實際硬盤空間是有區別的

在這裏插入圖片描述

五種IO模型

  • 注意:我這裏的用戶空間就是應用程序空間
1.阻塞BIO(blocking I/O)
  • A拿着一支魚竿在河邊釣魚,並且一直在魚竿前等,在等的時候不做其他的事情,十分專心。只有魚上鉤的時,才結束掉等的動作,把魚釣上來。

  • 在內核將數據準備好之前,系統調用會一直等待所有的套接字,默認的是阻塞方式。
    在這裏插入圖片描述

2.非阻塞NIO(noblocking I/O)
  • B也在河邊釣魚,但是B不想將自己的所有時間都花費在釣魚上,在等魚上鉤這個時間段中,B也在做其他的事情(一會看看書,一會讀讀報紙,一會又去看其他人的釣魚等),但B在做這些事情的時候,每隔一個固定的時間檢查魚是否上鉤。一旦檢查到有魚上鉤,就停下手中的事情,把魚釣上來。 B在檢查魚竿是否有魚,是一個輪詢的過程。

在這裏插入圖片描述

3.異步AIO(asynchronous I/O)
  • C也想釣魚,但C有事情,於是他僱來了D、E、F,讓他們幫他等待魚上鉤,一旦有魚上鉤,就打電話給C,C就會將魚釣上去。
    在這裏插入圖片描述
    當應用程序請求數據時,內核一方面去取數據報內容返回,另一方面將程序控制權還給應用進程,應用進程繼續處理其他事情,是一種非阻塞的狀態。
4.信號驅動IO(signal blocking I/O)
  • G也在河邊釣魚,但與A、B、C不同的是,G比較聰明,他給魚竿上掛一個鈴鐺,當有魚上鉤的時候,這個鈴鐺就會被碰響,G就會將魚釣上來。
    在這裏插入圖片描述
    信號驅動IO模型,應用進程告訴內核:當數據報準備好的時候,給我發送一個信號,對SIGIO信號進行捕捉,並且調用我的信號處理函數來獲取數據報。
5.IO多路轉接(I/O multiplexing)
  • H同樣也在河邊釣魚,但是H生活水平比較好,H拿了很多的魚竿,一次性有很多魚竿在等,H不斷的查看每個魚竿是否有魚上鉤。增加了效率,減少了等待的時間。
    在這裏插入圖片描述
    IO多路轉接是多了一個select函數,select函數有一個參數是文件描述符集合,對這些文件描述符進行循環監聽,當某個文件描述符就緒時,就對這個文件描述符進行處理。

  • IO多路轉接是屬於阻塞IO,但可以對多個文件描述符進行阻塞監聽,所以效率較阻塞IO的高。

什麼是比特(Bit),什麼是字節(Byte),什麼是字符(Char),它們長度是多少,各有什麼區別

  • Bit最小的二進制單位 ,是計算機的操作部分取值0或者1
  • Byte是計算機中存儲數據的單元,是一個8位的二進制數,(計算機內部,一個字節可表示一個英文字母,兩個字節可表示一個漢字。) 取值(-128-127)
  • Char是用戶的可讀寫的最小單位,他只是抽象意義上的一個符號。如‘5’,‘中’,‘¥’ 等等等等。在java裏面由16位bit組成Char 取值(0-65535)
  • Bit 是最小單位 計算機他只能認識0或者1
  • Byte是8個字節 是給計算機看的
  • 字符 是看到的東西 一個字符=二個字節

什麼叫對象序列化,什麼是反序列化,實現對象序列化需要做哪些工作

  • 對象序列化,將對象以二進制的形式保存在硬盤上
  • 反序列化;將二進制的文件轉化爲對象讀取
  • 實現serializable接口,不想讓字段放在硬盤上就加transient

在實現序列化接口是時候一般要生成一個serialVersionUID字段,它叫做什麼,一般有什麼用

  • 如果用戶沒有自己聲明一個serialVersionUID,接口會默認生成一個serialVersionUID
  • 但是強烈建議用戶自定義一個serialVersionUID,因爲默認的serialVersinUID對於class的細節非常敏感,反序列化時可能會導致InvalidClassException這個異常。
  • (比如說先進行序列化,然後在反序列化之前修改了類,那麼就會報錯。因爲修改了類,對應的SerialversionUID也變化了,而序列化和反序列化就是通過對比其SerialversionUID來進行的,一旦SerialversionUID不匹配,反序列化就無法成功。

怎麼生成SerialversionUID

  • 可序列化類可以通過聲明名爲 “serialVersionUID” 的字段(該字段必須是靜態 (static)、最終 (final) 的 long 型字段)顯式聲明其自己的 serialVersionUID

  • 兩種顯示的生成方式(當你一個類實現了Serializable接口,如果沒有顯示的定義serialVersionUID,Eclipse會提供這個提示功能告訴你去定義 。在Eclipse中點擊類中warning的圖標一下,Eclipse就會自動給定兩種生成的方式。

BufferedReader屬於哪種流,它主要是用來做什麼的,它裏面有那些經典的方法

  • 屬於處理流中的緩衝流,可以將讀取的內容存在內存裏面,有readLine()方法

Java中流類的超類主要有那些?

  • 超類代表頂端的父類(都是抽象類)

  • java.io.InputStream

  • java.io.OutputStream

  • java.io.Reader

  • java.io.Writer

爲什麼圖片、視頻、音樂、文件等 都是要字節流來讀取

  • 這個很基礎,你看看你電腦文件的屬性就好了,CPU規定了計算機存儲文件都是按字節算的

在這裏插入圖片描述

IO的常用類和方法,以及如何使用

注意,如果懂IO的普通文件讀寫操作可以直接點擊此處跳過,直接看網絡操作IO編程,那個纔是重點,點擊即會跳轉

前面講了那麼多廢話,現在我們開始進入主題,後面很長,從開始的文件操作到後面的網絡IO操作都會有例子:
在這裏插入圖片描述
注意,如果懂IO的普通文件讀寫操作可以直接點擊此處跳過,直接看網絡操作IO編程,那個纔是重點,點擊即會跳轉

IO基本操作講解

  • 這裏的基本操作就是普通的讀取操作,如果想要跟深入的瞭解不同的IO開發場景必須先了解IO的基本操作
1 按字符流讀取文件
1.1 按字符流的·節點流方式讀取
  • 如果我們要取的數據基本單位是字符,那麼用(字符流)這種方法讀取文件就比較適合。比如:讀取test.txt文件

註釋:

  • 字符流:以字符爲單位,每次次讀入或讀出是16位數據。其只能讀取字符類型數據。
    (Java代碼接收數據爲一般爲char數組,也可以是別的)

  • 字節流:以字節爲單位,每次次讀入或讀出是8位數據。可以讀任何類型數據,圖片、文件、音樂視頻等。
    (Java代碼接收數據只能爲byte數組)

  • FileReader 類:(字符輸入流)
    注意:new FileReader(“D:\test.txt”);//文件必須存在

package com.test.io;

import java.io.FileReader;
import java.io.IOException;

public class TestFileReader {
    public static void main(String[] args) throws IOException {
        int num=0;
        //字符流接收使用的char數組
        char[] buf=new char[1024];
        //字符流、節點流打開文件類
        FileReader fr = new FileReader("D:\\test.txt");//文件必須存在
        //FileReader.read():取出字符存到buf數組中,如果讀取爲-1代表爲空即結束讀取。
        //FileReader.read():讀取的是一個字符,但是java虛擬機會自動將char類型數據轉換爲int數據,
        //如果你讀取的是字符A,java虛擬機會自動將其轉換成97,如果你想看到字符可以在返回的字符數前加(char)強制轉換如
        while((num=fr.read(buf))!=-1) { }
        //檢測一下是否取到相應的數據
        for(int i=0;i<buf.length;i++) {
            System.out.print(buf[i]);
        }
    }
}
  • 運行結果:
    在這裏插入圖片描述·在這裏插入圖片描述

1.2 按字符流的·處理流方式讀取
  • 效果是一樣,但是給了我們有不同的選擇操作。進行了一個小封裝,加緩衝功能,避免頻繁讀寫硬盤。我這只是簡單演示,處理流其實還有很多操作

  • BufferedReader 類: 字符輸入流使用的類,加緩衝功能,避免頻繁讀寫硬盤

package com.test.io;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TestBufferedReader {
    public static void main(String[] args) throws IOException {
        int num=0;
        //字符流接收使用的String數組
        String[] bufstring=new String[1024];
        //字符流、節點流打開文件類
        FileReader fr = new FileReader("D:\\test.txt");//文件必須存在
        //字符流、處理流讀取文件類
        BufferedReader br = new BufferedReader(fr);
        //臨時接收數據使用的變量
        String line=null;
        //BufferedReader.readLine():單行讀取,讀取爲空返回null
        while((line=br.readLine())!=null) {
            bufstring[num]=line;
            num++;
        }
        br.close();//關閉文件
        for(int i=0;i<num;i++) {
            System.out.println(bufstring[i]);
        }
    }
}
  • 測試效果一樣
    在這裏插入圖片描述

2 按字符流寫出文件
2.1 按字符流的·節點流方式寫出
  • 寫出字符,使用(字符流)這種方法寫出文件比較適合。比如:輸出內容添加到test.txt文件

  • FileWriter類:(字符輸出流),如果寫出文件不存在會自動創建一個相對應的文件。使用FileWriter寫出文件默認是覆蓋原文件,如果要想在源文件添加內容不覆蓋的話,需要構造參數添加true參數:看示例瞭解

package com.test.io;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

public class TestFileWriter {
    public static void main(String[] args) throws IOException {
        //File是操作文件類
        File file = new File("D:\\test.txt");//文件必須存在
        //字符流、節點流寫出文件類
        //new FileWriter(file,true),這個true代表追加,不寫就代表覆蓋文件
        FileWriter out=new FileWriter(file,true);
        //寫入的字節,\n代表換行
        String str="\nholler";
        //寫入
        out.write(str);
        out.close();
    }
}
  • 運行效果
    在這裏插入圖片描述

2.2 按字符流的·處理流方式寫出
  • BufferedWriter : 增加緩衝功能,避免頻繁讀寫硬盤。
    我這裏: //new FileWriter(file),這裏我只給了他文件位置,我沒加true代表覆蓋源文件
package com.test.io;

import java.io.*;

public class TestBufferedWriter {
    public static void main(String[] args) throws IOException {
        //File是操作文件類
        File file = new File("D:\\test.txt");//文件必須存在
        //字符流、節點流寫出文件類
        //new FileWriter(file),這個我沒加true代表覆蓋文件
        Writer writer = new FileWriter(file);
        ////字符流、處理流寫出文件類
        BufferedWriter bw = new BufferedWriter(writer);
        bw.write("\n小心");
        bw.close();
        writer.close();
    }
}
  • 運行效果
    在這裏插入圖片描述

3 按字節流寫入寫出文件
3.1 按字節流的·節點流寫入寫出文件
  • 如果我們要取的數據 圖片、文件、音樂視頻等類型,就必須使用字節流進行讀取寫出

註釋:

  • 字符流:以字符爲單位,每次次讀入或讀出是16位數據。其只能讀取字符類型數據。
    (Java代碼接收數據爲一般爲char數組,也可以是別的)

  • 字節流:以字節爲單位,每次次讀入或讀出是8位數據。可以讀任何類型數據,圖片、文件、音樂視頻等。
    (Java代碼接收數據只能爲byte數組)

  • FileInputStream:(字節輸入流)

  • FileOutputStream:(字節輸出流)

package com.test.io;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class TestFileOutputStream {
    public static void main(String[] args) throws IOException {
        //創建字節輸入流、節點流方式讀取文件
        FileInputStream fis = new FileInputStream("D:\\Akie秋繪 - Lemon(Cover:米津玄師).mp3");
        //創建字節輸入流、節點流方式輸出文件
        FileOutputStream fos = new FileOutputStream("D:\\copy.mp3");

        //根據文件大小做一個字節數組
        byte[] arr = new byte[fis.available()];
        //將文件上的所有字節讀取到數組中
        fis.read(arr);
        //將數組中的所有字節一次寫到了文件上
        fos.write(arr);
        fis.close();
        fos.close();
    }
}
  • 運行之前:
    在這裏插入圖片描述
    運行之後:
    在這裏插入圖片描述

3.2 按字節流的·處理流寫入寫出文件
  • FileInputStream:(字節輸入流)
  • FileOutputStream:(字節輸出流)
  • BufferedInputStream:(帶緩衝區字節輸入流)
  • BufferedOutputStream:(帶緩衝區字節輸入流)
    帶緩衝區的處理流,緩衝區的作用的主要目的是:避免每次和硬盤打交道,提高數據訪問的效率。
package com.test.io;

import java.io.*;

public class TestBufferedOutputStream {
    //創建文件輸入流對象,關聯致青春.mp3
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("D:\\copy.mp3");
        //創建緩衝區對fis裝飾
        BufferedInputStream bis = new BufferedInputStream(fis);
        //創建輸出流對象,關聯copy.mp3
        FileOutputStream fos = new FileOutputStream("D:\\copy2.mp3");
        //創建緩衝區對fos裝飾
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        //循環直接輸出
        int i;
        while((i = bis.read()) != -1) {
            bos.write(i);
        }
        bis.close();
        bos.close();
    }
}
  • 運行之前:
    在這裏插入圖片描述
    運行之後:
    在這裏插入圖片描述

網絡操作IO講解

  • 我這使用Socket簡單的來模擬網絡編程IO會帶來的問題

  • 不懂Socket可以看我之前的文章,這個東西很容易懂的,就是基於TCP實現的網絡通信,比http要快,很多實現網絡通信的框架都是基於Socket來實現

網絡操作IO編程演變歷史

1 BIO編程會出現什麼問題?
  • BIO是阻塞的

  • 例子: 阻塞IO(blocking I/O)
    A拿着一支魚竿在河邊釣魚,並且一直在魚竿前等,在等的時候不做其他的事情,十分專心。只有魚上鉤的時,才結束掉等的動作,把魚釣上來。
    在這裏插入圖片描述

  • 看起來沒問題,但是我很多請求一起發送請求資源怎麼辦:
    在這裏插入圖片描述
    那不是要等待第一個人資源完成後後面的人才可以繼續?
    因爲BIO是阻塞的所以讀取寫出操作都是非常浪費資源的

BIO代碼示例:後面有代碼,往後移動一點點,認真看,代碼學習量很足

  • 我這有三個類,我模擬啓動服務端,然後啓動客戶端,模擬客戶端操作未完成的時候啓動第二個客戶端
    在這裏插入圖片描述
  1. 啓動服務端(後面有代碼,我這是教運行順序
    在這裏插入圖片描述
  2. 啓動第一個客戶端,發現服務器顯示連接成功
    先不要在控制檯 輸入 ,模擬堵塞。(我的代碼輸入了就代表請求完成了)
    在這裏插入圖片描述·在這裏插入圖片描述
  3. 啓動第二個客戶端,發現服務端沒效果,而客戶端連接成功(在堵塞當中)
    我這啓動了倆個Client,注意看,(這倆個代碼是一樣的)
    在這裏插入圖片描述·在這裏插入圖片描述
  4. 第一個客戶控制檯輸入,輸入完後就會關閉第一個客戶端,
    在看服務端發現第二個客戶端連接上來了
    在這裏插入圖片描述·在這裏插入圖片描述

BIO通信代碼:

  • TCP協議Socket使用BIO進行通信:服務端(先執行)
package com.test.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

//TCP協議Socket使用BIO進行通信:服務端
public class BIOServer {
    // 在main線程中執行下面這些代碼
    public static void main(String[] args) {
        //使用Socket進行網絡通信
        ServerSocket server = null;
        Socket socket = null;
        //基於字節流
        InputStream in = null;
        OutputStream out = null;
        try {
            server = new ServerSocket(8000);
            System.out.println("服務端啓動成功,監聽端口爲8000,等待客戶端連接...");
            while (true){
                socket = server.accept(); //等待客戶端連接
                System.out.println("客戶連接成功,客戶信息爲:" + socket.getRemoteSocketAddress());
                in = socket.getInputStream();
                byte[] buffer = new byte[1024];
                int len = 0;
                //讀取客戶端的數據
                while ((len = in.read(buffer)) > 0) {
                    System.out.println(new String(buffer, 0, len));
                }
                //向客戶端寫數據
                out = socket.getOutputStream();
                out.write("hello!".getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • TCP協議Socket使用BIO進行通信:客戶端(第二執行)
package com.test.io;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

//TCP協議Socket使用BIO進行通信:客戶端
public class Client01 {
    public static void main(String[] args) throws IOException {
        //創建套接字對象socket並封裝ip與port
        Socket socket = new Socket("127.0.0.1", 8000);
        //根據創建的socket對象獲得一個輸出流
        //基於字節流
        OutputStream outputStream = socket.getOutputStream();
        //控制檯輸入以IO的形式發送到服務器
        System.out.println("TCP連接成功 \n請輸入:");
        String str = new Scanner(System.in).nextLine();
        byte[] car = str.getBytes();
        outputStream.write(car);
        System.out.println("TCP協議的Socket發送成功");
        //刷新緩衝區
        outputStream.flush();
        //關閉連接
        socket.close();
    }
}
  • TCP協議Socket使用BIO進行通信:客戶端(第三執行)
package com.test.io;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

//TCP協議Socket:客戶端
public class Client02 {
    public static void main(String[] args) throws IOException {
        //創建套接字對象socket並封裝ip與port
        Socket socket = new Socket("127.0.0.1", 8000);
        //根據創建的socket對象獲得一個輸出流
        //基於字節流
        OutputStream outputStream = socket.getOutputStream();
        //控制檯輸入以IO的形式發送到服務器
        System.out.println("TCP連接成功 \n請輸入:");
        String str = new Scanner(System.in).nextLine();
        byte[] car = str.getBytes();
        outputStream.write(car);
        System.out.println("TCP協議的Socket發送成功");
        //刷新緩衝區
        outputStream.flush();
        //關閉連接
        socket.close();
    }
}

爲了解決堵塞問題,可以使用多線程,請看下面

2 多線程解決BIO編程會出現的問題

這時有人就會說,我多線程不就解決了嗎?

  • 使用多線程是可以解決堵塞等待時間很長的問題,因爲他可以充分發揮CPU
  • 然而系統資源是有限的,不能過多的新建線程,線程過多帶來線程上下文的切換,從來帶來更大的性能損耗
    在這裏插入圖片描述

萬一請求越來越多,線程越來越多那我CPU不就炸了?
在這裏插入圖片描述
多線程BIO代碼示例:

  • 四個客戶端,這次我多複製了倆個一樣客戶端類
    在這裏插入圖片描述
    先啓動服務端,在啓動所有客戶端,測試,發現連接成功(後面有代碼
    在這裏插入圖片描述
    在所有客戶端輸入消息(Client01、Client02這些是我在客戶端輸入的消息):發現沒有問題
    在這裏插入圖片描述

多線程BIO通信代碼:

  • 服務端的代碼,客戶端的代碼還是上面之前的代碼
package com.test.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

//TCP協議Socket使用多線程BIO進行通行:服務端
public class BIOThreadService {
    public static void main(String[] args) {
        try {
            ServerSocket server = new ServerSocket(8000);
            System.out.println("服務端啓動成功,監聽端口爲8000,等待客戶端連接... ");
            while (true) {
                Socket socket = server.accept();//等待客戶連接
                System.out.println("客戶連接成功,客戶信息爲:" + socket.getRemoteSocketAddress());
                //針對每個連接創建一個線程, 去處理I0操作
                //創建多線程創建開始
                Thread thread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            InputStream in = socket.getInputStream();
                            byte[] buffer = new byte[1024];
                            int len = 0;
                            //讀取客戶端的數據
                            while ((len = in.read(buffer)) > 0) {
                                System.out.println(new String(buffer, 0, len));
                            }
                            //向客戶端寫數據
                            OutputStream out = socket.getOutputStream();
                            out.write("hello".getBytes());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

爲了解決線程太多,這時又來了,線程池

3 線程池解決多線程BIO編程會出現的問題

這時有人就會說,我TM用線程池?
在這裏插入圖片描述

  • 線程池固然可以解決這個問題,萬一需求量還不夠還要擴大線程池。當是這是我們自己靠着自己的思想完成的IO操作,Socket 上來了就去創建線程去搶奪CPU資源,MD,線程都TM做IO去了,CPU也不舒服呀

  • 這時呢:Jdk官方坐不住了,兄弟BIO的問題交給我,我來給你解決:NIO的誕生

線程池BIO代碼示例:

  • 四個客戶端
    在這裏插入圖片描述
    先啓動服務端,在啓動所有客戶端,測試,(後面有代碼
    在這裏插入圖片描述
    在所有客戶端輸入消息(Client01、Client02這些是我在客戶端輸入的消息):發現沒有問題
    在這裏插入圖片描述

線程池BIO通信代碼:

  • 服務端的代碼,客戶端的代碼還是上面的代碼
package com.test.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//TCP協議Socket使用線程池BIO進行通行:服務端
public class BIOThreadPoolService {
    public static void main(String[] args) {
        //創建線程池
        ExecutorService executorService = Executors.newFixedThreadPool(30);
        try {
            ServerSocket server = new ServerSocket(8000);
            System.out.println("服務端啓動成功,監聽端口爲8000,等待客戶端連接...");
            while (true) {
                Socket socket = server.accept();//等待客戶連接
                System.out.println("客戶連接成功,客戶信息爲:" + socket.getRemoteSocketAddress());
                //使用線程池中的線程去執行每個對應的任務
                executorService.execute(new Thread(new Runnable() {
                    public void run() {
                        try {
                            InputStream in = socket.getInputStream();
                            byte[] buffer = new byte[1024];
                            int len = 0;
                            //讀取客戶端的數據
                            while ((len = in.read(buffer)) > 0) {
                                System.out.println(new String(buffer, 0, len));
                            }
                            //向客戶端寫數據
                            OutputStream out = socket.getOutputStream();
                            out.write("hello".getBytes());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                })
                );
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


4 使用NIO實現網絡通信
  • NIO是JDK1.4提供的操作,他的流還是流,沒有改變,服務器實現的還是一個連接一個線程,當是:客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時才啓動一個線程進行處理。NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,併發侷限於應用中,編程比較複雜,JDK1.4之後開始支持。

在這裏插入圖片描述
看不懂介紹可以認真看看代碼實例,其實不難

什麼是通道(Channel)
  • Channel是一個對象,可以通過它讀取和寫入數據。
    通常我們都是將數據寫入包含一個或者多個字節的緩衝區,然後再將緩存區的數據寫入到通道中,將數據從通道讀入緩衝區,再從緩衝區獲取數據。

  • Channel 類似於原I/O中的流(Stream),但有所區別:

    • 流是單向的,通道是雙向的,可讀可寫。
    • 流讀寫是阻塞的,通道可以異步讀寫。
什麼是選擇器(Selector)
  • Selector可以稱他爲通道的集合,每次客戶端來了之後我們會把Channel註冊到Selector中並且我們給他一個狀態,在用死循環來環判斷(判斷是否做完某個操作,完成某個操作後改變不一樣的狀態)狀態是否發生變化,知道IO操作完成後在退出死循環
什麼是Buffer(緩衝區)
  • Buffer 是一個緩衝數據的對象, 它包含一些要寫入或者剛讀出的數據。

  • 在普通的面向流的 I/O 中,一般將數據直接寫入或直接讀到 Stream 對象中。當是有了Buffer(緩衝區)後,數據第一步到達的是Buffer(緩衝區)中

  • 緩衝區實質上是一個數組(底層完全是數組實現的,感興趣可以去看一下)。通常它是一個字節數組,內部維護幾個狀態變量,可以實現在同一塊緩衝區上反覆讀寫(不用清空數據再寫)。

代碼實例:
  • 目錄結構
    在這裏插入圖片描述
  • 運行示例,先運行服務端,在運行所有客戶端控制檯輸入消息就好了。:我這客戶端和服務端代碼有些修該變,後面有代碼
    在這裏插入圖片描述
  • 服務端示例,先運行,想要搞定NIO請認真看代碼示例,真的很清楚
package com.test.io;

import com.lijie.iob.RequestHandler;

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 NIOServer {
    public static void main(String[] args) throws IOException {
        //111111111
        //Service端的Channel,監聽端口的
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        //設置爲非阻塞
        serverChannel.configureBlocking(false);
        //nio的api規定這樣賦值端口
        serverChannel.bind(new InetSocketAddress(8000));
        //顯示Channel是否已經啓動成功,包括綁定在哪個地址上
        System.out.println("服務端啓動成功,監聽端口爲8000,等待客戶端連接..."+ serverChannel.getLocalAddress());

        //22222222
        //聲明selector選擇器
        Selector selector = Selector.open();
        //這句話的含義,是把selector註冊到Channel上面,
        //每個客戶端來了之後,就把客戶端註冊到Selector選擇器上,默認狀態是Accepted
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        //33333333
        //創建buffer緩衝區,聲明大小是1024,底層使用數組來實現的
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        RequestHandler requestHandler = new RequestHandler();

        //444444444
        //輪詢,服務端不斷輪詢,等待客戶端的連接
        //如果有客戶端輪詢上來就取出對應的Channel,沒有就一直輪詢
        while (true) {
            int select = selector.select();
            if (select == 0) {
                continue;
            }
            //有可能有很多,使用Set保存Channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                //使用SelectionKey來獲取連接了客戶端和服務端的Channel
                SelectionKey key = iterator.next();
                //判斷SelectionKey中的Channel狀態如何,如果是OP_ACCEPT就進入
                if (key.isAcceptable()) {
                    //從判斷SelectionKey中取出Channel
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    //拿到對應客戶端的Channel
                    SocketChannel clientChannel = channel.accept();
                    //把客戶端的Channel打印出來
                    System.out.println("客戶端通道信息打印:" + clientChannel.getRemoteAddress());
                    //設置客戶端的Channel設置爲非阻塞
                    clientChannel.configureBlocking(false);
                    //操作完了改變SelectionKey中的Channel的狀態OP_READ
                    clientChannel.register(selector, SelectionKey.OP_READ);
                }
                //到此輪訓到的時候,發現狀態是read,開始進行數據交互
                if (key.isReadable()) {
                    //以buffer作爲數據橋樑
                    SocketChannel channel = (SocketChannel) key.channel();
                    //數據要想讀要先寫,必須先讀取到buffer裏面進行操作
                    channel.read(buffer);
                    //進行讀取
                    String request = new String(buffer.array()).trim();
                    buffer.clear();
                    //進行打印buffer中的數據
                    System.out.println(String.format("客戶端發來的消息: %s : %s", channel.getRemoteAddress(), request));
                    //要返回數據的話也要先返回buffer裏面進行返回
                    String response = requestHandler.handle(request);
                    //然後返回出去
                    channel.write(ByteBuffer.wrap(response.getBytes()));
                }
                iterator.remove();
            }
        }
    }
}
  • 客戶端示例:(我這用的不是之前的了,有修改)運行起來客戶端控制檯輸入消息就好了。
    要模擬測試,請複製粘貼改一下,修改客戶端的類名就行了,四個客戶端代碼一樣的,
    在這裏插入圖片描述
package com.test.io;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

//TCP協議Socket:客戶端
public class Client01 {
    public static void main(String[] args) throws IOException {
        //創建套接字對象socket並封裝ip與port
        Socket socket = new Socket("127.0.0.1", 8000);
        //根據創建的socket對象獲得一個輸出流
        OutputStream outputStream = socket.getOutputStream();
        //控制檯輸入以IO的形式發送到服務器
        System.out.println("TCP連接成功 \n請輸入:");
        while(true){
            byte[] car = new Scanner(System.in).nextLine().getBytes();
            outputStream.write(car);
            System.out.println("TCP協議的Socket發送成功");
            //刷新緩衝區
            outputStream.flush();
        }
    }
}


5 使用Netty實現網絡通信
  • Netty是由JBOSS提供的一個Java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

  • Netty 是一個基於NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。Netty相當簡化和流線化了網絡應用的編程開發過程,例如,TCP和UDP的Socket服務開發。
    在這裏插入圖片描述
    Netty是由NIO演進而來,使用過NIO編程的用戶就知道NIO編程非常繁重,Netty是能夠能跟好的使用NIO

  • Netty的原裏就是NIO,他是基於NIO的一個完美的封裝,並且優化了NIO,使用他非常方便,簡單快捷

  • 我直接上代碼:
    在這裏插入圖片描述

  • 1、先添加依賴:

		<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.16.Final</version>
        </dependency>
  • 2、NettyServer 模板,看起來代碼那麼多,其實只需要添加一行消息就好了
  • 請認真看中間的代碼
package com.lijie.iob;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.codec.string.StringDecoder;

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast("encoder", new ObjectEncoder());
                            pipeline.addLast(" decoder", new io.netty.handler.codec.serialization.ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));

                            //重點,其他的都是複用的
                            //這是真正的I0的業務代碼,把他封裝成一個個的個Hand1e類就行了
                            //把他當成 SpringMVC的Controller
                            pipeline.addLast(new NettyServerHandler());


                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            ChannelFuture f = b.bind(8000).sync();
            System.out.println("服務端啓動成功,端口號爲:" + 8000);
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
  • 3、需要做的IO操作,重點是繼承ChannelInboundHandlerAdapter類就好了
package com.lijie.iob;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    RequestHandler requestHandler = new RequestHandler();

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(String.format("客戶端信息: %s", channel.remoteAddress()));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        String request = (String) msg;
        System.out.println(String.format("客戶端發送的消息 %s : %s", channel.remoteAddress(), request));
        String response = requestHandler.handle(request);
        ctx.write(response);
        ctx.flush();
    }
}
  • 4 客戶的代碼還是之前NIO的代碼,我在複製下來一下吧
package com.test.io;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

//TCP協議Socket:客戶端
public class Client01 {
    public static void main(String[] args) throws IOException {
        //創建套接字對象socket並封裝ip與port
        Socket socket = new Socket("127.0.0.1", 8000);
        //根據創建的socket對象獲得一個輸出流
        OutputStream outputStream = socket.getOutputStream();
        //控制檯輸入以IO的形式發送到服務器
        System.out.println("TCP連接成功 \n請輸入:");
        while(true){
            byte[] car = new Scanner(System.in).nextLine().getBytes();
            outputStream.write(car);
            System.out.println("TCP協議的Socket發送成功");
            //刷新緩衝區
            outputStream.flush();
        }
    }
}
  • 運行測試,還是之前那樣,啓動服務端,在啓動所有客戶端控制檯輸入就好了:
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章