1. 程式人生 > >Netty實戰 IM即時通訊系統(二)Netty簡介

Netty實戰 IM即時通訊系統(二)Netty簡介

##

Netty實戰 IM即時通訊系統(二)Netty簡介

零、 目錄

  1. IM系統簡介
  • Netty 簡介
  • Netty 環境配置
  • 服務端啟動流程
  • 實戰: 客戶端和服務端雙向通訊
  • 資料傳輸載體ByteBuf介紹
  • 客戶端與服務端通訊協議編解碼
  • 實現客戶端登入
  • 實現客戶端與服務端收發訊息
  • pipeline與channelHandler
  • 構建客戶端與服務端pipeline
  • 拆包粘包理論與解決方案
  • channelHandler的生命週期
  • 使用channelHandler的熱插拔實現客戶端身份校驗
  • 客戶端互聊原理與實現
  • 群聊的發起與通知
  • 群聊的成員管理(加入與退出,獲取成員列表)
  • 群聊訊息的收發及Netty效能優化
  • 心跳與空閒檢測
  • 總結
  • 擴充套件

二、 Netty簡介

  1. 回顧IO程式設計
    1. 場景: 客戶端每隔兩秒傳送一個帶有時間戳的“hello world”給服務端 , 服務端收到之後列印。

    2. 程式碼:

       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();
           }
       }
      
    3. IO程式設計,模型在客戶端較少的場景下執行良好 , 但是客戶端比較多的業務來說 , 單機服務端可能需要支撐成千上萬的連線, IO模型可能就不太合適了 , 原因:

      1. 在傳統的IO模型中 , 每一個連線建立成功之後都需要一個執行緒來維護 , 每個執行緒包含一個while死迴圈, 那麼1W個連線就對應1W個執行緒 , 繼而1W個死迴圈。
      2. 執行緒資源受限: 執行緒是作業系統中非常寶貴的資源 , 同一時刻有大量的執行緒處於阻塞狀態是非常嚴重的資源浪費,作業系統開銷太大。
      3. 執行緒切換效率低下: 單機CPU核數固定 , 執行緒爆炸之後作業系統頻繁的進行執行緒切換 , 應用效能幾句下降
      4. IO程式設計中 , 資料讀寫是以位元組流為單位。
    4. 為了解決這些問題 , JDK1.4之後提出了NIO

  2. NIO 程式設計
    1. NIO 是如何解決一下三個問題。

      1. 執行緒資源受限
        1. NIO程式設計模型中 , 新來一個連線不再建立一個新的執行緒, 而是可以把這條連線直接繫結在某個固定的執行緒 , 然後這條連線所有的讀寫都由這個執行緒來負責 , 那麼他是怎麼做到的? IO  與 NIO 對比
          1. 如上圖所示,IO 模型中,一個連線來了,會建立一個執行緒,對應一個 while 死迴圈,死迴圈的目的就是不斷監測這條連線上是否有資料可以讀,大多數情況下,1w 個連線裡面同一時刻只有少量的連線有資料可讀,因此,很多個 while 死迴圈都白白浪費掉了,因為讀不出啥資料。
          2. 而在 NIO 模型中,他把這麼多 while 死迴圈變成一個死迴圈,這個死迴圈由一個執行緒控制,那麼他又是如何做到一個執行緒,一個 while 死迴圈就能監測1w個連線是否有資料可讀的呢? 這就是 NIO 模型中 selector 的作用,一條連線來了之後,現在不建立一個 while 死迴圈去監聽是否有資料可讀了,而是直接把這條連線註冊到 selector 上,然後,通過檢查這個 selector,就可以批量監測出有資料可讀的連線,進而讀取資料,下面我再舉個非常簡單的生活中的例子說明 IO 與 NIO 的區別。
          3. 在一家幼兒園裡,小朋友有上廁所的需求,小朋友都太小以至於你要問他要不要上廁所,他才會告訴你。幼兒園一共有 100 個小朋友,有兩種方案可以解決小朋友上廁所的問題:
            1. 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100 個小朋友就需要 100 個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領著他去上,這就是IO模型,一個連線對應一個執行緒。
            2. 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是 NIO 模型,所有小朋友都註冊到同一個老師,對應的就是所有的連線都註冊到一個執行緒,然後批量輪詢。
          4. 這就是 NIO 模型解決執行緒資源受限的方案,實際開發過程中,我們會開多個執行緒,每個執行緒都管理著一批連線,相對於 IO 模型中一個執行緒管理一條連線,消耗的執行緒資源大幅減少
      2. 執行緒切換效率低下
        1. 由於NIO模型中執行緒數量大大降低 , 執行緒切換的效率也因此大幅度提高
      3. IO讀寫面向流
        1. IO讀寫是面向流的 , 一次性只能從流中讀取一個或多個位元組 , 並且讀完之後無法再次讀取 , 你需要自己快取資料 , 而NIO的讀寫是面向Buffer的 , 你可以隨意讀取裡面的任何一個位元組資料 , 不需要你自己快取資料 , 這一切只需要移動讀寫指標即可。
    2. 原生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();
      	}
      	
      }   
      
      1. 通常NIO 模型中會有兩個執行緒每個執行緒中繫結一個輪詢器selector , 在我們的例子中serverSelector負責輪詢是否有新的連線 , clientSelector 負責輪詢連線中是否有資料可讀。
      2. 服務端檢測到新的連線之後 , 不在建立一個新的執行緒 , 而是直接將連線註冊到clientSelector中
      3. clientorSelector 被一個while死迴圈抱著 , 如果在某一時刻有多個連線資料可讀 ,資料將會被clientSelector.select() 方法輪詢出來。 進而批量處理 。
      4. 資料的讀寫面向buffer 而不是面向流。
    3. 原生NIO 進行網路開發的缺點:

      1. JDK 的NIO 程式設計需要了解很多概念, 程式設計複雜 , 對NIO 入門很不友好 , 程式設計模型不友好 , ByteBuffer的API簡直反人類 (這是書裡這麼說的 , 不要噴我)。
      2. 對NIO 程式設計來說 , 一個比較適合的執行緒模型能充分發揮它的優勢 , 而JDK沒有給你實現 , 你需要自己實現 , 就連簡單的協議拆包都要自己實現 (我感覺這樣才根據創造力呀 )
      3. JDK NIO 底層由epoll 實現 , 該實現飽受詬病的空輪訓bug會導致cpu 飆升100%
      4. 專案龐大之後 , 自己實現的NIO 很容易出現各類BUG , 維護成本高 (作者怎麼把自己的過推向JDK haha~)
      5. 正因為如此 , 我連客戶端的程式碼都懶得給你寫了 (這作者可真夠懶的) , 你可以直接使用IOClient 和NIO_Server 通訊
    4. JDK 的NIO 猶如帶刺的玫瑰 , 雖然美好 , 讓人嚮往 , 但是使用不當會讓你抓耳撓腮 , 痛不欲生 , 正因為如此 , Netty橫空出世!(作者這才華 嘖嘖嘖~)

  3. Netty 程式設計
    1. Netty到底是何方神聖(被作者吹上天了都) , 用依據簡單的話來說就是: Netty 封裝了JDK 的NIO , 讓你使用更加乾爽 (乾爽???) , 你不用在寫一大堆複雜的程式碼了 , 用官方的話來說就是: Netty是一個非同步事件驅動的網路應用框架 , 用於快速開發可維護的高效能伺服器和客戶端。
    2. Netty 相比 JDK 原生NIO 的優點 :
      1. 使用NIO 需要了解太多概念, 程式設計複雜 , 一不小心 BUG 橫飛
      2. Netty 底層IO模型隨意切換 , 而這一切只需要小小的改動 , 改改引數 , Netty樂意直接從NIO模型轉換為IO 模型 。
      3. Netty 自帶的拆包解包 , 異常檢測可以讓你從NIO 的繁重細節中脫離出來 , 讓你只關心業務邏輯 。
      4. Netty 解決了JDK 的很多包括空輪訓在內的BUG
      5. Netty社群活躍 , 遇到問題可以輕鬆解決
      6. Netty 已經經歷各大RPC 框架 , 訊息中間價 , 分散式通訊中介軟體線上的廣泛驗證 , 健壯性無比強大
    3. 程式碼例項
      1. maven 依賴

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.6.Final</version>
        </dependency>
        
      2. 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);
         	}
         }
        
        1. 這麼一小段程式碼就實現了我們前面NIO 程式設計中所有的功能 , 包括服務端啟動 , 接收新連線 , 列印客戶端傳來的資料。
        2. 將NIO 中的概念與IO模型結合起來理解:
          1. boss 對應 IOServer 中接收新連線建立執行緒 , 主要負責建立新連線
          2. worker 對應 IOServer 中負責讀取資料的執行緒 , 主要用於資料讀取語句業務邏輯處理 。
          3. 詳細邏輯會在後續深入討論
      3. 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);
           	}
           }
        

        }

      4. 在客戶端程式中 , group 對應了我們IOClient 中 新起的執行緒。

      5. 剩下的邏輯 我們在後文中詳細分析 , 現在你可以把 Netty_server_02 和Netty_client_02 複製到 你的IDE 中 執行起來 感受世界 的美好 (注意 先啟動 服務端 再啟動客戶端 )

      6. 使用Netty 之後 整個世界都美好了, 一方面 Netty 對NIO 封裝的如此完美 , 另一方面 , 使用Netty 之後 , 網路通訊這塊的效能問題幾乎不用操心 , 盡情的讓Netty 榨乾你的CPU 吧~~