1. 程式人生 > >Netty實戰 IM即時通訊系統(五)客戶端啟動流程

Netty實戰 IM即時通訊系統(五)客戶端啟動流程

##

Netty實戰 IM即時通訊系統(五)客戶端啟動流程

零、 目錄

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

五、 客戶端啟動流程

  1. 客戶端啟動demo

     /**
      * 客戶端啟動流程
      * */
     public class Test_05_客戶端啟動流程 {
     	public static void main(String[] args) {
     		NioEventLoopGroup workerGroup = new NioEventLoopGroup();
     		
     		Bootstrap bootstrap = new Bootstrap();
     		bootstrap
     			// 指定執行緒模型
     			.group(workerGroup)
     			// 指定IO 模型
     			.channel(NioSocketChannel.class)
     			// 指定業務處理邏輯
     			.handler(new ChannelInitializer<NioSocketChannel>() {
     
     				@Override
     				protected void initChannel(NioSocketChannel ch) throws Exception {
     					
     				}
     			});
     		// 建立連線
     		bootstrap.connect("127.0.0.1" , 8000)
     			.addListener(future ->{
     				if(future.isSuccess()) {
     					System.out.println("連線成功");
     				}else {
     					System.out.println("連線失敗");
     				}
     			});
     	}
     
     }
    
    1. 從上面的程式碼可以看出 , 客戶端的啟動引導類是BootStrap , 負責啟動客戶端以及連線服務端 , 而上面一小節我們在描述服務端啟動的時候, 這個引導類是 ServerBootStrap , 引導類建立完成之後我們描述一下客戶端啟動流程:
      1. 首先和服務端的啟動流程一樣 , 我們需要給特指定執行緒模型, 驅動著連線的資料讀寫
      2. 然後我們指定IO 模型為 NioSocketChannel , 表示IO模型為 NIO
      3. 接著, 我們給引導類指定一個handler , 這裡主要就是定義連線的業務處理邏輯 , 不理解沒有關係 , 我們在後面會詳解
      4. 配置完執行緒模型 , IO 模型 , 業務處理邏輯之後 , 呼叫connect() 方法進行連線 , 可以看到connect() 方法有兩個引數 , 第一個引數可以填寫IP或域名 ,第二個引數填寫的是埠號 , 由於connect() 方法返回的是一個Future , 也就是說這個方法是非同步的 , 我們通過addListener方法可以監聽到連線是否成功 , 進而列印連線狀態
    2. 到這裡一個客戶端的demo 就完成了 , 其實只要和客戶端Socket 程式設計模型對應起來 , 這裡的三個概念就會顯得非常簡單
  2. 失敗重連

    1. 在網路差的情況下 , 客戶端第一次連線可能會失敗 , 這個時候我們可能會嘗試重新連線 , 重新連線的邏輯寫在連線失敗的邏輯塊裡

       // 建立連線
       bootstrap.connect("127.0.0.1" , 8000)
       	.addListener(future ->{
       		if(future.isSuccess()) {
       			System.out.println("連線成功");
       		}else {
       			System.out.println("連線失敗");
       			
       			//TODO: 重新連線邏輯
       		}
       	});
      
    2. 重新連線時依然是呼叫相同的邏輯 , 所以我們把連線的程式碼抽取出來, 實現程式碼複用 , 在連線失敗的情況下使用遞迴的方法 實現重連

       public static void connect(Bootstrap bootstrap, String IP, int port) {
       	// 建立連線
       	bootstrap.connect(IP, port).addListener(future -> {
       		if (future.isSuccess()) {
       			System.out.println("連線成功");
       		} else {
       			System.out.println("連線失敗,執行重連");
       			connect(bootstrap, IP, port);
       		}
       	});
       }
      
      1. 以上程式碼就實現了重連機制 , 但是在通常情況下連線失敗不會立即重連 , 而是通過一個指數退避的方式 , 比如 每隔1秒、2秒、4秒、8秒 , 以2的次冪來實現建立連線 , 然後到達一定次數之後就放棄重連

         	connect(bootstrap , "127.0.0.1" , 8000 , 5);
        
         	public static void connect(Bootstrap bootstrap, String IP, int port ,int maxRetry , int... retryIndex) {
         	// 建立連線
         	bootstrap.connect(IP, port).addListener(future -> {
         		// 由於閉包特性  不能修改外部的變數 所有需要在閉包內定義一個相同的變數  拷貝外部變數的值
         		int[] finalRetryIndex ;
         		if (future.isSuccess()) {
         			System.out.println("連線成功");
         		} else if(maxRetry == 0){
         			System.out.println("到達重試最大次數,放棄重連");
         		}else {
         			// 初始化  重試計數
         			if(retryIndex.length == 0) {
         				finalRetryIndex = new int[] {0};
         			}else {
         				finalRetryIndex = retryIndex;
         			}
         			//計算時間間隔
         			int delay = 1 << finalRetryIndex[0];
         			// 執行重試
         			System.out.println(new Date()+"連線失敗,剩餘重連次數:"+maxRetry+","+delay+"秒後執行第"+(finalRetryIndex[0]+1)+"次重連...");
         			bootstrap.config().group().schedule(()->{
         				connect(bootstrap, IP, port , maxRetry-1 , finalRetryIndex[0]+1);
         			}, delay, TimeUnit.SECONDS);
         		}
         	});
         }
         
        
         執行結果:
         Thu Dec 27 11:04:19 CST 2018連線失敗,剩餘重連次數:5,1秒後執行第1次重連...
         Thu Dec 27 11:04:21 CST 2018連線失敗,剩餘重連次數:4,2秒後執行第2次重連...
         Thu Dec 27 11:04:24 CST 2018連線失敗,剩餘重連次數:3,4秒後執行第3次重連...
         Thu Dec 27 11:04:29 CST 2018連線失敗,剩餘重連次數:2,8秒後執行第4次重連...
         Thu Dec 27 11:04:38 CST 2018連線失敗,剩餘重連次數:1,16秒後執行第5次重連...
         到達重試最大次數,放棄重連
        
        1. 從上面的程式碼中我們可以看到 , 通過判斷是否連線成功以及剩餘重試次數 , 分別執行不同的邏輯
          1. 如果連線成功則列印連線成功的訊息
          2. 如果連線失敗 , 但是重試次數已經用完則放棄連線
          3. 如果連線失敗 , 但是連線沒有用完則計算下一次重試時間間隔 , 然後定時重連
        2. 從上面程式碼中我們可以看到 , 定時任務是呼叫bootstrap.config().group().schedule() , 其中bootStrap.config()這個方法返回的是BootStrapConfig , 他是對BootStrap 引數配置的抽象 , 然後ootstrap.config().group() 返回的就是我們一開始設定的執行緒模型workerGroup , 最後呼叫schedule() 方法就可以實現定時任務邏輯了。
  3. 客戶端啟動其他方法

    1. attr(): attr()方法可以給客戶端channel也就是NioSocketChannel繫結自定義屬性 , 然後我們通過channel.attr()取出屬性。 說白了就是給NioSocketChannel維護一個Map 而已

       //設定屬性
       bootstrap.attr(AttributeKey.newInstance("clientName"), "NettyClient");
       //業務邏輯
       bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
      
       	@Override
       	protected void initChannel(NioSocketChannel ch) throws Exception {
       		// 取出屬性
       		Attribute<Object> attr = ch.attr(AttributeKey.valueOf("clientName"));
       		System.out.println("客戶端名稱:"+attr.get());
       	}
       });
      
    2. option(): option()可以給連線設定一些TCP底層的相關屬性 : (ChannelOption相關引數詳解在 上一節《服務端啟動流程》中有連線地址)

       // 設定TCP 相關的屬性
       // 設定連線超時時間
       bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
       // 開啟TCP 心跳機制
       bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
      
  4. 總結:

    1. 在本節中我們學習了Netty客戶端啟動的流程 , 一句話來說就是: 建立一個引導類 , 然後給他指定執行緒 , IO模型 , 指定業務邏輯 ,連線特定的IP:port 客戶端就啟動起來了
    2. 然後我們學習到 connect()方法時非同步的 , 我們可以通過非同步回撥機制來實現指數退避重連機制。
    3. 最後我們討論了Netty客戶端啟動的額外引數 , 只要包括給客戶端Channel 繫結自定義屬性 , 設定TCP底層引數。
  5. 疑問:

    1. 客戶端Channel設定的attr是否會被服務端接收到,並且以此進行必要的引數傳遞?
      1. 答: 客戶端Channel 會被服務端接收到。