Netty事件監聽和處理(下)【有福利】

情情說發表於2018-04-04

上一篇 介紹了事件監聽、責任鏈模型、socket介面和IO模型、執行緒模型等基本概念,以及Netty的整體結構,這篇就來說下Netty三大核心模組之一:事件監聽和處理。

前面提到,Netty是一個NIO框架,它將IO通道的建立、可讀、可寫等狀態變化,抽象成事件,以責任鏈的方式進行傳遞,可以在處理鏈上插入自定義的Handler,對感興趣的事件進行監聽和處理。

通過介紹,你會了解到:

  • 事件監聽和處理模型
  • 事件監聽:EventLoop
  • 事件處理:ChannelPipeline和ChannelHandler
  • 使用Netty實現Websocket協議

文章末尾有福利 ~

事件監聽和處理模型

進行網路程式設計時,一般的編寫過程是這樣的:

  • 建立服務端Socket,監聽某個埠;
  • 當有客戶端連線時,會建立一個新的客戶端Socket,監聽資料的可讀、可寫狀態,每一個連線請求都會建立一個客戶端Socket;
  • 讀取和寫入資料都會呼叫Socket提供的介面,介面列表在上一篇提到過;

傳統的模型,每個客戶端Socket會建立一個單獨的執行緒監聽socket事件,一方面系統可建立的執行緒數有限,限制了併發數,一方面執行緒過多,執行緒切換頻繁,導致效能嚴重下降。

隨著作業系統IO模型的發展,可以採用多路複用IO,一個執行緒監聽多個Socket,另外,服務端處理客戶端連線,與客戶端Socket的監聽,可以在不同的執行緒進行處理。

Netty就是採用多路複用IO進行事件監聽,另外,使用不同的執行緒分別處理客戶端的連線、資料讀寫。

整個處理結構如下圖,簡單說明下:

  • Boss EventLoopGroup主要處理客戶端的connect事件,包含多個EventLoop,每個EventLoop一個執行緒;
  • Worker EventLoopGroup主要處理客戶端Socket的資料read、write事件,包含多個EventLoop,每個EventLoop一個執行緒;
  • 無論是Boos還是Worker,事件的處理都是通過Channel Pipleline組織的,它是責任鏈模式的實現,包含一個或多個Handler;
  • 偵聽一個埠,只會繫結到Boss EventLoopGroup中的一個Eventloop;
  • Worker EventLoopGroup中的一個Eventloop,可以監聽多個客戶端Socket;

事件監聽和處理模型

EventLoop

一個EventLoop其實和一個特定的執行緒繫結, 並且在其生命週期內, 繫結的執行緒都不會再改。

EventLoop肩負著兩種任務:

  • 第一個是作為 IO 執行緒, 執行與 Channel 相關的 IO 操作, 包括 呼叫select等待就緒的IO事件、讀寫資料與資料的處理等;
  • 第二個任務是作為任務佇列, 執行 taskQueue 中的任務, 例如使用者呼叫eventLoop.schedule提交的定時任務也是這個執行緒執行的;

第一個任務比較好理解,主要解釋下第二個:從socket資料到資料處理,再到寫入響應資料,Netty都在一個執行緒中處理,主要是為了執行緒安全考慮,減少競爭和執行緒切換,通過任務佇列的方式,可以在使用者執行緒提交處理邏輯,在Eventloop中執行。

整個EventLoop乾的事情就是select -> processIO -> runAllTask,processIO處理IO事件相關的邏輯,runAllTask處理任務佇列中的任務,如果執行的任務過多,會影響IO事件的處理,所以會限制任務處理的時間,整個處理過程如下圖:

EventLoop處理過程

EventLoop的run程式碼如下:

protected void run() {
     for (; ; ) {
         oldWakenUp = wakenUp.getAndSet(false);
         try {
             if (hasTasks()) { //如果有任務,快速返回
                 selectNow();
             } else {
                 select(); //如果沒任務,等待事件返回
                 if (wakenUp.get()) {
                     selector.wakeup();
                 }
             }
             cancelledKeys = 0;
             final long ioStartTime = System.nanoTime();
             needsToSelectAgain = false;

             //處理IO事件
             if (selectedKeys != null) {
                 processSelectedKeysOptimized(selectedKeys.flip());
             } else {
                 processSelectedKeysPlain(selector.selectedKeys());
             }

             //計算IO處理時間
             final long ioTime = System.nanoTime() - ioStartTime;
             final int ioRatio = this.ioRatio; //預設為50

             //處理提交的任務
             runAllTasks(ioTime * (100 - ioRatio) / ioRatio);

             if (isShuttingDown()) {
                 closeAll();
                 if (confirmShutdown()) {
                     break;
                 }
             }
         } catch (Throwable t) {
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
             }
         }
     }
 }
