Java面試題IO篇
- 所有題目均來自互聯網,整合不易希望大家沉下心來,認真學習,一鍵三連加關注就是對博主最大的支持!
- 菜鳥教程地址:https://www.runoob.com/java/java-files-io.html

文章目錄
- Java面試題IO篇
- 前言
- BIO、NIO、AIO、Netty
- 一. 什么是IO
- 二. 在了解不同的IO之前先了解:同步與異步,阻塞與非阻塞的區別
- 三. 什么是BIO
- 四. 什么是NIO
- 五. 什么是AIO
- 六. 什么是Netty
- 七. BIO和NIO、AIO的區別
- 八. IO流的分類
- 九. 什么是內核空間
- 十. 五種IO模型
- 1. 阻塞BIO(blocking I/O)
- 2. 非阻塞NIO(noblocking I/O)
- 3. 異步AIO(asynchronous I/O)
- 4. 信號驅動IO(signal blocking I/O)
- 5. IO多路轉接(I/O multiplexing)
- 十一. 什么是位元(Bit),什么是位元組(Byte),什么是字符(Char),它們長度是多少,各有什么區別?
- 十二. 什么叫物件序列化,什么是反序列化,實作物件序列化需要做哪些作業
- 十三. 在實作序列化介面是時候一般要生成一個serialVersionUID欄位,它叫做什么,一般有什么用 ?
- 十四. 怎么生成SerialversionUID
- 十五. BufferedReader屬于哪種流,它主要是用來做什么的,它里面有那些經典的方法 ?
- 十六. Java中流類的超類主要有那些?
- 十七. 為什么圖片、視頻、音樂、檔案等 都是要位元組流來讀取
- IO的常用類和方法以,及如何使用
- 一. IO基本操作講解
- 按字符流讀取檔案
- 1. 按字符流的節點流方式讀取
- 2. 按字符流的處理流方式讀取
- 按字符流寫出檔案
- 1. 按字符流的節點流方式寫出
- 2. 按字符流的處理流方式寫出
- 按位元組流寫入寫出檔案
- 1. 按位元組流的節點流寫入寫出檔案
- 2. 按位元組流的處理流寫入寫出檔案
- 二. 網路操作IO講解
- 網路操作IO編程演變歷史
- 1. BIO編程會出現什么問題?
- 2. 多執行緒解決BIO編程會出現的問題
- 3. 執行緒池解決多執行緒BIO編程會出現的問題
- 4 使用NIO實作網路通信
- 5.使用Netty實作網路通信
- IO練習題
前言

送給大家一句話:平凡的腳步也可以走完偉大的行程 !
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: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的基本操作
按字符流讀取檔案

1. 按字符流的節點流方式讀取
- 如果我們要取的資料基本單位是字符,那么用(字符流)這種方法讀取檔案就比較適合,比如:讀取test.txt檔案
注釋:
- 字符流 :以字符為單位,每次次讀入或讀出是16位資料,其只能讀取字符型別資料, (Java代碼接收資料為一般為 char陣列,也可以是別的 )
- 位元組流:以位元組為單位,每次次讀入或讀出是8位資料,可以讀任何型別資料,圖片、檔案、音樂視頻等, (Java代碼接收資料只能為 byte陣列 )
- FileReader 類:(字符輸入流) 注意:new FileReader(“D:\test.txt”);//檔案必須存在,
import java.io.FileReader;
import java.io.IOException;
public class TestFileReader {
public static void main(String[] args) throws IOException {
int num = 0;
char[] buf = new char[1024];
//字符流、節點流打開檔案類
FileReader fr = new FileReader("C:\\Users\\mzc\\Desktop\\審管聯動\\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]);
}
}
}


