標籤:調用 dde 中心 XML 遠程調用 dha 序列化 bootstra 服務端
簡單的通訊過程https://www.cnblogs.com/SteveLee/p/9860507.html
上一篇我們已經全面的介紹過《基於gRPC服務發現與服務治理的方案》,我們先複習一下RPC的調用過程(筆者會在這一節的幾篇文章中反覆的強調這個程序呼叫方案),看
根據上面圖,服務化原理可以分為3步:
服務端啟動並且向註冊中心發送服務資訊,註冊中心收到後會定時監控服務狀態(常見心跳檢測);
用戶端需要開始調用服務的時候,首先去註冊中心擷取服務資訊;
用戶端建立遠程調用串連,串連後服務端返回處理資訊;
第3步又可以細分,下面說說遠端程序呼叫的原理:
目標:用戶端怎麼調用遠程機器上的公開方法
服務發現,向註冊中心擷取服務(這裡需要做的有很多:拿到多個服務時需要做負載平衡,同機房過濾、版本過濾、服務路由過濾、統一網關等);
用戶端發起調用,將需要調用的服務、方法、參數進行組裝;
序列化編碼組裝的訊息,這裡可以使用json,也可以使用xml,也可以使用protobuf,也可以使用hessian,幾種方案的序列化速度還有序列化後佔用位元組大小都是選擇的重要指標,對內筆者建議使用高效的protobuf,它基於TCP/IP二進位進行序列化,體積小,速度快。
傳輸協議,可以使用傳統的io阻塞傳輸,也可以使用高效的nio傳輸(Netty);
服務端收到後進行還原序列化,然後進行相應的處理;
服務端序列化response資訊並且返回;
用戶端收到response資訊並且還原序列化;
正如上面第三步的第4條所提到,C類向S類調用時,可以選擇RPC或者RESTful,而作為內部通訊,筆者強烈建議使用RPC的方式去調用S類上的所有服務,RPC對比RESTful如下:
優點:
序列化採用二進位訊息,效能好/效率高(空間和時間效率都很不錯);
序列化還原序列化直接對應程式中的資料類,不需要解析後在進行映射(XML,JSON都是這種方式);
相比http協議,沒有無用的header,簡化傳輸資料的大小,且基於TCP層傳輸,速度更快,容量更小;
Netty等一些框架組成(重點,也是本篇介紹的主要架構);
缺點:
使用複雜,維護成本和學習成本較高,調試困難;
因為基於HTTP2,絕大部多數HTTP Server、Nginx都尚不支援,即Nginx不能將GRPC請求作為HTTP請求來負載平衡,而是作為普通的TCP請求。(nginx1.9版本已支援);
二進位可讀性差,或者幾乎沒有任何直接可讀性,需要專門的工具進行還原序列化;
預設不具備動態特性(可以通過動態定義產生訊息類型或者動態編譯支援,後續會介紹利用Rosyln進行動態編譯的特性);
通訊傳輸利器Netty(Net is DotNetty)介紹
(先埋怨一下微軟大大)我們做NET開發,十分羨慕JAVA上能有NETTY, SPRING, STRUTS, DUBBO等等優秀架構,而我們NET就只有乾瞪眼,哎,無賴之前生態圈沒做好,恨鐵不成鋼啊。不過由於近來Net Core的發布,慢慢也拉回了一小部分屬於微軟的天下,打住,閑話扯到這兒。
DotNetty是Azure團隊仿照(幾乎可以這麼說)JAVA的Netty而出來的(目前已實現Netty的一部分),目前在Github上的Star有1.8K+,地址:https://github.com/Azure/DotNetty,沒有任何文檔,和代碼中少量的注釋。雖然比Netty出來晚了很多年,不過我們NET程式員們也該慶幸了,在自己的平台上終於能用上類似Netty這樣強大的通訊架構了。
傳統通訊的問題:
我們使用通用的應用程式或者類庫來實現互相通訊,比如,我們經常使用一個 HTTP 用戶端庫來從 網頁伺服器上擷取資訊,或者通過 web 服務來執行一個遠端調用。
然而,有時候一個通用的協議或他的實現並沒有很好的滿足需求。比如我們無法使用一個通用的 HTTP 伺服器來處理大檔案、電子郵件以及近即時訊息,比如金融資訊和多人遊戲資料。我們需要一個高度最佳化的協議來處理一些特殊的情境。例如你可能想實現一個最佳化了的 Ajax 的聊天應用、媒體流傳輸或者是大檔案傳輸器,你甚至可以自己設計和實現一個全新的協議來準確地實現你的需求。
另一個不可避免的情況是當你不得不處理遺留的專有協議來確保與舊系統的互通性。在這種情況下,重要的是我們如何才能快速實現協議而不犧牲應用的穩定性和效能。
解決:
Netty 是一個提供 asynchronous event-driven (非同步事件驅動)的網路應用程式框架,是一個用以快速開發高效能、可擴充協議的伺服器和用戶端。
換句話說,Netty 是一個 NIO 用戶端伺服器架構,使用它可以快速簡單地開發網路應用程式,比如伺服器和用戶端的協議。Netty 大大簡化了網路程式的開發過程比如 TCP 和 UDP 的 socket 服務的開發。
“快速和簡單”並不意味著應用程式會有難維護和效能低的問題,Netty 是一個精心設計的架構,它從許多協議的實現中吸收了很多的經驗比如 FTP、SMTP、HTTP、許多二進位和基於文本的傳統協議.因此,Netty 已經成功地找到一個方式,在不失靈活性的前提下來實現開發的簡易性,高效能,穩定性。
有一些使用者可能已經發現其他的一些網路架構也聲稱自己有同樣的優勢,所以你可能會問是 Netty 和它們的不同之處。答案就是 Netty 的哲學設計理念。Netty 從開始就為使用者提供了使用者體驗最好的 API 以及實現設計。正是因為 Netty 的哲學設計理念,才讓您得以輕鬆地閱讀本指南並使用 Netty。
(DotNetty的架構和實現是怎麼回事,筆者不太清楚,但完全可參考Netty官方的文檔來學習和使用DotNetty相關的API介面)
DotNetty中幾個重要的庫(程式集):
DotNetty.Buffers: 對記憶體緩衝區管理的封裝。
DotNetty.Codecs: 對編解碼是封裝,包括一些基礎基類的實現,我們在項目中自訂的協議,都要繼承該項目的特定基類和實現。
DotNetty.Codecs.Mqtt: MQTT(訊息佇列遙測傳輸)編解碼是封裝,包括一些基礎基類的實現。
DotNetty.Codecs.Protobuf: Protobuf 編解碼是封裝,包括一些基礎基類的實現。
DotNetty.Codecs.ProtocolBuffers: ProtocolBuffers編解碼是封裝,包括一些基礎基類的實現。
DotNetty.Codecs.Redis: Redis 協議編解碼是封裝,包括一些基礎基類的實現。
DotNetty.Common: 公用的類庫項目,封裝線程池,並行任務和常用協助類的封裝。
DotNetty.Handlers: 封裝了常用的管道處理器,比如Tls編解碼,逾時機制,心跳檢查,日誌等。
DotNetty.Transport: DotNetty核心的實現,Socket基礎架構,通訊模式:非同步非阻塞。
DotNetty.Transport.Libuv: DotNetty自己實現基於Libuv (高效能的,事件驅動的I/O庫) 核心的實現。
常用的庫有Codecs, Common, Handlers, Buffers, Transport,目前Azure團隊正在實現其他Netty中的API(包括非公用Netty的API),讓我們拭目以待吧。
直接上點對點之間通訊的栗子
DotNetty的Example檔案夾下有許多官方提供的執行個體,有拋棄服務執行個體(Discard),有應答服務執行個體(echo),有Telnet服務執行個體等等,為了實現直接點對點通訊,筆者採用了Echo的demo,此後的RPC調用也會基於Echo而實現,注釋詳細,直接上接收端(Server)的代碼:
View Code
IEventLoopGroup 是用來處理I/O操作的多線程事件迴圈器,DotNetty 提供了許多不同的 EventLoopGroup 的實現用來處理不同的傳輸。在這個例子中我們實現了一個服務端的應用,因此會有2個 IEventLoopGroup 會被使用。第一個經常被叫做‘boss’,用來接收進來的串連。第二個經常被叫做‘worker’,用來處理已經被接收的串連,一旦‘boss’接收到串連,就會把串連資訊註冊到‘worker’上。
ServerBootstrap 是一個啟動 Transport 服務的輔助啟動類。你可以在這個服務中直接使用 Channel,但是這會是一個複雜的處理過程,在很多情況下你並不需要這樣做。
這裡我們指定使用 TcpServerChannel類來舉例說明一個新的 Channel 如何接收進來的串連。
ChannelInitializer 是一個特殊的處理類,他的目的是協助使用者配置一個新的 Channel,當你的程式變的複雜時,可能你會增加更多的處理類到 pipline 上,然後提取這些匿名類到最頂層的類上。
你可以設定這裡指定的 Channel 實現的配置參數。我們正在寫一個TCP/IP 的服務端,因此我們被允許設定 socket 的參數選項比如tcpNoDelay 和 keepAlive。
綁定連接埠然後啟動服務,這裡我們在機器上綁定了機器網卡上的設定連接埠,當然現在你可以多次調用 bind() 方法(基於不同綁定地址)。
使用完成後,優雅的釋放掉指定的工作群組線程,當然,你可以選擇關閉程式,但這並不推薦。
Server端的事件處理代碼:
上一部分代碼中加粗地方的實現
View Code
DiscardServerHandler 繼承自 ChannelInboundHandlerAdapter,這個類實現了IChannelHandler介面,IChannelHandler提供了許多事件處理的介面方法,然後你可以覆蓋這些方法。現在僅僅只需要繼承 ChannelInboundHandlerAdapter 類而不是你自己去實現介面方法。
這裡我們覆蓋了 chanelRead() 事件處理方法。每當從用戶端收到新的資料時,這個方法會在收到訊息時被調用,這個例子中,收到的訊息的類型是 ByteBuf。
為了響應或顯示用戶端發來的資訊,為此,我們將在控制台中列印出用戶端傳來的資料。
然後,我們將用戶端傳來的訊息通過context.WriteAsync寫回到用戶端。
當然,步驟4隻是將流緩衝到上下文中,並沒執行真正的寫入操作,通過執行Flush將流資料寫入管道,並通過context傳回給傳來的用戶端。
Client端代碼:
重點看注釋的地方,其他地方跟Server端沒有任何區別
View Code
初始化一個緩衝區的大小。
預設緩衝區接受的資料類型為bytes[],當然這樣也更加便於序列化成流。
將緩衝區的流直接資料寫入到Channel管道中。該管道一般為連結通訊的另一端(C端)。
建立死迴圈,這樣做的目的是為了測試每次都必須從用戶端輸入的資料,通過服務端迴路一次後,再進行下一次的輸入操作。
Client端的事件處理代碼:
View Code
非常簡單,將資料流顯示到控制台。
實現結果
至此,我們使用DotNetty架構搭建簡單的應答伺服器就這樣做好了,很簡單,實現效果如下:
C端主動向S端主動發送資料後,S端收到資料,在控制台列印出資料,並回傳給C端,當然,S端還可以做很多很多的事情。
DotNetty內部調試記錄分析
雖然DotNetty官方沒有提供任何技術文檔,但官方卻提供了詳細的調試記錄,很多時候,我們學習者其實也可以通過調試記錄來分析某一個功能的實現流程。我們可以通過將DotNetty的內部輸入輸出記錄列印到控制台上。
InternalLoggerFactory.DefaultFactory.AddProvider(new ConsoleLoggerProvider((s, level) => true, false));
可以看到服務端的列印記錄一下多出來了許多許多,有大部分是屬於DotNetty內部調試時的列印記錄,我們只著重看如下的部分。
複製代碼
dbug: SRV-LSTN[0]
[id: 0x3e8afca1] HANDLER_ADDED
dbug: SRV-LSTN[0]
[id: 0x3e8afca1] REGISTERED (1)
dbug: SRV-LSTN[0]
[id: 0x3e8afca1] BIND: 0.0.0.0:8007 (2)
wait the client input
dbug: SRV-LSTN[0]
[id: 0x3e8afca1, 0.0.0.0:8007] ACTIVE (3)
dbug: SRV-LSTN[0]
[id: 0x3e8afca1, 0.0.0.0:8007] READ (4)
dbug: SRV-LSTN[0]
[id: 0x3e8afca1, 0.0.0.0:8007] RECEIVED: [id: 0x7bac2775, 127.0.0.1:64073 :> 127.0.0.1:8007] (5)
dbug: SRV-LSTN[0]
[id: 0x3e8afca1, 0.0.0.0:8007] RECEIVED_COMPLETE (6)
dbug: SRV-LSTN[0]
[id: 0x3e8afca1, 0.0.0.0:8007] READ (7)
dbug: SRV-CONN[0]
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] HANDLER_ADDED (8)
dbug: SRV-CONN[0]
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] REGISTERED (9)
dbug: SRV-CONN[0]
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] ACTIVE (10)
dbug: SRV-CONN[0]
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] READ (11)
dbug: DotNetty.Buffers.AbstractByteBuffer[0] (12)
-Dio.netty.buffer.bytebuf.checkAccessible: True
dbug: SRV-CONN[0]
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] RECEIVED: 14B (13)
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|100000000| 00 0C 68 65 6C 6C 6F 20 77 6F 72 6C 64 21 |..hello world! |
+--------+-------------------------------------------------+----------------+
Received from client: hello world!
dbug: SRV-CONN[0] (14)
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] WRITE: 2B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|100000000| 00 0C |.. |
+--------+-------------------------------------------------+----------------+
dbug: SRV-CONN[0] (15)
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] WRITE: 12B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|100000000| 68 65 6C 6C 6F 20 77 6F 72 6C 64 21 |hello world! |
+--------+-------------------------------------------------+----------------+
dbug: SRV-CONN[0] (16)
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] RECEIVED_COMPLETE
dbug: SRV-CONN[0] (17)
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] FLUSH
dbug: SRV-CONN[0] (18)
[id: 0x7bac2775, 127.0.0.1:64073 => 127.0.0.1:8007] READ
複製代碼
咋一看,有18個操作,好像有點太多了,其實不然,還有很多很多的內部調試細節並沒列印到控制台上。
通過手動建立的背景工作執行緒組,並將這組線程註冊到管道中,這個管道可以是基於SOCKER,可以基於IChannel(1);
綁定自定的IP地址和連接埠號碼到自訂管道上(2);
啟用自訂管道(3);
開始讀取(其實也是開始監聽)(4);
收到來自id為0x7bac2775的用戶端串連請求,建立串連,並繼續開始監聽(5)(6)(7);
從第8步開始,日誌已經變成id為0x7bac2775的記錄了,當然一樣包含註冊管道,啟用管道,開始監聽等等與S端一模一樣的操作(8)(9)(10)(11)
當筆者輸入一條"hello world!"資料後,DotNetty.Buffers.AbstractByteBuffer會進行資料類型檢查,以便確認能將資料放入到管道中。(12)
將資料發送到S端,資料大小為14B,hello world前有兩個點,代表這是資料頭,緊接著再發送兩個點,但沒有任何資料,代表資料已經結束。DotNetty將資料的十六進位儲存位用易懂的方式表現了出來,很人性化。(13)(14)
S端收到資料沒有任何加工和處理,馬上將資料回傳到C端。(15)(16)
最後,當這個過程完成後,需要將緩衝區的資料強制寫入到管道中,所以會執行一次Flush操作,整個傳輸完成。接下來,不管是C端還是S端,繼續將自己的狀態改成READ,用於監聽管道中的各種情況,比如串連狀態,資料轉送等等(17)。
總結
對於剛開始接觸Socket編程的朋友而言,這是個噩夢,因為Socket編程的複雜性不會比多線程容易,甚至會更複雜。協議,壓縮,傳輸,多線程,監聽,流量控制等等一系列問題擺在面前,因此而誕生了Netty這樣優秀的開源架構,但是Netty是個半成品,因為你需要基於他來實現自己想要的協議,傳輸等等自訂動作,而底層的內容,你完全不用關心。不像某些架構,比如Newtonsoft.Json這樣的功能性架構,不用配置,不用自訂,直接拿來用就可以了。
雖然DotNetty幫我們實現了底層大量的操作,但如果不熟悉或者一點也不懂網路通訊,同樣對上面的代碼是一頭霧水,為何?行情需要,我們程式員天天都在趕業務,哪有時間去瞭解和學習更多的細節...通過將調試記錄列印出來,並逐行挨個的對照代碼進行分析,就會慢慢開始理解最簡單的通訊流程了。
本篇只是實現了基於DotNetty最簡單的通訊過程,也只是將資料做了一下迴路,並沒做到任何與RPC有關的調用,下一篇我們開始講這個例子深入,介紹基於DotNetty的RPC調用。
簡單的通訊過程