老套路,學(xué)習(xí)某一門技術(shù)或者框架的時候,第一步當(dāng)然是要了解下面這幾樣?xùn)|西。
為了讓你更好地了解 Netty 以及它誕生的原因,先從傳統(tǒng)的網(wǎng)絡(luò)編程說起吧!
還是要從 BIO 說起 傳統(tǒng)的阻塞式通信流程 早期的 Java 網(wǎng)絡(luò)相關(guān)的 API(java.net
包) 使用 Socket(套接字)進行網(wǎng)絡(luò)通信,不過只支持阻塞函數(shù)使用。
要通過互聯(lián)網(wǎng)進行通信,至少需要一對套接字:
運行于服務(wù)器端的 Server Socket。 Socket 網(wǎng)絡(luò)通信過程如下圖所示:
https://www./socket-programming
Socket 網(wǎng)絡(luò)通信過程簡單來說分為下面 4 步:
對應(yīng)到服務(wù)端和客戶端的話,是下面這樣的。
服務(wù)器端:
創(chuàng)建 ServerSocket
對象并且綁定地址(ip)和端口號(port):server.bind(new InetSocketAddress(host, port))
通過 accept()
方法監(jiān)聽客戶端請求 連接建立后,通過輸入流讀取客戶端發(fā)送的請求信息 通過輸出流向客戶端發(fā)送響應(yīng)信息 客戶端:
創(chuàng)建Socket
對象并且連接指定的服務(wù)器的地址(ip)和端口號(port):socket.connect(inetSocketAddress)
連接建立后,通過輸出流向服務(wù)器端發(fā)送請求信息 通過輸入流獲取服務(wù)器響應(yīng)的信息 一個簡單的 demo 為了便于理解,我寫了一個簡單的代碼幫助各位老鐵理解一下。
服務(wù)端:
public class HelloServer { private static final Logger logger = LoggerFactory.getLogger(HelloServer.class); public void start (int port) { //1.創(chuàng)建 ServerSocket 對象并且綁定一個端口 try (ServerSocket server = new ServerSocket(port);) { Socket socket; //2.通過 accept()方法監(jiān)聽客戶端請求, 這個方法會一直阻塞到有一個連接建立 while ((socket = server.accept()) != null ) { logger.info('client connected' ); try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) { //3.通過輸入流讀取客戶端發(fā)送的請求信息 Message message = (Message) objectInputStream.readObject(); logger.info('server receive message:' + message.getContent()); message.setContent('new content' ); //4.通過輸出流向客戶端發(fā)送響應(yīng)信息 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
在調(diào)用 accept()
等待客戶端的連接請求時會阻塞,直到收到客戶端發(fā)送的連接請求才會繼續(xù)往下執(zhí)行代碼,因此我們需要要為每個 Socket 連接開啟一個線程(可以通過線程池來做)。
上述服務(wù)端的代碼只是為了演示,并沒有考慮多個客戶端連接并發(fā)的情況。
客戶端:
/** * @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. 創(chuàng)建Socket對象并且指定服務(wù)器的地址和端口號 try (Socket socket = new Socket(host, port)) { ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); //2.通過輸出流向服務(wù)器端發(fā)送請求信息 objectOutputStream.writeObject(message); //3.通過輸入流獲取服務(wù)器響應(yīng)的信息 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()); } }
發(fā)送的消息實體類 :
/** * @author shuang.kou * @createTime 2020年05月11日 17:02:00 */ @Data @AllArgsConstructor public class Message implements Serializable { private String content; }
首先運行服務(wù)端,然后再運行客戶端,控制臺輸出如下:
服務(wù)端:
[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
資源消耗嚴(yán)重的問題 很明顯,我上面演示的代碼片段有一個很嚴(yán)重的問題:只能同時處理一個客戶端的連接,如果需要管理多個客戶端的話,就需要為我們請求的客戶端單獨創(chuàng)建一個線程。 如下圖所示:
對應(yīng)的 Java 代碼可能是下面這樣的:
new Thread(() -> { // 創(chuàng)建 socket 連接 }).start();
但是,這樣會導(dǎo)致一個很嚴(yán)重的問題:資源浪費 。
我們知道線程是很寶貴的資源,如果我們?yōu)槊恳淮芜B接都用一個線程處理的話,就會導(dǎo)致線程越來越好,最好達到了極限之后,就無法再創(chuàng)建線程處理請求了。處理的不好的話,甚至可能直接就宕機掉了。
很多人就會問了:那有沒有改進的方法呢?
線程池雖可以改善,但終究未從根本解決問題 當(dāng)然有!比較簡單并且實際的改進方法就是使用線程池。線程池還可以讓線程的創(chuàng)建和回收成本相對較低,并且我們可以指定線程池的可創(chuàng)建線程的最大數(shù)量,這樣就不會導(dǎo)致線程創(chuàng)建過多,機器資源被不合理消耗。
ThreadFactory threadFactory = Executors.defaultThreadFactory(); ExecutorService threadPool = new ThreadPoolExecutor(10 , 100 , 1 , TimeUnit.MINUTES, new ArrayBlockingQueue<>(100 ), threadFactory); threadPool.execute(() -> { // 創(chuàng)建 socket 連接 });
但是,即使你再怎么優(yōu)化和改變。也改變不了它的底層仍然是同步阻塞的 BIO 模型的事實,因此無法從根本上解決問題。
為了解決上述的問題,Java 1.4 中引入了 NIO ,一種同步非阻塞的 I/O 模型。
再看 NIO Netty 實際上就基于 Java NIO 技術(shù)封裝完善之后得到一個高性能框架,熟悉 NIO 的基本概念對于學(xué)習(xí)和更好地理解 Netty 還是很有必要的!
初識 NIO NIO 是一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,對應(yīng) java.nio
包,提供了 Channel , Selector,Buffer 等抽象。
NIO 中的 N 可以理解為 Non-blocking,已經(jīng)不在是 New 了(已經(jīng)出來很長時間了)。
NIO 支持面向緩沖(Buffer)的,基于通道(Channel)的 I/O 操作方法。
NIO 提供了與傳統(tǒng) BIO 模型中的 Socket
和 ServerSocket
相對應(yīng)的 SocketChannel
和 ServerSocketChannel
兩種不同的套接字通道實現(xiàn),兩種通道都支持阻塞和非阻塞兩種模式:
阻塞模式 : 基本不會被使用到。使用起來就像傳統(tǒng)的網(wǎng)絡(luò)編程一樣,比較簡單,但是性能和可靠性都不好。對于低負(fù)載、低并發(fā)的應(yīng)用程序,勉強可以用一下以提升開發(fā)速率和更好的維護性非阻塞模式 :與阻塞模式正好相反,非阻塞模式對于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用來說非常友好,但是編程麻煩,這個是大部分人詬病的地方。所以, 也就導(dǎo)致了 Netty 的誕生。NIO 核心組件解讀 NIO 包含下面幾個核心的組件:
這些組件之間的關(guān)系是怎么的呢?
http:///articles/java-socket-io-and-nio
NIO 使用 Channel(通道)和 Buffer(緩沖區(qū))傳輸數(shù)據(jù),數(shù)據(jù)總是從緩沖區(qū)寫入通道,并從通道讀取到緩沖區(qū)。在面向流的 I/O 中,可以將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對象中。在 NIO 庫中,所有數(shù)據(jù)都是通過 Buffer(緩沖區(qū))處理的 。Channel 可以看作是 Netty 的網(wǎng)絡(luò)操作抽象類,對應(yīng)于 JDK 底層的 Socket NIO 利用 Selector (選擇器)來監(jiān)視多個通道的對象,如數(shù)據(jù)到達,連接打開等。因此,單線程可以監(jiān)視多個通道中的數(shù)據(jù)。 當(dāng)我們將 Channel 注冊到 Selector 中的時候, 會返回一個 Selection Key 對象, Selection Key 則表示了一個特定的通道對象和一個特定的選擇器對象之間的注冊關(guān)系。通過 Selection Key 我們可以獲取哪些 IO 事件已經(jīng)就緒了,并且可以通過其獲取 Channel 并對其進行操作。 Selector(選擇器,也可以理解為多路復(fù)用器)是 NIO(非阻塞 IO)實現(xiàn)的關(guān)鍵。它使用了事件通知相關(guān)的 API 來實現(xiàn)選擇已經(jīng)就緒也就是能夠進行 I/O 相關(guān)的操作的任務(wù)的能力。
簡單來說,整個過程是這樣的:
將 Channel 注冊到 Selector 中。 調(diào)用 Selector 的 select()
方法,這個方法會阻塞; 到注冊在 Selector 中的某個 Channel 有新的 TCP 連接或者可讀寫事件的話,這個 Channel 就會處于就緒狀態(tài),會被 Selector 輪詢出來。 然后通過 SelectionKey 可以獲取就緒 Channel 的集合,進行后續(xù)的 I/O 操作。 NIO 為啥更好? 相比于傳統(tǒng)的 BIO 模型來說, NIO 模型的最大改進是:
使用比較少的線程便可以管理多個客戶端的連接,提高了并發(fā)量并且減少的資源消耗(減少了線程的上下文切換的開銷) 在沒有 I/O 操作相關(guān)的事情的時候,線程可以被安排在其他任務(wù)上面,以讓線程資源得到充分利用。 使用 NIO 編寫代碼太難了 一個使用 NIO 編寫的 Server 端如下,可以看出還是整體還是比較復(fù)雜的,并且代碼讀起來不是很直觀,并且還可能由于 NIO 本身會存在 Bug。
很少使用 NIO,很大情況下也是因為使用 NIO 來創(chuàng)建正確并且安全的應(yīng)用程序的開發(fā)成本和維護成本都比較大。所以,一般情況下我們都會使用 Netty 這個比較成熟的高性能框架來做(Apace Mina 與之類似,但是 Netty 使用的更多一點)。
重要角色 Netty 登場 簡單用 3 點概括一下 Netty 吧!
Netty 是一個基于 NIO 的 client-server(客戶端服務(wù)器)框架,使用它可以快速簡單地開發(fā)網(wǎng)絡(luò)應(yīng)用程序。 它極大地簡化并簡化了 TCP 和 UDP 套接字服務(wù)器等網(wǎng)絡(luò)編程,并且性能以及安全性等很多方面甚至都要更好。 支持多種協(xié)議如 FTP,SMTP,HTTP 以及各種二進制和基于文本的傳統(tǒng)協(xié)議。 用官方的總結(jié)就是:Netty 成功地找到了一種在不妥協(xié)可維護性和性能的情況下實現(xiàn)易于開發(fā),性能,穩(wěn)定性和靈活性的方法。
Netty 特點 根據(jù)官網(wǎng)的描述,我們可以總結(jié)出下面一些特點:
統(tǒng)一的 API,支持多種傳輸類型,阻塞和非阻塞的。 比直接使用 Java 核心 API 有更高的吞吐量、更低的延遲、更低的資源消耗和更少的內(nèi)存復(fù)制。 安全性不錯,有完整的 SSL/TLS 以及 StartTLS 支持。 成熟穩(wěn)定,經(jīng)歷了大型項目的使用和考驗,而且很多開源項目都使用到了 Netty 比如我們經(jīng)常接觸的 Dubbo、RocketMQ 等等。 使用 Netty 能做什么? 這個應(yīng)該是老鐵們最關(guān)心的一個問題了,憑借自己的了解,簡單說一下,理論上 NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用來做網(wǎng)絡(luò)通信 :
作為 RPC 框架的網(wǎng)絡(luò)通信工具 :我們在分布式系統(tǒng)中,不同服務(wù)節(jié)點之間經(jīng)常需要相互調(diào)用,這個時候就需要 RPC 框架了。不同服務(wù)指點的通信是如何做的呢?可以使用 Netty 來做。比如我調(diào)用另外一個節(jié)點的方法的話,至少是要讓對方知道我調(diào)用的是哪個類中的哪個方法以及相關(guān)參數(shù)吧!實現(xiàn)一個自己的 HTTP 服務(wù)器 :通過 Netty 我們可以自己實現(xiàn)一個簡單的 HTTP 服務(wù)器,這個大家應(yīng)該不陌生。說到 HTTP 服務(wù)器的話,作為 Java 后端開發(fā),我們一般使用 Tomcat 比較多。一個最基本的 HTTP 服務(wù)器可要以處理常見的 HTTP Method 的請求,比如 POST 請求、GET 請求等等。實現(xiàn)一個即時通訊系統(tǒng) :使用 Netty 我們可以實現(xiàn)一個可以聊天類似微信的即時通訊系統(tǒng),這方面的開源項目還蠻多的,可以自行去 Github 找一找。消息推送系統(tǒng) :市面上有很多消息推送系統(tǒng)都是基于 Netty 來做的。哪些開源項目用到了 Netty? 我們平常經(jīng)常接觸的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
可以說大量的開源項目都用到了 Netty,所以掌握 Netty 有助于你更好的使用這些開源項目并且讓你有能力對其進行二次開發(fā)。
實際上還有很多很多優(yōu)秀的項目用到了 Netty,Netty 官方也做了統(tǒng)計,統(tǒng)計結(jié)果在這里:?? https:///wiki/related-projects.html 。
后記 RPC 框架源碼已經(jīng)開源了,地址:?? https://github.com/Snailclimb/guide-rpc-framework