Netty基礎篇:Netty是什麼?

在大家閱讀文章之前,我想在這個特別的日子裡送給大家一條網易雲的熱評

Netty基礎篇:Netty是什麼?

前言

在開始瞭解Netty是什麼之前,我們先來回顧一下,如果我們需要實現一個客戶端與服務端通信的程序,使用傳統的IO編程,應該如何來實現?

IO編程

我們簡化下場景:客戶端每隔兩秒發送一個帶有時間戳的"hello world"給服務端,服務端收到之後打印。

為了方便演示,下面例子中,服務端和客戶端各一個類,把這兩個類拷貝到你的IDE中,先後運行 IOServer.java和 IOClient.java可看到效果。

下面是傳統的IO編程中服務端實現

Netty基礎篇:Netty是什麼?

server端首先創建了一個 serverSocket來監聽8000端口,然後創建一個線程,線程裡面不斷調用阻塞方法 serversocket.accept();獲取新的連接,見(1),當獲取到新的連接之後,給每條連接創建一個新的線程,這個線程負責從該連接中讀取數據,見(2),然後讀取數據是以字節流的方式,見(3)。

下面是傳統的IO編程中客戶端實現

Netty基礎篇:Netty是什麼?

客戶端的代碼相對簡單,連接上服務端8000端口之後,每隔2秒,我們向服務端寫一個帶有時間戳的 "hello world"。

IO編程模型在客戶端較少的情況下運行良好,但是對於客戶端比較多的業務來說,單機服務端可能需要支撐成千上萬的連接,IO模型可能就不太合適了,我們來分析一下原因。

上面的demo,從服務端代碼中我們可以看到,在傳統的IO模型中,每個連接創建成功之後都需要一個線程來維護,每個線程包含一個while死循環,那麼1w個連接對應1w個線程,繼而1w個while死循環,這就帶來如下幾個問題:

  1. 線程資源受限:線程是操作系統中非常寶貴的資源,同一時刻有大量的線程處於阻塞狀態是非常嚴重的資源浪費,操作系統耗不起
  2. 線程切換效率低下:單機cpu核數固定,線程爆炸之後操作系統頻繁進行線程切換,應用性能急劇下降。
  3. 除了以上兩個問題,IO編程中,我們看到數據讀寫是以字節流為單位,效率不高。

為了解決這三個問題,JDK在1.4之後提出了NIO。

NIO編程

關於NIO相關的文章網上也有很多,這裡不打算詳細深入分析,下面簡單描述一下NIO是如何解決以上三個問題的。

線程資源受限

NIO編程模型中,新來一個連接不再創建一個新的線程,而是可以把這條連接直接綁定到某個固定的線程,然後這條連接所有的讀寫都由這個線程來負責,那麼他是怎麼做到的?我們用一幅圖來對比一下IO與NIO

Netty基礎篇:Netty是什麼?

如上圖所示,IO模型中,一個連接來了,會創建一個線程,對應一個while死循環,死循環的目的就是不斷監測這條連接上是否有數據可以讀,大多數情況下,1w個連接裡面同一時刻只有少量的連接有數據可讀,因此,很多個while死循環都白白浪費掉了,因為讀不出啥數據。

而在NIO模型中,他把這麼多while死循環變成一個死循環,這個死循環由一個線程控制,那麼他又是如何做到一個線程,一個while死循環就能監測1w個連接是否有數據可讀的呢? 這就是NIO模型中selector的作用,一條連接來了之後,現在不創建一個while死循環去監聽是否有數據可讀了,而是直接把這條連接註冊到selector上,然後,通過檢查這個selector,就可以批量監測出有數據可讀的連接,進而讀取數據,下面我再舉個非常簡單的生活中的例子說明IO與NIO的區別。

在一家幼兒園裡,小朋友有上廁所的需求,小朋友都太小以至於你要問他要不要上廁所,他才會告訴你。幼兒園一共有100個小朋友,有兩種方案可以解決小朋友上廁所的問題:

  1. 每個小朋友配一個老師。每個老師

    隔段

    時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領著他去上,這就是IO模型,一個連接對應一個線程。
  2. 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是NIO模型,所有小朋友都註冊到同一個老師,對應的就是所有的連接都註冊到一個線程,然後批量輪詢。

這就是NIO模型解決線程資源受限的方案,實際開發過程中,我們會開多個線程,每個線程都管理著一批連接,相對於IO模型中一個線程管理一條連接,消耗的線程資源大幅減少

線程切換效率低下

由於NIO模型中線程數量大大降低,線程切換效率因此也大幅度提高

IO讀寫以字節為單位

NIO解決這個問題的方式是數據讀寫不再以字節為單位,而是以字節塊為單位。IO模型中,每次都是從操作系統底層一個字節一個字節地讀取數據,而NIO維護一個緩衝區,每次可以從這個緩衝區裡面讀取一塊的數據, 這就好比一盤美味的豆子放在你面前,你用筷子一個個夾(每次一個),肯定不如要勺子挖著吃(每次一批)效率來得高。

簡單講完了JDK NIO的解決方案之後,我們接下來使用NIO的方案替換掉IO的方案,我們先來看看,如果用JDK原生的NIO來實現服務端,該怎麼做

前方高能預警:以下代碼可能會讓你感覺極度不適,如有不適,請跳過

