Netty概述


一、Netty介紹

  1. Netty是由JBOSS提供的一個java開源框架
  2. Netty是一個異步的、基於事件驅動的網絡應用框架,用以快速開發高性能‘高可靠性的網絡IO程序
  3. Netty主要針對在TCP協議下,面向Clients端的高並發應用,或者Peer-to-Peer場景下的大量數據持續傳輸的應用
  4. Netty本質是一個NIO框架,適用於服務器通訊相關的多種應用場景
  5. 要透徹理解Netty,需要先學習NIO。

二、IO模型

IO模型簡單理解,就是用什么樣的通道進行數據的發送和接收,很大程度上決定了程序通信的性能。

Java共支持3種網絡編程模型IO模式:BIO、NIO、AIO

  1. Java BIO:同步並阻塞(傳統阻塞型),服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷。
  2. Java NIO:同步非阻塞,服務器實現模式為一個線程處理多個請求(連接),即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有IO請求就進行處理
  3. Java AIO(NIO.2):異步非阻塞,AIO引入異步通道的概念,采用了Proactor模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,一般適用於連接數較多且連接時間較長的應用。

三種模式適用場景

  • BIO適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,並發局限於應用中,JDK1.4以前的唯一選擇,但程序簡單易理解。
  • NIO適用於連接數目多且連接比較短的架構,比如聊天服務器,彈幕系統,服務器間通訊等。編程比較復雜,JDK1.4開始支持。
  • AIO適用於連接數目多且連接比較長的架構,比如相冊服務器,充分調用OS參與並發操作,編程比較復雜,JDK7開始支持。

三、NIO與零拷貝

  1. 零拷貝是網絡編程的關鍵,很多性能優化都離不開。
  2. 在Java程序中,常用的零拷貝有mmap(內存映射)和sendFile
  3. 零拷貝是指沒有cpu拷貝

mmap優化

  1. mmap通過內存映射,將文件映射到內核緩沖區,同時,用戶空間可以共享內核空間的數據。這樣,在進行網絡傳輸時,就可以減少內核空間到用戶控件的拷貝。

sendFile優化

  1. Linux2.1版本提供了sendFile函數,其基本原理為:數據根本不經過用戶態,直接從內核緩沖區進入到SocketBuffer,同時,由於和用戶態完全無關,就減少了一次上下文切換。
  2. linux在2.4版本中,做了一些修改,避免了從內核緩沖區拷貝到SocketBuffer的操作,直接拷貝到協議棧,從而再一次減少了數據拷貝

mmap和sendFile的區別

  1. mmap適合小數據量讀寫,sendFile適合大文件傳輸。
  2. mmap需要4次上下文切換,3次數據拷貝;sendFile需要3次上下文切換,最少2次數據拷貝。
  3. sendFile可以利用DMA方式,減少CPU拷貝,mmap則不能(必須從內核拷貝到Socket緩沖區)

四、線程模型

  1. 不同的線程模型,對程序的性能有很大的影響。
  2. 目前存在的線程模型有:
    1. 傳統阻塞I/O服務模型
    2. Reactor模式
  3. 根據Reactor的數量和處理資源池線程的數量不同,有3中典型的實現
    1. 單Reactor單線程
    2. 單Reactor多線程
    3. 主從Reactor多線程
  4. Netty線程模式(Netty主要基於主從Reactor多線程模型做了一定的改進,其中主從Reactor多線程模型有多個Reactor)

Reactor模式

Reactor對應的叫法:1.反應器模式 2.分發者模式 3.通知者模式

  1. 基於I/O復用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理。
  2. 基於線程池復用線程資源:不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。

Reactor模式中核心組成:

  1. Reactor:Reactor在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對IO事件做出反應。它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯系人。
  2. Handlers:處理程序執行I/O事件要完成的實際事件,類似於客戶想要與之交談的公司的實際官員。Reactor通過調度適當的處理程序來響應I/O事件,處理程序執行非阻塞操作。

單Reactor單線程

  1. Select是前面I/O復用模型介紹額標准網絡編程API,可以實現應用程序通過一個阻塞對象監聽多路連接請求
  2. Reactor對象通過Select監控客戶端請求事件,受到事件后通過Dispatch進行分發
  3. 如果是建立連接請求事件,則由Acceptor通過Accept處理連接請求,然后創建一個Handler對象處理連接完成后的后續業務處理。
  4. 如果不是簡介事件,則Reactor會分發調用連接對應的Handler來響應
  5. Handler會完成Read-》業務處理-》Send的完整業務流程。

優缺點:

  1. 優點:模型簡單,沒有多線程、進程通信、競爭問題,全部都在一個線程中完成。
  2. 缺點:性能問題,只有一個線程,無法發揮多核CPU性能。Handler在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。
  3. 缺點:可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接受和處理外部消息,造成節點故障。