2. 按字符流的處理流方式讀取
- 效果是一樣,但是給了我們有不同的選擇操作,進行了一個小封裝,加緩沖功能,避免頻繁讀寫硬碟,我這只是簡單演示,處理流其實還有很多操作
- BufferedReader 類: 字符輸入流使用的類,加緩沖功能,避免頻繁讀寫硬碟
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* @author mengzhichao
* @create 2021-07-20-9:12
*/
public class Main {
public static void main(String[] args) throws IOException {
int num = 0;
//字符流接收使用的String陣列
String[] bufstring = new String[1024];
//字符流、節點流打開檔案類
FileReader fr = new FileReader("C:\\Users\\mzc\\Desktop\\審管聯動\\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]);
}
}
}


按字符流寫出檔案

1. 按字符流的節點流方式寫出
- 寫出字符,使用(字符流)這種方法寫出檔案比較適合,比如:輸出內容添加到test.txt檔案
- FileWriter類:(字符輸出流),如果寫出檔案不存在會自動創建一個相對應的檔案,使用FileWriter寫出檔案默認是覆寫原檔案,如果要想在源檔案添加內容不覆寫的話,需要構造引數添加true引數:看示例了解
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
/**
* @author mengzhichao
* @create 2021-07-20-9:12
*/
public class Main {
public static void main(String[] args) throws IOException {
//File是操作檔案類
File file = new File("C:\\Users\\mzc\\Desktop\\審管聯動\\test.txt");//檔案必須存在
//字符流、節點流寫出檔案類
//new FileWriter(file,true),這個true代表追加,不寫就代表覆寫檔案
FileWriter out = new FileWriter(file, true);
//寫入的位元組,\n代表換行
String str = "\n哈嘍";
//寫入
out.write(str);
out.close();
}
}


2. 按字符流的處理流方式寫出
- BufferedWriter : 增加緩沖功能,避免頻繁讀寫硬碟, 我這里: //new FileWriter(file),這里我只給了他檔案位置,我沒加true代表覆寫源檔案
import java.io.*;
/**
* @author mengzhichao
* @create 2021-07-20-9:12
*/
public class Main {
public static void main(String[] args) throws IOException {
//File是操作檔案類
File file = new File("C:\\Users\\mzc\\Desktop\\審管聯動\\test.txt");//檔案必須存在
//字符流、節點流寫出檔案類
//new FileWriter(file),這個我沒加true代表覆寫檔案
Writer writer = new FileWriter(file);
字符流、處理流寫出檔案類
BufferedWriter bw = new BufferedWriter(writer);
bw.write("\n我直接覆寫");
bw.close();
writer.close();
}
}


按位元組流寫入寫出檔案

1. 按位元組流的節點流寫入寫出檔案
如果我們要取的資料 圖片、檔案、音樂視頻等型別,就必須使用位元組流進行讀取寫出- 字符流 :以字符為單位,每次次讀入或讀出是16位資料,其只能讀取字符型別資料, (Java代碼接收資料為一般為 char陣列,也可以是別的 )
- 位元組流:以位元組為單位,每次次讀入或讀出是8位資料,可以讀任何型別資料,圖片、檔案、音樂視頻等, (Java代碼接收資料只能為 byte陣列 )
- FileInputStream:(位元組輸入流)
- FileOutputStream:(位元組輸出流)
import java.io.*;
/**
* @author mengzhichao
* @create 2021-07-20-9:12
*/
public class Main {
public static void main(String[] args) throws IOException {
//創建位元組輸入流、節點流方式讀取檔案
FileInputStream fis = new FileInputStream("C:\\Users\\mzc\\Desktop\\審管聯動\\稻香.mp3");
//創建位元組輸入流、節點流方式輸出檔案
FileOutputStream fos = new FileOutputStream("C:\\Users\\mzc\\Desktop\\審管聯動\\copy.mp3");
//根據檔案大小做一個位元組陣列
byte[] arr = new byte[fis.available()];
//將檔案上的所有位元組讀取到陣列中
fis.read(arr);
//將陣列中的所有位元組一次寫到了檔案上
fos.write(arr);
fis.close();
fos.close();
}
}


