新聞中心
01 前言

創(chuàng)新互聯(lián)公司服務項目包括禹王臺網(wǎng)站建設(shè)、禹王臺網(wǎng)站制作、禹王臺網(wǎng)頁制作以及禹王臺網(wǎng)絡營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,禹王臺網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟效益。目前,我們服務的客戶以成都為中心已經(jīng)輻射到禹王臺省份的部分城市,未來相信會繼續(xù)擴大服務區(qū)域并繼續(xù)獲得客戶的支持與信任!
這段時間自己在看一些Java中BIO和NIO之類的東西,看了很多博客,發(fā)現(xiàn)各種關(guān)于NIO的概念說的天花亂墜頭頭是道,可以說是非常的完整,但是整個看下來之后,自己對NIO還是一知半解的狀態(tài),所以這篇文章不會提到很多的概念,而是站在一個實踐的角度,寫一些我自己關(guān)于NIO的見解,站在實踐過后的高度下再回去看概念,應該對概念會有一個更好的理解。
02 實現(xiàn)一個簡易單線程服務器
要講明白BIO和NIO,首先我們應該自己實現(xiàn)一個簡易的服務器,不用太復雜,單線程即可。
2.1 為什么使用單線程作為演示?
因為在單線程環(huán)境下可以很好地對比出BIO和NIO的一個區(qū)別,當然我也會演示在實際環(huán)境中BIO的所謂一個請求對應一個線程的狀況。
2.2 服務端
- public class Server {
- public static void main(String[] args) {
- byte[] buffer = new byte[1024];
- try {
- ServerSocket serverSocket = new ServerSocket(8080);
- System.out.println("服務器已啟動并監(jiān)聽8080端口");
- while (true) {
- System.out.println();
- System.out.println("服務器正在等待連接...");
- Socket socket = serverSocket.accept();
- System.out.println("服務器已接收到連接請求...");
- System.out.println();
- System.out.println("服務器正在等待數(shù)據(jù)...");
- socket.getInputStream().read(buffer);
- System.out.println("服務器已經(jīng)接收到數(shù)據(jù)");
- System.out.println();
- String content = new String(buffer);
- System.out.println("接收到的數(shù)據(jù):" + content);
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
2.3 客戶端
- public class Consumer {
- public static void main(String[] args) {
- try {
- Socket socket = new Socket("127.0.0.1",8080);
- socket.getOutputStream().write("向服務器發(fā)數(shù)據(jù)".getBytes());
- socket.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
2.4 代碼解析
我們首先創(chuàng)建了一個服務端類,在類中實現(xiàn)實例化了一個SocketServer并綁定了8080端口。之后調(diào)用accept方法來接收連接請求,并且調(diào)用read方法來接收客戶端發(fā)送的數(shù)據(jù)。最后將接收到的數(shù)據(jù)打印。
完成了服務端的設(shè)計后,我們來實現(xiàn)一個客戶端,首先實例化Socket對象,并且綁定ip為127.0.0.1(本機),端口號為8080,調(diào)用write方法向服務器發(fā)送數(shù)據(jù)。
2.5 運行結(jié)果
當我們啟動服務器,但客戶端還沒有向服務器發(fā)起連接時,控制臺結(jié)果如下:
當客戶端啟動并向服務器發(fā)送數(shù)據(jù)后,控制臺結(jié)果如下:
2.6 結(jié)論
從上面的運行結(jié)果,首先我們至少可以看到,在服務器啟動后,客戶端還沒有連接服務器時,服務器由于調(diào)用了accept方法,將一直阻塞,直到有客戶端請求連接服務器。
03 對客戶端功能進行擴展
在上文中,我們實現(xiàn)的客戶端的邏輯主要是,建立Socket --> 連接服務器 --> 發(fā)送數(shù)據(jù),我們的數(shù)據(jù)是在連接服務器之后就立即發(fā)送的,現(xiàn)在我們來對客戶端進行一次擴展,當我們連接服務器后,不立即發(fā)送數(shù)據(jù),而是等待控制臺手動輸入數(shù)據(jù)后,再發(fā)送給服務端。(服務端代碼保持不變)
3.1 代碼
- public class Consumer {
- public static void main(String[] args) {
- try {
- Socket socket = new Socket("127.0.0.1",8080);
- String message = null;
- Scanner sc = new Scanner(System.in);
- message = sc.next();
- socket.getOutputStream().write(message.getBytes());
- socket.close();
- sc.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
3.2 測試
當服務端啟動,客戶端還沒有請求連接服務器時,控制臺結(jié)果如下:
當服務端啟動,客戶端連接服務端,但沒有發(fā)送數(shù)據(jù)時,控制臺結(jié)果如下:
當服務端啟動,客戶端連接服務端,并且發(fā)送數(shù)據(jù)時,控制臺結(jié)果如下:
3.3 結(jié)論
從上文的運行結(jié)果中我們可以看到,服務器端在啟動后,首先需要等待客戶端的連接請求(第一次阻塞),如果沒有客戶端連接,服務端將一直阻塞等待,然后當客戶端連接后,服務器會等待客戶端發(fā)送數(shù)據(jù)(第二次阻塞),如果客戶端沒有發(fā)送數(shù)據(jù),那么服務端將會一直阻塞等待客戶端發(fā)送數(shù)據(jù)。服務端從啟動到收到客戶端數(shù)據(jù)的這個過程,將會有兩次阻塞的過程。這就是BIO的非常重要的一個特點,BIO會產(chǎn)生兩次阻塞,第一次在等待連接時阻塞,第二次在等待數(shù)據(jù)時阻塞。
04 BIO
4.1 在單線程條件下BIO的弱點
在上文中,我們實現(xiàn)了一個簡易的服務器,這個簡易的服務器是以單線程運行的,其實我們不難看出,當我們的服務器接收到一個連接后,并且沒有接收到客戶端發(fā)送的數(shù)據(jù)時,是會阻塞在read()方法中的,那么此時如果再來一個客戶端的請求,服務端是無法進行響應的。換言之,在不考慮多線程的情況下,BIO是無法處理多個客戶端請求的。
4.2 BIO如何處理并發(fā)
在剛才的服務器實現(xiàn)中,我們實現(xiàn)的是單線程版的BIO服務器,不難看出,單線程版的BIO并不能處理多個客戶端的請求,那么如何能使BIO處理多個客戶端請求呢。
其實不難想到,我們只需要在每一個連接請求到來時,創(chuàng)建一個線程去執(zhí)行這個連接請求,就可以在BIO中處理多個客戶端請求了,這也就是為什么BIO的其中一條概念是服務器實現(xiàn)模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理。
4.3 多線程BIO服務器簡易實現(xiàn)
- public class Server {
- public static void main(String[] args) {
- byte[] buffer = new byte[1024];
- try {
- ServerSocket serverSocket = new ServerSocket(8080);
- System.out.println("服務器已啟動并監(jiān)聽8080端口");
- while (true) {
- System.out.println();
- System.out.println("服務器正在等待連接...");
- Socket socket = serverSocket.accept();
- new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("服務器已接收到連接請求...");
- System.out.println();
- System.out.println("服務器正在等待數(shù)據(jù)...");
- try {
- socket.getInputStream().read(buffer);
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println("服務器已經(jīng)接收到數(shù)據(jù)");
- System.out.println();
- String content = new String(buffer);
- System.out.println("接收到的數(shù)據(jù):" + content);
- }
- }).start();
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
4.4 運行結(jié)果
很明顯,現(xiàn)在我們的服務器的狀態(tài)就是一個線程對應一個請求,換言之,服務器為每一個連接請求都創(chuàng)建了一個線程來處理。
4.5 多線程BIO服務器的弊端
多線程BIO服務器雖然解決了單線程BIO無法處理并發(fā)的弱點,但是也帶來一個問題:如果有大量的請求連接到我們的服務器上,但是卻不發(fā)送消息,那么我們的服務器也會為這些不發(fā)送消息的請求創(chuàng)建一個單獨的線程,那么如果連接數(shù)少還好,連接數(shù)一多就會對服務端造成極大的壓力。所以如果這種不活躍的線程比較多,我們應該采取單線程的一個解決方案,但是單線程又無法處理并發(fā),這就陷入了一種很矛盾的狀態(tài),于是就有了NIO。
05 NIO
5.1 NIO的引入
我們先來看看單線程模式下BIO服務器的代碼,其實NIO需要解決的最根本的問題就是存在于BIO中的兩個阻塞,分別是等待連接時的阻塞和等待數(shù)據(jù)時的阻塞。
- public class Server {
- public static void main(String[] args) {
- byte[] buffer = new byte[1024];
- try {
- ServerSocket serverSocket = new ServerSocket(8080);
- System.out.println("服務器已啟動并監(jiān)聽8080端口");
- while (true) {
- System.out.println();
- System.out.println("服務器正在等待連接...");
- //阻塞1:等待連接時阻塞
- Socket socket = serverSocket.accept();
- System.out.println("服務器已接收到連接請求...");
- System.out.println();
- System.out.println("服務器正在等待數(shù)據(jù)...");
- //阻塞2:等待數(shù)據(jù)時阻塞
- socket.getInputStream().read(buffer);
- System.out.println("服務器已經(jīng)接收到數(shù)據(jù)");
- System.out.println();
- String content = new String(buffer);
- System.out.println("接收到的數(shù)據(jù):" + content);
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
我們需要再老調(diào)重談的一點是,如果單線程服務器在等待數(shù)據(jù)時阻塞,那么第二個連接請求到來時,服務器是無法響應的。如果是多線程服務器,那么又會有為大量空閑請求產(chǎn)生新線程從而造成線程占用系統(tǒng)資源,線程浪費的情況。
那么我們的問題就轉(zhuǎn)移到,如何讓單線程服務器在等待客戶端數(shù)據(jù)到來時,依舊可以接收新的客戶端連接請求。
5.2 模擬NIO解決方案
如果要解決上文中提到的單線程服務器接收數(shù)據(jù)時阻塞,而無法接收新請求的問題,那么其實可以讓服務器在等待數(shù)據(jù)時不進入阻塞狀態(tài),問題不就迎刃而解了嗎?
(1)第一種解決方案(等待連接時和等待數(shù)據(jù)時不阻塞)
- public class Server {
- public static void main(String[] args) throws InterruptedException {
- ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
- try {
- //Java為非阻塞設(shè)置的類
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- serverSocketChannel.bind(new InetSocketAddress(8080));
- //設(shè)置為非阻塞
- serverSocketChannel.configureBlocking(false);
- while(true) {
- SocketChannel socketChannel = serverSocketChannel.accept();
- if(socketChannel==null) {
- //表示沒人連接
- System.out.println("正在等待客戶端請求連接...");
- Thread.sleep(5000);
- }else {
- System.out.println("當前接收到客戶端請求連接...");
- }
- if(socketChannel!=null) {
- //設(shè)置為非阻塞
- socketChannel.configureBlocking(false);
- byteBuffer.flip();//切換模式 寫-->讀
- int effective = socketChannel.read(byteBuffer);
- if(effective!=0) {
- String content = Charset.forName("utf-8").decode(byteBuffer).toString();
- System.out.println(content);
- }else {
- System.out.println("當前未收到客戶端消息");
- }
- }
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
運行結(jié)果
不難看出,在這種解決方案下,雖然在接收客戶端消息時不會阻塞,但是又開始重新接收服務器請求,用戶根本來不及輸入消息,服務器就轉(zhuǎn)向接收別的客戶端請求了,換言之,服務器弄丟了當前客戶端的請求。
(2)解決方案二(緩存Socket,輪詢數(shù)據(jù)是否準備好)
- public class Server {
- public static void main(String[] args) throws InterruptedException {
- ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
- List
socketList = new ArrayList (); - try {
- //Java為非阻塞設(shè)置的類
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- serverSocketChannel.bind(new InetSocketAddress(8080));
- //設(shè)置為非阻塞
- serverSocketChannel.configureBlocking(false);
- while(true) {
- SocketChannel socketChannel = serverSocketChannel.accept();
- if(socketChannel==null) {
- //表示沒人連接
- System.out.println("正在等待客戶端請求連接...");
- Thread.sleep(5000);
- }else {
- System.out.println("當前接收到客戶端請求連接...");
- socketList.add(socketChannel);
- }
- for(SocketChannel socket:socketList) {
- socket.configureBlocking(false);
- int effective = socket.read(byteBuffer);
- if(effective!=0) {
- byteBuffer.flip();//切換模式 寫-->讀
- String content = Charset.forName("UTF-8").decode(byteBuffer).toString();
- System.out.println("接收到消息:"+content);
- byteBuffer.clear();
- }else {
- System.out.println("當前未收到客戶端消息");
- }
- }
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
運行結(jié)果
代碼解析
在解決方案一中,我們采用了非阻塞方式,但是發(fā)現(xiàn)一旦非阻塞,等待客戶端發(fā)送消息時就不會再阻塞了,而是直接重新去獲取新客戶端的連接請求,這就會造成客戶端連接丟失,而在解決方案二中,我們將連接存儲在一個list集合中,每次等待客戶端消息時都去輪詢,看看消息是否準備好,如果準備好則直接打印消息??梢钥吹?,從頭到尾我們一直沒有開啟第二個線程,而是一直采用單線程來處理多個客戶端的連接,這樣的一個模式可以很完美地解決BIO在單線程模式下無法處理多客戶端請求的問題,并且解決了非阻塞狀態(tài)下連接丟失的問題。
(3)存在的問題(解決方案二)
從剛才的運行結(jié)果中其實可以看出,消息沒有丟失,程序也沒有阻塞。但是,在接收消息的方式上可能有些許不妥,我們采用了一個輪詢的方式來接收消息,每次都輪詢所有的連接,看消息是否準備好,測試用例中只是三個連接,所以看不出什么問題來,但是我們假設(shè)有1000萬連接,甚至更多,采用這種輪詢的方式效率是極低的。另外,1000萬連接中,我們可能只會有100萬會有消息,剩下的900萬并不會發(fā)送任何消息,那么這些連接程序依舊要每次都去輪詢,這顯然是不合適的。
真實NIO中如何解決
在真實NIO中,并不會在Java層上來進行一個輪詢,而是將輪詢的這個步驟交給我們的操作系統(tǒng)來進行,他將輪詢的那部分代碼改為操作系統(tǒng)級別的系統(tǒng)調(diào)用(select函數(shù),在linux環(huán)境中為epoll),在操作系統(tǒng)級別上調(diào)用select函數(shù),主動地去感知有數(shù)據(jù)的socket。
06 關(guān)于使用select/epoll和直接在應用層做輪詢的區(qū)別
我們在之前實現(xiàn)了一個使用Java做多個客戶端連接輪詢的邏輯,但是在真正的NIO源碼中其實并不是這么實現(xiàn)的,NIO使用了操作系統(tǒng)底層的輪詢系統(tǒng)調(diào)用 select/epoll(windows:select,linux:epoll),那么為什么不直接實現(xiàn)而要去調(diào)用系統(tǒng)來做輪詢呢?
6.1 select底層邏輯
假設(shè)有A、B、C、D、E五個連接同時連接服務器,那么根據(jù)我們上文中的設(shè)計,程序?qū)闅v這五個連接,輪詢每個連接,獲取各自數(shù)據(jù)準備情況,那么和我們自己寫的程序有什么區(qū)別呢?
首先,我們寫的Java程序其本質(zhì)在輪詢每個Socket的時候也需要去調(diào)用系統(tǒng)函數(shù),那么輪詢一次調(diào)用一次,會造成不必要的上下文切換開銷。
而Select會將五個請求從用戶態(tài)空間全量復制一份到內(nèi)核態(tài)空間,在內(nèi)核態(tài)空間來判斷每個請求是否準備好數(shù)據(jù),完全避免頻繁的上下文切換。所以效率是比我們直接在應用層寫輪詢要高的。
如果select沒有查詢到到有數(shù)據(jù)的請求,那么將會一直阻塞(是的,select是一個阻塞函數(shù))。如果有一個或者多個請求已經(jīng)準備好數(shù)據(jù)了,那么select將會先將有數(shù)據(jù)的文件描述符置位,然后select返回。返回后通過遍歷查看哪個請求有數(shù)據(jù)。
select的缺點
底層存儲依賴bitmap,處理的請求是有上限的,為1024。
文件描述符是會置位的,所以如果當被置位的文件描述符需要重新使用時,是需要重新賦空值的。
fd(文件描述符)從用戶態(tài)拷貝到內(nèi)核態(tài)仍然有一筆開銷。
select返回后還要再次遍歷,來獲知是哪一個請求有數(shù)據(jù)。
6.2 poll函數(shù)底層邏輯
poll的工作原理和select很像,先來看一段poll內(nèi)部使用的一個結(jié)構(gòu)體。
struct pollfd{ int fd; short events; short revents;}
poll同樣會將所有的請求拷貝到內(nèi)核態(tài),和select一樣,poll同樣是一個阻塞函數(shù),當一個或多個請求有數(shù)據(jù)的時候,也同樣會進行置位,但是它置位的是結(jié)構(gòu)體pollfd中的events或者revents置位,而不是對fd本身進行置位,所以在下一次使用的時候不需要再進行重新賦空值的操作。poll內(nèi)部存儲不依賴bitmap,而是使用pollfd數(shù)組的這樣一個數(shù)據(jù)結(jié)構(gòu),數(shù)組的大小肯定是大于1024的。解決了select 1、2兩點的缺點。
6.3 epoll
epoll是最新的一種多路IO復用的函數(shù)。這里只說說它的特點。
epoll和上述兩個函數(shù)最大的不同是,它的fd是共享在用戶態(tài)和內(nèi)核態(tài)之間的,所以可以不必進行從用戶態(tài)到內(nèi)核態(tài)的一個拷貝,這樣可以節(jié)約系統(tǒng)資源;另外,在select和poll中,如果某個請求的數(shù)據(jù)已經(jīng)準備好,它們會將所有的請求都返回,供程序去遍歷查看哪個請求存在數(shù)據(jù),但是epoll只會返回存在數(shù)據(jù)的請求,這是因為epoll在發(fā)現(xiàn)某個請求存在數(shù)據(jù)時,首先會進行一個重排操作,將所有有數(shù)據(jù)的fd放到最前面的位置,然后返回(返回值為存在數(shù)據(jù)請求的個數(shù)N),那么我們的上層程序就可以不必將所有請求都輪詢,而是直接遍歷epoll返回的前N個請求,這些請求都是有數(shù)據(jù)的請求。
07 Java中BIO和NIO的概念
通常一些文章都是在開頭放上概念,但是我這次選擇將概念放在結(jié)尾,因為通過上面的實操,相信大家對Java中BIO和NIO都有了自己的一些理解,這時候再來看概念應該會更好理解一些了。
7.1 先來個例子理解一下概念,以銀行取款為例
同步 : 自己親自出馬持銀行卡到銀行取錢(使用同步IO時,Java自己處理IO讀寫)。
異步 : 委托一小弟拿銀行卡到銀行取錢,然后給你(使用異步IO時,Java將IO讀寫委托給OS處理,需要將數(shù)據(jù)緩沖區(qū)地址和大小傳給OS(銀行卡和密碼),OS需要支持異步IO操作API)。
阻塞 : ATM排隊取款,你只能等待(使用阻塞IO時,Java調(diào)用會一直阻塞到讀寫完成才返回)。
非阻塞 : 柜臺取款,取個號,然后坐在椅子上做其它事,等號廣播會通知你辦理,沒到號你就不能去,你可以不斷問大堂經(jīng)理排到了沒有,大堂經(jīng)理如果說還沒到你就不能去(使用非阻塞IO時,如果不能讀寫Java調(diào)用會馬上返回,當IO事件分發(fā)器會通知可讀寫時再繼續(xù)進行讀寫,不斷循環(huán)直到讀寫完成)。
7.2 Java對BIO、NIO的支持
Java BIO (blocking I/O): 同步并阻塞,服務器實現(xiàn)模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。
Java NIO (non-blocking I/O): 同步非阻塞,服務器實現(xiàn)模式為一個請求一個線程,即客戶端發(fā)送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。
7.3 BIO、NIO適用場景分析
BIO方式適用于連接數(shù)目比較小且固定的架構(gòu),這種方式對服務器資源要求比較高,并發(fā)局限于應用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
NIO方式適用于連接數(shù)目多且連接比較短(輕操作)的架構(gòu),比如聊天服務器,并發(fā)局限于應用中,編程比較復雜,JDK1.4開始支持。
08 結(jié)語
本文介紹了一些關(guān)于JavaBIO和NIO從自己實操的角度上的一些理解,我個人認為這樣去理解BIO和NIO會比光看概念會有更深的理解,也希望各位同學可以自己去敲一遍,通過程序的運行結(jié)果得出自己對JavaBIO和NIO的理解。
本文名稱:BIO和NIO了解多少呢?一起從實踐角度重新理解下吧
文章分享:http://www.5511xx.com/article/cohoegs.html


咨詢
建站咨詢