單Reactor多線程

handler只負責響應事件,不做具體的業務處理,會分發給后面的worker線程池的某個線程處理。

優缺點:

  1. 優點:可以充分利用多核CPU性能
  2. 缺點:多線程數據共享和訪問比較復雜,reactor處理所有的事件的監聽和響應,在單線程運行,在高並發場景容易出現瓶頸。

主從Reactor多線程

優缺點:

  1. 優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續業務處理
  2. 優點:父線程與子線程的數據交互簡單,Reactor主線程只需要把新連接傳給子線程,子線程無需返回數據。
  3. 缺點:編程復雜度較高。

Netty模型

Netty主要基於主從Reactors多線程模型。

  1. BossGroup線程維護Selector,只關注Accept
  2. 當接收到Accept事件,獲取到對應的SocketChannel,封裝成NIOSocketChannel並注冊到Workder線程(事件循環),並進行維護
  3. 當Worker線程監聽到selector中通道發生自己感興趣的事件后,就進行處理(交由handler),注意handler已經加入到通道。

  1. Netty抽象出兩組線程池BossGroup專門負責接收客戶端的連接,WorkerGroup專門負責網絡的讀寫。
  2. BossGroup和WorkerGroup類型都是NioEventLoopGroup
  3. NioEventLoopGroup相當於一個事件循環組,這個組中含有多個事件循環,每一個事件循環是NioEventLoop
  4. NioEventLoop表示一個不斷循環的執行處理任務的線程,每個NioEvenLoop都有一個selector,用於監聽綁定在其上的socket的網絡通訊
  5. NioEventLoopGroup可以有多個線程,即可以含有多個NioEventLoop
  6. 每個Boss NioEventLoop循環執行的步驟有三步
    1. 輪詢accept事件
    2. 處理accept事件,與client建立連接,生成NioSocketChannel,並將其注冊到某個worker NioEventLoop上的selector
    3. 處理任務隊列的任務,即runAllTasks
  7. 每個worker NioEventLoop循環執行的步驟
    1. 輪詢read,write事件
    2. 處理I/O事件,即read,write事件在對應NioSocketChannel處理
    3. 處理任務隊列的任務,即runAllTasks
  8. 每個Worker NioEventLoop處理業務時,會使用pipeline,pipeline中包含了channel,即通過pipeline可以獲取到對應通道。管道中維護了很多處理器。

方案再說明

  1. Netty抽象出兩組線程池,BossGroup專門負責接收客戶端連接,WorkerGroup專門負責網絡讀寫操作。
  2. NioEventLoop表示一個不斷循環執行處理任務的線程,每個NioEventLoop都有一個selector,用於監聽綁定在其上的socket網絡通道。
  3. NioEventLoop內部采用串行化設計,從消息的讀取-》編碼-》發送,始終由IO線程NioEventLoop負責
    1. NioEventLoopGroup下包含多個NioEventLoop
    2. 每個NioEventLoop中包含有一個Selector,一個taskQueue
    3. 每個NioEventLoop的Selector上可以注冊監聽多個NioChannel
    4. 每個NioChannel只會綁定在唯一的NioEventLoop上
    5. 每個NioChannel都綁定有一個自己的ChannelPipeline

五、異步模型

  1. 異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的組件在完成后,通過狀態、通知和回調來通知調用者。
  2. Netty中的I/O操作是異步的,包括Bind、Write、Connect等操作會簡單的返回一個ChannelFuture。
  3. 調用者並不能立即獲得結果,而是通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲取IO操作結果
  4. Netty的異步模型是建立在future和callback之上的。重點說Future,它的核心思想是:假設一個方法fun,計算過程可能非常耗時,等待fun返回顯然不合適。那么可以在調用fun的時候,立馬返回一個future,后序可以通過Future去監控方法fun的處理過程(即:Future-Listener機制)

Future-Listener機制

  1. 當Future對象剛剛創建時,處於非完成狀態,調用者可以通過返回的ChannelFuture來獲取操作執行的狀態,注冊監聽函數來執行完成后的操作。
  2. 常見如下操作:
    1. 通過isDone方法來判斷當前操作是否完成
    2. 通過isSuccess方法來判斷已完成的當前操作是否成功。
    3. 通過getCause方法來獲取當前操作失敗的原因
    4. 通過isCancelled方法來判斷已完成的當前操作是否被取消
    5. 通過addListener方法來注冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;如果Future對象已完成,則通知指定的監聽器

六、核心模塊

