Netty、Redis、ZooKeeper高并發實戰 點選檢視第二章 點選檢視第三章

第1章
高并發時代的必備技能
高并發時代已然到來,Netty、Redis、ZooKeeper是高并發時代的必備工具。
1.1 Netty為何這麼火
Netty是JBOSS提供的一個Java開源架構,是基于NIO的用戶端/伺服器程式設計架構,它既能快速開發高并發、高可用、高可靠性的網絡伺服器程式,也能開發高可用、高可靠的用戶端程式。
注:NOI是指非阻塞輸入輸出(Non-Blocking IO),也稱非阻塞IO。另外,本書為了行文上的一緻性,把輸入輸出的英文縮寫統一為IO,而不用I/O。
1.1.1 Netty火熱的程度
Netty已經有了成百上千的分布式中間件、各種開源項目以及各種商業項目的應用。例如火爆的Kafka、RocketMQ等消息中間件、火熱的ElasticSearch開源搜尋引擎、大資料處理Hadoop的RPC架構Avro、主流的分布式通信架構Dubbo,它們都使用了Netty。總之,使用Netty開發的項目,已經有點數不過來了……
Netty之是以受青睐,是因為Netty提供異步的、事件驅動的網絡應用程式架構和工具。作為一個異步架構,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,使用者可以友善地主動擷取或者通過通知機制獲得IO操作結果。
與JDK原生NIO相比,Netty提供了相對十分簡單易用的API,因而非常适合網絡程式設計。Netty主要是基于NIO來實作的,在Netty中也可以提供阻塞IO的服務。
Netty之是以這麼火,與它的巨大優點是密不可分的,大緻可以總結如下:
- API使用簡單,開發門檻低。
- 功能強大,預置了多種編解碼功能,支援多種主流協定。
- 定制能力強,可以通過ChannelHandler對通信架構進行靈活擴充。
- 性能高,與其他業界主流的NIO架構對比,Netty的綜合性能最優。
- 成熟、穩定,Netty修複了已經發現的所有JDK NIO中的BUG,業務開發人員不需要再為NIO的BUG而煩惱。
- 社群活躍,版本疊代周期短,發現的BUG可以被及時修複。
1.1.2 Netty是面試的必殺器
Netty是網際網路中間件領域使用最廣泛、最核心的網絡通信架構之一。幾乎所有網際網路中間件或者大資料領域均離不開Netty,掌握Netty是作為一名國中級工程師邁向進階工程師重要的技能之一。
目前來說,主要的網際網路公司,例如阿裡、騰訊、美團、新浪、淘寶等,在進階工程師的面試過程中,就經常會問一些高性能通信架構方面的問題,還會問一些“你有沒有讀過什麼著名架構的源代碼?”等類似的問題。
如果掌握了Netty相關的技術問題,更進一步說,如果你能全面地閱讀和掌握Netty源代碼,相信面試大公司時,一定底氣十足,成功在握。
1.2 高并發利器Redis
任何高并發的系統,不可或缺的就是緩存。Redis緩存目前已經成為緩存的事實标準。
1.2.1 什麼是Redis
Redis是Remote Dictionary Server(遠端字典伺服器)的縮寫,最初是作為資料庫的工具來使用的。是目前使用廣泛、高效的一款開源緩存。Redis使用C語言開發,将資料儲存在記憶體中,可以看成是一款純記憶體的資料庫,是以它的資料存取速度非常快。一些經常用并且建立時間較長的内容,可以緩存到Redis中,而應用程式能以極快的速度存取這些内容。舉例來說,如果某個頁面經常會被通路到,而建立頁面時需要多次通路資料庫、造成網頁内容的生成時間較長,那麼就可以使用Redis将這個頁面緩存起來,進而減輕了網站的負擔,降低了網站的延遲。
Redis通過鍵-值對(Key-Value Pair)的形式來存儲資料,類似于Java中的Map映射。Redis的Key鍵,隻能是string字元串類型。Redis的Value值類型包括:string字元類型、map映射類型、list清單類型、set集合類型、sortedset有序集合類型。
Redis的主要應用場景:緩存(資料查詢、短連接配接、新聞内容、商品内容等)、分布式會話(Session)、聊天室的線上好友清單、任務隊列(秒殺、搶購、12306等)、應用排行榜、通路統計、資料過期處理(可以精确到毫秒)。
1.2.2 Redis成為緩存事實标準的原因
相對于其他的鍵-值對(Key-Value)記憶體資料庫(如Memcached)而言,Redis具有如下特點:
(1)速度快 不需要等待磁盤的IO,在記憶體之間進行的資料存儲和查詢,速度非常快。當然,緩存的資料總量不能太大,因為受到實體記憶體空間大小的限制。
(2)豐富的資料結構 除了string之外,還有list、hash、set、sortedset,一共五種類型。
(3)單線程,避免了線程切換和鎖機制的性能消耗。
(4)可持久化 支援RDB與AOF兩種方式,将記憶體中的資料寫入外部的實體儲存設備。
(5)支援釋出/訂閱。
(6)支援Lua腳本。
(7)支援分布式鎖 在分布式系統中,如果不同的節點需要訪同到一個資源,往往需要通過互斥機制來防止彼此幹擾,并且保證資料的一緻性。在這種情況下,需要使用到分布式鎖。分布式鎖和Java的鎖用于實作不同線程之間的同步通路,原理上是類似的。
(8)支援原子操作和事務 Redis事務是一組指令的集合。一個事務中的指令要麼都執行,要麼都不執行。如果指令在運作期間出現錯誤,不會自動復原。
(9)支援主-從(Master-Slave)複制與高可用(Redis Sentinel)叢集(3.0 版本以上)
(10)支援管道 Redis管道是指用戶端可以将多個指令一次性發送到伺服器,然後由伺服器一次性傳回所有結果。管道技術的優點是:在批量執行指令的應用場景中,可以大大減少網絡傳輸的開銷,提高性能。
1.3 分布式利器ZooKeeper
突破了單體瓶頸之後的高并發,就必須靠叢集了,而叢集的分布式架構和協調,一定少不了可靠的分布式協調工具,ZooKeeper就是目前極為重要的分布式協調工具。
1.3.1 什麼是ZooKeeper
ZooKeeper最早起源于雅虎公司研究院的一個研究小組。在當時,研究人員發現,在雅虎内部很多大型的系統需要依賴一個類似的系統進行分布式協調,但是這些系統往往存在分布式單點問題。是以雅虎的開發人員就試圖開發一個通用的無單點問題的分布式協調架構。在項目初期給這個項目命名的時候,準備和很多項目一樣,按照雅虎公司的慣例要用動物的名字來命名的(例如著名的Pig項目)。在進行探讨取什麼名字的時候,研究院的首席科學家Raghu Ramakrishnan開玩笑說:再這樣下去,我們這兒就變成動物園了。此話一出,大家紛紛表示就叫動物園管理者吧,于是,ZooKeeper的名字由此誕生了。當然,取名ZooKeeper,也絕非沒有一點兒道理。ZooKeeper的功能,正好是用來協調分布式環境的,協調各個以動物命名的分布式元件,是以,ZooKeeper這個名字也就“名副其實”了。
1.3.2 ZooKeeper的優勢
ZooKeeper對不同系統環境的支援都很好,在絕大多數主流的作業系統上都能夠正常運作,如:GNU/Linux、Sun Solaris、Win32以及MacOS等。需要注意的是,ZooKeeper官方文檔中特别強調,由于FreeBSD系統的JVM(Java Virtual Machine,即Java虛拟機)對Java的NIO Selector選擇器支援得不是很好,是以不建議在FreeBSD系統上部署生産環境的ZooKeeper伺服器。
ZooKeeper的核心優勢是,實作了分布式環境的資料一緻性,簡單地說:每時每刻我們通路ZooKeeper的樹結構時,不同的節點傳回的資料都是一緻的。也就是說,對ZooKeeper進行資料通路時,無論是什麼時間,都不會引起髒讀、重複讀。注:髒讀是指在資料庫存取中無效資料的讀出。
ZooKeeper提供的功能都是分布式系統中非常底層且必不可少的基本功能,如果開發者自己來實作這些功能而且要達到高吞吐、低延遲同時的還要保持一緻性和可用性,實際上是非常困難的。是以,借助ZooKeeper提供的這些功能,開發者就可以輕松在ZooKeeper之上建構自己的各種分布式系統。
1.4 高并發IM的綜合實踐
為了友善交流和學習,筆者組織了一幫高性能發燒友,成立了一個高性能社群,叫作“瘋狂創客圈”。同時,牽頭組織社群的小夥伴們,應用Netty、Redis、ZooKeeper持續疊代一個高并發學習項目,叫作“CrazyIM”。
1.4.1 高并發IM的學習價值
為什麼要開始一個高并發IM(即時通信)的實踐呢?
首先,通過實踐完成一個分布式、高并發的IM系統,具有相當的技術挑戰性。這一點,對于從事傳統的企業級Web開發者來說,相當于進入了一片全新的天地。企業級Web,QPS(Query Per Second,每秒查詢率)峰值可能在1000以内,甚至在100以内,沒有多少技術挑戰性和含金量,屬于重複性的CRUD的體力活。注:CRUD是指Create(建立)、Retrieve(查詢)、Update(更新)和Delete(删除)。而一個分布式、高并發的IM系統,面臨的QPS峰值可能在十萬、百萬、千萬,甚至上億級别。對于此縱深階層化的、遞進的高并發需求,将無極限地考驗着系統的性能。需要不斷地從通信協定、到系統的架構進行優化,對技術能力是一種非常極緻的考驗和訓練。
其次,就具有不同QPS峰值規模的IM系統而言,它們所處的使用者需求環境是不一樣的。這就造成了不同使用者規模的IM系統,各自具有一定的市場需求和實際需要,因而它們不一定都需要上億級的高并發。但是,作為一個頂級的架構師,就應該具備全棧式的架構能力,對不同使用者規模的、差異化的應用場景,提供和架構出與對應的應用場景相比對的高并發IM系統。也就是說,IM系統綜合性相對較強,相關的技術需要覆寫到滿足各種不同應用場景的網絡傳輸、分布式協調、分布式緩存、服務化架構等。
接下來具體看看高并發IM的應用場景吧。
1.4.2 龐大的應用場景
一切高實時性通信、消息推送的應用場景,都需要高并發IM。随着移動網際網路、AI的飛速發展,高性能、高并發IM,有着非常廣泛的應用場景。
高并發IM典型的應用場景如下:私信、聊天、大規模推送、視訊會議、彈幕、抽獎、互動遊戲、基于位置的應用(Uber、滴滴司機位置)、線上教育、智能家居等,如圖1-1所示。
圖1-1 高并發IM典型的應用場景
尤其是對于APP開發的小夥伴們來說,IM已經成為大多數APP的标配。在移動網際網路時代,推送(Push)服務成為APP應用不可或缺的重要組成部分,推送服務可以提升使用者的活躍度和留存率。我們的手機每天接收到各種各樣的廣告和提示消息等,它們大多數都是通過推送服務實作的。
随着5G時代物聯網的發展,未來所有接入物聯網的智能裝置,都将是IM系統的用戶端,這就意味着推送服務會在未來面臨海量的裝置和終端接入。為了支援這些千萬級、上億級的終端,一定是需要強悍的背景系統。
有這麼多的應用場景,對于想成長為Java高手的小夥伴們,高并發IM都是繞不開的一個話題。
對于想在背景有所成就的小夥伴們來說,高并發IM實踐,更是在與終極BOSS PK之前的一場不可避免的打怪練手。
1.5 Netty、Redis、ZooKeeper實踐計劃
下面從最基礎的NIO入手,列出一個大緻12天的實踐計劃,幫助大家深入掌握Netty、Redis、ZooKeeper。
1.5.1 第1天:Java NIO實踐
實踐一:使用FileChannel複制檔案
通過使用Channel通道,完成複制檔案。
本環節的目标是掌握以下知識:Java NIO中ByteBuffer、Channel兩個重要元件的使用。
接着是更新實戰的案例,使用檔案Channel通道的transferFrom方法,完成高效率的檔案複制。
實踐二:使用SocketChannel傳輸檔案
本環節的目标是掌握以下知識:
- 非阻塞用戶端在發起連接配接後,需要不斷的自旋,檢測連接配接是否完成的。
- SocketChannel傳輸通道的read讀取方法、write寫入方法。
- 在SocketChannel傳輸通道關閉前,盡量發送一個輸出結束标志到對方端。
實踐三:使用DatagramChannel傳輸資料
用戶端使用DatagramChannel發送資料,伺服器端使用DatagramChannel接收資料。
- 使用接收資料方法receive,使用發送資料方法send。
- DatagramChannel和SocketChannel兩種通道,在發送、接收資料上的不同。
實踐四:使用NIO實作Discard伺服器
用戶端功能:發送一個資料包到伺服器端,然後關閉連接配接。伺服器端也很簡單,收到用戶端的資料,直接丢棄。
- Selector 選擇器的注冊,以及選擇器的查詢。
- SelectionKey選擇鍵方法的使用。
- 根據SelectionKey方法的四種IO事件類型,完成對應的IO處理。
1.5.2 第2天:Reactor反應器模式實踐
實踐一:單線程Reactor反應器模式的實作
使用單線程Reactor反應器模式,設計和實作一個EchoServer回顯伺服器。功能很簡單:伺服器端讀取用戶端的輸入,然後回顯到用戶端。
- 單線程Reactor反應器模式兩個重要角色——Reactor反應器、Handler處理器的編寫。
- SelectionKey選擇鍵兩個重要的方法——attach和attachment方法的使用。
實踐二:多線程Reactor反應器模式
使用多線程Reactor反應器模式,設計一個EchoServer回顯伺服器,主要的更新方式為:
- 引入ThreadPool線程池,将負責IOHandler輸入輸出處理器的執行,放入獨立的線程池中,與負責服務監聽和IO事件查詢的反應器線程相隔離。
- 将反應器線程拆分為多個SubReactor子反應器線程,同時,引入多個Selector選擇器,每一個子反應器線程負責一個選擇器讀取用戶端的輸入,回顯到用戶端。
- 線程池的使用。
- 多線程反應器模式的實作。
1.5.3 第3天:異步回調模式實踐
實踐一:使用線程join方式,通過阻塞式異步調用的方式,實作泡茶喝的執行個體
為了在計算機中,實作華羅庚的課文《統籌方法》泡茶喝的流程,可以設計三條線程:主線程、清洗線程、燒水線程,主要介紹如下:
- 主線程(MainThread)的工作是:啟動清洗線程、啟動燒水線程,等清洗、燒水的工作完成後,泡茶喝。
- 清洗線程(WashThread)的工作是:洗茶壺、洗茶杯。
- 燒水線程(HotWarterThread)的工作是:洗好水壺,灌上涼水,放在火上,一直等水燒開。
不同的版本的join方法的使用。
實踐二:使用FutureTask類和Callable接口,啟動阻塞式的異步調用,并且擷取異步線程的結果。
還是實作華羅庚的課文《統籌方法》泡茶喝的流程,可以設計三條線程:主線程、清洗線程、燒水線程,主要改進如下:
- 主線程(MainThread)的工作是:啟動清洗線程、啟動燒水線程,然後阻塞,等待異步線程的傳回值,根據異步線程的傳回值,決定後續的動作。
- 清洗線程(WashThread)在異步執行完成之後,有傳回值。
- 燒水線程(HotWarterThread)在異步執行完成之後,有傳回值。
- Callable(可調用)接口的使用;Callable接口和Runnable(可執行)接口的不同。
- FutureTask異步任務類的使用。
實踐三:使用ListenableFuture類和FutureCallback接口,啟動非阻塞異步調用,并且完成異步回調。
- 主線程(MainThread)的工作是:啟動清洗線程、啟動燒水線程,并且設定異步完成後的回調方法,這裡主線程不阻塞等待,而是去幹其他事情,例如讀報紙。
- 清洗線程(WashThread)在異步執行完成之後,執行回調方法。
- 燒水線程(HotWarterThread)在異步執行完成之後,執行回調方法。
- FutureCallback接口的使用;FutureCallback接口和Callable接口的差別和聯系。
- ListenableFuture異步任務類的使用,以及為異步任務設定回調方法。
1.5.4 第4天:Netty基礎實踐
**實踐一:Netty中Handler處理器的生命周期
操作步驟如下:**
定義一個非常簡單的入站處理器—InHandlerDemo。這個類繼承于ChannelInboundHandlerAdapter擴充卡,它實作了基類的所有的入站處理方法,并在每一個方法的實作中,都加上了必要的輸出資訊。
編寫一個單元測試代碼:将這個處理器加入到一個EmbeddedChannel嵌入式通道的流水線中。
通過writeInbound方法,向EmbeddedChannel寫一個模拟的入站ByteBuf資料包。InHandlerDemo作為一個入站處理器,就會處理到該ByteBuf資料包。
通過輸出,可以觀測到處理器的生命周期。
- Netty中Handler處理器的生命周期。
- EmbeddedChannel嵌入式通道的使用。
實踐二:ByteBuf的基本使用
操作步驟如下:
使用Netty的預設配置設定器,配置設定了一個初始容量為9個位元組,最大上限為100個位元組的ByteBuf緩沖區。
向ByteBuf寫資料,觀測ByteBuf的屬性變化。
從ByteBuf讀資料,觀測ByteBuf的屬性變化。
- ByteBuf三個重要屬性:readerIndex(讀指針)、writerIndex(寫指針)、maxCapacity(最大容量)。
- ByteBuf讀寫過程中,以上三個重要屬性的變化規律。
**實踐三:使用Netty,實作EchoServer回顯伺服器
前面實作過Java NIO版本的EchoServer回顯伺服器,學習了Netty後,設計和實作一個Netty版本的EchoServer回顯伺服器。功能很簡單:伺服器端讀取用戶端的輸入,然後将資料包直接回顯到用戶端。**
- 伺服器端ServerBootstrap的裝配和使用。
- 伺服器端NettyEchoServerHandler入站處理器的channelRead入站處理方法的編寫。
- 伺服器端實作Netty的ByteBuf緩沖區的讀取、回顯。
- 用戶端Bootstrap的裝配和使用。
- 用戶端NettyEchoClientHandler入站處理器中接受回顯的資料,并且釋放記憶體。
- 用戶端實作多種方式釋放ByteBuf,包括:自動釋放、手動釋放。
1.5.5 第5天:解碼器(Decoder)與編碼器(Encoder)實踐
實踐一:整數解碼實踐
具體步驟如下:
定義一個非常簡單的整數解碼器—Byte2IntegerDecoder。這個類繼承于ByteToMessageDecoder位元組碼解碼抽象類,并實作基類的decode抽象方法,将ByteBuf緩沖區中的資料,解碼成以一個一個的Integer對象。
定義一個非常簡單的整數處理器—IntegerProcessHandler。讀取上一站的入站資料,把它轉換成整數,并且顯示在Console控制台。
編寫一個整數解碼實戰的測試用例。在測試用例中,建立了一個EmbeddedChannel嵌入式的通道執行個體,将兩個自己的入站處理器Byte2IntegerDecoder、IntegerProcessHandler加入到通道的流水線上。通過writeInbound方法,向EmbeddedChannel寫入一個模拟的入站ByteBuf資料包。
通過輸出,可以觀察整數解碼器的解碼結果。
- 如何基于Netty的ByteToMessageDecoder位元組碼解碼抽象類,實作自己的ByteBuf二進制位元組到POJO對象的解碼。
- 使用ByteToMessageDecoder,如何管理ByteBuf的應用計數。
實踐二:整數相加的解碼器實踐
繼承ReplayingDecoder基礎解碼器,編寫一個整數相加的解碼器:一次解碼兩個整數,并把這兩個資料相加之和,作為解碼的結果。
使用前面定義的整數處理器——IntegerProcessHandler。讀取上一站的入站資料,把它轉換成整數,并且顯示在Console控制台。
使用前面定義的測試類,測試整數相加的解碼器,并且檢視結果是否正确。
- 如何基于ReplayingDecoder解碼器抽象類,實作自己的ByteBuf二進制位元組到POJO對象的解碼。
- ReplayingDecoder的成員屬性——state階段屬性的使用。
- ReplayingDecoder的重要方法——checkpoint(IntegerAddDecoder.Status)方法的使用。
實踐三:基于Head-Content協定的字元串分包解碼器
繼承ReplayingDecoder基礎解碼器,編寫一個字元串分包解碼器StringReplayDecoder。
在StringReplayDecoder的decode方法中,分兩步:第1步,解碼出字元串的長度;第2步,按照第一個階段的字元串長度,解碼出字元串的内容。
編寫一個簡單的業務處理器StringProcessHandler。其功能是:讀取上一站的入站資料,把它轉換成字元串,并且顯示在Console控制台。
建立了一個EmbeddedChannel嵌入式的通道執行個體,将兩個自己的入站處理器StringReplayDecoder、StringProcessHandler加入到通道的流水線上。為了測試入站處理器,使用writeInbound方法,向嵌入式通道EmbeddedChannel寫入了100個ByteBuf入站緩沖;每一個ByteBuf緩沖,僅僅包含一個字元串。EmbeddedChannel通道接收到入站資料後,pipeline流水線上的兩個入站處理器,就能不斷地處理這些入站資料:将接收到的二進制位元組,解碼成一個一個的字元串,然後逐個地顯示在Console控制台上。
- 如何基于ReplayingDecoder解碼器抽象類,實作自己的ByteBuf二進制位元組到字元串的解碼。
- 鞏固ReplayingDecoder的成員屬性——state階段屬性的使用。
- 鞏固ReplayingDecoder的重要方法——checkpoint(IntegerAddDecoder.Status)方法的使用。
實踐四:多字段Head-Content協定資料包解析實踐
使用LengthFieldBasedFrameDecoder解碼器,解碼複雜的Head-Content協定。例如協定中包含版本号、魔數等多個其他的資料字段。
使用前面所編寫那一個簡單的業務處理器StringProcessHandler。其功能是:讀取上一站的入站資料,把它轉換成字元串,并且顯示在Console控制台上。
建立一個EmbeddedChannel嵌入式的通道執行個體,将第一步和第二步的兩個入站處理器LengthFieldBasedFrameDecoder、StringProcessHandler加入到通道的流水線上。為了測試入站處理器,使用writeInbound方法,向嵌入式通道EmbeddedChannel寫入100個ByteBuf入站緩沖;每一個ByteBuf緩沖,僅僅包含一個字元串。EmbeddedChannel通道接收到入站資料後,pipeline流水線上的兩個入站處理器,就能不斷地處理到這些入站資料:将接到的二進制位元組,解碼成一個一個的字元串,然後逐個地顯示在控制台上。
- LengthFieldBasedFrameDecoder解碼器的使用。
- LengthFieldBasedFrameDecoder解碼器的長度的矯正公式,計算公式為:内容字段的偏移-長度字段的偏移-長度字段的長度。
1.5.6 第6天:JSON和ProtoBuf序列化實踐
實踐一:JSON通信實踐
用戶端将POJO轉成JSON字元串,編碼後發送到伺服器端。伺服器端接收用戶端的資料包,并解碼成JSON,轉成POJO。
用戶端的編碼過程:
先通過谷歌的Gson架構,将POJO序列化成JSON字元串;然後使用Netty内置的StringEncoder編碼器,将JSON字元串編碼成二進制位元組數組;最後,使用LengthFieldPrepender編碼器(Netty内置),将二進制位元組數組編碼成Head-Content格式的二進制資料包。
伺服器端的解碼過程:
先使用LengthFieldBasedFrameDecoder(Netty内置的自定義長度資料包解碼器),解碼Head-Content二進制資料包,解碼出Content字段的二進制内容。
然後,使用StringDecoder字元串解碼器(Netty内置的解碼器),将二進制内容解碼成JSON字元串。
最後,使用自定義的JsonMsgDecoder解碼器,将JSON字元串解碼成POJO對象。
編寫一個JsonMsgDecoder自定義的JSON解碼器。将JSON字元串,解碼成特定的POJO對象。
分别組裝好伺服器端、用戶端的流水線,運作程式,檢視兩端的通信結果。
- LengthFieldPrepender編碼器的使用:在發送端使用它加上Head-Content的頭部長度。
- JsonMsgDecoder的編寫。
- JSON傳輸時,用戶端流水線編碼器的組裝,伺服器端流水線解碼器的組裝。
實踐二:ProtoBuf通信實踐
設計一個簡單的用戶端/伺服器端傳輸程式:用戶端将ProtoBuf的POJO編碼成二進制資料包,發送到伺服器端;伺服器端接收用戶端的資料包,并解碼成ProtoBuf的POJO。
設計好需要傳輸的ProtoBuf的“.proto”協定檔案,并且生成ProtoBuf的POJO和Builder:
在“.proto”協定檔案中,僅僅定義了一個消息結構體,并且該消息結構體也非常簡單,隻包含兩個字段:消息ID、消息内容。
使用protobuf-maven-plugin插件,生成message的POJO類和Builder(構造者)類的Java代碼。
先使用Netty内置的ProtobufEncoder,将ProtobufPOJO對象編碼成二進制的位元組數組。
然後,使用Netty内置的ProtobufVarint32LengthFieldPrepender編碼器,加上varint32格式的可變長度。
Netty會将完成了編碼後的Length+Content格式的二進制位元組碼,發送到伺服器端。
先使用Netty内置的ProtobufVarint32FrameDecoder,根據varint32格式的可變長度值,從入站資料包中,解碼出二進制Protobuf位元組碼。
然後,可以使用Netty内置的ProtobufDecoder解碼器,将Protobuf位元組碼解碼成Protobuf POJO對象。
最後,自定義一個ProtobufBussinessDecoder解碼器,處理ProtobufPOJO對象。
- “.proto”基礎協定。
- Netty内置的ProtobufEncoder、ProtobufDecoder兩個專用的傳輸Protobuf序列化資料的編碼器/解碼器的使用。
-
Netty内置的兩個ProtoBuf專用的可變長度
Head-Content協定的半包編碼、解碼處理器:ProtobufVarint32LengthFieldPrepender編碼器、ProtobufVarint32FrameDecoderProtobufEncoder解碼器的使用。
1.5.7 第7~10天:基于Netty的單聊實戰
實踐一:自定義ProtoBuf編碼器/解碼器
為單聊系統,設計一套自定義的“.proto”協定檔案;然後通過maven插件生成Protobuf Builder和POJO。
繼承Netty提供的MessageToByteEncoder編碼器,編寫一個自定義的Protobuf編碼器,完成Head-Content協定的複雜資料包的編碼。
通過自定義編碼器,最終将ProtobufPOJO編碼成Head-Content協定的二進制ByteBuf幀。
繼承Netty提供的ByteToMessageDecoder解碼器,編寫一個自定義的Protobuf解碼器,完成Head-Content協定的複雜資料包的解碼。
通過自定義的解碼器,最終将Head-Content協定的複雜資料包,解碼出Protobuf POJO。
- 設計複雜的“.proto”協定檔案。
- 自定義的Protobuf編碼器的編寫。
實踐二:登入實踐
業務邏輯:
- 用戶端發送登入資料包。
- 伺服器端進行使用者資訊驗證。
- 伺服器端建立Session會話。
- 伺服器端将登入結果資訊傳回給用戶端,包括成功标志、Session ID等。
從用戶端到伺服器端再到用戶端,大緻有以下幾個步驟。
編寫LoginConsoleCommand控制台指令類,從用戶端收集使用者ID和密碼。
編寫用戶端LoginSender發送器類,組裝Protobuf資料包,通過用戶端通道發送到伺服器端。
編寫伺服器端UserLoginHandler入站處理器,處理收到的登入消息,完成資料的校驗後,将資料包交給業務處理器LoginMsgProcesser,進行異步的處理。
編寫伺服器端LoginMsgProcesser(業務處理器),将處理結果,寫入使用者綁定的子通道。
編寫用戶端業務處理器LoginResponceHandler,處理登入響應,例如設定登入的狀态,儲存會話的SessionID等。
- Netty知識的綜合運用。
- Channel通道容器功能的使用。
實踐三:單聊實踐
單聊的業務邏輯:
- 當使用者A登入成功之後,按照單聊的消息格式,發送所要的消息。
- 這裡的消息格式設定為——userId:content。其中的userId,就是消息接收方目标使用者B的userId;其中的content,表示聊天的内容。
- 伺服器端收到消息後,根據目标userId,進行消息幀的轉發,發送到使用者B所在的用戶端。
- 用戶端使用者B收到使用者A發來的消息,顯示在自己的控制台上。
從用戶端到伺服器端再到用戶端,大緻有5個步驟。
當使用者A登入成功之後,按照單聊的消息格式,發送所要的消息。
這裡的消息格式設定為——userId:content,其中的userId,就是消息接收方目标使用者B的userId,其中的content,表示聊天的内容。
CommandController在完成聊天内容和目标使用者的收集後,調用chatSender發送器執行個體,将聊天消息組裝成Protobuf消息幀,通過用戶端channel通道發往伺服器端。
編寫伺服器端的消息轉發處理器ChatRedirectHandler類,其功能是,對使用者登入進行判斷:如果沒有登入,則不能發送消息;開啟異步的消息轉發,由負責轉發的chatRedirectProcesser執行個體,完成消息轉發。
編寫負責異步消息轉發的ChatRedirectProcesser類,功能如下:根據目标使用者ID,找出所有的伺服器端的會話清單(Session List),然後對應每一個會話,發送一份消息。
編寫用戶端ChatMsgHandler處理器,主要的工作如下:對消息類型進行判斷,判斷是否為聊天請求Protobuf資料包。如果不是,直接将消息交給流水線的下一站;如果是聊天消息,則将聊天消息顯示在控制台上。
本環節目标是掌握以下知識:
- 伺服器端會話(Session)的管理。
1.5.8 第11天:ZooKeeper實踐計劃
實踐一:分布式ID生成器
自定義一個分布式ID生成器——IDMaker類,通過建立ZK的臨時順序節點的方法,生成全局唯一的ID。
基于自定義的IDMaker,編寫單元測試的用例,生成ID。
- 分布式ID生成器原理。
- ZooKeeper用戶端的使用。
實踐二:使用ZK實作SnowFlake ID算法
編寫一個實作SnowFlake ID算法的ID生成器——SnowflakeIdGenerator類,生成全局唯一的ID。
基于自定義的SnowflakeIdGenerator,編寫單元測試的用例,生成ID。
- SnowFlake ID算法原理。
1.5.9 第12天:Redis實踐計劃
實踐一:使用RedisTemplate模闆類完成Redis的緩存CRUD操作
将RedisTemplate模闆類的大部分緩存操作,封裝成一個自己的緩存操作Service服務,稱之為CacheOperationService類。
編寫業務類UserServiceImplWithTemplate類,使用CacheOperationService類,完成User對象的緩存CRUD。
編寫測試用例,通路UserServiceImplWithTemplate類。觀察在進行User對象的查詢時,能優先使用緩存資料,是否省去資料庫通路的時間。
- RedisTemplate模闆類的使用。
- Jedis的用戶端API。
- RedisTemplate模闆API。
實踐二:使用RedisTemplate模闆類完成Redis的緩存CRUD操作
使用RedisCallback的doInRedis回調方法,在doInRedis回調方法中,直接使用實參RedisConnection連接配接類執行個體,完成Redis的操作。
編寫業務類UserServiceImplInTemplate類,使用RedisTemplate模闆執行個體去執行RedisCallback回調執行個體,完成User對象的緩存CRUD。
編寫測試用例,通路UserServiceImplInTemplate類。觀察在進行User對象的查詢時,優先使用緩存資料,看看是否省去了資料庫通路的時間。
- RedisCallback回調接口的使用。
實踐三:使用Spring緩存注解,完成Redis的緩存CRUD操作
編寫業務類UserServiceImplWithAnn類,使用緩存注解,完成User對象的緩存CRUD。
編寫測試用例,通路UserServiceImplWithAnn類。觀察在進行User對象的查詢時,優先使用緩存資料,看看是否省去了資料庫通路的時間。
- Spring緩存注解的使用。
- Spring緩存注解的配置。
1.6 本章小結
本章簡單地給大家介紹了高并發時代,以及從業人員必須掌握的Netty、Redis、ZooKeeper等分布式高性能工具。同時,列出了一個大緻12天的實踐計劃。
12天的實踐計劃不是綜合性的大型實踐,而是一些掌握單點知識和技能的小型實踐案例。隻有清楚地掌握了Netty、Redis、ZooKeeper這些工具,才能具備大型開發實踐的能力。
在完成了單個功能點的實踐案例之後,建議大家加入高性能社群“瘋狂創客圈”,進一步參與高并發聊天項目“CrazyIM”的持續疊代,積累真正的實踐經驗。