Netty基礎篇:Netty是什麼?


Netty基礎篇:Netty是什麼?

相信大部分沒有接觸過NIO的同學應該會直接跳過代碼來到這一行:原來使用JDK原生NIO的API實現一個簡單的服務端通信程序是如此複雜!

複雜得我都沒耐心解釋這一坨代碼的執行邏輯(開個玩笑),我們還是先對照NIO來解釋一下幾個核心思路

  1. NIO模型中通常會有兩個線程,每個線程綁定一個輪詢器selector,在我們這個例子中 serverSelector負責輪詢是否有新的連接, clientSelector負責輪詢連接是否有數據可讀
  2. 服務端監測到新的連接之後,不再創建一個新的線程,而是直接將新連接綁定到 clientSelector上,這樣就不用IO模型中1w個while循環在死等,參見(1)
  3. clientSelector被一個while死循環包裹著,如果在某一時刻有多條連接有數據可讀,那麼通過 clientSelector.select(1)方法可以輪詢出來,進而批量處理,參見(2)
  4. 數據的讀寫以內存塊為單位,參見(3)

其他的細節部分,我不願意多講,因為實在是太複雜,你也不用對代碼的細節深究到底。總之,強烈不建議直接基於JDK原生NIO來進行網絡開發,下面是我總結的原因

  • JDK的NIO編程需要了解很多的概念,編程複雜,對NIO入門非常不友好,編程模型不友好,ByteBuffer的api簡直反人類
  • 對NIO編程來說,一個比較合適的線程模型能充分發揮它的優勢,而JDK沒有給你實現,你需要自己實現,就連簡單的自定義協議拆包都要你自己實現
  • JDK的NIO底層由epoll實現,該實現飽受詬病的空輪訓bug會導致cpu飆升100%
  • 項目龐大之後,自行實現的NIO很容易出現各類bug,維護成本較高,上面這一坨代碼我都不能保證沒有bug

正因為如此,我客戶端代碼都懶得寫給你看了==!,你可以直接使用 IOClient.java與 NIOServer.java通信

JDK的NIO猶如帶刺的玫瑰,雖然美好,讓人嚮往,但是使用不當會讓你抓耳撓腮,痛不欲生,正因為如此,Netty橫空出世!

Netty編程

那麼Netty到底是何方神聖? 用一句簡單的話來說就是:Netty封裝了JDK的NIO,讓你用得更爽,你不用再寫一大堆複雜的代碼了。 用官方正式的話來說就是:Netty是一個異步事件驅動的網絡應用框架,用於快速開發可維護的高性能服務器和客戶端。

下面是我總結的使用Netty不使用JDK原生NIO的原因

  • 使用JDK自帶的NIO需要了解太多的概念,編程複雜,一不小心bug橫飛
  • Netty底層IO模型隨意切換,而這一切只需要做微小的改動,改改參數,Netty可以直接從NIO模型變身為IO模型
  • Netty自帶的拆包解包,異常檢測等機制讓你從NIO的繁重細節中脫離出來,讓你只需要關心業務邏輯
  • Netty解決了JDK的很多包括空輪詢在內的bug
  • Netty底層對線程,selector做了很多細小的優化,精心設計的reactor線程模型做到非常高效的併發處理
  • 自帶各種協議棧讓你處理任何一種通用協議都幾乎不用親自動手
  • Netty社區活躍,遇到問題隨時郵件列表或者issue
  • Netty已經歷各大rpc框架,消息中間件,分佈式通信中間件線上的廣泛驗證,健壯性無比強大

看不懂沒有關係,這些我們在後續的課程中我們都可以學到,接下來我們用Netty的版本來重新實現一下本文開篇的功能吧

首先,引入Maven依賴

Netty基礎篇:Netty是什麼?

然後,下面是服務端實現部分

Netty基礎篇:Netty是什麼?

這麼一小段代碼就實現了我們前面NIO編程中的所有的功能,包括服務端啟動,接受新連接,打印客戶端傳來的數據,怎麼樣,是不是比JDK原生的NIO編程優雅許多?

初學Netty的時候,由於大部分人對NIO編程缺乏經驗,因此,將Netty裡面的概念與IO模型結合起來可能更好理解

  • boos對應了 IOServer.java中的接收新連接線程,主要負責創建新連接
  • worker對應 IOClient.java中的負責讀取數據的線程,主要用於讀取數據以及業務邏輯處理

然後剩下的邏輯我在後面的系列文章中會詳細分析,你可以先把這段代碼拷貝到你的IDE裡面,然後運行main函數

然後下面是客戶端NIO的實現部分

Netty基礎篇:Netty是什麼?

在客戶端程序中, group對應了我們 IOClient.java中main函數起的線程,剩下的邏輯我在後面的文章中會詳細分析,現在你要做的事情就是把這段代碼拷貝到你的IDE裡面,然後運行main函數,最後回到 NettyIOServer.java的控制檯,你會看到效果。

使用Netty之後是不是覺得整個世界都美好了,一方面Netty對NIO封裝得如此完美,寫出來的代碼非常優雅,另外一方面,使用Netty之後,網絡通信這塊的性能問題幾乎不用操心,盡情地讓Netty榨乾你的CPU吧。


分享到:


相關文章: