概述
Ice是一個開源的綜合性RPC框架,以高性能和原生支持微服務的架構而著稱。提供了很多可以直接使用的組件,如注冊中心IceGrid,部署工具IcePatch2,防火牆穿透Glacier2,發布訂閱服務IceStorm等。這樣的好處就是降低了學習成本和基於中間件的二次開發工作量。同時弊端也很明顯就是生態比較封閉,缺乏像Spring Cloud那樣全面的技術棧,如服務網關,鏈路追蹤等,因此替換引入組件成本會比較高。
在國內開源社區和技術論壇的討論很少見,資料也比較少,基本參考就是官方英文技術文檔和源碼。有人可能頭已經大了,話說看官方技術文檔和閱讀源碼不也是程序猿應該修煉的基本功嘛,英文不行只能邊看邊學了。
以下是Ice框架的特點:
- 可壓縮,高效的二進制協議
- 支持同步,異步調用和多種通信協議,如TCP,UDP,WebSockets,Buletooth;
- 支持多語言實現,如C++,C#,Java,Python等,和跨操作系統,跨平台部署;
- 支持基於原生的SSL/TLS的安全加密通信;
- 支持自動的服務發現;
- 支持面向對象編程模型;
架構
重要抽象概念
Ice有很多抽象概念和術語,基本上這些概念都是繼承現有的概念,新增加的抽象很少。所以學習其他RPC架構的同學很容易理解這些概念。
客戶端服務端(Client and Server)
客戶端和服務端都是相對,可以轉換的;
在一次請求交互中,發請求一方為客戶端,處理請求的一方為服務端,同時服務端也能是請求的發起方,這時它充當的角色就是客戶端。
Ice對象(Ice Object)
這里的Ice對象是特指提供接口服務的對象,而不是我們面向對象編程時的泛指對象。
它有幾個特征:
- 一個Ice對象可能有多個接口類(interface),一個接口類可能包含一系列操作(operation)即函數。
- 一個Ice對象應該有全局唯一的標識。
對象適配器(Object Adaptor)
可以看做一個Ice對象集合,維護對象ID與對象的映射關系。
代理(Proxies)
是Ice對象在客戶端本地的代理,包含向下調用接口和服務地址信息,客戶端通過調用接口將請求發送到指定服務端的Ice對象。
一般代理信息以“對象ID:服務地址”的形式表示,其中服務地址是以協議+地址+端口表示。如
SimplePrinter:default -p 10000
上面這種固定ip端口的形式,又稱為直接代理(Direct Proxies)。
還有一種間接代理(Indirect Proxies)是通過定位服務來獲取服務的路由信息。定位服務類似於DNS的域名服務,間接代理的代理信息就相當於URL。
間接代理有兩種形式,對象ID或者對象適配器ID;這種通過對象ID就可以訪問的對象又稱為眾所周知的對象(well-known object)。
SimplePrinter
SimplePrinter@PrinterAdapter
服務者(Servants)
服務端的人工實現的實際服務對象
復制(Replication)
Ice的復制就是將對象適配器(Object Adaptor)與多個地址關聯。就是同個服務在多個機器上運行着實例,以避免單機故障。
客戶端的代理支持復制的格式,一個對象ID對應多個連接端點。如下
SimplePrinter:tcp -h server1 -p 10001:tcp -h server2 -p 10002
復制組(Replica Groups)
上面基於直接代理的復制,缺點很明顯,對象到服務端點的映射關系是固定。Ice支持一種更有用的復制形式--復制組。
復制組可以看做是一個有動態復制功能的適配器集合。動態復制的能力來自於定位服務,因為定位服務提供了復制組與路由的動態映射關系查詢。
復制組有唯一的標識符,由一系列對象適配器組成,一個對象適配器最多屬於一個復制組。當一個復制組創建之后,它的標識符可以用來替換間接代理中的適配器標識符。
例如一個標識符為PrinterAdapters的復制組用在代理中:
SimplePrinter@PrinterAdapters
至多一次語義(At-Most-Once Semantics)
就是說一個請求操作至多被執行一次,如果請求傳遞失敗,將會拋一個異常,而不會去重試。只有確認失敗了,才會重試。
這個語義對於不是冪等性的操作是安全的,比如++i,操作一次和操作兩次得到結果是不一樣的。
同步方法調用
調用RPC時,線程被掛起,直到結果返回。
異步方法調用(AMI)
調用RPC時,立即返回,線程可以處理其他工作,等結果返回時會通知該線程
異步方法處理
相當於服務端的AMI,這個請求可能不會被立即處理。適用於需要等待數據,耗時較長的調用過程。
單向調用
就是沒有出參的RPC,調用方不需要知道執行的結果。
描述性語言Slice
這是專門為ICE提供的描述性語言,主要是為了定義接口,數據類型。使客戶端服務端的開發獨立於語言,
可以通過slice工具生成你想要使用的語言,如C++,C#,Java等,生成的這些代碼使你只需要關注接口的實現,而不需要關注底層邏輯。
客戶端和服務端的結構
Ice客戶端和服務端的內部邏輯結構:
客戶端和服務端程序是由應用代碼、Ice庫代碼和由slice生成的代碼構成的。
- Ice Core包含支撐通訊的運行時代碼,對應用開發屏蔽了底層的網絡通信,線程,數據編解碼,以及很多網絡相關的問題。Ice Core是以一系列庫的形式提供給客戶端,服務端使用。Ice Core的配置管理是通過Ice API來操作的。
- Proxy Code代理代碼由slice定義產生,是服務端Ice對象的代理。它主要有兩個功能,一是為客戶端應用提供向下調用接口,以請求消息的形式發給服務端,然后服務端調用相關聯的接口;二是數據結構的編解碼,以便於網絡傳輸。
- Skeleton Code 骨架代碼由slice定義產生,提供向上調用接口,允許Ice運行時將客戶端請求傳遞到你實現的服務端代碼。同時也包含了對參數的解碼,返回結果合異常的編碼。
環境部署
參考官方文檔:https://doc.zeroc.com/ice/3.7/release-notes/using-the-linux-binary-distributions
HelloWord
以下簡單demo是以c++11實現的。完成源碼見:https://github.com/GodMonking/ice-demo/tree/main/simple
定義slice文件
slice文件命名必須以.ice結尾。如Printer.ice
module Demo { interface Printer { void printString(string s); } }
編譯slice文件生成c++代碼:
slice2cpp Printer.ice
服務端代碼
#include <Ice/Ice.h> #include <Printer.h> using namespace std; using namespace Demo; class PrinterI : public Printer { public: virtual void printString(string s, const Ice::Current&) override; }; void PrinterI::printString(string s, const Ice::Current&) { cout << s << endl; } int main(int argc, char* argv[]) { try { //初始化通信器 Ice::CommunicatorHolder ich(argc, argv); //根據名稱、連接端點創建一個對象適配器 auto adapter = ich->createObjectAdapterWithEndpoints("SimplePrinterAdapter", "default -p 10000"); //創建一個servant對象,將其添加到對象適配器 auto servant = make_shared<PrinterI>(); adapter->add(servant, Ice::stringToIdentity("SimplePrinter")); //激活對象適配器,監聽服務請求 adapter->activate(); ich->waitForShutdown(); } catch(const std::exception& e) { cerr << e.what() << endl; return 1; } return 0; }
由Printer.ice生成的Printer.h包含骨架代碼(Skeleton Code),一個接口類包含一個純虛接口如下,詳細代碼省略:
#include <Ice/Ice.h> #include <Printer.h> using namespace std; using namespace Demo; … namespace Demo { class Printer : public virtual Ice::Object { public: virtual void printString(std::string, const Ice::Current&) = 0; }; }
編譯服務端代碼:
c++ -I. -DICE_CPP11_MAPPING -c Printer.cpp Server.cpp c++ -o server Printer.o Server.o -lIce++11
客戶端代碼
#include <Ice/Ice.h> #include <Printer.h> #include <stdexcept> using namespace std; using namespace Demo; int main(int argc, char* argv[]) { try { //初始化通信器 Ice::CommunicatorHolder ich(argc, argv); //通過 “對象ID:對象適配器連接端點” 獲取代理對象 auto base = ich->stringToProxy("SimplePrinter:default -p 10000"); //轉類型校驗是否合法 auto printer = Ice::checkedCast<PrinterPrx>(base); if(!printer) { throw std::runtime_error("Invalid proxy"); } //通過代理對象調用接口 printer->printString("Hello World!"); } catch(const std::exception& e) { cerr << e.what() << endl; return 1; } return 0; }
編譯客戶端代碼:
c++ -I. -DICE_CPP11_MAPPING -c Printer.cpp Client.cpp c++ -o client Printer.o Client.o -lIce++11
運行程序
運行服務端程序:
./server
執行客戶端程序:
./client
服務顯示結果:
結尾
這篇文章主要是介紹了Ice框架的優缺點,架構組成以及一個簡單Demo。后續文章會介紹幾種服務部署方案。