Java修炼之道--I/O
原作地址:https://github.com/frank-lam/2019_campus_apply
Java IO
Java 的 I/O 大概可以分成以下幾類:
- 磁盤操作:File
- 字節(jié)操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 對(duì)象操作:Serializable
- 網(wǎng)絡(luò)操作:Socket
- 新的輸入/輸出:NIO
1、磁盤操作(File)
File 類可以用于表示文件和目錄的信息,但是它不表示文件的內(nèi)容。
遞歸地輸出一個(gè)目錄下所有文件:
public static void listAllFiles(File dir) {if (dir == null || !dir.exists()) {return;}if (dir.isFile()) {System.out.println(dir.getName());return;}for (File file : dir.listFiles()) {listAllFiles(file);} }2、字節(jié)操作(*Stream)
使用字節(jié)流操作進(jìn)行文件復(fù)制:
public static void copyFile(String src, String dist) throws IOException {FileInputStream in = new FileInputStream(src);FileOutputStream out = new FileOutputStream(dist);byte[] buffer = new byte[20 * 1024];// read() 最多讀取 buffer.length 個(gè)字節(jié)// 返回的是實(shí)際讀取的個(gè)數(shù)// 返回 -1 的時(shí)候表示讀到 eof,即文件尾while (in.read(buffer, 0, buffer.length) != -1) {out.write(buffer);}in.close();out.close(); }Java I/O 使用了裝飾者模式來實(shí)現(xiàn)。以 InputStream 為例,InputStream 是抽象組件,FileInputStream 是 InputStream 的子類,屬于具體組件,提供了字節(jié)流的輸入操作。FilterInputStream 屬于抽象裝飾者,裝飾者用于裝飾組件,為組件提供額外的功能,例如 BufferedInputStream 為 FileInputStream 提供緩存的功能。
實(shí)例化一個(gè)具有緩存功能的字節(jié)流對(duì)象時(shí),只需要在 FileInputStream 對(duì)象上再套一層 BufferedInputStream 對(duì)象即可。
FileInputStream fileInputStream = new FileInputStream(filePath); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);DataInputStream 裝飾者提供了對(duì)更多數(shù)據(jù)類型進(jìn)行輸入的操作,比如 int、double 等基本類型。
3、字符操作(Reader | Writer)
不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲(chǔ)單元都是字節(jié),而不是字符。但是在程序中操作的通常是字符形式的數(shù)據(jù),因此需要提供對(duì)字符進(jìn)行操作的方法。
- InputStreamReader 實(shí)現(xiàn)從字節(jié)流解碼成字符流;
- OutputStreamWriter 實(shí)現(xiàn)字符流編碼成為字節(jié)流。
逐行輸出文本文件的內(nèi)容:
public static void readFileContent(String filePath) throws IOException {FileReader fileReader = new FileReader(filePath);BufferedReader bufferedReader = new BufferedReader(fileReader);String line;while ((line = bufferedReader.readLine()) != null) {System.out.println(line);}// 裝飾者模式使得 BufferedReader 組合了一個(gè) Reader 對(duì)象// 在調(diào)用 BufferedReader 的 close() 方法時(shí)會(huì)去調(diào)用 fileReader 的 close() 方法// 因此只要一個(gè) close() 調(diào)用即可bufferedReader.close(); }編碼就是把字符轉(zhuǎn)換為字節(jié),而解碼是把字節(jié)重新組合成字符。
如果編碼和解碼過程使用不同的編碼方式那么就出現(xiàn)了亂碼。
- GBK 編碼中,中文字符占 2 個(gè)字節(jié),英文字符占 1 個(gè)字節(jié);
- UTF-8 編碼中,中文字符占 3 個(gè)字節(jié),英文字符占 1 個(gè)字節(jié);
- UTF-16be 編碼中,中文字符和英文字符都占 2 個(gè)字節(jié)。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相應(yīng)地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 使用雙字節(jié)編碼 UTF-16be,這不是指 Java 只支持這一種編碼方式,而是說 char 這種類型使用 UTF-16be 進(jìn)行編碼。char 類型占 16 位,也就是兩個(gè)字節(jié),Java 使用這種雙字節(jié)編碼是為了讓一個(gè)中文或者一個(gè)英文都能使用一個(gè) char 來存儲(chǔ)。
String 可以看成一個(gè)字符序列,可以指定一個(gè)編碼方式將它轉(zhuǎn)換為字節(jié)序列,也可以指定一個(gè)編碼方式將一個(gè)字節(jié)序列轉(zhuǎn)換為 String。
String str1 = "中文"; byte[] bytes = str1.getBytes("UTF-8"); String str2 = new String(bytes, "UTF-8"); System.out.println(str2);在調(diào)用無參數(shù) getBytes() 方法時(shí),默認(rèn)的編碼方式不是 UTF-16be。雙字節(jié)編碼的好處是可以使用一個(gè) char 存儲(chǔ)中文和英文,而將 String 轉(zhuǎn)為 bytes[] 字節(jié)數(shù)組就不再需要這個(gè)好處,因此也就不再需要雙字節(jié)編碼。getBytes() 的默認(rèn)編碼方式與平臺(tái)有關(guān),一般為 UTF-8。
byte[] bytes = str1.getBytes();4、Java序列化,如何實(shí)現(xiàn)序列化和反序列化,常見的序列化協(xié)議有哪些?
Java序列化定義
(1)Java序列化是指把Java對(duì)象轉(zhuǎn)換為字節(jié)序列的過程,而Java反序列化是指把字節(jié)序列恢復(fù)為Java對(duì)象的過程;
(2)序列化:對(duì)象序列化的最主要的用處就是在傳遞和保存對(duì)象的時(shí)候,保證對(duì)象的完整性和可傳遞性。序列化是把對(duì)象轉(zhuǎn)換成有序字節(jié)流,以便在網(wǎng)絡(luò)上傳輸或者保存在本地文件中。序列化后的字節(jié)流保存了Java對(duì)象的狀態(tài)以及相關(guān)的描述信息。序列化機(jī)制的核心作用就是對(duì)象狀態(tài)的保存與重建。
(3)反序列化:客戶端從文件中或網(wǎng)絡(luò)上獲得序列化后的對(duì)象字節(jié)流后,根據(jù)字節(jié)流中所保存的對(duì)象狀態(tài)及描述信息,通過反序列化重建對(duì)象。
(4)本質(zhì)上講,序列化就是把實(shí)體對(duì)象狀態(tài)按照一定的格式寫入到有序字節(jié)流,反序列化就是從有序字節(jié)流重建對(duì)象,恢復(fù)對(duì)象狀態(tài)。
如何實(shí)現(xiàn)序列化和反序列化,底層怎么實(shí)現(xiàn)
1、JDK類庫(kù)中序列化和反序列化API
(1)java.io.ObjectOutputStream:表示對(duì)象輸出流;
它的writeObject(Object obj)方法可以對(duì)參數(shù)指定的obj對(duì)象進(jìn)行序列化,把得到的字節(jié)序列寫到一個(gè)目標(biāo)輸出流中;
(2)java.io.ObjectInputStream:表示對(duì)象輸入流;
它的readObject()方法源輸入流中讀取字節(jié)序列,再把它們反序列化成為一個(gè)對(duì)象,并將其返回;
2、實(shí)現(xiàn)序列化的要求
只有實(shí)現(xiàn)了 Serializable 或 Externalizable 接口的類的對(duì)象才能被序列化,否則拋出異常!
3、實(shí)現(xiàn)Java對(duì)象序列化與反序列化的方法
假定一個(gè)User類,它的對(duì)象需要序列化,可以有如下三種方法:
- 若 User 類僅僅實(shí)現(xiàn)了 Serializable 接口,則可以按照以下方式進(jìn)行序列化和反序列化
- ObjectOutputStream 采用默認(rèn)的序列化方式,對(duì) User 對(duì)象的非 transient 的實(shí)例變量進(jìn)行序列化。
- ObjcetInputStream 采用默認(rèn)的反序列化方式,對(duì)對(duì) User 對(duì)象的非 transient 的實(shí)例變量進(jìn)行反序列化。
- 若User類僅僅實(shí)現(xiàn)了Serializable接口,并且還定義了 readObject(ObjectInputStream in) 和writeObject(ObjectOutputSteam out),則采用以下方式進(jìn)行序列化與反序列化。
- ObjectOutputStream 調(diào)用 User 對(duì)象的 writeObject(ObjectOutputStream out) 的方法進(jìn)行序列化。
- ObjectInputStream 會(huì)調(diào)用 User 對(duì)象的 readObject(ObjectInputStream in) 的方法進(jìn)行反序列化。
- 若User類實(shí)現(xiàn)了 Externalnalizable 接口,且 User 類必須實(shí)現(xiàn) readExternal(ObjectInput in) 和 writeExternal(ObjectOutput out) 方法,則按照以下方式進(jìn)行序列化與反序列化。
- ObjectOutputStream 調(diào)用 User 對(duì)象的 writeExternal(ObjectOutput out)) 的方法進(jìn)行序列化。
- ObjectInputStream 會(huì)調(diào)用User對(duì)象的 readExternal(ObjectInput in) 的方法進(jìn)行反序列化。
4、JDK類庫(kù)中序列化的步驟
步驟一:創(chuàng)建一個(gè)對(duì)象輸出流,它可以包裝一個(gè)其它類型的目標(biāo)輸出流,如文件輸出流:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\object.out"));步驟二:通過對(duì)象輸出流的writeObject()方法寫對(duì)象:
oos.writeObject(new User("xuliugen", "123456", "male"));5、JDK類庫(kù)中反序列化的步驟
步驟一:創(chuàng)建一個(gè)對(duì)象輸入流,它可以包裝一個(gè)其它類型輸入流,如文件輸入流:
ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));步驟二:通過對(duì)象輸出流的readObject()方法讀取對(duì)象:
User user = (User) ois.readObject();說明:為了正確讀取數(shù)據(jù),完成反序列化,必須保證向?qū)ο筝敵隽鲗憣?duì)象的順序與從對(duì)象輸入流中讀對(duì)象的順序一致。
6、序列化和反序列化的示例
為了更好地理解Java序列化與反序列化,舉一個(gè)簡(jiǎn)單的示例如下:
public class SerialDemo {public static void main(String[] args) throws IOException, ClassNotFoundException {//序列化FileOutputStream fos = new FileOutputStream("object.out");ObjectOutputStream oos = new ObjectOutputStream(fos);User user1 = new User("xuliugen", "123456", "male");oos.writeObject(user1);oos.flush();oos.close();//反序列化FileInputStream fis = new FileInputStream("object.out");ObjectInputStream ois = new ObjectInputStream(fis);User user2 = (User) ois.readObject();System.out.println(user2.getUserName()+ " " + user2.getPassword() + " " + user2.getSex());//反序列化的輸出結(jié)果為:xuliugen 123456 male} }public class User implements Serializable {private String userName;private String password;private String sex;//全參構(gòu)造方法、get和set方法省略 }相關(guān)注意事項(xiàng)
1、序列化時(shí),只對(duì)對(duì)象的狀態(tài)進(jìn)行保存,而不管對(duì)象的方法;
2、當(dāng)一個(gè)父類實(shí)現(xiàn)序列化,子類自動(dòng)實(shí)現(xiàn)序列化,不需要顯式實(shí)現(xiàn) Serializable 接口;
3、當(dāng)一個(gè)對(duì)象的實(shí)例變量引用其他對(duì)象,序列化該對(duì)象時(shí)也把引用對(duì)象進(jìn)行序列化;
4、并非所有的對(duì)象都可以序列化,至于為什么不可以,有很多原因了,比如:
- 安全方面的原因,比如一個(gè)對(duì)象擁有private,public等field,對(duì)于一個(gè)要傳輸?shù)膶?duì)象,比如寫到文件,或者進(jìn)行RMI傳輸?shù)鹊?#xff0c;在序列化進(jìn)行傳輸?shù)倪^程中,這個(gè)對(duì)象的private等域是不受保護(hù)的;
- 資源分配方面的原因,比如socket,thread類,如果可以序列化,進(jìn)行傳輸或者保存,也無法對(duì)他們進(jìn)行重新的資源分配,而且,也是沒有必要這樣實(shí)現(xiàn);
5、聲明為static和transient類型的成員數(shù)據(jù)不能被序列化。因?yàn)閟tatic代表類的狀態(tài),transient代表對(duì)象的臨時(shí)數(shù)據(jù)。
6、序列化運(yùn)行時(shí)使用一個(gè)稱為 serialVersionUID 的版本號(hào)與每個(gè)可序列化類相關(guān)聯(lián),該序列號(hào)在反序列化過程中用于驗(yàn)證序列化對(duì)象的發(fā)送者和接收者是否為該對(duì)象加載了與序列化兼容的類。為它賦予明確的值。顯式地定義serialVersionUID有兩種用途:
- 在某些場(chǎng)合,希望類的不同版本對(duì)序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
- 在某些場(chǎng)合,不希望類的不同版本對(duì)序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。
7、Java有很多基礎(chǔ)類已經(jīng)實(shí)現(xiàn)了serializable接口,比如String , Vector等。但是也有一些沒有實(shí)現(xiàn)serializable接口的;
8、如果一個(gè)對(duì)象的成員變量是一個(gè)對(duì)象,那么這個(gè)對(duì)象的數(shù)據(jù)成員也會(huì)被保存!這是能用序列化解決深拷貝的重要原因;
ArrayList 序列化和反序列化的實(shí)現(xiàn) :ArrayList 中存儲(chǔ)數(shù)據(jù)的數(shù)組是用 transient 修飾的,因?yàn)檫@個(gè)數(shù)組是動(dòng)態(tài)擴(kuò)展的,并不是所有的空間都被使用,因此就不需要所有的內(nèi)容都被序列化。通過重寫序列化和反序列化方法,使得可以只序列化數(shù)組中有內(nèi)容的那部分?jǐn)?shù)據(jù)。
private transient Object[] elementData;參考資料:
- 序列化和反序列化的底層實(shí)現(xiàn)原理是什么? - CSDN博客
常見的序列化協(xié)議有哪些
COM主要用于Windows平臺(tái),并沒有真正實(shí)現(xiàn)跨平臺(tái),另外COM的序列化的原理利用了編譯器中虛表,使得其學(xué)習(xí)成本巨大。
CORBA是早期比較好的實(shí)現(xiàn)了跨平臺(tái),跨語言的序列化協(xié)議。COBRA的主要問題是參與方過多帶來的版本過多,版本之間兼容性較差,以及使用復(fù)雜晦澀。
XML & SOAP
- XML是一種常用的序列化和反序列化協(xié)議,具有跨機(jī)器,跨語言等優(yōu)點(diǎn)。
- SOAP(Simple Object Access protocol) 是一種被廣泛應(yīng)用的,基于XML為序列化和反序列化協(xié)議的結(jié)構(gòu)化消息傳遞協(xié)議。SOAP具有安全、可擴(kuò)展、跨語言、跨平臺(tái)并支持多種傳輸層協(xié)議。
JSON(JavaScript Object Notation)
- 這種Associative array格式非常符合工程師對(duì)對(duì)象的理解。
- 它保持了XML的人眼可讀(Human-readable)的優(yōu)點(diǎn)。
- 相對(duì)于XML而言,序列化后的數(shù)據(jù)更加簡(jiǎn)潔。
- 它具備javascript的先天性支持,所以被廣泛應(yīng)用于Web browser的應(yīng)用常景中,是Ajax的事實(shí)標(biāo)準(zhǔn)協(xié)議。
- 與XML相比,其協(xié)議比較簡(jiǎn)單,解析速度比較快。
- 松散的Associative array使得其具有良好的可擴(kuò)展性和兼容性。
Thrift是Facebook開源提供的一個(gè)高性能,輕量級(jí)RPC服務(wù)框架,其產(chǎn)生正是為了滿足當(dāng)前大數(shù)據(jù)量、分布式、跨語言、跨平臺(tái)數(shù)據(jù)通訊的需求。Thrift在空間開銷和解析性能上有了比較大的提升,對(duì)于對(duì)性能要求比較高的分布式系統(tǒng),它是一個(gè)優(yōu)秀的RPC解決方案;但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并沒有透出序列化和反序列化接口,這導(dǎo)致其很難和其他傳輸層協(xié)議共同使用
Protobuf具備了優(yōu)秀的序列化協(xié)議的所需的眾多典型特征
- 標(biāo)準(zhǔn)的IDL和IDL編譯器,這使得其對(duì)工程師非常友好。
- 序列化數(shù)據(jù)非常簡(jiǎn)潔,緊湊,與XML相比,其序列化之后的數(shù)據(jù)量約為1/3到1/10。
- 解析速度非常快,比對(duì)應(yīng)的XML快約20-100倍。
- 提供了非常友好的動(dòng)態(tài)庫(kù),使用非常簡(jiǎn)介,反序列化只需要一行代碼。由于其解析性能高,序列化后數(shù)據(jù)量相對(duì)少,非常適合應(yīng)用層對(duì)象的持久化場(chǎng)景
Avro的產(chǎn)生解決了JSON的冗長(zhǎng)和沒有IDL的問題,Avro屬于Apache Hadoop的一個(gè)子項(xiàng)目。 Avro提供兩種序列化格式:JSON格式或者Binary格式。Binary格式在空間開銷和解析性能方面可以和Protobuf媲美,JSON格式方便測(cè)試階段的調(diào)試。適合于高性能的序列化服務(wù)。
幾種協(xié)議的對(duì)比
- XML序列化(Xstream)無論在性能和簡(jiǎn)潔性上比較差;
- Thrift與Protobuf相比在時(shí)空開銷方面都有一定的劣勢(shì);
- Protobuf和Avro在兩方面表現(xiàn)都非常優(yōu)越。
5、同步和異步
同步IO:
- 讀寫IO時(shí)代碼等數(shù)據(jù)返回后才繼續(xù)執(zhí)行后續(xù)代碼
- 代碼編寫簡(jiǎn)單,CPU執(zhí)行效率低
- JDK提供的java.io是同步IO
異步IO:
- 讀寫IO時(shí)僅發(fā)出請(qǐng)求,然后立即執(zhí)行后續(xù)代碼
- 代碼編寫復(fù)雜,CPU執(zhí)行效率高
- JDK提供的java.nio是異步IO
6、Java中的NIO,BIO,AIO分別是什么
- 同步阻塞IO(BIO):用戶進(jìn)程發(fā)起一個(gè)IO操作以后,必須等待IO操作的真正完成后,才能繼續(xù)運(yùn)行;
- 同步非阻塞IO(NIO):用戶進(jìn)程發(fā)起一個(gè)IO操作以后,可做其它事情,但用戶進(jìn)程需要經(jīng)常詢問IO操作是否完成,這樣造成不必要的CPU資源浪費(fèi);
異步非阻塞IO(AIO):用戶進(jìn)程發(fā)起一個(gè)IO操作然后,立即返回,等IO操作真正的完成以后,應(yīng)用程序會(huì)得到IO操作完成的通知。類比Future模式。
- 先來個(gè)例子理解一下概念,以銀行取款為例:
- 同步 : 自己親自出馬持銀行卡到銀行取錢(使用同步IO時(shí),Java自己處理IO讀寫)。
- 異步 : 委托一小弟拿銀行卡到銀行取錢,然后給你(使用異步IO時(shí),Java將IO讀寫委托給OS處理,需要將數(shù)據(jù)緩沖區(qū)地址和大小傳給OS(銀行卡和密碼),OS需要支持異步IO操作API)。
- 阻塞 : ATM排隊(duì)取款,你只能等待(使用阻塞IO時(shí),Java調(diào)用會(huì)一直阻塞到讀寫完成才返回)。
- 非阻塞 : 柜臺(tái)取款,取個(gè)號(hào),然后坐在椅子上做其它事,等號(hào)廣播會(huì)通知你辦理,沒到號(hào)你就不能去,你可以不斷問大堂經(jīng)理排到了沒有,大堂經(jīng)理如果說還沒到你就不能去(使用非阻塞IO時(shí),如果不能讀寫Java調(diào)用會(huì)馬上返回,當(dāng)IO事件分發(fā)器會(huì)通知可讀寫時(shí)再繼續(xù)進(jìn)行讀寫,不斷循環(huán)直到讀寫完成)。
BIO
定義:BIO 全稱Block-IO 是一種阻塞同步的通信模式。我們常說的Stock IO 一般指的是BIO。是一個(gè)比較傳統(tǒng)的通信方式,模式簡(jiǎn)單,使用方便。但并發(fā)處理能力低,通信耗時(shí),依賴網(wǎng)速。
BIO 設(shè)計(jì)原理:
服務(wù)器通過一個(gè) Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端請(qǐng)求和為每個(gè)客戶端創(chuàng)建一個(gè)新的線程進(jìn)行鏈路處理。典型的一請(qǐng)求一應(yīng)答模式。若客戶端數(shù)量增多,頻繁地創(chuàng)建和銷毀線程會(huì)給服務(wù)器打開很大的壓力。后改良為用線程池的方式代替新增線程,被稱為偽異步IO。
服務(wù)器提供IP地址和監(jiān)聽的端口,客戶端通過TCP的三次握手與服務(wù)器連接,連接成功后,雙放才能通過套接字(Stock)通信。
小結(jié):
BIO模型中通過 Socket 和 ServerSocket 完成套接字通道的實(shí)現(xiàn)。阻塞,同步,建立連接耗時(shí)。
為了改進(jìn)這種一連接一線程的模型,我們可以使用線程池來管理這些線程(需要了解更多請(qǐng)參考前面提供的文章),實(shí)現(xiàn)1個(gè)或多個(gè)線程處理N個(gè)客戶端的模型(但是底層還是使用的同步阻塞I/O),通常被稱為“偽異步I/O模型“。
實(shí)現(xiàn)很簡(jiǎn)單,我們只需要將新建線程的地方,交給線程池管理即可。
我們知道,如果使用 CachedThreadPool 線程池(不限制線程數(shù)量,如果不清楚請(qǐng)參考文首提供的文章),其實(shí)除了能自動(dòng)幫我們管理線程(復(fù)用),看起來也就像是1:1的客戶端:線程數(shù)模型,而使用 FixedThreadPool 我們就有效的控制了線程的最大數(shù)量,保證了系統(tǒng)有限的資源的控制,實(shí)現(xiàn)了N:M的偽異步 I/O 模型。
但是,正因?yàn)橄拗屏司€程數(shù)量,如果發(fā)生大量并發(fā)請(qǐng)求,超過最大數(shù)量的線程就只能等待,直到線程池中的有空閑的線程可以被復(fù)用。而對(duì) Socket 的輸入流就行讀取時(shí),會(huì)一直阻塞,直到發(fā)生:
- 有數(shù)據(jù)可讀
- 可用數(shù)據(jù)以及讀取完畢
- 發(fā)生空指針或 I/O 異常
所以在讀取數(shù)據(jù)較慢時(shí)(比如數(shù)據(jù)量大、網(wǎng)絡(luò)傳輸慢等),大量并發(fā)的情況下,其他接入的消息,只能一直等待,這就是最大的弊端。
而后面即將介紹的NIO,就能解決這個(gè)難題。
NIO
NIO(官方:New IO),也叫Non-Block IO 是一種同步非阻塞的通信模式。
NIO 設(shè)計(jì)原理:
NIO相對(duì)于BIO來說一大進(jìn)步。客戶端和服務(wù)器之間通過Channel通信。NIO可以在Channel進(jìn)行讀寫操作。這些Channel都會(huì)被注冊(cè)在Selector多路復(fù)用器上。Selector通過一個(gè)線程不停的輪詢這些Channel。找出已經(jīng)準(zhǔn)備就緒的Channel執(zhí)行IO操作。
NIO 通過一個(gè)線程輪詢,實(shí)現(xiàn)千萬個(gè)客戶端的請(qǐng)求,這就是非阻塞NIO的特點(diǎn)。
1)緩沖區(qū)Buffer:它是NIO與BIO的一個(gè)重要區(qū)別。BIO是將數(shù)據(jù)直接寫入或讀取到Stream對(duì)象中。而NIO的數(shù)據(jù)操作都是在緩沖區(qū)中進(jìn)行的。緩沖區(qū)實(shí)際上是一個(gè)數(shù)組。Buffer最常見的類型是ByteBuffer,另外還有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
2)通道Channel:和流不同,通道是雙向的。NIO可以通過Channel進(jìn)行數(shù)據(jù)的讀,寫和同時(shí)讀寫操作。通道分為兩大類:一類是網(wǎng)絡(luò)讀寫(SelectableChannel),一類是用于文件操作(FileChannel),我們使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子類。
3)多路復(fù)用器Selector:NIO編程的基礎(chǔ)。多路復(fù)用器提供選擇已經(jīng)就緒的任務(wù)的能力。就是Selector會(huì)不斷地輪詢注冊(cè)在其上的通道(Channel),如果某個(gè)通道處于就緒狀態(tài),會(huì)被Selector輪詢出來,然后通過SelectionKey可以取得就緒的Channel集合,從而進(jìn)行后續(xù)的IO操作。服務(wù)器端只要提供一個(gè)線程負(fù)責(zé)Selector的輪詢,就可以接入成千上萬個(gè)客戶端,這就是JDK NIO庫(kù)的巨大進(jìn)步。
小結(jié):NIO模型中通過SocketChannel和ServerSocketChannel完成套接字通道的實(shí)現(xiàn)。非阻塞/阻塞,同步,避免TCP建立連接使用三次握手帶來的開銷。
AIO (NIO.2)
- 異步非阻塞,服務(wù)器實(shí)現(xiàn)模式為一個(gè)有效請(qǐng)求一個(gè)線程,客戶端的I/O請(qǐng)求都是由OS先完成了再通知服務(wù)器應(yīng)用去啟動(dòng)線程進(jìn)行處理.
- AIO方式使用于連接數(shù)目多且連接比較長(zhǎng)(重操作)的架構(gòu),比如相冊(cè)服務(wù)器,充分調(diào)用OS參與并發(fā)操作,編程比較復(fù)雜,JDK7開始支持。
AIO 并沒有采用NIO的多路復(fù)用器,而是使用異步通道的概念。其read,write方法的返回類型都是Future對(duì)象。而Future模型是異步的,其核心思想是:去主函數(shù)等待時(shí)間。
小結(jié):AIO模型中通過AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道的實(shí)現(xiàn)。非阻塞,異步。
總結(jié)
另外,I/O屬于底層操作,需要操作系統(tǒng)支持,并發(fā)也需要操作系統(tǒng)的支持,所以性能方面不同操作系統(tǒng)差異會(huì)比較明顯。
參考:
- Java BIO、NIO、AIO 學(xué)習(xí)-力量來源于赤誠(chéng)的愛!-51CTO博客
- Netty序章之BIO NIO AIO演變 - JavaEE教程 - SegmentFault 思否
- Java 網(wǎng)絡(luò)IO編程總結(jié)(BIO、NIO、AIO均含完整實(shí)例代碼) - CSDN博客
- Java IO Tutorial
7、BIO,NIO,AIO區(qū)別
- BIO(同步阻塞):客戶端和服務(wù)器連接需要三次握手,使用簡(jiǎn)單,但吞吐量小
- NIO(同步非阻塞):客戶端與服務(wù)器通過Channel連接,采用多路復(fù)用器輪詢注冊(cè)的Channel。提高吞吐量和可靠性。
- AIO(異步非阻塞):NIO的升級(jí)版,采用異步通道實(shí)現(xiàn)異步通信,其read和write方法均是異步方法。
8、Stock通信的偽代碼實(shí)現(xiàn)流程
9、網(wǎng)絡(luò)操作
Java 中的網(wǎng)絡(luò)支持:
- InetAddress:用于表示網(wǎng)絡(luò)上的硬件資源,即 IP 地址;
- URL:統(tǒng)一資源定位符;
- Sockets:使用 TCP 協(xié)議實(shí)現(xiàn)網(wǎng)絡(luò)通信;
- Datagram:使用 UDP 協(xié)議實(shí)現(xiàn)網(wǎng)絡(luò)通信。
InetAddress
沒有公有構(gòu)造函數(shù),只能通過靜態(tài)方法來創(chuàng)建實(shí)例。
InetAddress.getByName(String host); InetAddress.getByAddress(byte[] address);URL
可以直接從 URL 中讀取字節(jié)流數(shù)據(jù)。
public static void main(String[] args) throws IOException {URL url = new URL("http://www.baidu.com");// 字節(jié)流InputStream is = url.openStream();// 字符流InputStreamReader isr = new InputStreamReader(is, "utf-8");BufferedReader br = new BufferedReader(isr);String line = br.readLine();while (line != null) {System.out.println(line);line = br.readLine();}br.close(); }Sockets
- ServerSocket:服務(wù)器端類
- Socket:客戶端類
- 服務(wù)器和客戶端通過 InputStream 和 OutputStream 進(jìn)行輸入輸出。
參考資料:
- 使用TCP/IP的套接字(Socket)進(jìn)行通信 - alps_01 - 博客園
Datagram
- DatagramPacket:數(shù)據(jù)包類
- DatagramSocket:通信類
什么是Socket?
TCP用主機(jī)的IP地址加上主機(jī)上的端口號(hào)作為TCP連接的端點(diǎn),這種端點(diǎn)就叫做套接字(socket)或插口。
套接字用(IP地址:端口號(hào))表示。
Socket是進(jìn)程通訊的一種方式,即調(diào)用這個(gè)網(wǎng)絡(luò)庫(kù)的一些API函數(shù)實(shí)現(xiàn)分布在不同主機(jī)的相關(guān)進(jìn)程之間的數(shù)據(jù)交換。
socket是網(wǎng)絡(luò)編程的基礎(chǔ),本文用打電話來類比socket通信中建立TCP連接的過程。
socket函數(shù):表示你買了或者借了一部手機(jī)。
bind函數(shù):告訴別人你的手機(jī)號(hào)碼,讓他們給你打電話。
listen函數(shù):打開手機(jī)的鈴聲,而不是靜音,這樣有電話時(shí)可以立馬反應(yīng)。listen函數(shù)的第二個(gè)參數(shù),最大連接數(shù),表示最多有幾個(gè)人可以同時(shí)撥打你的號(hào)碼。不過我們的手機(jī),最多只能有一個(gè)人打進(jìn)來,要不然就提示占線。
connect函數(shù):你的朋友知道了你的號(hào)碼,通過這個(gè)號(hào)碼來聯(lián)系你。在他等待你回應(yīng)的時(shí)候,不能做其他事情,所以connect函數(shù)是阻塞的。
accept函數(shù):你聽到了電話鈴聲,接電話,accept it!然后“喂”一聲,你的朋友聽到你的回應(yīng),知道電話已經(jīng)打進(jìn)去了。至此,一個(gè)TCP連接建立了。
read/write函數(shù):連接建立后,TCP的兩端可以互相收發(fā)消息,這時(shí)候的連接是全雙工的。對(duì)應(yīng)打電話中的電話煲。
close函數(shù):通話完畢,一方說“我掛了”,另一方回應(yīng)"你掛吧",然后將連接終止。實(shí)際的close(sockfd)有些不同,它不止是終止連接,還把手機(jī)也歸還,不在占有這部手機(jī),就當(dāng)是公用電話吧。
注意到,上述連接是阻塞的,你一次只能響應(yīng)一個(gè)用戶的連接請(qǐng)求,但在實(shí)際網(wǎng)絡(luò)編程中,一個(gè)服務(wù)器服務(wù)于多個(gè)客戶,上述方案也就行不通了,怎么辦?想一想10086,移動(dòng)的聲訊服務(wù)臺(tái),也是只有一個(gè)號(hào)碼,它怎么能同時(shí)服務(wù)那么多人呢?可以這樣理解,在你打電話到10086時(shí),總服務(wù)臺(tái)會(huì)讓一個(gè)接線員來為你服務(wù),而它自己卻繼續(xù)監(jiān)聽有沒有新的電話接入。在網(wǎng)絡(luò)編程中,這個(gè)過程類似于fork一個(gè)子進(jìn)程,建立實(shí)際的通信連接,而主進(jìn)程繼續(xù)監(jiān)聽。10086的接線員是有限的,所以當(dāng)連接的人數(shù)達(dá)到上線時(shí),它會(huì)放首歌給你聽,忙等待,直到有新的空閑接線員為止。
實(shí)際網(wǎng)絡(luò)編程中,處理并發(fā)的方式還有select/poll/epoll等。
下面是一個(gè)實(shí)際的socket通信過程:
Socket的特點(diǎn)
轉(zhuǎn)載于:https://www.cnblogs.com/chung567115/p/9765281.html
總結(jié)
以上是生活随笔為你收集整理的Java修炼之道--I/O的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python基础:函数的介绍及应用
- 下一篇: Disconf 学习系列之全网最详细的最