JDK10都发布了,nio你了解多少?
前言
只有光頭才能變強(qiáng)回顧前面:
- 給女朋友講解什么是代理模式
- 包裝模式就是這么簡單啦
本來我預(yù)想是先來回顧一下傳統(tǒng)的IO模式的,將傳統(tǒng)的IO模式的相關(guān)類理清楚(因?yàn)镮O的類很多)。
但是,發(fā)現(xiàn)在整理的過程已經(jīng)有很多優(yōu)秀的文章了,而我自己來整理的話可能達(dá)不到他們的水平。并且傳統(tǒng)的IO估計(jì)大家都會(huì)用,而NIO就不一定了。
下面我就貼幾張我認(rèn)為整理比較優(yōu)秀的思維導(dǎo)圖(下面會(huì)給出圖片來源地址,大家可前往閱讀):
按操作方式分類結(jié)構(gòu)圖:
字節(jié)流的輸入和輸出對(duì)照?qǐng)D:
字符流的輸入和輸出對(duì)照?qǐng)D:
按操作對(duì)象分類結(jié)構(gòu)圖:
上述圖片原文地址,知乎作者@小明:
- https://zhuanlan.zhihu.com/p/28286559
還有閱讀傳統(tǒng)IO源碼的優(yōu)秀文章:
- https://blog.csdn.net/panweiwei1994/article/details/78046000
相信大家看完上面兩個(gè)給出的鏈接+理解了包裝模式就是這么簡單啦,傳統(tǒng)的IO應(yīng)該就沒什么事啦~~
而NIO對(duì)于我來說可以說是挺陌生的,在當(dāng)初學(xué)的時(shí)候是接觸過的。但是一直沒有用它,所以停留認(rèn)知:nio是jdk1.4開始有的,比傳統(tǒng)IO高級(jí)。
相信很多初學(xué)者都跟我一樣,對(duì)NIO是不太了解的。而我們現(xiàn)在jdk10都已經(jīng)發(fā)布了,jdk1.4的nio都不知道,這有點(diǎn)說不過去了。
所以我花了幾天去了解NIO的核心知識(shí)點(diǎn),期間看了《Java 編程思想》和《瘋狂Java 講義》的nio模塊。但是,會(huì)發(fā)現(xiàn)看完了之后還是很迷,不知道NIO這是干嘛用的,而網(wǎng)上的資料與書上的知識(shí)點(diǎn)沒有很好地對(duì)應(yīng)。
- 網(wǎng)上的資料很多都以IO的五種模型為基礎(chǔ)來講解NIO,而IO這五種模型其中又涉及到了很多概念:同步/異步/阻塞/非阻塞/多路復(fù)用,而不同的人又有不同的理解方式。
- 還有涉及到了unix的select/epoll/poll/pselect,fd這些關(guān)鍵字,沒有相關(guān)基礎(chǔ)的人看起來簡直是天書
- 這就導(dǎo)致了在初學(xué)時(shí)認(rèn)為nio遠(yuǎn)不可及
我在找資料的過程中也收藏了好多講解NIO的資料,這篇文章就是以初學(xué)的角度來理解NIO。也算是我這兩天看NIO的一個(gè)總結(jié)吧。
- 希望大家可以看了之后知道什么是NIO,NIO的核心知識(shí)點(diǎn)是什么,會(huì)使用NIO~
那么接下來就開始吧,如果文章有錯(cuò)誤的地方請(qǐng)大家多多包涵,不吝在評(píng)論區(qū)指正哦~
聲明:本文使用JDK1.8一、NIO的概述
JDK 1.4中的java.nio.*包中引入新的Java I/O庫,其目的是提高速度。實(shí)際上,“舊”的I/O包已經(jīng)使用NIO重新實(shí)現(xiàn)過,即使我們不顯式的使用NIO編程,也能從中受益。
- nio翻譯成 no-blocking io 或者 new io 都無所謂啦,都說得通~
在《Java編程思想》讀到“即使我們不顯式的使用NIO編程,也能從中受益”的時(shí)候,我是挺在意的,所以:我們測試一下使用NIO復(fù)制文件和傳統(tǒng)IO復(fù)制文件的性能:
import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel;public class SimpleFileTransferTest {private long transferFile(File source, File des) throws IOException {long startTime = System.currentTimeMillis();if (!des.exists())des.createNewFile();BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des));//將數(shù)據(jù)源讀到的內(nèi)容寫入目的地--使用數(shù)組byte[] bytes = new byte[1024 * 1024];int len;while ((len = bis.read(bytes)) != -1) {bos.write(bytes, 0, len);}long endTime = System.currentTimeMillis();return endTime - startTime;}private long transferFileWithNIO(File source, File des) throws IOException {long startTime = System.currentTimeMillis();if (!des.exists())des.createNewFile();RandomAccessFile read = new RandomAccessFile(source, "rw");RandomAccessFile write = new RandomAccessFile(des, "rw");FileChannel readChannel = read.getChannel();FileChannel writeChannel = write.getChannel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);//1M緩沖區(qū)while (readChannel.read(byteBuffer) > 0) {byteBuffer.flip();writeChannel.write(byteBuffer);byteBuffer.clear();}writeChannel.close();readChannel.close();long endTime = System.currentTimeMillis();return endTime - startTime;}public static void main(String[] args) throws IOException {SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();File sourse = new File("F:\\電影\\[電影天堂www.dygod.cn]猜火車-cd1.rmvb");File des = new File("X:\\Users\\ozc\\Desktop\\io.avi");File nio = new File("X:\\Users\\ozc\\Desktop\\nio.avi");long time = simpleFileTransferTest.transferFile(sourse, des);System.out.println(time + ":普通字節(jié)流時(shí)間");long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);System.out.println(timeNio + ":NIO時(shí)間");}}我分別測試了文件大小為13M,40M,200M的:
1.1為什么要使用NIO
可以看到使用過NIO重新實(shí)現(xiàn)過的傳統(tǒng)IO根本不虛,在大文件下效果還比NIO要好(當(dāng)然了,個(gè)人幾次的測試,或許不是很準(zhǔn))
- 而NIO要有一定的學(xué)習(xí)成本,也沒有傳統(tǒng)IO那么好理解。
那這意味著我們可以不使用/學(xué)習(xí)NIO了嗎?
答案是否定的,IO操作往往在兩個(gè)場景下會(huì)用到:
- 文件IO
- 網(wǎng)絡(luò)IO
NIO的魅力:在網(wǎng)絡(luò)中使用IO就可以體現(xiàn)出來了!
- 后面會(huì)說到網(wǎng)絡(luò)中使用NIO,不急哈~
二、NIO快速入門
首先我們來看看IO和NIO的區(qū)別:
-
可簡單認(rèn)為:IO是面向流的處理,NIO是面向塊(緩沖區(qū))的處理
- 面向流的I/O 系統(tǒng)一次一個(gè)字節(jié)地處理數(shù)據(jù)。
- 一個(gè)面向塊(緩沖區(qū))的I/O系統(tǒng)以塊的形式處理數(shù)據(jù)。
NIO主要有三個(gè)核心部分組成:
- buffer緩沖區(qū)
- Channel管道
- Selector選擇器
2.1buffer緩沖區(qū)和Channel管道
在NIO中并不是以流的方式來處理數(shù)據(jù)的,而是以buffer緩沖區(qū)和Channel管道配合使用來處理數(shù)據(jù)。
簡單理解一下:
- Channel管道比作成鐵路,buffer緩沖區(qū)比作成火車(運(yùn)載著貨物)
而我們的NIO就是通過Channel管道運(yùn)輸著存儲(chǔ)數(shù)據(jù)的Buffer緩沖區(qū)的來實(shí)現(xiàn)數(shù)據(jù)的處理!
-
要時(shí)刻記住:Channel不與數(shù)據(jù)打交道,它只負(fù)責(zé)運(yùn)輸數(shù)據(jù)。與數(shù)據(jù)打交道的是Buffer緩沖區(qū)
- Channel-->運(yùn)輸
- Buffer-->數(shù)據(jù)
相對(duì)于傳統(tǒng)IO而言,流是單向的。對(duì)于NIO而言,有了Channel管道這個(gè)概念,我們的讀寫都是雙向的(鐵路上的火車能從廣州去北京、自然就能從北京返還到廣州)!
2.1.1buffer緩沖區(qū)核心要點(diǎn)
我們來看看Buffer緩沖區(qū)有什么值得我們注意的地方。
Buffer是緩沖區(qū)的抽象類:
其中ByteBuffer是用得最多的實(shí)現(xiàn)類(在管道中讀寫字節(jié)數(shù)據(jù))。
拿到一個(gè)緩沖區(qū)我們往往會(huì)做什么?很簡單,就是讀取緩沖區(qū)的數(shù)據(jù)/寫數(shù)據(jù)到緩沖區(qū)中。所以,緩沖區(qū)的核心方法就是:
- put()
- get()
Buffer類維護(hù)了4個(gè)核心變量屬性來提供關(guān)于其所包含的數(shù)組的信息。它們是:
-
容量Capacity
- 緩沖區(qū)能夠容納的數(shù)據(jù)元素的最大數(shù)量。容量在緩沖區(qū)創(chuàng)建時(shí)被設(shè)定,并且永遠(yuǎn)不能被改變。(不能被改變的原因也很簡單,底層是數(shù)組嘛)
-
上界Limit
- 緩沖區(qū)里的數(shù)據(jù)的總數(shù),代表了當(dāng)前緩沖區(qū)中一共有多少數(shù)據(jù)。
-
位置Position
- 下一個(gè)要被讀或?qū)懙脑氐奈恢?/strong>。Position會(huì)自動(dòng)由相應(yīng)的 get( )和 put( )函數(shù)更新。
-
標(biāo)記Mark
- 一個(gè)備忘位置。用于記錄上一次讀寫的位置。
2.1.2buffer代碼演示
首先展示一下是如何創(chuàng)建緩沖區(qū)的,核心變量的值是怎么變化的。
public static void main(String[] args) {// 創(chuàng)建一個(gè)緩沖區(qū)ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 看一下初始時(shí)4個(gè)核心變量的值System.out.println("初始時(shí)-->limit--->"+byteBuffer.limit());System.out.println("初始時(shí)-->position--->"+byteBuffer.position());System.out.println("初始時(shí)-->capacity--->"+byteBuffer.capacity());System.out.println("初始時(shí)-->mark--->" + byteBuffer.mark());System.out.println("--------------------------------------");// 添加一些數(shù)據(jù)到緩沖區(qū)中String s = "Java3y";byteBuffer.put(s.getBytes());// 看一下初始時(shí)4個(gè)核心變量的值System.out.println("put完之后-->limit--->"+byteBuffer.limit());System.out.println("put完之后-->position--->"+byteBuffer.position());System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());System.out.println("put完之后-->mark--->" + byteBuffer.mark());}運(yùn)行結(jié)果:
現(xiàn)在我想要從緩存區(qū)拿數(shù)據(jù),怎么拿呀??NIO給了我們一個(gè)flip()方法。這個(gè)方法可以改動(dòng)position和limit的位置!
還是上面的代碼,我們flip()一下后,再看看4個(gè)核心屬性的值會(huì)發(fā)生什么變化:
很明顯的是:
- limit變成了position的位置了
- 而position變成了0
看到這里的同學(xué)可能就會(huì)想到了:當(dāng)調(diào)用完filp()時(shí):limit是限制讀到哪里,而position是從哪里讀
一般我們稱filp()為“切換成讀模式”
- 每當(dāng)要從緩存區(qū)的時(shí)候讀取數(shù)據(jù)時(shí),就調(diào)用filp()“切換成讀模式”。
切換成讀模式之后,我們就可以讀取緩沖區(qū)的數(shù)據(jù)了:
// 創(chuàng)建一個(gè)limit()大小的字節(jié)數(shù)組(因?yàn)榫椭挥衛(wèi)imit這么多個(gè)數(shù)據(jù)可讀)byte[] bytes = new byte[byteBuffer.limit()];// 將讀取的數(shù)據(jù)裝進(jìn)我們的字節(jié)數(shù)組中byteBuffer.get(bytes);// 輸出數(shù)據(jù)System.out.println(new String(bytes, 0, bytes.length));
隨后輸出一下核心變量的值看看:
讀完我們還想寫數(shù)據(jù)到緩沖區(qū),那就使用clear()函數(shù),這個(gè)函數(shù)會(huì)“清空”緩沖區(qū):
- 數(shù)據(jù)沒有真正被清空,只是被遺忘掉了
2.1.3FileChannel通道核心要點(diǎn)
Channel通道只負(fù)責(zé)傳輸數(shù)據(jù)、不直接操作數(shù)據(jù)的。操作數(shù)據(jù)都是通過Buffer緩沖區(qū)來進(jìn)行操作!
// 1. 通過本地IO的方式來獲取通道FileInputStream fileInputStream = new FileInputStream("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是這么簡單.md");// 得到文件的輸入通道FileChannel inchannel = fileInputStream.getChannel();// 2. jdk1.7后通過靜態(tài)方法.open()獲取通道FileChannel.open(Paths.get("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是這么簡單2.md"), StandardOpenOption.WRITE);使用FileChannel配合緩沖區(qū)實(shí)現(xiàn)文件復(fù)制的功能:
使用內(nèi)存映射文件的方式實(shí)現(xiàn)文件復(fù)制的功能(直接操作緩沖區(qū)):
通道之間通過transfer()實(shí)現(xiàn)數(shù)據(jù)的傳輸(直接操作緩沖區(qū)):
2.1.4直接與非直接緩沖區(qū)
- 非直接緩沖區(qū)是需要經(jīng)過一個(gè):copy的階段的(從內(nèi)核空間copy到用戶空間)
- 直接緩沖區(qū)不需要經(jīng)過copy階段,也可以理解成--->內(nèi)存映射文件,(上面的圖片也有過例子)。
使用直接緩沖區(qū)有兩種方式:
- 緩沖區(qū)創(chuàng)建的時(shí)候分配的是直接緩沖區(qū)
- 在FileChannel上調(diào)用map()方法,將文件直接映射到內(nèi)存中創(chuàng)建
2.1.5scatter和gather、字符集
這個(gè)知識(shí)點(diǎn)我感覺用得挺少的,不過很多教程都有說這個(gè)知識(shí)點(diǎn),我也拿過來說說吧:
- 分散讀取(scatter):將一個(gè)通道中的數(shù)據(jù)分散讀取到多個(gè)緩沖區(qū)中
- 聚集寫入(gather):將多個(gè)緩沖區(qū)中的數(shù)據(jù)集中寫入到一個(gè)通道中
分散讀取
聚集寫入
字符集(只要編碼格式和解碼格式一致,就沒問題了)
三、IO模型理解
文件的IO就告一段落了,我們來學(xué)習(xí)網(wǎng)絡(luò)中的IO~~~為了更好地理解NIO,我們先來學(xué)習(xí)一下IO的模型~
根據(jù)UNIX網(wǎng)絡(luò)編程對(duì)I/O模型的分類,在UNIX可以歸納成5種I/O模型:
- 阻塞I/O
- 非阻塞I/O
- I/O多路復(fù)用
- 信號(hào)驅(qū)動(dòng)I/O
- 異步I/O
3.0學(xué)習(xí)I/O模型需要的基礎(chǔ)
3.0.1文件描述符
Linux 的內(nèi)核將所有外部設(shè)備都看做一個(gè)文件來操作,對(duì)一個(gè)文件的讀寫操作會(huì)調(diào)用內(nèi)核提供的系統(tǒng)命令(api),返回一個(gè)file descriptor(fd,文件描述符)。而對(duì)一個(gè)socket的讀寫也會(huì)有響應(yīng)的描述符,稱為socket fd(socket文件描述符),描述符就是一個(gè)數(shù)字,指向內(nèi)核中的一個(gè)結(jié)構(gòu)體(文件路徑,數(shù)據(jù)區(qū)等一些屬性)。
- 所以說:在Linux下對(duì)文件的操作是利用文件描述符(file descriptor)來實(shí)現(xiàn)的。
3.0.2用戶空間和內(nèi)核空間
為了保證用戶進(jìn)程不能直接操作內(nèi)核(kernel),保證內(nèi)核的安全,操心系統(tǒng)將虛擬空間劃分為兩部分
- 一部分為內(nèi)核空間。
- 一部分為用戶空間。
3.0.3I/O運(yùn)行過程
我們來看看IO在系統(tǒng)中的運(yùn)行是怎么樣的(我們以read為例)
可以發(fā)現(xiàn)的是:當(dāng)應(yīng)用程序調(diào)用read方法時(shí),是需要等待的--->從內(nèi)核空間中找數(shù)據(jù),再將內(nèi)核空間的數(shù)據(jù)拷貝到用戶空間的。
- 這個(gè)等待是必要的過程!
下面只講解用得最多的3個(gè)I/0模型:
- 阻塞I/O
- 非阻塞I/O
- I/O多路復(fù)用
3.1阻塞I/O模型
在進(jìn)程(用戶)空間中調(diào)用recvfrom,其系統(tǒng)調(diào)用直到數(shù)據(jù)包到達(dá)且被復(fù)制到應(yīng)用進(jìn)程的緩沖區(qū)中或者發(fā)生錯(cuò)誤時(shí)才返回,在此期間一直等待。
3.2非阻塞I/O模型
recvfrom從應(yīng)用層到內(nèi)核的時(shí)候,如果沒有數(shù)據(jù)就直接返回一個(gè)EWOULDBLOCK錯(cuò)誤,一般都對(duì)非阻塞I/O模型進(jìn)行輪詢檢查這個(gè)狀態(tài),看內(nèi)核是不是有數(shù)據(jù)到來。
3.3I/O復(fù)用模型
前面也已經(jīng)說了:在Linux下對(duì)文件的操作是利用文件描述符(file descriptor)來實(shí)現(xiàn)的。
在Linux下它是這樣子實(shí)現(xiàn)I/O復(fù)用模型的:
- 調(diào)用select/poll/epoll/pselect其中一個(gè)函數(shù),傳入多個(gè)文件描述符,如果有一個(gè)文件描述符就緒,則返回,否則阻塞直到超時(shí)。
比如poll()函數(shù)是這樣子的:int poll(struct pollfd *fds,nfds_t nfds, int timeout);
其中 pollfd 結(jié)構(gòu)定義如下:
struct pollfd {int fd; /* 文件描述符 */short events; /* 等待的事件 */short revents; /* 實(shí)際發(fā)生了的事件 */ };
- (1)當(dāng)用戶進(jìn)程調(diào)用了select,那么整個(gè)進(jìn)程會(huì)被block;
- (2)而同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket;
- (3)當(dāng)任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回;
- (4)這個(gè)時(shí)候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程(空間)。
- 所以,I/O 多路復(fù)用的特點(diǎn)是通過一種機(jī)制一個(gè)進(jìn)程能同時(shí)等待多個(gè)文件描述符,而這些文件描述符其中的任意一個(gè)進(jìn)入讀就緒狀態(tài),select()函數(shù)就可以返回。
select/epoll的優(yōu)勢并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。
3.4I/O模型總結(jié)
正經(jīng)的描述都在上面給出了,不知道大家理解了沒有。下面我舉幾個(gè)例子總結(jié)一下這三種模型:
阻塞I/O:
-
Java3y跟女朋友去買喜茶,排了很久的隊(duì)終于可以點(diǎn)飲料了。我要綠研,謝謝。可是喜茶不是點(diǎn)了單就能立即拿,于是我在喜茶門口等了一小時(shí)才拿到綠研。
- 在門口干等一小時(shí)
非阻塞I/O:
-
Java3y跟女朋友去買一點(diǎn)點(diǎn),排了很久的隊(duì)終于可以點(diǎn)飲料了。我要波霸奶茶,謝謝。可是一點(diǎn)點(diǎn)不是點(diǎn)了單就能立即拿,同時(shí)服務(wù)員告訴我:你大概要等半小時(shí)哦。你們先去逛逛吧~于是Java3y跟女朋友去玩了幾把斗地主,感覺時(shí)間差不多了。于是又去一點(diǎn)點(diǎn)問:請(qǐng)問到我了嗎?我的單號(hào)是xxx。服務(wù)員告訴Java3y:還沒到呢,現(xiàn)在的單號(hào)是XXX,你還要等一會(huì),可以去附近耍耍。問了好幾次后,終于拿到我的波霸奶茶了。
- 去逛了下街、斗了下地主,時(shí)不時(shí)問問到我了沒有
I/O復(fù)用模型:
-
Java3y跟女朋友去麥當(dāng)勞吃漢堡包,現(xiàn)在就厲害了可以使用微信小程序點(diǎn)餐了。于是跟女朋友找了個(gè)地方坐下就用小程序點(diǎn)餐了。點(diǎn)餐了之后玩玩斗地主、聊聊天什么的。時(shí)不時(shí)聽到廣播在復(fù)述XXX請(qǐng)取餐,反正我的單號(hào)還沒到,就繼續(xù)玩唄。~~等聽到廣播的時(shí)候再取餐就是了。時(shí)間過得挺快的,此時(shí)傳來:Java3y請(qǐng)過來取餐。于是我就能拿到我的麥辣雞翅漢堡了。
- 聽廣播取餐,廣播不是為我一個(gè)人服務(wù)。廣播喊到我了,我過去取就Ok了。
四、使用NIO完成網(wǎng)絡(luò)通信
4.1NIO基礎(chǔ)繼續(xù)講解
回到我們最開始的圖:
NIO被叫為 no-blocking io,其實(shí)是在網(wǎng)絡(luò)這個(gè)層次中理解的,對(duì)于FileChannel來說一樣是阻塞。
我們前面也僅僅講解了FileChannel,對(duì)于我們網(wǎng)絡(luò)通信是還有幾個(gè)Channel的~
所以說:我們通常使用NIO是在網(wǎng)絡(luò)中使用的,網(wǎng)上大部分討論NIO都是在網(wǎng)絡(luò)通信的基礎(chǔ)之上的!說NIO是非阻塞的NIO也是網(wǎng)絡(luò)中體現(xiàn)的!
從上面的圖我們可以發(fā)現(xiàn)還有一個(gè)Selector選擇器這么一個(gè)東東。從一開始我們就說過了,nio的核心要素有:
- Buffer緩沖區(qū)
- Channel通道
- Selector選擇器
我們在網(wǎng)絡(luò)中使用NIO往往是I/O模型的多路復(fù)用模型!
- Selector選擇器就可以比喻成麥當(dāng)勞的廣播。
- 一個(gè)線程能夠管理多個(gè)Channel的狀態(tài)
4.2NIO阻塞形態(tài)
為了更好地理解,我們先來寫一下NIO在網(wǎng)絡(luò)中是阻塞的狀態(tài)代碼,隨后看看非阻塞是怎么寫的就更容易理解了。
- 是阻塞的就沒有Selector選擇器了,就直接使用Channel和Buffer就完事了。
客戶端:
public class BlockClient {public static void main(String[] args) throws IOException {// 1. 獲取通道SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));// 2. 發(fā)送一張圖片給服務(wù)端吧FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夾\\1.png"), StandardOpenOption.READ);// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數(shù)據(jù)打交道的呢ByteBuffer buffer = ByteBuffer.allocate(1024);// 4.讀取本地文件(圖片),發(fā)送到服務(wù)器while (fileChannel.read(buffer) != -1) {// 在讀之前都要切換成讀模式buffer.flip();socketChannel.write(buffer);// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)buffer.clear();}// 5. 關(guān)閉流fileChannel.close();socketChannel.close();} }服務(wù)端:
public class BlockServer {public static void main(String[] args) throws IOException {// 1.獲取通道ServerSocketChannel server = ServerSocketChannel.open();// 2.得到文件通道,將客戶端傳遞過來的圖片寫到本地項(xiàng)目下(寫模式、沒有則創(chuàng)建)FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);// 3. 綁定鏈接server.bind(new InetSocketAddress(6666));// 4. 獲取客戶端的連接(阻塞的)SocketChannel client = server.accept();// 5. 要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數(shù)據(jù)打交道的呢ByteBuffer buffer = ByteBuffer.allocate(1024);// 6.將客戶端傳遞過來的圖片保存在本地中while (client.read(buffer) != -1) {// 在讀之前都要切換成讀模式buffer.flip();outChannel.write(buffer);// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)buffer.clear();}// 7.關(guān)閉通道outChannel.close();client.close();server.close();} }結(jié)果就可以將客戶端傳遞過來的圖片保存在本地了:
此時(shí)服務(wù)端保存完圖片想要告訴客戶端已經(jīng)收到圖片啦:
客戶端接收服務(wù)端帶過來的數(shù)據(jù):
如果僅僅是上面的代碼是不行的!這個(gè)程序會(huì)阻塞起來!
- 因?yàn)榉?wù)端不知道客戶端還有沒有數(shù)據(jù)要發(fā)過來(與剛開始不一樣,客戶端發(fā)完數(shù)據(jù)就將流關(guān)閉了,服務(wù)端可以知道客戶端沒數(shù)據(jù)發(fā)過來了),導(dǎo)致服務(wù)端一直在讀取客戶端發(fā)過來的數(shù)據(jù)。
- 進(jìn)而導(dǎo)致了阻塞!
于是客戶端在寫完數(shù)據(jù)給服務(wù)端時(shí),顯式告訴服務(wù)端已經(jīng)發(fā)完數(shù)據(jù)了!
4.3NIO非阻塞形態(tài)
如果使用非阻塞模式的話,那么我們就可以不顯式告訴服務(wù)器已經(jīng)發(fā)完數(shù)據(jù)了。我們下面來看看怎么寫:
客戶端:
public class NoBlockClient {public static void main(String[] args) throws IOException {// 1. 獲取通道SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));// 1.1切換成非阻塞模式socketChannel.configureBlocking(false);// 2. 發(fā)送一張圖片給服務(wù)端吧FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夾\\1.png"), StandardOpenOption.READ);// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數(shù)據(jù)打交道的呢ByteBuffer buffer = ByteBuffer.allocate(1024);// 4.讀取本地文件(圖片),發(fā)送到服務(wù)器while (fileChannel.read(buffer) != -1) {// 在讀之前都要切換成讀模式buffer.flip();socketChannel.write(buffer);// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)buffer.clear();}// 5. 關(guān)閉流fileChannel.close();socketChannel.close();} }服務(wù)端:
public class NoBlockServer {public static void main(String[] args) throws IOException {// 1.獲取通道ServerSocketChannel server = ServerSocketChannel.open();// 2.切換成非阻塞模式server.configureBlocking(false);// 3. 綁定連接server.bind(new InetSocketAddress(6666));// 4. 獲取選擇器Selector selector = Selector.open();// 4.1將通道注冊到選擇器上,指定接收“監(jiān)聽通道”事件server.register(selector, SelectionKey.OP_ACCEPT);// 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒while (selector.select() > 0) {// 6. 獲取當(dāng)前選擇器所有注冊的“選擇鍵”(已就緒的監(jiān)聽事件)Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 7. 獲取已“就緒”的事件,(不同的事件做不同的事)while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();// 接收事件就緒if (selectionKey.isAcceptable()) {// 8. 獲取客戶端的鏈接SocketChannel client = server.accept();// 8.1 切換成非阻塞狀態(tài)client.configureBlocking(false);// 8.2 注冊到選擇器上-->拿到客戶端的連接為了讀取通道的數(shù)據(jù)(監(jiān)聽讀就緒事件)client.register(selector, SelectionKey.OP_READ);} else if (selectionKey.isReadable()) { // 讀事件就緒// 9. 獲取當(dāng)前選擇器讀就緒狀態(tài)的通道SocketChannel client = (SocketChannel) selectionKey.channel();// 9.1讀取數(shù)據(jù)ByteBuffer buffer = ByteBuffer.allocate(1024);// 9.2得到文件通道,將客戶端傳遞過來的圖片寫到本地項(xiàng)目下(寫模式、沒有則創(chuàng)建)FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);while (client.read(buffer) > 0) {// 在讀之前都要切換成讀模式buffer.flip();outChannel.write(buffer);// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)buffer.clear();}}// 10. 取消選擇鍵(已經(jīng)處理過的事件,就應(yīng)該取消掉了)iterator.remove();}}} }還是剛才的需求:服務(wù)端保存了圖片以后,告訴客戶端已經(jīng)收到圖片了。
在服務(wù)端上只要在后面寫些數(shù)據(jù)給客戶端就好了:
在客戶端上要想獲取得到服務(wù)端的數(shù)據(jù),也需要注冊在register上(監(jiān)聽讀事件)!
public class NoBlockClient2 {public static void main(String[] args) throws IOException {// 1. 獲取通道SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));// 1.1切換成非阻塞模式socketChannel.configureBlocking(false);// 1.2獲取選擇器Selector selector = Selector.open();// 1.3將通道注冊到選擇器中,獲取服務(wù)端返回的數(shù)據(jù)socketChannel.register(selector, SelectionKey.OP_READ);// 2. 發(fā)送一張圖片給服務(wù)端吧FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夾\\1.png"), StandardOpenOption.READ);// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數(shù)據(jù)打交道的呢ByteBuffer buffer = ByteBuffer.allocate(1024);// 4.讀取本地文件(圖片),發(fā)送到服務(wù)器while (fileChannel.read(buffer) != -1) {// 在讀之前都要切換成讀模式buffer.flip();socketChannel.write(buffer);// 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)buffer.clear();}// 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒while (selector.select() > 0) {// 6. 獲取當(dāng)前選擇器所有注冊的“選擇鍵”(已就緒的監(jiān)聽事件)Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 7. 獲取已“就緒”的事件,(不同的事件做不同的事)while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();// 8. 讀事件就緒if (selectionKey.isReadable()) {// 8.1得到對(duì)應(yīng)的通道SocketChannel channel = (SocketChannel) selectionKey.channel();ByteBuffer responseBuffer = ByteBuffer.allocate(1024);// 9. 知道服務(wù)端要返回響應(yīng)的數(shù)據(jù)給客戶端,客戶端在這里接收int readBytes = channel.read(responseBuffer);if (readBytes > 0) {// 切換讀模式responseBuffer.flip();System.out.println(new String(responseBuffer.array(), 0, readBytes));}}// 10. 取消選擇鍵(已經(jīng)處理過的事件,就應(yīng)該取消掉了)iterator.remove();}}}}測試結(jié)果:
下面就簡單總結(jié)一下使用NIO時(shí)的要點(diǎn):
- 將Socket通道注冊到Selector中,監(jiān)聽感興趣的事件
- 當(dāng)感興趣的時(shí)間就緒時(shí),則會(huì)進(jìn)去我們處理的方法進(jìn)行處理
- 每處理完一次就緒事件,刪除該選擇鍵(因?yàn)槲覀円呀?jīng)處理完了)
4.4管道和DataGramChannel
這里我就不再講述了,最難的TCP都講了,UDP就很簡單了。
UDP:
管道:
五、總結(jié)
總的來說NIO也是一個(gè)比較重要的知識(shí)點(diǎn),因?yàn)樗菍W(xué)習(xí)netty的基礎(chǔ)~
想以一篇來完全講解NIO顯然是不可能的啦,想要更加深入了解NIO可以往下面的鏈接繼續(xù)學(xué)習(xí)~
參考資料:
- https://www.zhihu.com/question/29005375---如何學(xué)習(xí)Java的NIO?
- http://ifeve.com/java-nio-all/---Java NIO 系列教程
- https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html-----NIO 入門
- https://blog.csdn.net/anxpp/article/details/51503329-----Linux 網(wǎng)絡(luò) I/O 模型簡介(圖文)
- https://wangjingxin.top/2016/10/21/decoration/-----談?wù)刯ava的NIO和AIO
- https://www.yiibai.com/java_nio/-----Java NIO教程
- https://blog.csdn.net/cowthan/article/details/53563206------Java 8:Java 的新IO (nio)
- https://blog.csdn.net/youyou1543724847/article/details/52748785-------JAVA NIO(1.基本概念,基本類)
- https://www.cnblogs.com/zingp/p/6863170.html-----IO模式和IO多路復(fù)用
- https://www.cnblogs.com/Evsward/p/nio.html----掌握NIO,程序人生
- https://blog.csdn.net/anxpp/article/details/51512200----Java 網(wǎng)絡(luò)IO編程總結(jié)(BIO、NIO、AIO均含完整實(shí)例代碼)
- https://zhuanlan.zhihu.com/p/24393775?refer=hinus---進(jìn)擊的Java新人
- 《Java編程思想》
- 《瘋狂Java 講義》
文章的目錄導(dǎo)航:
- https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang
總結(jié)
以上是生活随笔為你收集整理的JDK10都发布了,nio你了解多少?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: winRAR 没有右键选项
- 下一篇: python开发一个区块链只需40多行