複製程式碼

ChannelPipeline和ChannelHandler

ChannelPipeline是一個介面,其有一個預設的實現類DefaultChannelPipeline,內部有兩個屬性:head和tail, 這兩者都實現了ChannelHandler介面,對應處理鏈的頭和尾。

 protected DefaultChannelPipeline(Channel channel) {
     this.channel = ObjectUtil.checkNotNull(channel, "channel");
     succeededFuture = new SucceededChannelFuture(channel, null);
     voidPromise =  new VoidChannelPromise(channel, true);

     tail = new TailContext(this);
     head = new HeadContext(this);

     head.next = tail;
     tail.prev = head;
}
複製程式碼

每個Channel建立時,會建立一個ChannelPipeline物件,來處理channel的各種事件,可以在執行時動態進行動態修改其中的 ChannelHandler。

ChannelHandler承載業務處理邏輯的地方,我們接觸最多的類,可以自定義Handler,加入處理鏈中,實現自定義邏輯。

ChannelHandler 可分為兩大類:ChannelInboundHandler 和 ChannelOutboundHandle,分別對應入站和出站訊息的處理,用於資料讀取和資料寫入。它們提供了介面方法供我們實現,用來處理各種事件:

public interface ChannelInboundHandler extends ChannelHandler {
	void channelRegistered(ChannelHandlerContext ctx) throws Exception;
	void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
	void channelActive(ChannelHandlerContext ctx) throws Exception;
	void channelInactive(ChannelHandlerContext ctx) throws Exception;
	void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
	void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
	void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
	void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception;
}
複製程式碼

自定義Handler時,一般繼承ChannelInboundHandlerAdapter或 ChannelOutboundHandlerAdapter。

需要注意的是,不建議在 ChannelHandler 中直接實現耗時或阻塞的操作,因為這可能會阻塞 Netty 工作執行緒,導致 Netty 無法及時響應 IO 處理。

ChannelPipline

使用Netty實現Websocket協議

Websocket協議

不是本篇的重點,簡單說明下:

  • 是一種長連線協議,大部分瀏覽器都支援,通過websocket,服務端可以主動發訊息給客戶端;
  • Websocket協議,在握手階段使用HTTP協議,握手完成之後,走Websocket自己的協議;
  • Websocket是一種二進位制協議;
初始化

Netty提供了ChannelInitializer類方便我們初始化,建立WebSocketServerInitializer類,繼承ChannelInitializer類,用於新增ChannelHandler:

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> {

	@Resource
	private CustomTextFrameHandler customTextFrameHandler;

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("codec-http", new HttpServerCodec());
        pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
        
        pipeline.addLast("websocket-protocal-handler",new WebSocketServerProtocolHandler());
        pipeline.addLast("custome-handler", customTextFrameHandler);
    }
}
複製程式碼

分析下這幾個Handler,都是Netty預設提供的:

  • HttpServerCodec:用於解析Http請求,主要在握手階段進行處理;
  • HttpObjectAggregator:用於合併Http請求頭和請求體,主要在握手階段進行處理;
  • WebSocketServerProtocolHandler:處理Websocket協議;
  • CustomTextFrameHandler:自定義的Handler,用於新增自己的業務邏輯。

是不是很方便,經過WebSocketServerProtocolHandler處理後,讀取出來的就是文字資料了,不用自己處理資料合包、拆包問題。

CustomTextFrameHandler

自定義的Handler,進行業務處理:

public class CustomTextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(final ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
        final String content = frame.text();
        System.out.println("接收到資料:"+content);   
        
        // 回覆資料
        TextWebSocketFrame respFrame = new TextWebSocketFrame("我收到了你的資料");
        if (ctx.channel().isWritable()) {
		      ChannelFuture future = ctx.writeAndFlush(respFrame);
		  }			        
    }
}
複製程式碼

福利說明

最後,說下福利:小愛音響F碼。

準備了2份,主要為了感謝「微信公眾號」和「掘金社群」的朋友,每一份包括1個小愛音響F碼和1個小愛音響 mini F碼。

小米手機F碼源自於英文單詞”Friend”,是小米公司提供給小米核心使用者及為小米做出貢獻的網友的優先購買權,如果您有小米F碼的話無需等待即可直接利用小米F碼購買相關產品!

簡單來說,F碼就是不用搶了,可以直接購買 ~

抽獎截止時間

4月9號中午12點

抽獎規則
掘金社群
  • 需要關注我的掘金賬號才有效,個人主頁

  • 使用微信抽獎助手隨機抽取for掘金社群;

    掘金抽獎

微信公眾號
  • 需要關注我的微信公眾號才有效;

    情情說

  • 使用微信抽獎助手隨機抽取for微信公眾號;

    微信公眾號抽獎

相關文章