發佈於 今天 09:10html
你們好,我是 「後端技術進階」 做者,一個熱愛技術的少年。java
@[toc]git
以爲不錯的話,歡迎 star!ღ( ´・ᴗ・` )比心github
- Netty 從入門到實戰系列文章地址:https://github.com/Snailclimb/netty-practical-tutorial 。
- RPC 框架源碼地址:https://github.com/Snailclimb/guide-rpc-framework
老套路,學習某一門技術或者框架的時候,第一步固然是要了解下面這幾樣東西。編程
爲了讓你更好地瞭解 Netty 以及它誕生的緣由,先從傳統的網絡編程提及吧!segmentfault
早期的 Java 網絡相關的 API(java.net
包) 使用 Socket(套接字)進行網絡通訊,不過只支持阻塞函數使用。後端
要經過互聯網進行通訊,至少須要一對套接字:安全
Socket 網絡通訊過程以下圖所示:服務器
https://www.javatpoint.com/so...
Socket 網絡通訊過程簡單來講分爲下面 4 步:
對應到服務端和客戶端的話,是下面這樣的。
服務器端:
ServerSocket
對象而且綁定地址(ip)和端口號(port): server.bind(new InetSocketAddress(host, port))
accept()
方法監聽客戶端請求客戶端:
Socket
對象而且鏈接指定的服務器的地址(ip)和端口號(port):socket.connect(inetSocketAddress)
爲了便於理解,我寫了一個簡單的代碼幫助各位小夥伴理解。
服務端:
public class HelloServer { private static final Logger logger = LoggerFactory.getLogger(HelloServer.class); public void start(int port) { //1.建立 ServerSocket 對象而且綁定一個端口 try (ServerSocket server = new ServerSocket(port);) { Socket socket; //2.經過 accept()方法監聽客戶端請求, 這個方法會一直阻塞到有一個鏈接創建 while ((socket = server.accept()) != null) { logger.info("client connected"); try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) { //3.經過輸入流讀取客戶端發送的請求信息 Message message = (Message) objectInputStream.readObject(); logger.info("server receive message:" + message.getContent()); message.setContent("new content"); //4.經過輸出流向客戶端發送響應信息 objectOutputStream.writeObject(message); objectOutputStream.flush(); } catch (IOException | ClassNotFoundException e) { logger.error("occur exception:", e); } } } catch (IOException e) { logger.error("occur IOException:", e); } } public static void main(String[] args) { HelloServer helloServer = new HelloServer(); helloServer.start(6666); } }
ServerSocket
的 accept()
方法是阻塞方法,也就是說 ServerSocket
在調用 accept()
等待客戶端的鏈接請求時會阻塞,直到收到客戶端發送的鏈接請求才會繼續往下執行代碼,所以咱們須要要爲每一個 Socket 鏈接開啓一個線程(能夠經過線程池來作)。
上述服務端的代碼只是爲了演示,並無考慮多個客戶端鏈接併發的狀況。
客戶端:
/** * @author shuang.kou * @createTime 2020年05月11日 16:56:00 */ public class HelloClient { private static final Logger logger = LoggerFactory.getLogger(HelloClient.class); public Object send(Message message, String host, int port) { //1. 建立Socket對象而且指定服務器的地址和端口號 try (Socket socket = new Socket(host, port)) { ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); //2.經過輸出流向服務器端發送請求信息 objectOutputStream.writeObject(message); //3.經過輸入流獲取服務器響應的信息 ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); return objectInputStream.readObject(); } catch (IOException | ClassNotFoundException e) { logger.error("occur exception:", e); } return null; } public static void main(String[] args) { HelloClient helloClient = new HelloClient(); helloClient.send(new Message("content from client"), "127.0.0.1", 6666); System.out.println("client receive message:" + message.getContent()); } }
發送的消息實體類:
/** * @author shuang.kou * @createTime 2020年05月11日 17:02:00 */ @Data @AllArgsConstructor public class Message implements Serializable { private String content; }
首先運行服務端,而後再運行客戶端,控制檯輸出以下:
服務端:
[main] INFO github.javaguide.socket.HelloServer - client connected [main] INFO github.javaguide.socket.HelloServer - server receive message:content from client
客戶端:
client receive message:new content
很明顯,我上面演示的代碼片斷有一個很嚴重的問題:只能同時處理一個客戶端的鏈接,若是須要管理多個客戶端的話,就須要爲咱們請求的客戶端單首創建一個線程。 以下圖所示:
對應的 Java 代碼多是下面這樣的:
new Thread(() -> { // 建立 socket 鏈接 }).start();
可是,這樣會致使一個很嚴重的問題:資源浪費。
咱們知道線程是很寶貴的資源,若是咱們爲每一次鏈接都用一個線程處理的話,就會致使線程愈來愈好,最好達到了極限以後,就沒法再建立線程處理請求了。處理的很差的話,甚至可能直接就宕機掉了。
不少人就會問了:那有沒有改進的方法呢?
固然有! 比較簡單而且實際的改進方法就是使用線程池。線程池還可讓線程的建立和回收成本相對較低,而且咱們能夠指定線程池的可建立線程的最大數量,這樣就不會致使線程建立過多,機器資源被不合理消耗。
ThreadFactory threadFactory = Executors.defaultThreadFactory(); ExecutorService threadPool = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(100), threadFactory); threadPool.execute(() -> { // 建立 socket 鏈接 });
可是,即便你再怎麼優化和改變。也改變不了它的底層仍然是同步阻塞的 BIO 模型的事實,所以沒法從根本上解決問題。
爲了解決上述的問題,Java 1.4 中引入了 NIO ,一種同步非阻塞的 I/O 模型。
Netty 實際上就基於 Java NIO 技術封裝完善以後獲得一個高性能框架,熟悉 NIO 的基本概念對於學習和更好地理解 Netty 仍是頗有必要的!
NIO 是一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,對應 java.nio
包,提供了 Channel , Selector,Buffer 等抽象。
NIO 中的 N 能夠理解爲 Non-blocking,已經不在是 New 了(已經出來很長時間了)。
NIO 支持面向緩衝(Buffer)的,基於通道(Channel)的 I/O 操做方法。
NIO 提供了與傳統 BIO 模型中的 Socket
和 ServerSocket
相對應的 SocketChannel
和 ServerSocketChannel
兩種不一樣的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式:
NIO 包含下面幾個核心的組件:
這些組件之間的關係是怎麼的呢?
Selector(選擇器,也能夠理解爲多路複用器)是 NIO(非阻塞 IO)實現的關鍵。它使用了事件通知相關的 API 來實現選擇已經就緒也就是可以進行 I/O 相關的操做的任務的能力。
簡單來講,整個過程是這樣的:
select()
方法,這個方法會阻塞;相比於傳統的 BIO 模型來講, NIO 模型的最大改進是:
一個使用 NIO 編寫的 Server 端以下,能夠看出仍是總體仍是比較複雜的,而且代碼讀起來不是很直觀,而且還可能因爲 NIO 自己會存在 Bug。
不多使用 NIO,很大狀況下也是由於使用 NIO 來建立正確而且安全的應用程序的開發成本和維護成本都比較大。因此,通常狀況下咱們都會使用 Netty 這個比較成熟的高性能框架來作(Apace Mina 與之相似,可是 Netty 使用的更多一點)。
簡單用 3 點歸納一下 Netty 吧!
用官方的總結就是:Netty 成功地找到了一種在不妥協可維護性和性能的狀況下實現易於開發,性能,穩定性和靈活性的方法。
根據官網的描述,咱們能夠總結出下面一些特色:
這個應該是老鐵們最關心的一個問題了,憑藉本身的瞭解,簡單說一下,理論上 NIO 能夠作的事情 ,使用 Netty 均可以作而且更好。Netty 主要用來作網絡通訊 :
咱們日常常常接觸的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
能夠說大量的開源項目都用到了 Netty,因此掌握 Netty 有助於你更好的使用這些開源項目而且讓你有能力對其進行二次開發。
實際上還有不少不少優秀的項目用到了 Netty,Netty 官方也作了統計,統計結果在這裏:https://netty.io/wiki/related... 。
RPC 框架源碼已經開源了,地址:https://github.com/Snailclimb...