2. 按位元組流的處理流寫入寫出檔案
- FileInputStream:(位元組輸入流)
- FileOutputStream:(位元組輸出流)
- BufferedInputStream:(帶緩沖區位元組輸入流)
- BufferedOutputStream:(帶緩沖區位元組輸入流) 帶緩沖區的處理流,緩沖區的作用的主要目的是:避免每次和硬碟打交道,提高資料訪問的效率,
import java.io.*;
/**
* @author mengzhichao
* @create 2021-07-20-9:12
*/
public class Main {
public static void main(String[] args) throws IOException {
//創建檔案輸入流物件,關聯致青春.mp3
FileInputStream fis = new FileInputStream("C:\\Users\\mzc\\Desktop\\審管聯動\\稻香.mp3");
//創建緩沖區對fis裝飾
BufferedInputStream bis = new BufferedInputStream(fis);
//創建輸出流物件,關聯copy.mp3
FileOutputStream fos = new FileOutputStream("C:\\Users\\mzc\\Desktop\\審管聯動\\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代碼示例:( 后面有代碼,往后移動一點點,認真看,代碼學習量很足 )
- 我這有三個類,我模擬啟動服務端,然后啟動客戶端,模擬客戶端操作未完成的時候啟動第二個客戶端

-
啟動服務端(BIOServer)

-
啟動第一個客戶端(Client1),發現服務器顯示連接成功 先不要在控制臺輸入 ,模擬堵塞,(我的代碼輸入了就代表請求完成了)


-
啟動第二個客戶端(Client2), 發現服務端沒效果 ,而客戶端連接成功(在堵塞當中) 我這啟動了倆個Client,注意看,(這倆個代碼是一樣的)


-
第一個客戶控制臺輸入,輸入完后就會關閉第一個客戶端, 在看服務端發現第二個客戶端連接上來了


BIO通信代碼:
- BIOServer(TCP協議Socket使用BIO進行通信:服務端)
package com.mzc.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author mengzhichao TCP協議Socket使用BIO進行通信:服務端
* @create 2021-07-20-11:07
*/
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();
}
}
}
- Client1(TCP協議Socket使用BIO進行通信:客戶端1)
package com.mzc.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @author mengzhichao TCP協議Socket使用BIO進行通信:客戶端1
* @create 2021-07-20-11:07
*/
public class Client1 {
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();
}
}
- Client2(TCP協議Socket使用BIO進行通信:客戶端2)
package com.mzc.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @author mengzhichao TCP協議Socket:客戶端2
* @create 2021-07-20-11:07
*/
public class Client2 {
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
- 然而系統資源是有限的,不能過多的新建執行緒,執行緒過多帶來執行緒背景關系的切換,從來帶來更大的性能損耗

多執行緒BIO代碼示例: ( 后面有代碼,往后移動一點點,認真看,代碼學習量很足 )
- 四個客戶端,這次我多復制了倆個一樣客戶端類

-
先啟動服務端,在啟動所有客戶端,測驗,發現連接成功

-
在所有客戶端輸入訊息(
Client1、Client2這些是我在客戶端輸入的訊息)發現沒有問題

多執行緒BIO通信代碼:
- BIOThreadService (TCP協議Socket使用多執行緒BIO進行通行:服務端)
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author mengzhichao TCP協議Socket使用多執行緒BIO進行通行:服務端
* @create 2021-07-20-11:07
*/
public class BIOThreadServer {
// 在main執行緒中執行下面這些代碼
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() {
@Override
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代碼示例:(老樣子,代碼在后面)

- 先啟動服務端,在啟動所有客戶端,測驗

- 在所有客戶端輸入訊息(
Client1、Client2這些是我在客戶端輸入的訊息)發現沒有問題

執行緒池BIO通信代碼:
- BIOThreadPoolService (TCP協議Socket使用執行緒池BIO進行通行:服務端)
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 Runnable() {
@Override
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.mzc.nio;
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;
/**
* @author mengzhichao
* @create 2021-07-20-14:57
*/
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.mzc.nio;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @author mengzhichao TCP協議Socket:客戶端
* @create 2021-07-20-14:57
*/
public class Client1 {
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,使用他非常方便,簡單快捷
我直接上代碼
- 先添加依賴:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.16.Final</version>
</dependency>
- NettyServer 模板,看起來代碼那么多,
其實只需要添加一行訊息就好了
- 請認真看中間的代碼
package com.mzc.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
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;
import java.nio.channels.SocketChannel;
/**
* @author mengzhichao
* @create 2021-07-21-15:23
*/
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();
}
}
}
- 需要做的IO操作,重點是繼承ChannelInboundHandlerAdapter類就好了
package com.mzc.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @author mengzhichao
* @create 2021-07-21-15:38
*/
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();
}
}
- 客戶的代碼還是之前NIO的代碼,我在復制下來一下吧
package com.mzc.netty;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
/**
* @author mengzhichao TCP協議Socket:客戶端
* @create 2021-07-20-14:57
*/
public class Client1 {
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();
}
}
}
- 運行測驗,還是之前那樣,啟動服務端,在啟動所有客戶端控制臺輸入就好了:

