0. 起因
最近看文檔,發(fā)現(xiàn)一些組件是通過FastRPC來進(jìn)行溝通的,并且偶爾看到某些場(chǎng)景下在FastRPC上的時(shí)間消耗好像也蠻可觀隆圆,恰好FastRPC是開源的,因此決定看看FastRPC具體的實(shí)現(xiàn)翔烁。
1. RPC簡(jiǎn)介
當(dāng)初在學(xué)Java的時(shí)候渺氧,初遇RMI(Remote Method Invocation),感覺這是一個(gè)非常神奇的東西蹬屹,竟然可以調(diào)用別的機(jī)器上的方法侣背!多么好奇這是怎么實(shí)現(xiàn)的。后來漸漸明白慨默,其實(shí)RMI就是個(gè)RPC的另一個(gè)名字贩耐。那么,RPC的原理是什么呢厦取?
RPC全稱Remote Procedure Call(遠(yuǎn)程過程調(diào)用)潮太,所謂遠(yuǎn)程,就是執(zhí)行的例程(routine)位于另一個(gè)地址空間(關(guān)于地址空間的介紹可以參閱之前的文章——冰山之下:使用new申請(qǐng)內(nèi)存的背后
簡(jiǎn)單說可以認(rèn)為地址空間就是進(jìn)程),通常情況下是另一臺(tái)機(jī)器铡买。如圖1所示更鲁,RPC框架為調(diào)用者提供一個(gè)代理,這個(gè)代理中的方法和遠(yuǎn)程例程一一對(duì)應(yīng)奇钞。當(dāng)調(diào)用者調(diào)用了代理的一個(gè)方法后澡为,代理通過一種客戶端——服務(wù)器的方式來,將函數(shù)簽名景埃、參數(shù)等封裝成一個(gè)網(wǎng)絡(luò)請(qǐng)求媒至,發(fā)送給服務(wù)器。服務(wù)器接收到請(qǐng)求后通過解析獲得函數(shù)簽名谷徙、參數(shù)等信息拒啰,通過查表獲取到目標(biāo)例程地址,調(diào)用該例程并將參數(shù)傳遞給它完慧。例程計(jì)算完成后谋旦,將結(jié)果通過網(wǎng)絡(luò)返回到代理,代理將服務(wù)器返回的結(jié)果稍加處理骗随,成為調(diào)用者可以理解的數(shù)據(jù)格式,最終返回到調(diào)用者赴叹『枞荆客戶端和服務(wù)器的溝通細(xì)節(jié)是通過RPC框架來實(shí)現(xiàn)的,數(shù)據(jù)的序列化和反序列化乞巧、網(wǎng)絡(luò)請(qǐng)求與答復(fù)等細(xì)節(jié)是隱藏的涨椒,對(duì)于調(diào)用者來說,它并不知道它調(diào)用的方法來對(duì)數(shù)據(jù)的處理到底是自己的地址空間進(jìn)行還是在別的地址空間進(jìn)行绽媒。
舉個(gè)栗子:你到一家餐廳吃飯蚕冬,店里有一個(gè)服務(wù)員為你服務(wù)。你點(diǎn)完菜之后是辕,服務(wù)員就走進(jìn)了后廚囤热。但是,服務(wù)員并不是直接將你點(diǎn)的菜告訴他們店里的廚師获三,相反的旁蔼,他打電話到別的店點(diǎn)了外賣,外賣到了之后他將飯菜重新裝盤疙教,然后給你端了上來棺聊。但是你是不知道這個(gè)細(xì)節(jié)的,在你看來贞谓,這次吃飯和以往的任何一次沒有區(qū)別限佩,服務(wù)員端上的飯菜在哪里做的你是不知道的,你和平常一樣點(diǎn)了菜然后菜就上了,你并不需要自己去關(guān)心怎么找到外賣電話祟同,怎么將飯菜裝盤作喘。你只管點(diǎn)菜和吃,除了可能等的時(shí)間比以往略久耐亏,沒有任何區(qū)別徊都。這個(gè)栗子中,服務(wù)員就扮演著RPC 中代理的角色广辰,你就是調(diào)用者暇矫,外賣店就是服務(wù)器。
情況就是這么個(gè)情況择吊,但是由于RPC只是一種實(shí)現(xiàn)方法李根,并沒有形成標(biāo)準(zhǔn),因此有著很多不同的實(shí)現(xiàn)几睛,比如gRPC房轿、Dubbo、Thrift和FastRPC等所森。對(duì)于如何進(jìn)行序列化和反序列化囱持、如何通信,不同的框架有著不同的實(shí)現(xiàn)焕济。
2. FastRPC簡(jiǎn)介
FastRPC是一個(gè)XML-RPC協(xié)議的實(shí)現(xiàn)纷妆,它的特點(diǎn)是有多種數(shù)據(jù)序列化方式可選:二進(jìn)制、JSON晴弃、XML以及Base64,因?yàn)樗褂肏TTP協(xié)議作為載體上鞠,通過HTTP的頭的數(shù)據(jù)格式協(xié)商字段很容易知道數(shù)據(jù)的格式际邻。
FastRPC相對(duì)與其他框架來說非常簡(jiǎn)單,代碼主要就三部分:客戶端實(shí)現(xiàn)芍阎、服務(wù)端實(shí)現(xiàn)以及數(shù)據(jù)序列化與反序列化的實(shí)現(xiàn)世曾。也是因?yàn)樗?jiǎn)單了,它并沒有涉及到鑒權(quán)等方面的內(nèi)容谴咸,這些需要自己去考慮度硝。但是對(duì)于了解一個(gè)RPC框架的具體實(shí)現(xiàn)已經(jīng)足夠了。
3. FastRPC調(diào)用流程
RPC分為兩部分:運(yùn)行在本地址空間的客戶端部分以及運(yùn)行在其他地址空間的服務(wù)器部分寿冕。下面就來看看FastRPC對(duì)于著兩部分的實(shí)現(xiàn)蕊程。
3.1. FastRPC Server調(diào)用流程
FastRPC Server的實(shí)現(xiàn)如圖2所示,其工作流程如下:
- 將客戶端可以調(diào)用的例程依次進(jìn)行注冊(cè)驼唱,存放到一個(gè)注冊(cè)表藻茂,其實(shí)就是一個(gè)以函數(shù)名為鍵、函數(shù)地址為值的一個(gè)字典;
- 例程注冊(cè)完成后辨赐,開始啟動(dòng)對(duì)特定的端口的監(jiān)聽优俘;
- 當(dāng)有請(qǐng)求到達(dá)后,通過HTTP的頭確定內(nèi)容的序列化格式掀序,調(diào)用對(duì)應(yīng)的反序列化方法對(duì)數(shù)據(jù)進(jìn)行解析帆焕。
- 解析完成獲得函數(shù)名和參數(shù),通過查表或取到函數(shù)地址不恭,調(diào)用函數(shù)并將參數(shù)傳入叶雹;
- 函數(shù)返回后通過將數(shù)據(jù)返回請(qǐng)求者。
整個(gè)過程非常簡(jiǎn)單换吧。
3.2. FastRPC Client的調(diào)用流程
FastRPC Client的實(shí)現(xiàn)如圖3所示折晦,其工作流程如下:
- 首先進(jìn)行鏈接的配置,例如服務(wù)器地址沾瓦、端口號(hào)满着、最大等待時(shí)間等;
- 配置完成后等待客戶調(diào)用贯莺,客戶調(diào)用特定方法风喇,傳入了參數(shù)÷铺剑客戶端調(diào)用選定的序列化例程對(duì)數(shù)據(jù)進(jìn)行序列化瀑晒,并且根據(jù)數(shù)據(jù)填充HTTP頭信息惰说;
- 客戶端發(fā)起HTTP請(qǐng)求捌臊,將數(shù)據(jù)發(fā)送個(gè)服務(wù)器或链,等待服務(wù)器應(yīng)答秽誊;
- 獲取到服務(wù)器返回的數(shù)據(jù)后鲸沮,對(duì)數(shù)據(jù)進(jìn)行解析,返回給調(diào)用者锅论。
整個(gè)請(qǐng)求的核心就是下面的幾行代碼讼溺,位于frpcserverproxy.cc
中,首先將方法名和參數(shù)都序列化到本地的緩存最易,然后通過flush
方法寫到HTTP輸出流:
// fastrpc\src\frpcserverproxy.cc
...
try {
marshaller->packMethodCall(methodName);
// marshall all passed values until null pointer
while (const Value_t *value = va_arg(args, Value_t*))
feeder.feedValue(*value);
marshaller->flush();
} catch (const ResponseError_t &e) {}
...
// fastrpc\src\frpctreefeeder.cc
template <typename Marshaller_t>
void feedValueImpl(Marshaller_t &marshaller, const Value_t &value) {
switch(value.getType()) {
case Int_t::TYPE:
marshaller.packInt(Int(value).getValue());
break;
case Bool_t::TYPE:
marshaller.packBool(Bool(value).getValue());
break;
case Null_t::TYPE:
packNull(marshaller, value);
break;
...
// fastrpc\src\frpcbinmarshaller.cc
void BinMarshaller_t::packInt(Int_t::value_type value) {
if (protocolVersion.versionMajor > 2) {
// pack via zigzag encoding.
uint64_t zig = zigzagEncode(value);
unsigned int numType = getNumberType(
static_cast<Int_t::value_type>(zig));
//pack type
char type = FRPC_DATA_TYPE(INT, numType);
//pack number value
Number_t number(zig);
//write type
writer.write(&type, 1);
writer.write(number.data, getNumberSize(numType));
} else if (protocolVersion.versionMajor > 1) {
...
// fastrpc\src\frpchttpclient.cc
void HTTPClient_t::write(const char* data, unsigned int size) {
contentLenght += size;
if (size > (BUFFER_SIZE - queryStorage.back().size())) {
if (useChunks) {
sendRequest();
queryStorage.back().append(data, size);
} else {
if (size > BUFFER_SIZE) {
queryStorage.push_back(std::string(data, size));
} else {
queryStorage.back().append(data, size);
}
}
} else {
queryStorage.back().append(data, size);
}
}
// fastrpc\src\frpchttpclient.cc
void HTTPClient_t::sendRequest(bool last) {
SocketCloser_t closer(httpIO.socket());
std::string headerData;
if (!headersSent) {
HTTPHeader_t header;
StreamHolder_t os;
//create header
os.os << POST << ' ' << (url.isUnix() ? "/" : url.path) << ' '
<< (useHTTP10 ? HTTP10 : HTTP11) << "\r\n";
if (!useHTTP10) {
os.os << HOST << ": ";
if (!url.isUnix()) {
os.os << url.host << ':' << url.port;
}
os.os << "\r\n";
}
...
4. 總結(jié)
說白了怒坯,RPC最終就是一個(gè)服務(wù)器和客戶端。那么RPC和REST(Representational State Transfer藻懒,表示層狀態(tài)轉(zhuǎn)移)有什么區(qū)別呢剔猿??jī)烧叩膮^(qū)別就是在于對(duì)服務(wù)器請(qǐng)求的目的不同:REST需要服務(wù)器給出資源,例如一張圖片一段數(shù)據(jù)嬉荆,并且通過GET, PUT, UPDATE, DELETE
等特定方法表述希望對(duì)這個(gè)資源進(jìn)行的操作归敬;而RPC客戶端需要RPC服務(wù)器提供計(jì)算服務(wù),你用我希望的任何方式對(duì)我提供的數(shù)據(jù)進(jìn)行計(jì)算并告訴我結(jié)果。
5. References
[1] What differentiates a REST web service from a RPC-like one? 【stackoverflow】
[2] https://github.com/seznam/fastrpc