Bootstap、ServerBootstrap

  1. BootStrap意思是引導,一個Netty應用通常由一個Bootstrap開始,主要作用是配置整個Netty程序,串聯各個組件,Netty中Bootstrap類是客戶端程序的啟動引導類,ServerBootstrap是服務端啟動引導類。
  2. 常見方法有:
    • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),該方法用於服務器端,用來設置兩個EventLoop
    • public B group(EventLoopGroup group),該方法用於客戶端,用來設置一個EventLoop
    • public B channel(Class<? extends C> channelClass),該方法用來設置一個服務器端的通道實現
    • public B option(ChannelOption option, T value) ,用來給ServerChannel添加配置
    • public ServerBootstrap childOption(ChannelOption childOption, T value),用來給接收到的通道添加配置
    • public ServerBootstrap childHandler(ChannelHandler childHandler),該方法用來設置業務處理類(自定義handler)
    • public ChannelFuture bind(int inetPort),該方法應用於服務器端,用來設置占用的端口號
    • public ChannelFuture connect(String inetHost,int inetPort),該方法用於客戶端,用來連接服務器

Future、ChannelFuture

  1. Netty中所有IO操作都是異步的,不能立刻得知是否被正確處理。但是可以過一會等它執行完成或直接注冊一個監聽,具體的實現就是通過Future和ChannelFutures,他們可以注冊一個監聽,當操作執行成功或失敗時監聽會自動觸發注冊的監聽事件。
  2. 常用方法:
    • Channel channel(),返回當前正在進行IO操作的通道
    • ChannelFuture sync(),等待異步操作執行完畢

Selector

  1. Netty基於Selector對象實現I/O多路復用,通過Selector一個線程可以監聽多個連接的Channel事件。
  2. 當向一個Selector中注冊Channel后,Selector內部的機制就可以自動不斷地查詢(select)這些注冊的Channel是否有已經就緒的I/O事件,這樣程序就可以簡單地使用一個線程高效地管理多個Channel

ChannelHandler及其實現類

  1. ChannelHandler是一個接口,處理I/O事件或攔截I/O操作,並將其轉發到其ChannelPipeline中的下一個處理程序。
  2. ChannelHandler本身並沒有提供很多方法,因為這個接口有許多的方法需要實現,方便調用期間,可以繼承它的子類。

ChannelPipeline

  1. ChannelPipeline是一個Handler的集合,它負責處理和攔截inbound或者outbound的事件和操作,相當於一個貫穿Netty的鏈(可以這樣理解:ChannelPipeline是保存ChannelHandler的List,用於處理或攔截Channel的入站事件和出站操作)
  2. ChannelPipeline實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及Channel中各個的ChannelHandler如何相互交互
  3. 在Netty中每個Channel都有且只有一個ChannelPipeline與之對應。
    • 一個Channel包含一個ChannelPipeline,而ChannelPipeline中又維護了一個由ChannelHandlerContext組成的雙向鏈表,並且每個ChannelHandlerContext中又關聯着一個ChannelHandler
    • 入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表head往后傳遞到最后一個入站的handler,出站事件會從鏈表tail往前傳遞到最前一個出站的handler,兩種類型的handler互不干擾。

ChannelHandlerContext

  1. 保存Channel相關的所有上下文信息,同時關聯一個ChannelHandler對象
  2. 即ChannelHandlerContext中包含一個具體的事件處理器ChannelHandler,同時ChannelHandlerContext中也綁定了對應的pipeline和Channel的信息,方便對ChannelHandler進行調用
  3. 常用方法:
    • ChannelFuture close(),關閉通道
    • ChannelOutboundInvoker flush(),刷新
    • ChannelFuture writeAndFlush(Object msg),將數據寫到ChannelPipeline中當前ChannnelHandler的下一個ChannelHandler開始處理

ChannelOption

  1. Netty在創建Channel實例后,一般都需要設置ChannelOption參數
  2. 參數如下:
    • ChannelOption.SO_BACKLOG:對應TCP/IP協議listen函數中的backlog參數,用來初始化服務器可連接隊列大小。服務端處理客戶端連接請求是順序處理的,所以同一時間只能處理一個客戶端連接。多個客戶端來的時候,服務端將不能處理的客戶端連接請求放在隊列中等待處理,backlog參數指定了隊列的大小
    • ChannelOption.SO_KEEPALIVE:一直保持連接活動狀態

EventLoopGroup和其實現類NioEventLoopGroup

  1. EventLoopGroup是一組EventLoop的抽象,Netty為了更好的利用多核CPU資源,一般會有多個EventLoop同時工作,每個EventLoop維護者一個Selector實例。
  2. EventLoopGroup提供next接口,可以從組里面按照一定規則獲取其中一個EventLoop來處理任務。在Netty服務器端編程中,我們一般都需要提供兩個EventLoopGroup,例如:BossEventLoopGroup和WorkerEventLoopGroup
  3. 通常一個服務端口即一個ServerSocketChannel對應一個Selector和一個EventLoop線程。BossEventLoop負責接收客戶端的連接並將SocketChannel交給WorkerEventLoopGroup來進行IO處理

