瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備 【博客園總入口 】html
瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高併發核心編程》 大廠必備 + 大廠必備 + 大廠必備 【博客園總入口 】java
入大廠+漲工資必備: 高併發【 億級流量IM實戰】 實戰系列 【 SpringCloud Nginx秒殺】 實戰系列 【博客園總入口 】python
Netty很是複雜難懂,但有是必須掌握的內容,全網最易懂的Netty書籍爲《Netty Zookeeper Redis 高併發實戰》react
爲何說是全網最易懂的Netty書籍呢? 請參見《Netty Redis Zookeeper 高併發實戰》 一書的讀者評價面試
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備算法
Netty是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。Netty是基於nio的,它封裝了jdk的nio,讓咱們使用起來更加方法靈活。編程
典型的應用有:阿里分佈式服務框架 Dubbo,默認使用 Netty 做爲基礎通訊組件,還有 RocketMQ 也是使用 Netty 做爲通信的基礎。json
Netty常見的使用場景以下:bootstrap
Netty的對JDK自帶的NIO的API進行封裝,解決上述問題,主要特色有:設計模式
BIO:一個鏈接一個線程,客戶端有鏈接請求時服務器端就須要啓動一個線程進行處理。線程開銷大。
僞異步IO:將請求鏈接放入線程池,一對多,但線程仍是很寶貴的資源。
NIO:一個請求一個線程,但客戶端發送的鏈接請求都會註冊到多路複用器上,多路複用器輪詢到鏈接有I/O請求時才啓動一個線程進行處理。
AIO:一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啓動線程進行處理,
BIO是面向流的,NIO是面向緩衝區的;BIO的各類流是阻塞的。而NIO是非阻塞的;BIO的Stream是單向的,而NIO的channel是雙向的。
NIO的特色:事件驅動模型、單線程處理多任務、非阻塞I/O,I/O讀寫再也不阻塞,而是返回0、基於block的傳輸比基於流的傳輸更高效、更高級的IO函數zero-copy、IO多路複用大大提升了Java網絡應用的可伸縮性和實用性。基於Reactor線程模型。
在Reactor模式中,事件分發器等待某個事件或者可應用或個操做的狀態發生,事件分發器就把這個事件傳給事先註冊的事件處理函數或者回調函數,由後者來作實際的讀寫操做。如在Reactor中實現讀:註冊讀就緒事件和相應的事件處理器、事件分發器等待事件、事件到來,激活分發器,分發器調用事件對應的處理器、事件處理器完成實際的讀操做,處理讀到的數據,註冊新的事件,而後返還控制權。
Buffer:與Channel進行交互,數據是從Channel讀入緩衝區,從緩衝區寫入Channel中的
flip方法 : 反轉此緩衝區,將position給limit,而後將position置爲0,其實就是切換讀寫模式
clear方法 :清除此緩衝區,將position置爲0,把capacity的值給limit。
rewind方法 : 重繞此緩衝區,將position置爲0
DirectByteBuffer可減小一次系統空間到用戶空間的拷貝。但Buffer建立和銷燬的成本更高,不可控,一般會用內存池來提升性能。直接緩衝區主要分配給那些易受基礎系統的本機I/O 操做影響的大型、持久的緩衝區。若是數據量比較小的中小應用狀況下,能夠考慮使用heapBuffer,由JVM進行管理。
Channel:表示 IO 源與目標打開的鏈接,是雙向的,但不能直接訪問數據,只能與Buffer 進行交互。經過源碼可知,FileChannel的read方法和write方法都致使數據複製了兩次!
Selector可以使一個單獨的線程管理多個Channel,open方法可建立Selector,register方法向多路複用器器註冊通道,能夠監聽的事件類型:讀、寫、鏈接、accept。註冊事件後會產生一個SelectionKey:它表示SelectableChannel 和Selector 之間的註冊關係,wakeup方法:使還沒有返回的第一個選擇操做當即返回,喚醒的
緣由是:註冊了新的channel或者事件;channel關閉,取消註冊;優先級更高的事件觸發(如定時器事件),但願及時處理。
Selector在Linux的實現類是EPollSelectorImpl,委託給EPollArrayWrapper實現,其中三個native方法是對epoll的封裝,而EPollSelectorImpl. implRegister方法,經過調用epoll_ctl向epoll實例中註冊事件,還將註冊的文件描述符(fd)與SelectionKey的對應關係添加到fdToKey中,這個map維護了文件描述符與SelectionKey的映射。
fdToKey有時會變得很是大,由於註冊到Selector上的Channel很是多(百萬鏈接);過時或失效的Channel沒有及時關閉。fdToKey老是串行讀取的,而讀取是在select方法中進行的,該方法是非線程安全的。
Pipe:兩個線程之間的單向數據鏈接,數據會被寫到sink通道,從source通道讀取
NIO的服務端創建過程:Selector.open():打開一個Selector;ServerSocketChannel.open():建立服務端的Channel;bind():綁定到某個端口上。並配置非阻塞模式;register():註冊Channel和關注的事件到Selector上;select()輪詢拿到已經就緒的事件
JDK原生也有一套網絡應用程序API,可是存在一系列問題,主要以下:
Netty做爲異步事件驅動的網絡,高性能之處主要來自於其I/O模型和線程處理模型,前者決定如何收發數據,後者決定如何處理數據
用什麼樣的通道將數據發送給對方,BIO、NIO或者AIO,I/O模型在很大程度上決定了框架的性能
傳統阻塞型I/O(BIO)能夠用下圖表示:
特色
問題
在I/O複用模型中,會用到select,這個函數也會使進程阻塞,可是和阻塞I/O所不一樣的的,這兩個函數能夠同時阻塞多個I/O操做,並且能夠同時對多個讀操做,多個寫操做的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操做函數
Netty的非阻塞I/O的實現關鍵是基於I/O複用模型,這裏用Selector對象表示:
Netty的IO線程NioEventLoop因爲聚合了多路複用器Selector,能夠同時併發處理成百上千個客戶端鏈接。當線程從某客戶端Socket通道進行讀寫數據時,若沒有數據可用時,該線程能夠進行其餘任務。線程一般將非阻塞 IO 的空閒時間用於在其餘通道上執行 IO 操做,因此單獨的線程能夠管理多個輸入和輸出通道。
因爲讀寫操做都是非阻塞的,這就能夠充分提高IO線程的運行效率,避免因爲頻繁I/O阻塞致使的線程掛起,一個I/O線程能夠併發處理N個客戶端鏈接和讀寫操做,這從根本上解決了傳統同步阻塞I/O一鏈接一線程模型,架構的性能、彈性伸縮能力和可靠性都獲得了極大的提高。
傳統的I/O是面向字節流或字符流的,以流式的方式順序地從一個Stream 中讀取一個或多個字節, 所以也就不能隨意改變讀取指針的位置。
在NIO中, 拋棄了傳統的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能從Channel中讀取數據到Buffer中或將數據 Buffer 中寫入到 Channel。
基於buffer操做不像傳統IO的順序操做, NIO 中能夠隨意地讀取任意位置的數據
數據報如何讀取?讀取以後的編解碼在哪一個線程進行,編解碼後的消息如何派發,線程模型的不一樣,對性能的影響也很是大。
一般,咱們設計一個事件處理模型的程序有兩種思路
以GUI的邏輯處理爲例,說明兩種邏輯的不一樣:
這裏借用O’Reilly 大神關於事件驅動模型解釋圖
主要包括4個基本組件:
能夠看出,相對傳統輪詢模式,事件驅動有以下優勢:
Reactor是反應堆的意思,Reactor模型,是指經過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。 服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor模式也叫Dispatcher模式,即I/O多了複用統一監聽事件,收到事件後分發(Dispatch給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor模型中有2個關鍵組成:
取決於Reactor的數量和Hanndler線程數量的不一樣,Reactor模型有3個變種
能夠這樣理解,Reactor就是一個執行while (true) { selector.select(); …}循環的線程,會源源不斷的產生新的事件,稱做反應堆很貼切。
篇幅關係,這裏再也不具體展開Reactor特性、優缺點比較,有興趣的讀者能夠參考我以前另一篇文章:《理解高性能網絡模型》
Netty主要基於主從Reactors多線程模型(以下圖)作了必定的修改,其中主從Reactor多線程模型有多個Reactor:MainReactor和SubReactor:
這裏引用Doug Lee大神的Reactor介紹:Scalable IO in Java裏面關於主從Reactor多線程模型的圖
特別說明的是: 雖然Netty的線程模型基於主從Reactor多線程,借用了MainReactor和SubReactor的結構,可是實際實現上,SubReactor和Worker線程在同一個線程池中:
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap server = new ServerBootstrap(); server.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) 123456
上面代碼中的bossGroup 和workerGroup是Bootstrap構造方法中傳入的兩個對象,這兩個group均是線程池
異步的概念和同步相對。當一個異步過程調用發出後,調用者不能馬上獲得結果。實際處理這個調用的部件在完成後,經過狀態、通知和回調來通知調用者。
Netty中的I/O操做是異步的,包括bind、write、connect等操做會簡單的返回一個ChannelFuture,調用者並不能馬上得到結果,經過Future-Listener機制,用戶能夠方便的主動獲取或者經過通知機制得到IO操做結果。
當future對象剛剛建立時,處於非完成狀態,調用者能夠經過返回的ChannelFuture來獲取操做執行的狀態,註冊監聽函數來執行完成後的操,常見有以下操做:
例以下面的的代碼中綁定端口是異步操做,當綁定操做處理完,將會調用相應的監聽器處理邏輯
serverBootstrap.bind(port).addListener(future -> { if (future.isSuccess()) { System.out.println(new Date() + ": 端口[" + port + "]綁定成功!"); } else { System.err.println("端口[" + port + "]綁定失敗!"); } }); 12345678
相比傳統阻塞I/O,執行I/O操做後線程會被阻塞住, 直到操做完成;異步處理的好處是不會形成線程阻塞,線程在I/O操做期間能夠執行別的程序,在高併發情形下會更穩定和更高的吞吐量。
前面介紹完Netty相關一些理論介紹,下面從功能特性、模塊組件、運做過程來介紹Netty的架構設計
Bootstrap意思是引導,一個Netty應用一般由一個Bootstrap開始,主要做用是配置整個Netty程序,串聯各個組件,Netty中Bootstrap類是客戶端程序的啓動引導類,ServerBootstrap是服務端啓動引導類。
正如前面介紹,在Netty中全部的IO操做都是異步的,不能馬上得知消息是否被正確處理,可是能夠過一會等它執行完成或者直接註冊一個監聽,具體的實現就是經過Future和ChannelFutures,他們能夠註冊一個監聽,當操做執行成功或失敗時監聽會自動觸發註冊的監聽事件。
Netty網絡通訊的組件,可以用於執行網絡I/O操做。 Channel爲用戶提供:
不一樣協議、不一樣的阻塞類型的鏈接都有不一樣的 Channel 類型與之對應,下面是一些經常使用的 Channel 類型
Netty基於Selector對象實現I/O多路複用,經過 Selector, 一個線程能夠監聽多個鏈接的Channel事件, 當向一個Selector中註冊Channel 後,Selector 內部的機制就能夠自動不斷地查詢(select) 這些註冊的Channel是否有已就緒的I/O事件(例如可讀, 可寫, 網絡鏈接完成等),這樣程序就能夠很簡單地使用一個線程高效地管理多個 Channel 。
NioEventLoop中維護了一個線程和任務隊列,支持異步提交執行任務,線程啓動時會調用NioEventLoop的run方法,執行I/O任務和非I/O任務:
兩種任務的執行時間比由變量ioRatio控制,默認爲50,則表示容許非IO任務執行的時間與IO任務的執行時間相等。
NioEventLoopGroup,主要管理eventLoop的生命週期,能夠理解爲一個線程池,內部維護了一組線程,每一個線程(NioEventLoop)負責處理多個Channel上的事件,而一個Channel只對應於一個線程。
ChannelHandler是一個接口,處理I / O事件或攔截I / O操做,並將其轉發到其ChannelPipeline(業務處理鏈)中的下一個處理程序。
ChannelHandler自己並無提供不少方法,由於這個接口有許多的方法須要實現,方便使用期間,能夠繼承它的子類:
或者使用如下適配器類:
保存Channel相關的全部上下文信息,同時關聯一個ChannelHandler對象
保存ChannelHandler的List,用於處理或攔截Channel的入站事件和出站操做。 ChannelPipeline實現了一種高級形式的攔截過濾器模式,使用戶能夠徹底控制事件的處理方式,以及Channel中各個的ChannelHandler如何相互交互。
下圖引用Netty的Javadoc4.1中ChannelPipline的說明,描述了ChannelPipeline中ChannelHandler一般如何處理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler處理,並經過調用ChannelHandlerContext中定義的事件傳播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))轉發到其最近的處理程序。
I/O Request via Channel or ChannelHandlerContext | +---------------------------------------------------+---------------+ | ChannelPipeline | | | \|/ | | +---------------------+ +-----------+----------+ | | | Inbound Handler N | | Outbound Handler 1 | | | +----------+----------+ +-----------+----------+ | | /|\ | | | | \|/ | | +----------+----------+ +-----------+----------+ | | | Inbound Handler N-1 | | Outbound Handler 2 | | | +----------+----------+ +-----------+----------+ | | /|\ . | | . . | | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()| | [ method call] [method call] | | . . | | . \|/ | | +----------+----------+ +-----------+----------+ | | | Inbound Handler 2 | | Outbound Handler M-1 | | | +----------+----------+ +-----------+----------+ | | /|\ | | | | \|/ | | +----------+----------+ +-----------+----------+ | | | Inbound Handler 1 | | Outbound Handler M | | | +----------+----------+ +-----------+----------+ | | /|\ | | +---------------+-----------------------------------+---------------+ | \|/ +---------------+-----------------------------------+---------------+ | | | | | [ Socket.read() ] [ Socket.write() ] | | | | Netty Internal I/O Threads (Transport Implementation) | +-------------------------------------------------------------------+ 123456789101112131415161718192021222324252627282930313233343536373839
入站事件由自下而上方向的入站處理程序處理,如圖左側所示。 入站Handler處理程序一般處理由圖底部的I / O線程生成的入站數據。 一般經過實際輸入操做(例如SocketChannel.read(ByteBuffer))從遠程讀取入站數據。
出站事件由上下方向處理,如圖右側所示。 出站Handler處理程序一般會生成或轉換出站傳輸,例如write請求。 I/O線程一般執行實際的輸出操做,例如SocketChannel.write(ByteBuffer)。
在 Netty 中每一個 Channel 都有且僅有一個 ChannelPipeline 與之對應, 它們的組成關係以下:
一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表, 而且每一個 ChannelHandlerContext 中又關聯着一個 ChannelHandler。入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表head日後傳遞到最後一個入站的handler,出站事件會從鏈表tail往前傳遞到最前一個出站的handler,兩種類型的handler互不干擾。
初始化並啓動Netty服務端過程以下:
public static void main(String[] args) { // 建立mainReactor NioEventLoopGroup boosGroup = new NioEventLoopGroup(); // 建立工做線程組 NioEventLoopGroup workerGroup = new NioEventLoopGroup(); final ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap // 組裝NioEventLoopGroup .group(boosGroup, workerGroup) // 設置channel類型爲NIO類型 .channel(NioServerSocketChannel.class) // 設置鏈接配置參數 .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true) // 配置入站、出站事件handler .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { // 配置入站、出站事件channel ch.pipeline().addLast(...); ch.pipeline().addLast(...); } }); // 綁定端口 int port = 8080; serverBootstrap.bind(port).addListener(future -> { if (future.isSuccess()) { System.out.println(new Date() + ": 端口[" + port + "]綁定成功!"); } else { System.err.println("端口[" + port + "]綁定失敗!"); } }); } 12345678910111213141516171819202122232425262728293031323334353637
結合上面的介紹的Netty Reactor模型,介紹服務端Netty的工做架構圖:
server端包含1個Boss NioEventLoopGroup和1個Worker NioEventLoopGroup,NioEventLoopGroup至關於1個事件循環組,這個組裏包含多個事件循環NioEventLoop,每一個NioEventLoop包含1個selector和1個事件循環線程。
每一個Boss NioEventLoop循環執行的任務包含3步:
每一個Worker NioEventLoop循環執行的任務包含3步:
其中任務隊列中的task有3種典型使用場景
ctx.channel().eventLoop().execute(new Runnable() { @Override public void run() { //... } }); 1234567
ctx.channel().eventLoop().schedule(new Runnable() { @Override public void run() { } }, 60, TimeUnit.SECONDS); 1234567
Netty經過Reactor模型基於多路複用器接收並處理用戶請求,內部實現了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept事件,當接收到accept事件的請求時,把對應的socket封裝到一個NioSocketChannel中,並交給work線程池,其中work線程池負責請求的read和write事件,由對應的Handler處理。
單線程模型:全部I/O操做都由一個線程完成,即多路複用、事件分發和處理都是在一個Reactor線程上完成的。既要接收客戶端的鏈接請求,向服務端發起鏈接,又要發送/讀取請求或應答/響應消息。一個NIO 線程同時處理成百上千的鏈路,性能上沒法支撐,速度慢,若線程進入死循環,整個程序不可用,對於高負載、大併發的應用場景不合適。
多線程模型:有一個NIO 線程(Acceptor) 只負責監聽服務端,接收客戶端的TCP 鏈接請求;NIO 線程池負責網絡IO 的操做,即消息的讀取、解碼、編碼和發送;1 個NIO 線程能夠同時處理N 條鏈路,可是1 個鏈路只對應1 個NIO 線程,這是爲了防止發生併發操做問題。但在併發百萬客戶端鏈接或須要安全認證時,一個Acceptor 線程可能會存在性能不足問題。
主從多線程模型:Acceptor 線程用於綁定監聽端口,接收客戶端鏈接,將SocketChannel 從主線程池的Reactor 線程的多路複用器上移除,從新註冊到Sub 線程池的線程上,用於處理I/O 的讀寫等操做,從而保證mainReactor只負責接入認證、握手等操做;
TCP是以流的方式來處理數據,一個完整的包可能會被TCP拆分紅多個包進行發送,也可能把小的封裝成一個大的數據包發送。
TCP粘包/分包的緣由:
應用程序寫入的字節大小大於套接字發送緩衝區的大小,會發生拆包現象,而應用程序寫入數據小於套接字緩衝區大小,網卡將應用屢次寫入的數據發送到網絡上,這將會發生粘包現象;
進行MSS大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發生拆包
以太網幀的payload(淨荷)大於MTU(1500字節)進行ip分片。
解決方法
消息定長:FixedLengthFrameDecoder類
包尾增長特殊字符分割:
將消息分爲消息頭和消息體:LengthFieldBasedFrameDecoder類。分爲有頭部的拆包與粘包、長度字段在前且有頭部的拆包與粘包、多擴展頭部的拆包與粘包。
Netty 的零拷貝主要包含三個方面:
Netty 有兩種發送消息的方式:
Netty 默認是 CPU 處理器數的兩倍,bind 完以後啓動。
序列化(編碼)是將對象序列化爲二進制形式(字節數組),主要用於網絡傳輸、數據持久化等;而反序列化(解碼)則是將從網絡、磁盤等讀取的字節數組還原成原始對象,主要用於網絡傳輸對象的解碼,以便完成遠程調用。
影響序列化性能的關鍵因素:序列化後的碼流大小(網絡帶寬的佔用)、序列化的性能(CPU資源佔用);是否支持跨語言(異構系統的對接和開發語言切換)。
Java默認提供的序列化:沒法跨語言、序列化後的碼流太大、序列化的性能差
XML,優勢:人機可讀性好,可指定元素或特性的名稱。缺點:序列化數據只包含數據自己以及類的結構,不包括類型標識和程序集信息;只能序列化公共屬性和字段;不能序列化方法;文件龐大,文件格式複雜,傳輸佔帶寬。適用場景:當作配置文件存儲數據,實時數據轉換。
JSON,是一種輕量級的數據交換格式,優勢:兼容性高、數據格式比較簡單,易於讀寫、序列化後數據較小,可擴展性好,兼容性好、與XML相比,其協議比較簡單,解析速度比較快。缺點:數據的描述性比XML差、不適合性能要求爲ms級別的狀況、額外空間開銷比較大。適用場景(可替代XML):跨防火牆訪問、可調式性要求高、基於Web browser的Ajax請求、傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。
Fastjson,採用一種「假定有序快速匹配」的算法。優勢:接口簡單易用、目前java語言中最快的json庫。缺點:過於注重快,而偏離了「標準」及功能性、代碼質量不高,文檔不全。適用場景:協議交互、Web輸出、Android客戶端
Thrift,不只是序列化協議,仍是一個RPC框架。優勢:序列化後的體積小, 速度快、支持多種語言和豐富的數據類型、對於數據字段的增刪具備較強的兼容性、支持二進制壓縮編碼。缺點:使用者較少、跨防火牆訪問時,不安全、不具備可讀性,調試代碼時相對困難、不能與其餘傳輸層協議共同使用(例如HTTP)、沒法支持向持久層直接讀寫數據,即不適合作數據持久化序列化協議。適用場景:分佈式系統的RPC解決方案
Avro,Hadoop的一個子項目,解決了JSON的冗長和沒有IDL的問題。優勢:支持豐富的數據類型、簡單的動態語言結合功能、具備自我描述屬性、提升了數據解析速度、快速可壓縮的二進制數據形式、能夠實現遠程過程調用RPC、支持跨編程語言實現。缺點:對於習慣於靜態類型語言的用戶不直觀。適用場景:在Hadoop中作Hive、Pig和MapReduce的持久化數據格式。
Protobuf,將數據結構以.proto文件進行描述,經過代碼生成工具能夠生成對應數據結構的POJO對象和Protobuf相關的方法和屬性。優勢:序列化後碼流小,性能高、結構化數據存儲格式(XML JSON等)、經過標識字段的順序,能夠實現協議的前向兼容、結構化的文檔更容易管理和維護。缺點:須要依賴於工具生成代碼、支持的語言相對較少,官方只支持Java 、C++ 、python。適用場景:對性能要求高的RPC調用、具備良好的跨防火牆的訪問屬性、適合應用層對象的持久化
其它
protostuff 基於protobuf協議,但不須要配置proto文件,直接導包便可
Jboss marshaling 能夠直接序列化java類, 無須實java.io.Serializable接口
Message pack 一個高效的二進制序列化格式
Hessian 採用二進制協議的輕量級remoting onhttp工具
kryo 基於protobuf協議,只支持java語言,須要註冊(Registration),而後序列化(Output),反序列化(Input)
具體場景
對於公司間的系統調用,若是性能要求在100ms以上的服務,基於XML的SOAP協議是一個值得考慮的方案。
基於Web browser的Ajax,以及Mobile app與服務端之間的通信,JSON協議是首選。對於性能要求不過高,或者以動態類型語言爲主,或者傳輸數據載荷很小的的運用場景,JSON也是很是不錯的選擇。
對於調試環境比較惡劣的場景,採用JSON或XML可以極大的提升調試效率,下降系統開發成本。
當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具備必定的競爭關係。
對於T級別的數據的持久化應用場景,Protobuf和Avro是首要選擇。若是持久化後的數據存儲在hadoop子項目裏,Avro會是更好的選擇。
對於持久層非Hadoop項目,以靜態類型語言爲主的應用場景,Protobuf會更符合靜態類型語言工程師的開發習慣。因爲Avro的設計理念偏向於動態類型語言,對於動態語言爲主的應用場景,Avro是更好的選擇。
若是須要提供一個完整的RPC解決方案,Thrift是一個好的選擇。
若是序列化以後須要支持不一樣的傳輸層協議,或者須要跨防火牆訪問的高性能場景,Protobuf能夠優先考慮。
protobuf的數據類型有多種:bool、double、float、int3二、int6四、string、bytes、enum、message。protobuf的限定符:required: 必須賦值,不能爲空、optional:字段能夠賦值,也能夠不賦值、repeated: 該字段能夠重複任意次數(包括0次)、枚舉;只能用指定的常量集中的一個值做爲其值;
protobuf的基本規則:每一個消息中必須至少留有一個required類型的字段、包含0個或多個optional類型的字段;repeated表示的字段能夠包含0個或多個數據;[1,15]以內的標識號在編碼的時候會佔用一個字節(經常使用),[16,2047]以內的標識號則佔用2個字節,標識號必定不能重複、使用消息類型,也能夠將消息嵌套任意多層,可用嵌套消息類型來代替組。
protobuf的消息升級原則:不要更改任何已有的字段的數值標識;不能移除已經存在的required字段,optional和repeated類型的字段能夠被移除,但要保留標號不能被重用。新添加的字段必須是optional或repeated。由於舊版本程序沒法讀取或寫入新增的required限定符的字段。
編譯器爲每個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來建立消息類接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();
Netty中的使用:ProtobufVarint32FrameDecoder 是用於處理半包消息的解碼類;ProtobufDecoder(UserProto.User.getDefaultInstance())這是建立的UserProto.java文件中的解碼類;ProtobufVarint32LengthFieldPrepender 對protobuf協議的消息頭上加上一個長度爲32的整形字段,用於標誌這個消息的長度的類;ProtobufEncoder 是編碼類
將StringBuilder轉換爲ByteBuf類型:copiedBuffer()方法
NioEventLoopGroup(實際上是MultithreadEventExecutorGroup) 內部維護一個類型爲 EventExecutor children [], 默認大小是處理器核數 * 2, 這樣就構成了一個線程池,初始化EventExecutor時NioEventLoopGroup重載newChild方法,因此children元素的實際類型爲NioEventLoop。
線程啓動時調用SingleThreadEventExecutor的構造方法,執行NioEventLoop類的run方法,首先會調用hasTasks()方法判斷當前taskQueue是否有元素。若是taskQueue中有元素,執行 selectNow() 方法,最終執行selector.selectNow(),該方法會當即返回。若是taskQueue沒有元素,執行 select(oldWakenUp) 方法
select ( oldWakenUp) 方法解決了 Nio 中的 bug,selectCnt 用來記錄selector.select方法的執行次數和標識是否執行過selector.selectNow(),若觸發了epoll的空輪詢bug,則會反覆執行selector.select(timeoutMillis),變量selectCnt 會逐漸變大,當selectCnt 達到閾值(默認512),則執行rebuildSelector方法,進行selector重建,解決cpu佔用100%的bug。
rebuildSelector方法先經過openSelector方法建立一個新的selector。而後將old selector的selectionKey執行cancel。最後將old selector的channel從新註冊到新的selector中。rebuild後,須要從新執行方法selectNow,檢查是否有已ready的selectionKey。
接下來調用processSelectedKeys 方法(處理I/O任務),當selectedKeys != null時,調用processSelectedKeysOptimized方法,迭代 selectedKeys 獲取就緒的 IO 事件的selectkey存放在數組selectedKeys中, 而後爲每一個事件都調用 processSelectedKey 來處理它,processSelectedKey 中分別處理OP_READ;OP_WRITE;OP_CONNECT事件。
最後調用runAllTasks方法(非IO任務),該方法首先會調用fetchFromScheduledTaskQueue方法,把scheduledTaskQueue中已經超過延遲執行時間的任務移到taskQueue中等待被執行,而後依次從taskQueue中取任務執行,每執行64個任務,進行耗時檢查,若是已執行時間超過預先設定的執行時間,則中止執行非IO任務,避免非IO任務太多,影響IO任務的執行。
每一個NioEventLoop對應一個線程和一個Selector,NioServerSocketChannel會主動註冊到某一個NioEventLoop的Selector上,NioEventLoop負責事件輪詢。
Outbound 事件都是請求事件, 發起者是 Channel,處理者是 unsafe,經過 Outbound 事件進行通知,傳播方向是 tail到head。Inbound 事件發起者是 unsafe,事件的處理者是 Channel, 是通知事件,傳播方向是從頭至尾。
內存管理機制,首先會預申請一大塊內存Arena,Arena由許多Chunk組成,而每一個Chunk默認由2048個page組成。Chunk經過AVL樹的形式組織Page,每一個葉子節點表示一個Page,而中間節點表示內存區域,節點本身記錄它在整個Arena中的偏移地址。當區域被分配出去後,中間節點上的標記位會被標記,這樣就表示這個中間節點如下的全部節點都已被分配了。大於8k的內存分配在poolChunkList中,而PoolSubpage用於分配小於8k的內存,它會把一個page分割成多段,進行內存分配。
ByteBuf的特色:支持自動擴容(4M),保證put方法不會拋出異常、經過內置的複合緩衝類型,實現零拷貝(zero-copy);不須要調用flip()來切換讀/寫模式,讀取和寫入索引分開;方法鏈;引用計數基於AtomicIntegerFieldUpdater用於內存回收;PooledByteBuf採用二叉樹來實現一個內存池,集中管理內存的分配和釋放,不用每次使用都新建一個緩衝區對象。UnpooledHeapByteBuf每次都會新建一個緩衝區對象。
如今穩定推薦使用的主流版本仍是Netty4,Netty5 中使用了 ForkJoinPool,增長了代碼的複雜度,可是對性能的改善卻不明顯,因此這個版本不推薦使用,官網也沒有提供下載連接。
Netty 入門門檻相對較高,實際上是由於這方面的資料較少,並非由於他有多難,你們其實均可以像搞透 Spring 同樣搞透 Netty。在學習以前,建議先理解透整個框架原理結構,運行過程,能夠少走不少彎路。
瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高併發實戰》 面試必備 + 面試必備 + 面試必備
瘋狂創客圈 - Java高併發研習社羣,爲你們開啓大廠之門