原理和實戰完美詮釋NIO的強大之處
平時工作中,很大部分時間都投入了業務。我們對於一些框架、設計思想等都沒有太去的關注,第一個深入一個技術底層是比較枯燥與孤獨的;第二個就是沒有人帶領去用一個有趣或者通俗易懂的教導;但如果真的是明白了那些大牛們的思維方式,我們都會異口同聲的稱讚他們,真的就是牛逼啊。
學習底層的一些原理知識,我建議有2種方式:
1、多看原始碼,在原始碼中與之前接觸到的理論相結合,最後會恍然大悟;
2、多跟大神們交流,在沒接觸之前,你不會覺得自己有多菜;
之前寫過的Demo:
案例1: ofollow,noindex" target="_blank">https://github.com/chengcheng222e/io-learn.git
案例2: https://github.com/chengcheng222e/vernon-socket
案例3: https://github.com/chengcheng222e/vernon-netty.git
1、Socket通訊程式設計
1.1 只能接受一次連線的程式碼塊
package com.cyblogs.io.learn.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class BIOServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket=new ServerSocket(8080); System.out.println("BIOServer has started,listening on port:" + serverSocket.getLocalSocketAddress()); Socket clientSocket = serverSocket.accept(); //等待被接受 System.out.println("Conection from " + clientSocket.getRemoteSocketAddress()); Scanner input=new Scanner(clientSocket.getInputStream()); String request=input.nextLine(); System.out.println(request); String response="From BIOServer response: " + request + "\n"; clientSocket.getOutputStream().write(response.getBytes()); } }
我們啟動服務,然後通過 telnet
的方式來摸你 client
來發起通訊。
啟動服務
連線服務
服務端返回
這裡就會發現,如果我們不去發起一個連線,那麼服務端就就會一直停留在這裡。
Scanner input=new Scanner(clientSocket.getInputStream());
1.2 接受多次連線的程式碼塊
但這個程式有問題,就是接受到一個請求後,程式碼就接走完了。主程序 main
函式會立馬跳出。我們改造一下,希望得到的效果是,我們可以通過多個 client
去連線服務端。
package com.cyblogs.io.learn.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class BIOServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("BIOServer has started,listening on port:" + serverSocket.getLocalSocketAddress()); while (true) { Socket clientSocket = null; try { clientSocket = serverSocket.accept(); //等待被接受 System.out.println("Conection from " + clientSocket.getRemoteSocketAddress()); Scanner input = new Scanner(clientSocket.getInputStream()); while (true) { if (input.hasNext()) { String request = input.nextLine(); if ("quit".equals(request)) { break; } System.out.println(request); String response = "From BIOServer response " + request + "\n"; clientSocket.getOutputStream().write(response.getBytes()); } } } finally { if (clientSocket != null) clientSocket.close(); } } } }
這類就很能明顯的看出來。當多個客戶端連線之後,之後之前的客戶端退出之後,後面的的客戶端才能關於服務端進行互動。
上述的例子就是一個簡單的例子來描述一個IO的阻塞,而且非常的浪費資源。原因為是隻有一個執行緒,而且還是阻塞的。
2、如何提高效能?
如上圖所示,之前說是有一個執行緒在做核心業務的處理而且是阻塞的。那如果說每次來一個請求,我都單獨分出一個執行緒來做。但是為了考慮到每次來都分配一個,到時候CPU會處理不過來,我就利用一個執行緒池來管理一個固定大小的執行緒池。如果超過了我的處理能力,我就讓它排隊。感覺上這個思維比之前的就好很多,那具體我們再看看改造後的程式碼。
package com.cyblogs.io.learn.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class BIOServer { public static void main(String[] args) throws IOException { ExecutorService executor = Executors.newFixedThreadPool(3); ServerSocket serverSocket = new ServerSocket(8080); System.out.println("BIOServer has started,listening on port:" + serverSocket.getLocalSocketAddress()); RequestHandler requestHandler = new RequestHandler();// 為了讓多執行緒去切換 while (true) { Socket clientSocket = null; clientSocket = serverSocket.accept(); //等待被接受 System.out.println("Conection from " + clientSocket.getRemoteSocketAddress()); // 多執行緒處理 executor.submit(new ClientHandler(clientSocket, requestHandler)); } } }
package com.cyblogs.io.learn.bio; public class RequestHandler { public String handler(String request) { return "From BIOServer response " + request + "\n"; } }
package com.cyblogs.io.learn.bio; import java.io.IOException; import java.net.Socket; import java.util.Scanner; public class ClientHandler implements Runnable { private Socket clientSocket; private RequestHandler requestHandler; public ClientHandler(Socket clientSocket, RequestHandler requestHandler) { this.clientSocket = clientSocket; this.requestHandler = requestHandler; } public void run() { try { Scanner input = new Scanner(clientSocket.getInputStream()); while (true) { if (input.hasNext()) { String request = input.nextLine(); if ("quit".equals(request)) { break; } System.out.println(request); String response = requestHandler.handler(request); clientSocket.getOutputStream().write(response.getBytes()); } } } catch (IOException e) { throw new RuntimeException(e); } } }
為了摸你這個場景,我開啟4個客戶端。
第4個客戶端會處理阻塞中,那麼真正的堵塞在哪兒呢?
String request = input.nextLine();
所以,之前的場景,不管如何都屬於 BIO
的範疇, BIO
最終還是存在瓶頸。為了提高效能,就出現了 NIO
。
問題:之前的設計存在哪些設計缺陷?(留一個問題)
3、瞭解NIO的設計
之前的問題都是在於一直在等待用的輸入,所以每個執行緒還是一直非常被浪費。所以,解決問題的關鍵在於能不能不要一直等使用者的輸入,而是在真正在輸入的時候再創建出一個執行緒來處理。處理完畢後資源又會得到釋放。
package com.cyblogs.io.learn.nio; import com.cyblogs.io.learn.bio.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 { // Channel[Server Client] Selector Buffer public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress(8081)); System.out.println("BIOServer has started, listening on port: " + serverSocketChannel.getLocalAddress()); Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); RequestHandler requestHandler = new RequestHandler(); ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { // 整個過程,單執行緒只有這類是阻塞的 int select = selector.select(); if (select == 0) { continue; } Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = serverChannel.accept(); System.out.println("Connection from " + clientChannel.getRemoteAddress()); clientChannel.configureBlocking(false); clientChannel.register(selector, SelectionKey.OP_READ); } if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); channel.read(buffer); String request = new String(buffer.array()).trim(); buffer.clear(); System.out.println(String.format("From %s : %s", channel.getRemoteAddress(), request)); String response = requestHandler.handler(request); channel.write(ByteBuffer.wrap(response.getBytes())); } iterator.remove(); } } } }
但是,對於NIO最好修飾的一個框架就是Netty。可以參考一下上面vernon-netty的一個小demo。這邊文章主要是初步瞭解一個BIO到NIO過渡的手寫過程。後面我們再聊聊Netty是如何封裝NIO的。
Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.