Unpooled類

  1. 是Netty提供的一個專門用來操作緩沖區(即Netty的數據容器)的工具類
  2. 常用方法如下
    • public static ByteBuf copiedBuffer(CharSequence string, Charset charset)。

心跳檢測機制

Netty通過IdleStateHandler來處理空閑狀態。

/**
* IdleStateHandler:Netty提供的處理空閑狀態的處理器
* long readerIdleTime:表示多長時間沒有讀,就會發送一個心跳檢測包檢測是否連接
* long writerIdleTime:表示多長時間沒有寫,就會發送一個心跳檢測包檢測是否連接
* long allIdleTime:表示多長時間沒有讀寫,就會發送一個心跳檢測包檢測是否連接
* IdleStateEvent觸發后,就會傳遞給管道的下一個handler去處理,通過調用下一個handler的userEventTriggered,在該方法中處理
*/
public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }

pipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));

通過WebSocket編程實現服務器和客戶端長連接

protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//基於http協議,使用http編碼和解碼器
pipeline.addLast(new HttpServerCodec());
//以塊方式寫,添加ChunkedWriteHandler處理器
pipeline.addLast(new ChunkedWriteHandler());
/**
* http數據在傳輸的過程中是分段,HttpObejctAggregator,可以將多個段聚合
*/
pipeline.addLast(new HttpObjectAggregator(8192));
/**
* websocket的數據以幀(frame)形式傳遞
* 瀏覽器請求時ws://localhost:9999/hello 表示請求的uri
* WebSocketServerProtocolHandler 核心功能是將http協議升級為ws協議,保持長連接
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));

七、Google Protobuf

在編寫網絡應用程序時,因為數據在網絡中傳輸的都是二進制字節碼數據,在發送數據時就需要編碼,接收數據時需要解碼。

netty本身也提供了一些解編碼器,但是底層使用的仍是java序列化技術,而java序列化技術本身效率就不高,,存在以下問題:

  • 無法跨語言
  • 序列化后的體積太大,是二進制編碼的5倍多
  • 序列化性能太低。

使用protobuf進行結構化數據傳輸

先編寫Student.proto文件

syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的類名,同時也是文件名
//protobuf使用message管理數據
message  Student{ //會在StudentPOJO外部內生成一個內部類Student,它是真正發送的POJO對象
  int32 id = 1; //Student類中有一個屬性名字為id,類型為int32,protobuf的類型,在不同語言中對應的類型不同
  // 1表示序號,不是值
  string name = 2;
}

再進行編譯生成java文件

protoc --java_out=. Student.proto

將生成的文件加入到項目中,並且在服務端和客戶端都需要配置對應的編碼器和解碼器

pipeline.addLast("encoder",new ProtobufEncoder());
ch.pipeline().addLast("decoder",new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()))

構造對象的方式

StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("xxx").build();
ctx.writeAndFlush(student);

八、handler的調用機制

  1. ChannelHandler充當了處理入站和出站數據的應用邏輯的容器。例如,實現ChannelInboundHandler接口(或ChannelInboudHandlerAdapter),你就可以接收入站事件和數據,這些數據會被業務邏輯處理。當要給客戶端發送響應時,也可以從ChannelInboundHandler沖刷數據。業務邏輯通常寫在一個或者多個ChannelInboundHandler中。ChannelOutboundHandler原理一樣,只不過它是用來處理出站數據的。
  2. ChannelPipeline提供了ChannelHandler鏈的容器。以客戶端應用程序為例,如果事件的運動方向是從pipeline到socket,那么我們稱這些事件為出站的,即客戶端發送給服務端的數據會通過pipeline中的一系列ChannelOutboundHandler,並被這些Handler處理,反之則稱為入站的。

九、TCP粘包拆包原理

  1. TCP是面向連接的,面向流的,提供高可用性服務。收發兩端(客戶端和服務端)都要有一一成對的socket,因此,發送端為了將多個發給接收端的包,更為有效的發給對方,使用了優化方案(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣做雖然提高了效率,但是接收端就難於分辨成完整的數據包了,因為面向流的通信是無消息保護邊界的
  2. 由於TCP無消息保護邊界,需要在接收端處理消息邊界問題,也就是我們所說的粘包、拆包問題

解決方案

  1. 使用自定義協議+編解碼器來解決
  2. 關鍵就是要解決服務器每次讀取數據長度的問題,這個問題解決,就不會出現服務器多讀或少讀數據的問題,從而避免的TCP粘包、拆包問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM