Netty實戰 IM即時通訊系統(二)Netty簡介
阿新 • • 發佈:2018-12-27
##
零、 目錄
- IM系統簡介
- Netty 簡介
- Netty 環境配置
- 服務端啟動流程
- 實戰: 客戶端和服務端雙向通訊
- 資料傳輸載體ByteBuf介紹
- 客戶端與服務端通訊協議編解碼
- 實現客戶端登入
- 實現客戶端與服務端收發訊息
- pipeline與channelHandler
- 構建客戶端與服務端pipeline
- 拆包粘包理論與解決方案
- channelHandler的生命週期
- 使用channelHandler的熱插拔實現客戶端身份校驗
- 客戶端互聊原理與實現
- 群聊的發起與通知
- 群聊的成員管理(加入與退出,獲取成員列表)
- 群聊訊息的收發及Netty效能優化
- 心跳與空閒檢測
- 總結
- 擴充套件
二、 Netty簡介
- 回顧IO程式設計
-
場景: 客戶端每隔兩秒傳送一個帶有時間戳的“hello world”給服務端 , 服務端收到之後列印。
-
程式碼:
IOServer.java /** * @author 閃電俠 */ public class IOServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8000); // (1) 接收新連線執行緒 new Thread(() -> { while (true) { try { // (1) 阻塞方法獲取新的連線 Socket socket = serverSocket.accept(); // (2) 每一個新的連線都建立一個執行緒,負責讀取資料 new Thread(() -> { try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); // (3) 按位元組流方式讀取資料 while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { } }).start(); } catch (IOException e) { } } }).start(); } } IOClient.java /** * @author 閃電俠 */ public class IOClient { public static void main(String[] args) { new Thread(() -> { try { Socket socket = new Socket("127.0.0.1", 8000); while (true) { try { socket.getOutputStream().write((new Date() + ": hello world").getBytes()); Thread.sleep(2000); } catch (Exception e) { } } } catch (IOException e) { } }).start(); } }
-
IO程式設計,模型在客戶端較少的場景下執行良好 , 但是客戶端比較多的業務來說 , 單機服務端可能需要支撐成千上萬的連線, IO模型可能就不太合適了 , 原因:
- 在傳統的IO模型中 , 每一個連線建立成功之後都需要一個執行緒來維護 , 每個執行緒包含一個while死迴圈, 那麼1W個連線就對應1W個執行緒 , 繼而1W個死迴圈。
- 執行緒資源受限: 執行緒是作業系統中非常寶貴的資源 , 同一時刻有大量的執行緒處於阻塞狀態是非常嚴重的資源浪費,作業系統開銷太大。
- 執行緒切換效率低下: 單機CPU核數固定 , 執行緒爆炸之後作業系統頻繁的進行執行緒切換 , 應用效能幾句下降
- IO程式設計中 , 資料讀寫是以位元組流為單位。
-
為了解決這些問題 , JDK1.4之後提出了NIO
-
- NIO 程式設計
-
NIO 是如何解決一下三個問題。
- 執行緒資源受限
- NIO程式設計模型中 , 新來一個連線不再建立一個新的執行緒, 而是可以把這條連線直接繫結在某個固定的執行緒 , 然後這條連線所有的讀寫都由這個執行緒來負責 , 那麼他是怎麼做到的?
- 如上圖所示,IO 模型中,一個連線來了,會建立一個執行緒,對應一個 while 死迴圈,死迴圈的目的就是不斷監測這條連線上是否有資料可以讀,大多數情況下,1w 個連線裡面同一時刻只有少量的連線有資料可讀,因此,很多個 while 死迴圈都白白浪費掉了,因為讀不出啥資料。
- 而在 NIO 模型中,他把這麼多 while 死迴圈變成一個死迴圈,這個死迴圈由一個執行緒控制,那麼他又是如何做到一個執行緒,一個 while 死迴圈就能監測1w個連線是否有資料可讀的呢? 這就是 NIO 模型中 selector 的作用,一條連線來了之後,現在不建立一個 while 死迴圈去監聽是否有資料可讀了,而是直接把這條連線註冊到 selector 上,然後,通過檢查這個 selector,就可以批量監測出有資料可讀的連線,進而讀取資料,下面我再舉個非常簡單的生活中的例子說明 IO 與 NIO 的區別。
- 在一家幼兒園裡,小朋友有上廁所的需求,小朋友都太小以至於你要問他要不要上廁所,他才會告訴你。幼兒園一共有 100 個小朋友,有兩種方案可以解決小朋友上廁所的問題:
- 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100 個小朋友就需要 100 個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領著他去上,這就是IO模型,一個連線對應一個執行緒。
- 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是 NIO 模型,所有小朋友都註冊到同一個老師,對應的就是所有的連線都註冊到一個執行緒,然後批量輪詢。
- 這就是 NIO 模型解決執行緒資源受限的方案,實際開發過程中,我們會開多個執行緒,每個執行緒都管理著一批連線,相對於 IO 模型中一個執行緒管理一條連線,消耗的執行緒資源大幅減少
- NIO程式設計模型中 , 新來一個連線不再建立一個新的執行緒, 而是可以把這條連線直接繫結在某個固定的執行緒 , 然後這條連線所有的讀寫都由這個執行緒來負責 , 那麼他是怎麼做到的?
- 執行緒切換效率低下
- 由於NIO模型中執行緒數量大大降低 , 執行緒切換的效率也因此大幅度提高
- IO讀寫面向流
- IO讀寫是面向流的 , 一次性只能從流中讀取一個或多個位元組 , 並且讀完之後無法再次讀取 , 你需要自己快取資料 , 而NIO的讀寫是面向Buffer的 , 你可以隨意讀取裡面的任何一個位元組資料 , 不需要你自己快取資料 , 這一切只需要移動讀寫指標即可。
- 執行緒資源受限
-
原生NIO 實現
/** * 服務端 * */ class NIO_server_test_01{ public static void start () throws IOException { Selector serverSelect = Selector.open(); Selector clientSelect = Selector.open(); new Thread(() -> { try { ServerSocketChannel socketChannel = ServerSocketChannel.open(); socketChannel.socket().bind(new InetSocketAddress(8000)); // 監聽埠 socketChannel.configureBlocking(false); // 是否阻塞 socketChannel.register(serverSelect, SelectionKey.OP_ACCEPT); while ( true ) { // 檢測是否有新的連線 if(serverSelect.select(1) > 0) { // 1 是超時時間 select 方法返回當前連線數量 Set<SelectionKey> set = serverSelect.selectedKeys(); set.stream() .filter(key -> key.isAcceptable()) .collect(Collectors.toList()) .forEach(key ->{ try { // 每次來一個新的連線, 不需要建立新的執行緒 , 而是註冊到clientSelector SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept(); clientChannel.configureBlocking(false); clientChannel.register(serverSelect, SelectionKey.OP_ACCEPT); }catch(Exception e) { e.printStackTrace(); }finally { set.iterator().remove(); } }); } } }catch (Exception e) { e.printStackTrace(); } }).start(); new Thread(() -> { try { // 批量輪詢 有哪些連線有資料可讀 while ( true ) { if(clientSelect.select(1) > 0) { clientSelect.selectedKeys().stream() .filter(key -> key.isReadable()) .collect(Collectors.toList()) .forEach(key -> { try { SocketChannel clientChannl = (SocketChannel) key.channel(); ByteBuffer bf = ByteBuffer.allocate(1024); // 面向byteBuffer clientChannl.read(bf); bf.flip(); System.out.println(Charset.defaultCharset().newDecoder().decode(bf).toString()); }catch ( Exception e) { e.printStackTrace(); }finally { clientSelect.selectedKeys().iterator().remove(); key.interestOps(SelectionKey.OP_READ); } }); } } }catch (Exception e) { e.printStackTrace(); } }).start(); } }
- 通常NIO 模型中會有兩個執行緒每個執行緒中繫結一個輪詢器selector , 在我們的例子中serverSelector負責輪詢是否有新的連線 , clientSelector 負責輪詢連線中是否有資料可讀。
- 服務端檢測到新的連線之後 , 不在建立一個新的執行緒 , 而是直接將連線註冊到clientSelector中
- clientorSelector 被一個while死迴圈抱著 , 如果在某一時刻有多個連線資料可讀 ,資料將會被clientSelector.select() 方法輪詢出來。 進而批量處理 。
- 資料的讀寫面向buffer 而不是面向流。
-
原生NIO 進行網路開發的缺點:
- JDK 的NIO 程式設計需要了解很多概念, 程式設計複雜 , 對NIO 入門很不友好 , 程式設計模型不友好 , ByteBuffer的API簡直反人類 (這是書裡這麼說的 , 不要噴我)。
- 對NIO 程式設計來說 , 一個比較適合的執行緒模型能充分發揮它的優勢 , 而JDK沒有給你實現 , 你需要自己實現 , 就連簡單的協議拆包都要自己實現 (我感覺這樣才根據創造力呀 )
- JDK NIO 底層由epoll 實現 , 該實現飽受詬病的空輪訓bug會導致cpu 飆升100%
- 專案龐大之後 , 自己實現的NIO 很容易出現各類BUG , 維護成本高 (作者怎麼把自己的過推向JDK haha~)
- 正因為如此 , 我連客戶端的程式碼都懶得給你寫了 (這作者可真夠懶的) , 你可以直接使用IOClient 和NIO_Server 通訊
-
JDK 的NIO 猶如帶刺的玫瑰 , 雖然美好 , 讓人嚮往 , 但是使用不當會讓你抓耳撓腮 , 痛不欲生 , 正因為如此 , Netty橫空出世!(作者這才華 嘖嘖嘖~)
-
- Netty 程式設計
- Netty到底是何方神聖(被作者吹上天了都) , 用依據簡單的話來說就是: Netty 封裝了JDK 的NIO , 讓你使用更加乾爽 (乾爽???) , 你不用在寫一大堆複雜的程式碼了 , 用官方的話來說就是: Netty是一個非同步事件驅動的網路應用框架 , 用於快速開發可維護的高效能伺服器和客戶端。
- Netty 相比 JDK 原生NIO 的優點 :
- 使用NIO 需要了解太多概念, 程式設計複雜 , 一不小心 BUG 橫飛
- Netty 底層IO模型隨意切換 , 而這一切只需要小小的改動 , 改改引數 , Netty樂意直接從NIO模型轉換為IO 模型 。
- Netty 自帶的拆包解包 , 異常檢測可以讓你從NIO 的繁重細節中脫離出來 , 讓你只關心業務邏輯 。
- Netty 解決了JDK 的很多包括空輪訓在內的BUG
- Netty社群活躍 , 遇到問題可以輕鬆解決
- Netty 已經經歷各大RPC 框架 , 訊息中間價 , 分散式通訊中介軟體線上的廣泛驗證 , 健壯性無比強大
- 程式碼例項
-
maven 依賴
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.6.Final</version> </dependency>
-
NettyServer
/** * @author outman * */ class Netty_server_02 { public void start () { ServerBootstrap serverBootstrap = new ServerBootstrap(); NioEventLoopGroup boss = new NioEventLoopGroup(); NioEventLoopGroup woker = new NioEventLoopGroup(); serverBootstrap.group(boss ,woker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) throws Exception { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() { @Override protected void channelRead0(ChannelHandlerContext cxt, String msg) throws Exception { System.out.println(msg); } }); } }).bind(8000); } }
- 這麼一小段程式碼就實現了我們前面NIO 程式設計中所有的功能 , 包括服務端啟動 , 接收新連線 , 列印客戶端傳來的資料。
- 將NIO 中的概念與IO模型結合起來理解:
- boss 對應 IOServer 中接收新連線建立執行緒 , 主要負責建立新連線
- worker 對應 IOServer 中負責讀取資料的執行緒 , 主要用於資料讀取語句業務邏輯處理 。
- 詳細邏輯會在後續深入討論
-
NettyClient
/**
* @author outman
* */
class Netty_client_02 {public static void main(String[] args) throws InterruptedException { Bootstrap bootstrap = new Bootstrap(); NioEventLoopGroup group = new NioEventLoopGroup(); bootstrap.group(group).channel(NioSocketChannel.class).handler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) { ch.pipeline().addLast(new StringEncoder()); } }); Channel channel = bootstrap.connect("127.0.0.1", 8000).channel(); while (true) { channel.writeAndFlush(new Date() + ": hello world!"); Thread.sleep(2000); } }
}
-
在客戶端程式中 , group 對應了我們IOClient 中 新起的執行緒。
-
剩下的邏輯 我們在後文中詳細分析 , 現在你可以把 Netty_server_02 和Netty_client_02 複製到 你的IDE 中 執行起來 感受世界 的美好 (注意 先啟動 服務端 再啟動客戶端 )
-
使用Netty 之後 整個世界都美好了, 一方面 Netty 對NIO 封裝的如此完美 , 另一方面 , 使用Netty 之後 , 網路通訊這塊的效能問題幾乎不用操心 , 盡情的讓Netty 榨乾你的CPU 吧~~
-