IO練習題

- 練習一. 統計一個檔案calcCharNum.txt中字母
A和a出現的總次數,
package com.test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
//練習一:統計一個檔案calcCharNum.txt中字母‘A’和'a'出現的總次數,
/*
* 讀取檔案:FileInputStream
* 判斷單個字符出現的次數,一次只能讀一個,當讀到的內容相符時,相應數量加1
*/
public class TestOne {
public static void main(String[] args) {
// TODO Auto-generated method stub
//1.添加檔案路徑
File file=new File("E:\\calcCharNum.txt");
//2.創建流,讀取檔案
FileInputStream fis=null;
try {
fis=new FileInputStream(file);
int numA=0;
int numa=0;
int data=0;
while((data=fis.read())!=-1) {
if(new String((char)data+"").equals("a")) {
numa++;
}
if(new String((char)data+"").equals("A")) {
numA++;
}
}
System.out.println("a的個數:"+numa);
System.out.println("A的個數:"+numA);
System.out.println("總數:"+(numa+numA));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
- 練習二. 在電腦E盤下創建一個檔案為HelloWord.txt檔案,判斷它是檔案還是目錄,再創建一個目錄IOTest,之后將HelloWorld.txt移動到IOTest目錄下去,之后遍歷IOTest這個目錄下的檔案,
package com.test;
import java.io.File;
import java.io.IOException;
/*
* 練習二:在電腦E盤下創建一個檔案為HelloWord.txt檔案,
判斷它是檔案還是目錄,
再創建一個目錄IOTest,
之后將HelloWorld.txt移動到IOTest目錄下去,
之后遍歷IOTest這個目錄下的檔案,
*/
public class TestTwo {
public static void main(String[] args) {
// TODO Auto-generated method stub
//在E盤下創建檔案
File file=new File("E:","HeloWorld.txt");
//創建檔案
boolean isCreate;
try {
isCreate=file.createNewFile();
if(isCreate) {
System.out.println("創建檔案成功");
}else {
System.out.println("創建檔案失敗");
}
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println("創建檔案失敗");
}
//判斷是檔案還是目錄
if(file.isFile()) {
System.out.println("這是一個檔案");
}else {
System.out.println("這是一個目錄");
}
//創建目錄
File file2=new File("E:/IOTest");
file2.mkdirs();
//移動檔案至目錄下
if(file.renameTo(new File("E:/IOTest/HelloWorld.txt"))) {
System.out.println("檔案移動成功");
}else {
System.out.println("檔案移動失敗");
}
//遍歷目錄
String[] arrs=file2.list();
for (String string : arrs) {
System.out.println(string);
}
}
}
感謝瀏覽, 
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/289568.html
標籤:java
上一篇:java入門-----運算子
