來源1
來源2
RPC,全稱爲Remote Procedure Call,即遠程過程調用,它是一個計算機通信協議。它允許像調用本地服務一樣調用遠程服務。它可以有不同的實現方式。如RMI(遠程方法調用)、Hessian、Http invoker等。另外,RPC是與語言無關的。
1、本地過程調用
RPC就是要像調用本地的函數一樣去調遠程函數。在研究RPC前,我們先看看本地調用是怎麼調的。假設我們要調用函數Multiply來計算lvalue * rvalue的結果:
1 int Multiply(int l, int r) {
2 int y = l * r;
3 return y;
4 }
5
6 int lvalue = 10;
7 int rvalue = 20;
8 int l_times_r = Multiply(lvalue, rvalue);
那麼在第8行時,我們實際上執行了以下操作:
- 將 lvalue 和 rvalue 的值壓棧
- 進入Multiply函數,取出棧中的值10 和 20,將其賦予 l 和 r
- 執行第2行代碼,計算 l * r ,並將結果存在 y
- 將 y 的值壓棧,然後從Multiply返回
- 第8行,從棧中取出返回值 200 ,並賦值給
l_times_r
以上5步就是執行本地調用的過程。
2、遠程過程調用帶來的新問題
在遠程調用時,我們需要執行的函數體是在遠程的機器上的,也就是說,Multiply是在另一個進程中執行的。這就帶來了幾個新問題:
- Call ID映射。我們怎麼告訴遠程機器我們要調用Multiply,而不是Add或者FooBar呢?在本地調用中,函數體是直接通過函數指針來指定的,我們調用Multiply,編譯器就自動幫我們調用它相應的函數指針。但是在遠程調用中,函數指針是不行的,因爲兩個進程的地址空間是完全不一樣的。所以,在RPC中,所有的函數都必須有自己的一個ID。這個ID在所有進程中都是唯一確定的。客戶端在做遠程過程調用時,必須附上這個ID。然後我們還需要在客戶端和服務端分別維護一個
{函數 <--> Call ID}
的對應表。兩者的表不一定需要完全相同,但相同的函數對應的Call ID必須相同。當客戶端需要進行遠程調用時,它就查一下這個表,找出相應的Call ID,然後把它傳給服務端,服務端也通過查表,來確定客戶端需要調用的函數,然後執行相應函數的代碼。 - 序列化和反序列化。客戶端怎麼把參數值傳給遠程的函數呢?在本地調用中,我們只需要把參數壓到棧裏,然後讓函數自己去棧裏讀就行。但是在遠程過程調用時,客戶端跟服務端是不同的進程,不能通過內存來傳遞參數。甚至有時候客戶端和服務端使用的都不是同一種語言(比如服務端用C++,客戶端用Java或者Python)。這時候就需要客戶端把參數先轉成一個字節流,傳給服務端後,再把字節流轉成自己能讀取的格式。這個過程叫序列化和反序列化。同理,從服務端返回的值也需要序列化反序列化的過程。
- 網絡傳輸。遠程調用往往用在網絡上,客戶端和服務端是通過網絡連接的。所有的數據都需要通過網絡傳輸,因此就需要有一個網絡傳輸層。網絡傳輸層需要把Call ID和序列化後的參數字節流傳給服務端,然後再把序列化後的調用結果傳回客戶端。只要能完成這兩者的,都可以作爲傳輸層使用。因此,它所使用的協議其實是不限的,能完成傳輸就行。儘管大部分RPC框架都使用TCP協議,但其實UDP也可以,而gRPC乾脆就用了HTTP2。Java的Netty也屬於這層的東西。
所以,要實現一個RPC框架,其實只需要把以上三點實現了就基本完成了。Call ID映射可以直接使用函數字符串,也可以使用整數ID。映射表一般就是一個哈希表。序列化反序列化可以自己寫,也可以使用Protobuf或者FlatBuffers之類的。網絡傳輸庫可以自己寫socket,或者用asio,ZeroMQ,Netty之類。
原作者實現的一個RPC框架tinyrpc
rpc框架做的最重要的一件事情就是封裝,調用者和被調用者的通訊細節,客戶端代理負責向調用方法的方法名參數返回值包等信息根據通信協議組織成報文發送給服務端,服務端解析報文,根據客戶端傳遞的信息執行對應的方法,然後將返回值安裝協議組織成報文發送給客戶端,客戶端再解析出來。RPC屏蔽了底層的實現細節,讓調用者無需關注網絡通信,數據傳輸等細節。
3、RPC框架的實現思路
RPC的核心原理:RPC能夠讓本地應用簡單、高效地調用服務器中的過程(服務)。它主要應用在分佈式系統。如Hadoop中的IPC組件。但怎樣實現一個RPC框架呢?
從下面幾個方面思考,僅供參考:
- 1.通信模型:假設通信的爲A機器與B機器,A與B之間有通信模型,在Java中一般基於BIO或NIO;。
- 2.過程(服務)定位:使用給定的通信方式,與確定IP與端口及方法名稱確定具體的過程或方法;
- 3.遠程代理對象:本地調用的方法(服務)其實是遠程方法的本地代理,因此可能需要一個遠程代理對象,對於Java而言,遠程代理對象可以使用Java的動態對象實現,封裝了調用遠程方法調用;
- 4.序列化,將對象名稱、方法名稱、參數等對象信息進行網絡傳輸需要轉換成二進制傳輸,這裏可能需要不同的序列化技術方案。如:protobuf,Arvo等。
4、逐步實現簡單RPC框架
來源:一個簡單RPC框架是如何煉成的
試圖通過簡單的編程來模擬和解釋RPC的原理和過程,不是真實的RPC。
4.1、開局篇
總結下來就是有4塊核心內容
- RPC數據的傳輸。如上面的RPCConnector,RPCChannel。它們主要負責數據傳輸這一塊, 具體客戶端與服務器之間的連接是不是socket連接,是原始tcp連接還是使用http,這些RPC協議本身不做任何規定。那麼我們的任務就是抽象出這樣一個傳輸層。
- RPC消息。如上面的RPCProtocol, 以及encode,decode方法。 因爲RPC是遠程調用,所以沒辦法直接函數調用,於是就必須用一套專門的協議,去表示調用以及調用結果。另外,因爲實際應用基本都是跨機器連接,所以無法直接傳遞內存變量,也就是說還需要將消息編碼成 諸如字符串一類的可以跨設備傳輸的內容。具體的RPC消息的封裝協議很多,常見的是基於xml,json封裝的。那麼我們的任務就是抽象出這樣一個協議層。
- RPC服務註冊。如上面Callee –>export。 服務端具體支持哪些調用,收到來自客戶端的RPC請求後,怎樣去調用真正的需要執行的方法,這些內容也是一個完整的RPC框架必須考慮的。一些稍微高級一點的框架,都是可以服務自動註冊的,現在主流的RPC框架,還支持通過 IDL(Interface Definition Language)來定義遠程接口,實現跨語言的RPC 。那麼我們的任務就是抽象出一個RPC服務的註冊機制。
- RPC消息處理。如上面的RPCInvoker。這裏其實與RPC本身關係不大,一般就是考慮支持異步/同步調用。 這一部分,大概我也會做一些說明,但不是這個系列的重點。
4.2、制定RPC消息
下面,我們先看一個普通的過程調用
#-*-coding=utf-8-*-
class Client(object):
def __init__(self):
self.remote=None
# 內部是委託給遠程remote對象來獲取結果。這裏的遠程對象指另一個類實例。真正的功能是由另一個類的實例來完成的。
def sayHello(self):
if self.remote:return self.remote.sayHello()
else:return None
class Server(object):
def __init__(self):
pass
def sayHello(self):
return 'Hello World'
if __name__=='__main__':
server=Server()
client=Client()
client.remote=server#此時就把服務類的實例賦給了客戶類
print(client.sayHello())#當client.remote有值時,client.sayHello()方法返回結果是client.remote.sayHello(),而client.remote又被賦予了一個server對象;server對象具有sayHello()方法,其功能是返回'Hello World'值,即client.remote.sayHello()返回結果也是'Hello World',進行client.sayHello()的返回結果也是'Hello World'
結果輸出:Hello World
這是一個常見的過程調用的例子,client調用sayHello,實際委託給Server的sayHello方法來實現。但他不是RPC調用,因爲起碼不是遠程的,另外,也沒有我們提到的四個核心內容。於是我們的任務就是通過一點點的代碼修改,爲其引入RPC框架。
第一步,訂協議。
RPC請求:Request, 包含一個請求id 和 一個請求命令,如‘sayHello’
#Request的功能就是定義要傳遞的參數
class Request(object):
'''''
@RPC請求,包含命令id和請求內容兩部分。這個實現,與具體的RPC協議相關。
@這裏是簡化起見,採用python自身的字典作爲其協議的數據結構
'''
def __init__(self):
'''''
Constructor
'''
self.id=0#id的作用在於將Request和Response建立綁定關係.在異步調用時就有用
self.command=None#sayHello
def __str__(self):
return ''.join(('id: ', str(self.id), ' command: ', str(self.command)))
同樣的,對RPC Response,也定義如下
#Respone就是定義接收參數
class Respone(object):
'''''
@RPC回覆。 包含答覆id和執行結果兩部分內容。其中答覆id與對應的請求id一致。
@簡單起見,協議的實現使用python自家的字典
'''
def __init__(self):
'''''
Constructor
'''
self.id=0
self.result=None
def __str__(self):
return ''.join(('id: ', str(self.id), ' result: ', str(self.result)))
定義好協議之後,我就對Client稍作修改,將原來直接的接口調用self.remote.sayHello更改爲 send Request (command=’sayHello’)
# 內部是委託給遠程remote對象來獲取結果。
def sayHello(self):
req = Request()
req.id = 1
req.command = 'sayHello'
return self.request(req)
def request(self,req):
rsp=self.remote.procRequest(req)#將請求消息發送給遠程服務端。但因爲傳輸層這裏還沒實現,所以先暫時還是直接調用遠端接口
return rsp.result
然後,服務端也要相應修改,需要根據request請求中的command命令,調用具體的方法,並將執行結果封裝到Response中,返回給客戶端。
def procRequest(self, req):
rsp = Respone()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello()
else:
raise Exception("unknown command")
return rsp
整個過程如下:
#-*-coding=utf-8-*-
class Request(object):
'''''
@RPC請求,包含命令id和請求內容兩部分。這個實現,與具體的RPC協議相關。
@這裏是簡化起見,採用python自身的字典作爲其協議的數據結構
'''
def __init__(self):
'''''
Constructor
'''
self.id=0#id的作用在於將Request和Response建立綁定關係.在異步調用時就有用
self.command=None#sayHello
def __str__(self):
return ''.join(('id: ', str(self.id), ' command: ', str(self.command)))
class Respone(object):
'''''
@RPC回覆。 包含答覆id和執行結果兩部分內容。其中答覆id與對應的請求id一致。
@簡單起見,協議的實現使用python自家的字典
'''
def __init__(self):
'''''
Constructor
'''
self.id=0
self.result=None
def __str__(self):
return ''.join(('id: ', str(self.id), ' result: ', str(self.result)))
class Client(object):
def __init__(self):
self.remote=None
# 內部是委託給遠程remote對象來獲取結果。
def sayHello(self):
req = Request()
req.id = 1
req.command = 'sayHello'
return self.request(req)
def request(self,req):
rsp=self.remote.procRequest(req)#將請求消息發送給遠程服務端。但因爲傳輸層這裏還沒實現,所以先暫時還是直接調用遠端接口
return rsp.result
class Server(object):
def __init__(self):
pass
def sayHello(self):
return 'Hello World'
def procRequest(self, req):
rsp = Respone()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello()
else:
raise Exception("unknown command")
return rsp
if __name__=='__main__':
server=Server()
client=Client()
client.remote=server
print(client.sayHello())
輸出結果也爲:Hello World
到這裏,RPC框架中的RPC消息已經初具雛形,不過
- 我們並沒有實現相應的encode和decode方法,沒有基於可以跨設備的字符串傳輸,而是直接的內存變量傳遞。
- 現在的RPC request不支持帶參數的請求命令。如add(a, b), 如何在RPC消息中描述參數a,b 。
4.3、實現帶參數的RPC調用
既然是要帶參數,那隻能擴展原來的Request消息了,加個parameter成員,用於表示參數,具體的格式採用字典方式,{ ’arg1‘, arg1, ‘arg2’, arg2 ,….}。 這樣就可以解決多參數的表示問題。
class Request(object):
'''''
@RPC請求,包含命令id和請求內容兩部分。這個實現,與具體的RPC協議相關。
@這裏是簡化起見,採用python自身的字典作爲其協議的數據結構
'''
def __init__(self):
'''''
Constructor
'''
self.id=0#id的作用在於將Request和Response建立綁定關係.在異步調用時就有用
self.command=None#sayHello
self.parameter = {}
def __str__(self):
return ''.join(
('id: ', str(self.id), ' command: ', str(self.command), ' parameter: ', str(self.parameter)))
add(a=1, b=2)的RPC 請求就是這個樣子了
Request : id = 3, command = 'add', parameter = {'a':1, 'b':2}
對應的,客戶端的add方法,我們可以這麼寫
def add(self, a, b):
req = Request()
req.id = 3
req.command = 'add'
req.parameter = {'a':a, 'b':b}
return self.request(req)
那麼服務端收到這個RPC請求後,怎麼處理得到參數呢?一個傳統而稍顯笨拙的方式是:
def add(self, a, b):
return a + b
def procReqeust__add(self, req):
parameter = req.parameter
a = parameter.get('a')
b = parameter.get('b')
return self.add(a, b)
這種方式的缺點就是每一個RPC調用,都要怎麼處理一下,煩死了,沒有任何技術含量的純苦力活,但還考驗細心,一不小心搞錯a或者b的名字了,呵呵,等着被請喝茶吧。
修改如下:
def procReqeust__add(self, req):
parameter = req.parameter
return self.add(**parameter)
對上面**parameter
不懂的同學自行度娘。這裏只是簡單解釋一下:**parmater
的作用同那塊笨拙的代碼一樣,但有一個前提條件,即使add聲明時,參數變量名a,b不能變。
至此,使用這種新的方式,我們server的代碼就是這個樣子的,對沒有參數的方法,上面**也是可以放心使用的
def procRequest(self,req):
rsp = Response()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello(**req.parameter)
elif req.command == 'add':
rsp.result = self.add(**req.parameter)
else:
raise Exception("unknown command")
return rsp
整個過程如下:
#-*-coding=utf-8-*-
class Request(object):
'''''
@RPC請求,包含命令id和請求內容兩部分。這個實現,與具體的RPC協議相關。
@這裏是簡化起見,採用python自身的字典作爲其協議的數據結構
'''
def __init__(self):
'''''
Constructor
'''
self.id=0#id的作用在於將Request和Response建立綁定關係.在異步調用時就有用
self.command=None#sayHello
self.parameter={}
def __str__(self):
return ''.join(('id: ', str(self.id), ' command: ', str(self.command), ' parameter: ', str(self.parameter)))
class Respone(object):
'''''
@RPC回覆。 包含答覆id和執行結果兩部分內容。其中答覆id與對應的請求id一致。
@簡單起見,協議的實現使用python自家的字典
'''
def __init__(self):
'''''
Constructor
'''
self.id=0
self.result=None
def __str__(self):
return ''.join(('id: ', str(self.id), ' result: ', str(self.result)))
class Client(object):
def __init__(self):
self.remote=None
# 內部是委託給遠程remote對象來獲取結果。
def sayHello(self):
req = Request()
req.id = 1
req.command = 'sayHello'
return self.request(req)
def add(self, a, b):
req = Request()
req.id = 3
req.command = 'add'
req.parameter = {'a': a, 'b': b}
return self.request(req)
def request(self,req):
rsp=self.remote.procRequest(req)#將請求消息發送給遠程服務端。但因爲傳輸層這裏還沒實現,所以先暫時還是直接調用遠端接口
return rsp.result
class Server(object):
def __init__(self):
pass
def sayHello(self):
return 'Hello World'
def add(self, a, b):
return a + b
def procRequest(self, req):
rsp = Respone()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello(**req.parameter)
elif req.command == 'add':
rsp.result = self.add(**req.parameter)
else:
raise Exception("unknown command")
return rsp
if __name__=='__main__':
server=Server()
client=Client()
client.remote=server
print(client.sayHello())
print(client.add(2,3))
輸出結果如下:
Hello World
5
4.4、實現RPC消息的編解碼
實際的RPC應用基本都是跨機器連接,所以無法直接傳遞內存變量,也就是說還需要將消息編碼成 諸如字符串一類的可以跨設備傳輸的內容。具體的RPC消息的封裝協議很多,常見的是基於xml,json封裝的。但如果抽象一下,實際也就是一個編解碼,管你編碼成什麼內容呢,就是不編碼也可以。管他黑貓白貓,只要能傳過去,就是好貓。
利用python裏的兩個運算。 str 和eval。
假設 一個字典msg = { ‘a’ : 1, ‘b’ : 2}. 那麼str(msg) = ” { ‘a’ : 1, ‘b’ : 2}”, 注意變成字符串嘍。
然後eval(” { ‘a’ : 1, ‘b’ : 2}”)–>msg, 做一個eval運算,又從字符串變成 字典變量了。
於是編碼時,先將RPC消息轉換成dict,然後調用str編碼成字符串。
解碼時,先調用eval 得到dict對象,然後再轉換爲具體的RPC消息對象
設計已定,剩下的就只是code filling。
先修改一下原來Request的str方法,返回一個dict的字符串表示。對Response也做類似處理
class Request(object):
def __str__(self):
return str({'id': self.id, 'command': self.command, 'parameter': self.parameter})
然後引入encode方法
@classmethod
def encode(cls, message):
if isinstance(message, Request):
return str(message)
elif isinstance(message, Response):
return str(message)
elif isinstance(message, Notification):
return str(message)
else:
raise Exception('unknown type when encode')
同樣的,引入decode方法,稍微複雜一些。主要的麻煩在於如何區分解碼出來的是Response還是Request
我的辦法是比較投機的,直接根據字典的內容去判斷。有command字段的肯定是request,有result字段的肯定是response
@classmethod
def decode(cls, data):
info = eval(data)
if 'command' in info:
request = Request()
request.id = info.get('id')
request.command = info.get('command')
request.parameter = info.get('parameter', {})
return request
elif 'result' in info:
response = Response()
response.id = info.get('id')
response.result = info.get('result')
return response
elif 'message' in info:
note = Notification()
note.message = info.get('message')
return note
else:
raise Exception('unknown data when decode')
另外,client和server的代碼也要稍作調整。
整個過程如下:
#-*-coding=utf-8-*-
class Request(object):
'''''
@RPC請求,包含命令id和請求內容兩部分。這個實現,與具體的RPC協議相關。
@這裏是簡化起見,採用python自身的字典作爲其協議的數據結構
'''
def __init__(self):
'''''
Constructor
'''
self.id=0#id的作用在於將Request和Response建立綁定關係.在異步調用時就有用
self.command=None#sayHello
self.parameter={}
def __str__(self):
return str({'id': self.id, 'command': self.command, 'parameter': self.parameter})
class Response(object):
'''''
@RPC回覆。 包含答覆id和執行結果兩部分內容。其中答覆id與對應的請求id一致。
@簡單起見,協議的實現使用python自家的字典
'''
def __init__(self):
'''''
Constructor
'''
self.id=0
self.result=None
def __str__(self):
return str({'id': self.id, 'result': self.result})
class Client(object):
def __init__(self):
self.remote=None
# @classmethod
def encode(cls, message):
if isinstance(message, Request):
return str(message)
elif isinstance(message, Response):
return str(message)
# elif isinstance(message, Notification):
# return str(message)
else:
raise Exception('unknown type when encode')
# 內部是委託給遠程remote對象來獲取結果。
def sayHello(self):
req = Request()
req.id = 1
req.command = 'sayHello'
return self.request(self.encode(req))
def add(self, a, b):
req = Request()
req.id = 3
req.command = 'add'
req.parameter = {'a': a, 'b': b}
return self.request(self.encode(req))
def request(self,req):
rsp=self.remote.procRequest(req)#將請求消息發送給遠程服務端。但因爲傳輸層這裏還沒實現,所以先暫時還是直接調用遠端接口
return rsp.result
class Server(object):
def __init__(self):
pass
@classmethod
def decode(cls, data):
info = eval(data)
if 'command' in info:
request = Request()
request.id = info.get('id')
request.command = info.get('command')
request.parameter = info.get('parameter', {})
return request
elif 'result' in info:
response = Response()
response.id = info.get('id')
response.result = info.get('result')
return response
# elif 'message' in info:
# note = Notification()
# note.message = info.get('message')
# return note
else:
raise Exception('unknown data when decode')
def sayHello(self):
return 'Hello World'
def add(self, a, b):
return a + b
def procRequest(self, data):
req=self.decode(data)
rsp = Response()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello(**req.parameter)
elif req.command == 'add':
rsp.result = self.add(**req.parameter)
else:
raise Exception("unknown command")
return rsp
if __name__=='__main__':
server=Server()
client=Client()
client.remote=server
print(client.sayHello())
print(client.add(2,3))
結果如下:
Hello World
5
4.5、引入傳輸層
(原作者寫的不全,現在還沒把完整的還原出來代碼,先放在這裏,以後再補)
接下來處理數據傳輸。實際應用場景一般都是基於socket。socket代碼比較多,使用起來也比較麻煩。而且具體的傳輸通道使用socket或者其他的方式,如更上層的http,或者android裏的binder,都是可替換的,只是具體的一種實現而已。所以,這裏我就偷個懶,只是引入一個很簡單的Connection類,用來描述一下如何將數據傳輸 這一層給獨立出來。
首先簡單列出Connection類的實現,很簡單,就是兩個list,一個管發送,一個管接收。(實現沒有考慮多線程安全,實際是必須考慮的)。
需要說明的是,這裏的recv的實現約定是阻塞式的,也就是如果沒有收到任何數據,recv調用會一直阻塞。
class Connection(object):
'''''
@RPC 連接。一般說來,都是socket連接,這裏簡化起見,直接本地變量實現。
'''
def __init__(self, sending_msg_list, recving_msg_list):
'''''
Constructor
'''
self.sending_msg_list = sending_msg_list
self.recving_msg_list = recving_msg_list
def send(self, message):
self.sending_msg_list.append(message)
def recv(self):
while len(self.recving_msg_list) == 0: time.sleep(0.01)
return self.recving_msg_list.pop(0)
def isClosed(self):
return False
有了這個connection,剩下的就只要將rpc消息統統通過這個connection去發送,通過這個Connection去接收。
接着修改客戶端的request請求,不再直接調用server端的procRequest方法,而是將請求交給connection,去發送。 然後等待connection收到server端的回覆,將回復消息從connection中取出來。
def request(self, req):
# 全部簡化處理,不考慮線程安全問題,不考慮異步
# 先是將RPC消息發送到服務端,然後服務端就會處理,並將結果發回到客戶端,客戶端這邊接收處理結果。
# self.remote.procRequest(req) // 刪除
self.conn.send(req)
rsp = self.conn.recv()
return rsp.result
同樣的,修改服務端收到request請求後的處理。首先反覆調用connection.recv()方法讀取客戶端發過來的請求。當請求處理完成後,不再直接以函數返回的方式return,而是將rsp交給connection,由connection負責傳輸給client
# def procRequest(self, req): 調整參數列表,不再需要req
def procRequest(self):
# 循環讀取並處理收到的客戶端請求
while True:
req = self.conn.recv()
rsp = Response()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello()
elif req.command == 'whoAreYou':
rsp.result = self.whoAreYou()
else:
raise Exception("unknown command")
# return rsp # rsp也是通過connection最終傳給client,而不是直接函數返回
self.conn.send(rsp)
最後,列一下connection的初始化
slist = []
rlist = []
client = Client(Connection(slist, rlist))
server = Server(Connection(rlist, slist))
server.start()
總結,引入傳輸層的意義在於
1. 實現client與server端的解耦,client端不再需要持有server端的對象了。 這也是實現“遠程調用 ”所必須的。
2. 傳輸層的實現有很大的自由度,一般說來,他無需關心具體的RPC消息的格式,只需要完成數據的可靠傳輸就可以了。
3. 傳輸層具體基於socket,binder, 是採用http,udp,tcp這些都是自由的,根據需要選擇就可以了。也就是相當於一個可以自由拼接的組件。
4. 上面的模型實在過於簡單,沒有考慮多線程保護,沒有考慮異常。實際比較理想的情況,應該起碼有個類,Connector,以及Channel。其中channel只負責數據的傳輸,Connector負責管理channel。
4.6、引入服務註冊機制
接下來處理RPC服務的註冊機制。所謂註冊機制,就是Server需要聲明支持哪些rpc方法,然後當客戶端發送調用某個聲明的rpc方法之後,服務端能自動找到執行該請求的具體方法。以實際的例子爲例,這是現在server端處理RPC請求的代碼
def procRequest(self):
# 循環讀取並處理收到的客戶端請求
while True:
req = self.conn.recv()
rsp = Response()
rsp.id = req.id
if req.command == 'sayHello':
rsp.result = self.sayHello()
elif req.command == 'whoAreYou':
rsp.result = self.whoAreYou()
else:
raise Exception("unknown command")
self.conn.send(rsp)
上面的代碼有一個很不好的地方,很難穩定。Server端每次新增一個支持的rpc方法,就要修改這個procRequest方法。有什麼辦法可以避免嗎?有,就是引入服務註冊機制。在這裏,實際就是將command與具體的function object綁定起來,說穿了就是生成一個dict,
{‘sayHello’ : self.sayHello, 'whoAreYou': self.whoAreYou}。
有這樣的dict之後,收到req 之後,只要提取出command字段,然後從dict中找出對應的function,調用該function即可。
首先我們實現一個比較原始的服務註冊機制。
這個實現很簡單,self.services就是上面的dict。通過register()去註冊服務,通過get_service()去獲取服務名對應的function
class ServiceRegister(object):
'''''
@服務註冊 不考慮線程安全,這裏簡化起見,也不引入反射機制。
'''
def __init__(self):
'''''
Constructor
'''
self.services = {}
## 註冊具體的服務
# @param servicename: 服務名
# @param obj: 具體的對象
def register(self, obj, servicename):
if servicename in self.services:
print('warning: %s is already registered' % servicename)
else:
self.services[servicename] = obj
def get_service(self, servicename):
return self.services[servicename]
def list_service(self, servicename=None):
if servicename:
return str({servicename, self.services[servicename]})
else:
return str(self.services)
使用時,就是這個樣子的
服務註冊:
self.services.register(self.sayHello, 'Server.sayHello', )
self.services.register(self.whoAreYou, 'Server.whoAreYou')
self.services.register(self.add, 'Server.add')
服務查找
def proc(self, req):
rsp = Response()
rsp.id = req.id
rsp.result = ServiceCaller.call(self.services.get_service(req.command), req.parameter)
......
上面serviceCaller的實現,就是在RPC消息,實現帶參數的RPC請求中,提到的 func(**args)的技巧
class ServiceCaller():
def __init__(self):
pass
@classmethod
def call(cls, caller, parameter):
if not parameter or len(parameter) == 0:
return caller()
return caller(**parameter)
下面我再引入一個自動註冊服務的實現
class AutoServiceRegister(AbstractServiceRegister):
def register_class(self, obj, predicate=None):
if not (hasattr(obj, '__class__') and inspect.isclass(obj.__class__)):
return False
servicename = obj.__class__.__name__
for (name, attr) in inspect.getmembers(obj, predicate):
# 系統方法或者私有方法,不添加
if name.startswith('__') or name.startswith('_' + servicename + '__'): continue
#print(name)
if inspect.ismethod(attr): self.register_method(attr)
elif inspect.isfunction(attr): self.register_function(attr, servicename)
return True
使用
if __name__ == '__main__':
class AServer(object):
def __init__(self):
pass
def sayHello(self):
return 'Hello World'
def whoAreYou(self):
return 'I am server'
def __kaos(self):
pass
def _kaos(self):
pass
obj = AServer()
service = AutoServiceRegister()
print(service.register_class(obj))
print(service.list_services())
print(service.get_service('AServer.sayHello'))
執行結果如下
True
{'AServer': {'sayHello': <bound method AServer.sayHello of <__main__.AServer object at 0x000000000294EA90>>, 'whoAreYou': <bound method AServer.whoAreYou of <__main__.AServer object at 0x000000000294EA90>>, '_kaos': <bound method AServer._kaos of <__main__.AServer object at 0x000000000294EA90>>}}
<bound method AServer.sayHello of <__main__.AServer object at 0x000000000294EA90>>
詳細說明 一下原理,利用了類似的反射的技術。有興趣的同學可以先去了解一下inspect
register_class
表示自動搜索一個類對象中的成員方法,並將其作爲server端的rpc方法註冊進去。
以上面AServer爲例, 會自動將sayHello, whoAreYou 這兩個方法自動註冊進來。同時像__init__
,__kaos
,_kaos
之類的系統固有方法,或者私有方法,會自動剔除。- 註冊時,傳入的參數obj必須是class的instance,也就是類實例。雖然在python中,也支持類對象,但如果直接傳遞類對象,就會遇到如何初始化的難題。所以這裏一致要求,必須是類的實例。
if not (hasattr(obj, '__class__') and inspect.isclass(obj.__class__)):
return False
類實例的特點就是,包含__class__
成員,而且__class__
成員的值就是該類的類對象。inspect.isclass就是檢測是不是類對象
inspect.getmembers()
返回的是類對象的所有成員,包括系統固有方法以及私有方法
所以,先要將系統方法和私有方法剔除,然後通過inspect,檢查該成員是不是真的是function,就是可以被調用的。如果是,就註冊進來register_fucntion
,register_method
與普通的服務註冊基本一樣。就是添加(key,value)對。
總結:
- 引入服務註冊的方式也是爲了代碼解耦,將req的處理與具體的req消息內容解耦。
- 上面我們 引入了兩種服務註冊的方式,一種方式是普通的方式,逐個添加方法。另一種方式通過python的“反射”技術,自動查找一個類裏面的方法,並自動添加。
- 方案還是很粗糙的,實際有很多優化的地方。
5、Java實現RPC框架
5.1、實現技術方案
下面使用比較原始的方案實現RPC框架,採用Socket通信、動態代理與反射與Java原生的序列化。
5.2、RPC框架架構
RPC架構分爲三部分:
- 1)服務提供者,運行在服務器端,提供服務接口定義與服務實現類。
- 2)服務中心,運行在服務器端,負責將本地服務發佈成遠程服務,管理遠程服務,提供給服務消費者使用。
- 3)服務消費者,運行在客戶端,通過遠程代理對象調用遠程服務。
5.3、 具體實現
服務提供者接口定義與實現,代碼如下:
package com.rpcserver;
public interface HelloService {
String sayHi(String name);
}
HelloServices接口實現類:
package com.rpcserver;
public class HelloServiceImpl implements HelloService {
public String sayHi(String name) {
// TODO Auto-generated method stub
return "Hi, " + name;
}
}
服務中心代碼實現,代碼如下:
package com.rpcserver;
import java.io.IOException;
public interface Server {
public void stop();
public void start() throws IOException;
public void register(Class serviceInterface, Class impl);
public boolean isRunning();
public int getPort();
}
服務中心實現類:
package com.rpcserver;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ServiceCenter implements Server {
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static final HashMap < String, Class > serviceRegistry = new HashMap < String, Class > ();
private static boolean isRunning = false;
private static int port;
public ServiceCenter(int port) {
this.port = port;
}
public void stop() {
isRunning = false;
executor.shutdown();
}
public void start() throws IOException {
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(port));
System.out.println("start server");
try {
while (true) {
// 1.監聽客戶端的TCP連接,接到TCP連接後將其封裝成task,由線程池執行
executor.execute(new ServiceTask(server.accept()));
}
} finally {
server.close();
}
}
public void register(Class serviceInterface, Class impl) {
serviceRegistry.put(serviceInterface.getName(), impl);
}
public boolean isRunning() {
return isRunning;
}
public int getPort() {
return port;
}
private static class ServiceTask implements Runnable {
Socket clent = null;
public ServiceTask(Socket client) {
this.clent = client;
}
public void run() {
ObjectInputStream input = null;
ObjectOutputStream output = null;
try {
// 2.將客戶端發送的碼流反序列化成對象,反射調用服務實現者,獲取執行結果
input = new ObjectInputStream(clent.getInputStream());
String serviceName = input.readUTF();
String methodName = input.readUTF();
Class <? > [] parameterTypes = (Class <? > []) input.readObject();
Object[] arguments = (Object[]) input.readObject();
Class serviceClass = serviceRegistry.get(serviceName);
if (serviceClass == null) {
throw new ClassNotFoundException(serviceName + " not found");
}
Method method = serviceClass.getMethod(methodName, parameterTypes);
Object result = method.invoke(serviceClass.newInstance(), arguments);
// 3.將執行結果反序列化,通過socket發送給客戶端
output = new ObjectOutputStream(clent.getOutputStream());
output.writeObject(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (clent != null) {
try {
clent.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
客戶端的遠程代理對象:
package com.rpcserver;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.Socket;
public class RPCClient < T > {
public static < T > T getRemoteProxyObj(final Class <? > serviceInterface, final InetSocketAddress addr) {
// 1.將本地的接口調用轉換成JDK的動態代理,在動態代理中實現接口的遠程調用
return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class <? > [] {serviceInterface},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
try {
// 2.創建Socket客戶端,根據指定地址連接遠程服務提供者
socket = new Socket();
socket.connect(addr);
// 3.將遠程服務調用所需的接口類、方法名、參數列表等編碼後發送給服務提供者
output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(serviceInterface.getName());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);
// 4.同步阻塞等待服務器返回應答,獲取應答後返回
input = new ObjectInputStream(socket.getInputStream());
return input.readObject();
} finally {
if (socket != null) socket.close();
if (output != null) output.close();
if (input != null) input.close();
}
}
});
}
}
最後爲測試類(主程序):
package com.rpcserver;
import java.io.IOException;
import java.net.InetSocketAddress;
public class RPCTest {
public static void main(String[] args) throws IOException {
new Thread(new Runnable() {
public void run() {
try {
Server serviceServer = new ServiceCenter(8088);
serviceServer.register(HelloService.class, HelloServiceImpl.class);
serviceServer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
HelloService service = RPCClient.getRemoteProxyObj(HelloService.class, new InetSocketAddress("localhost", 8088));
System.out.println(service.sayHi("test"));
}
}
運行結果如下:
start server
Hi, test
5.4、總結
RPC本質爲消息處理模型,RPC屏蔽了底層不同主機間的通信細節,讓進程調用遠程的服務就像是本地的服務一樣。
5.5、可以改進的地方
這裏實現的簡單RPC框架是使用Java語言開發,與Java語言高度耦合,並且通信方式採用的Socket是基於BIO實現的,IO效率不高,還有Java原生的序列化機制佔內存太多,運行效率也不高。可以考慮從下面幾種方法改進。
- 1.可以採用基於JSON數據傳輸的RPC框架;
- 2.可以使用NIO或直接使用Netty替代BIO實現;
- 3.使用開源的序列化機制,如Hadoop Avro與Google protobuf等;
- 4.服務註冊可以使用Zookeeper進行管理,能夠讓應用更加穩定。
另外可參考:
簡單的RPC java實現
自定義的RPC的Java實現
三百行代碼完成一個簡單的rpc框架
6、Python中實現遠程調用(RPC、RMI)簡單例子
遠程調用一般分爲兩種,遠程過程調用(RPC)和遠程方法調用(RMI)。
6.1、RPC
RPC屬於函數級別的遠程調用,其多是通過HTTP傳輸數據,數據形式有XML、JSON、序列化數據等。在此,用python做一個xml-rpc的示例。 先給服務器端server.py
:
#-*- coding=uft-8- -*-
from SimpleXMLRPCServer import SimpleXMLRPCServer
def add(x, y):
return x + y
if __name__ == '__main__':
s = SimpleXMLRPCServer(('127.0.0.1', 8080))
s.register_function(add)
s.serve_forever()
#s是一個綁定了本地8080端口的服務器對象,register_function()方法將函數add註冊到s中。serve_forever()啓動服務器。 再給個客戶端client.py:
客戶端client.py
#-*- coding=uft-8- -*-
from xmlrpclib import ServerProxy
if __name__ == '__main__':
s = ServerProxy("http://127.0.0.1:8080")
print s.add(3,4)
現在,運行server.py,然後運行client.py,client.py所在的console會輸出7。
從上可以看到,利用現有RPC框架來實現自己的遠程過程調用還是很方便的,我們不用管信息發送接收實現,不用管編解碼;我們只要專注於服務器端編寫的自己的業務功能(就是一些功能函數),在服務器端配置好RPC服務器對象(如訪問地址和端口),然後將我們的業務函數註冊到服務器對象上,並啓動服務器對象,此時,服務器端就一直運行,等待客戶端的連接;客戶端利用遠程服務代理對象,利用這個代理對象我們可以利用服務器端的功能函數,就像利用本地的函數一樣。
我們用wireshark看一下這期間傳遞的數據是什麼樣子的,請求的數據:
<?xml version='1.0' ?>
<methodCall>
<methodName>
add
</methodName>
<params>
<param>
<value>
<int> 3 </int>
</value>
</param>
<param>
<value>
<int> 4 </int>
</value>
</param>
</params>
</methodCall>
響應的數據:
<?xml version='1.0' ?>
<methodResponse>
<params>
<param>
<value>
<int> 7 </int>
</value>
</param>
</params>
</methodResponse>
其他python實現的RPC服務例子:
python 簡單RPC示例
6.2、RMI
RMI意爲遠程方法調用,粒度比RPC要大,因爲它的基本單位是對象。其大致思路是這樣的:創建RMI服務器對象,將實例化的某個對象以指定的服務名稱(也可以是多個對象,但是服務名稱不應相同)註冊到RMI服務器對象中,之後啓動RMI服務器。服務器等待客戶端發送的數據(包括服務名稱、函數名、參數),將處理結果返回給客戶端。 Pyro4是一個基於python的RMI實現,下面我們用Pyro4創建一個RMI服務器,請看server2.py:未驗證
#-*-coding=utf-8-*-
import Pyro4
class GreetingMaker(object):
def get_fortune(self, name):
return "Hello, {0}. \n" .format(name)
greeting_maker=GreetingMaker()
daemon=Pyro4.Daemon()
uri=daemon.register(greeting_maker)
print("Ready. Object uri =", uri)
daemon.requestLoop()
#uri變量是Pyro4用自己的方法爲greeting_maker對象生成的uri,其中包括套接字以及爲greeting_maker生成的唯一的id。這個id相當於服務名稱,當然也可以指定更易懂的服務名稱。
下面是客戶端client2.py:
#-*-coding=utf-8-*-
import Pyro4
uri=raw_input(" Pyro uri : ").strip()
name=raw_input("Your name: ").strip()
greeting_maker=Pyro4.Proxy(uri)
print(greeting_maker.get_fortune(name))
這其中要輸入的uri也就是server2.py生成的uri。通過給Pyro4.Proxy傳遞greeting_maker
的uri,可以認爲和服務器端的greeting_maker
建立的連接,然後調用greeting_maker
的get_fortune()
方法。如果name是letian,那麼print greeting_maker.get_fortune(name)
的結果是Hello, letian.。
7、Netty介紹
Netty入門教程——認識Netty
Netty入門(一):零基礎“HelloWorld”詳細圖文步驟
Netty 4.x User Guide 中文翻譯《Netty 4.x 用戶指南》
Essential Netty in Action 《Netty 實戰(精髓)》
Netty官網
8、Jetty簡介
Jetty官方文檔翻譯
Jetty:The Definitive Reference
補充知識
9、ServiceFramework學習
來源:allwefantasy/ServiceFramework
Java序列化與反序列化
Java序列化與反序列化
Java序列化是指把Java對象轉換爲字節序列的過程;而Java反序列化是指把字節序列恢復爲Java對象的過程。
爲什麼需要序列化與反序列化
我們知道,當兩個進程進行遠程通信時,可以相互發送各種類型的數據,包括文本、圖片、音頻、視頻等, 而這些數據都會以二進制序列的形式在網絡上傳送。那麼當兩個Java進程進行通信時,能否實現進程間的對象傳送呢?答案是可以的。如何做到呢?這就需要Java序列化與反序列化了。換句話說,一方面,發送方需要把這個Java對象轉換爲字節序列,然後在網絡上傳送;另一方面,接收方需要從字節序列中恢復出Java對象。
當我們明晰了爲什麼需要Java序列化和反序列化後,我們很自然地會想Java序列化的好處。其好處一是實現了數據的持久化,通過序列化可以把數據永久地保存到硬盤上(通常存放在文件裏),二是,利用序列化實現遠程通信,即在網絡上傳送對象的字節序列。
如何實現Java序列化與反序列化
JDK類庫中序列化API
java.io.ObjectOutputStream:表示對象輸出流
它的writeObject(Object obj)方法可以對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中。
java.io.ObjectInputStream:表示對象輸入流
它的readObject()方法從輸入流中讀取字節序列,再把它們反序列化成爲一個對象,並將其返回。
實現序列化的要求
只有實現了Serializable或Externalizable接口的類的對象才能被序列化,否則拋出異常。
實現Java對象序列化與反序列化的方法
假定一個Student類,它的對象需要序列化,可以有如下三種方法:
方法一:若Student類僅僅實現了Serializable接口,則可以按照以下方式進行序列化和反序列化
ObjectOutputStream採用默認的序列化方式,對Student對象的非transient的實例變量進行序列化。
ObjcetInputStream採用默認的反序列化方式,對對Student對象的非transient的實例變量進行反序列化。
方法二:若Student類僅僅實現了Serializable接口,並且還定義了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),則採用以下方式進行序列化與反序列化。
ObjectOutputStream調用Student對象的writeObject(ObjectOutputStream out)的方法進行序列化。
ObjectInputStream會調用Student對象的readObject(ObjectInputStream in)的方法進行反序列化。
方法三:若Student類實現了Externalnalizable接口,且Student類必須實現readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,則按照以下方式進行序列化與反序列化。
ObjectOutputStream調用Student對象的writeExternal(ObjectOutput out))的方法進行序列化。
ObjectInputStream會調用Student對象的readExternal(ObjectInput in)的方法進行反序列化。
JDK類庫中序列化的步驟
步驟一:創建一個對象輸出流,它可以包裝一個其它類型的目標輸出流,如文件輸出流:
ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));
需要一個文件來存放序列化的數據
步驟二:通過對象輸出流的writeObject()方法寫對象:
out.writeObject(“Hello”);
out.writeObject(new Date());
JDK類庫中反序列化的步驟
步驟一:創建一個對象輸入流,它可以包裝一個其它類型輸入流,如文件輸入流:
ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\\objectfile.obj”));
步驟二:通過對象輸出流的readObject()方法讀取對象:
String obj1 = (String)in.readObject();
Date obj2 = (Date)in.readObject();
說明:爲了正確讀取數據,完成反序列化,必須保證向對象輸出流寫對象的順序與從對象輸入流中讀對象的順序一致。
Java序列化與反序列化的例子
爲了更好地理解Java序列化與反序列化,選擇方法一編碼實現。
Student類定義如下:
package com.jieke.io;
import java.io.Serializable;
/**
*Title:學生類
*Description:實現序列化接口的學生類
*Copyright: copyright(c) 2012
*Filename: Student.java
*@author Wang Luqing
*@version 1.0
*/
public class Student implements Serializable
{
private String name;
private char sex;
private int year;
private double gpa;
public Student()
{
}
public Student(String name,char sex,int year,double gpa)
{
this.name = name;
this.sex = sex;
this.year = year;
this.gpa = gpa;
}
public void setName(String name)
{
this.name = name;
}
public void setSex(char sex)
{
this.sex = sex;
}
public void setYear(int year)
{
this.year = year;
}
public void setGpa(double gpa)
{
this.gpa = gpa;
}
public String getName()
{
return this.name;
}
public char getSex()
{
return this.sex;
}
public int getYear()
{
return this.year;
}
public double getGpa()
{
return this.gpa;
}
}
把Student類的對象序列化到文件O:\\Java\\com\\jieke\\io\\student.txt
,並從該文件中反序列化,向console顯示結果。代碼如下:
import java.io.*;
/**
*Title:應用學生類
*Description:實現學生類實例的序列化與反序列化
*Copyright: copyright(c) 2012
*Filename: UseStudent.java
*@author Wang Luqing
*@version 1.0
*/
public class UseStudent
{
public static void main(String[] args)
{
Student st = new Student("Tom",'M',20,3.6);
File file = new File("O:\\Java\\com\\jieke\\io\\student.txt");
try
{
file.createNewFile();
}
catch(IOException e)
{
e.printStackTrace();
}
try
{
//Student對象序列化過程
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(st);
oos.flush();
oos.close();
fos.close();
//Student對象反序列化過程
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Student st1 = (Student) ois.readObject();
System.out.println("name = " + st1.getName());
System.out.println("sex = " + st1.getSex());
System.out.println("year = " + st1.getYear());
System.out.println("gpa = " + st1.getGpa());
ois.close();
fis.close();
}
catch(ClassNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
結果如下所示:
name = Tom
sex = M
year = 20
gpa = 3.6
總結:
1)Java序列化就是把對象轉換成字節序列,而Java反序列化就是把字節序列還原成Java對象。
2)採用Java序列化與反序列化技術,一是可以實現數據的持久化,在MVC模式中很是有用;二是可以對象數據的遠程通信。
序列化和反序列化
摘要
序列化和反序列化幾乎是工程師們每天都要面對的事情,但是要精確掌握這兩個概念並不容易:一方面,它們往往作爲框架的一部分出現而湮沒在框架之中;另一方面,它們會以其他更容易理解的概念出現,例如加密、持久化。然而,序列化和反序列化的選型卻是系統設計或重構一個重要的環節,在分佈式、大數據量系統設計裏面更爲顯著。恰當的序列化協議不僅可以提高系統的通用性、強健性、安全性、優化系統性能,而且會讓系統更加易於調試、便於擴展。本文從多個角度去分析和講解“序列化和反序列化”,並對比了當前流行的幾種序列化協議,期望對讀者做序列化選型有所幫助。
簡介
文章作者服務於美團推薦與個性化組,該組致力於爲美團用戶提供每天billion級別的高質量個性化推薦以及排序服務。從Terabyte級別的用戶行爲數據,到Gigabyte級別的Deal/Poi數據;從對實時性要求毫秒以內的用戶實時地理位置數據,到定期後臺job數據,推薦與重排序系統需要多種類型的數據服務。推薦與重排序系統客戶包括各種內部服務、美團客戶端、美團網站。爲了提供高質量的數據服務,爲了實現與上下游各系統進行良好的對接,序列化和反序列化的選型往往是我們做系統設計的一個重要考慮因素。
本文內容按如下方式組織:
- 第一部分給出了序列化和反序列化的定義,以及其在通訊協議中所處的位置。
- 第二部分從使用者的角度探討了序列化協議的一些特性。
- 第三部分描述在具體的實施過程中典型的序列化組件,並與數據庫組建進行了類比。
- 第四部分分別講解了目前常見的幾種序列化協議的特性,應用場景,並對相關組件進行舉例。
- 最後一部分,基於各種協議的特性,以及相關benchmark數據,給出了作者的技術選型建議。
一、定義以及相關概念
互聯網的產生帶來了機器間通訊的需求,而互聯通訊的雙方需要採用約定的協議,序列化和反序列化屬於通訊協議的一部分。通訊協議往往採用分層模型,不同模型每層的功能定義以及顆粒度不同,例如:TCP/IP協議是一個四層協議,而OSI模型卻是七層協議模型。在OSI七層協議模型中展現層(Presentation Layer)的主要功能是把應用層的對象轉換成一段連續的二進制串,或者反過來,把二進制串轉換成應用層的對象–這兩個功能就是序列化和反序列化。一般而言,TCP/IP協議的應用層對應與OSI七層協議模型的應用層,展示層和會話層,所以序列化協議屬於TCP/IP協議應用層的一部分。本文對序列化協議的講解主要基於OSI七層協議模型。
- 序列化: 將數據結構或對象轉換成二進制串的過程
- 反序列化:將在序列化過程中所生成的二進制串轉換成數據結構或者對象的過程
數據結構、對象與二進制串
不同的計算機語言中,數據結構,對象以及二進制串的表示方式並不相同。
數據結構和對象:對於類似Java這種完全面向對象的語言,工程師所操作的一切都是對象(Object),來自於類的實例化。在Java語言中最接近數據結構的概念,就是POJO(Plain Old Java Object)或者Javabean--那些只有setter/getter方法的類。而在C++這種半面向對象的語言中,數據結構和struct對應,對象和class對應。
二進制串:序列化所生成的二進制串指的是存儲在內存中的一塊數據。C++語言具有內存操作符,所以二進制串的概念容易理解,例如,C++語言的字符串可以直接被傳輸層使用,因爲其本質上就是以’\0’結尾的存儲在內存中的二進制串。在Java語言裏面,二進制串的概念容易和String混淆。實際上String 是Java的一等公民,是一種特殊對象(Object)。對於跨語言間的通訊,序列化後的數據當然不能是某種語言的特殊數據類型。二進制串在Java裏面所指的是byte[],byte是Java的8種原生數據類型之一(Primitive data types)。
二、序列化協議特性
每種序列化協議都有優點和缺點,它們在設計之初有自己獨特的應用場景。在系統設計的過程中,需要考慮序列化需求的方方面面,綜合對比各種序列化協議的特性,最終給出一個折衷的方案。
通用性
通用性有兩個層面的意義:
第一、技術層面,序列化協議是否支持跨平臺、跨語言。如果不支持,在技術層面上的通用性就大大降低了。
第二、流行程度,序列化和反序列化需要多方參與,很少人使用的協議往往意味着昂貴的學習成本;另一方面,流行度低的協議,往往缺乏穩定而成熟的跨語言、跨平臺的公共包。
強健性/魯棒性
以下兩個方面的原因會導致協議不夠強健:
第一、成熟度不夠,一個協議從制定到實施,到最後成熟往往是一個漫長的階段。協議的強健性依賴於大量而全面的測試,對於致力於提供高質量服務的系統,採用處於測試階段的序列化協議會帶來很高的風險。
第二、語言/平臺的不公平性。爲了支持跨語言、跨平臺的功能,序列化協議的制定者需要做大量的工作;但是,當所支持的語言或者平臺之間存在難以調和的特性的時候,協議制定者需要做一個艱難的決定–支持更多人使用的語言/平臺,亦或支持更多的語言/平臺而放棄某個特性。當協議的制定者決定爲某種語言或平臺提供更多支持的時候,對於使用者而言,協議的強健性就被犧牲了。
可調試性/可讀性
序列化和反序列化的數據正確性和業務正確性的調試往往需要很長的時間,良好的調試機制會大大提高開發效率。序列化後的二進制串往往不具備人眼可讀性,爲了驗證序列化結果的正確性,寫入方不得同時撰寫反序列化程序,或提供一個查詢平臺–這比較費時;另一方面,如果讀取方未能成功實現反序列化,這將給問題查找帶來了很大的挑戰–難以定位是由於自身的反序列化程序的bug所導致還是由於寫入方序列化後的錯誤數據所導致。對於跨公司間的調試,由於以下原因,問題會顯得更嚴重:
第一、支持不到位,跨公司調試在問題出現後可能得不到及時的支持,這大大延長了調試周期。
第二、訪問限制,調試階段的查詢平臺未必對外公開,這增加了讀取方的驗證難度。
如果序列化後的數據人眼可讀,這將大大提高調試效率, XML和JSON就具有人眼可讀的優點。
性能
性能包括兩個方面,時間複雜度和空間複雜度:
第一、空間開銷(Verbosity), 序列化需要在原有的數據上加上描述字段,以爲反序列化解析之用。如果序列化過程引入的額外開銷過高,可能會導致過大的網絡,磁盤等各方面的壓力。對於海量分佈式存儲系統,數據量往往以TB爲單位,巨大的的額外空間開銷意味着高昂的成本。
第二、時間開銷(Complexity),複雜的序列化協議會導致較長的解析時間,這可能會使得序列化和反序列化階段成爲整個系統的瓶頸。
可擴展性/兼容性
移動互聯時代,業務系統需求的更新週期變得更快,新的需求不斷涌現,而老的系統還是需要繼續維護。如果序列化協議具有良好的可擴展性,支持自動增加新的業務字段,而不影響老的服務,這將大大提供系統的靈活度。
安全性/訪問限制
在序列化選型的過程中,安全性的考慮往往發生在跨局域網訪問的場景。當通訊發生在公司之間或者跨機房的時候,出於安全的考慮,對於跨局域網的訪問往往被限制爲基於HTTP/HTTPS的80和443端口。如果使用的序列化協議沒有兼容而成熟的HTTP傳輸層框架支持,可能會導致以下三種結果之一:
第一、因爲訪問限制而降低服務可用性。
第二、被迫重新實現安全協議而導致實施成本大大提高。
第三、開放更多的防火牆端口和協議訪問,而犧牲安全性。
三、序列化和反序列化的組件
典型的序列化和反序列化過程往往需要如下組件:
- IDL(Interface description language)文件:參與通訊的各方需要對通訊的內容需要做相關的約定(Specifications)。爲了建立一個與語言和平臺無關的約定,這個約定需要採用與具體開發語言、平臺無關的語言來進行描述。這種語言被稱爲接口描述語言(IDL),採用IDL撰寫的協議約定稱之爲IDL文件。
- IDL Compiler:IDL文件中約定的內容爲了在各語言和平臺可見,需要有一個編譯器,將IDL文件轉換成各語言對應的動態庫。
- Stub/Skeleton Lib:負責序列化和反序列化的工作代碼。Stub是一段部署在分佈式系統客戶端的代碼,一方面接收應用層的參數,並對其序列化後通過底層協議棧發送到服務端,另一方面接收服務端序列化後的結果數據,反序列化後交給客戶端應用層;Skeleton部署在服務端,其功能與Stub相反,從傳輸層接收序列化參數,反序列化後交給服務端應用層,並將應用層的執行結果序列化後最終傳送給客戶端Stub。
- Client/Server:指的是應用層程序代碼,他們面對的是IDL所生存的特定語言的class或struct。
- 底層協議棧和互聯網:序列化之後的數據通過底層的傳輸層、網絡層、鏈路層以及物理層協議轉換成數字信號在互聯網中傳遞。
序列化組件與數據庫訪問組件的對比
數據庫訪問對於很多工程師來說相對熟悉,所用到的組件也相對容易理解。下表類比了序列化過程中用到的部分組件和數據庫訪問組件的對應關係,以便於大家更好的把握序列化相關組件的概念。
序列化組件 | 數據庫組件 | 說明 |
---|---|---|
IDL | DDL | 用於建表或者模型的語言 |
DL file | DB Schema | 表創建文件或模型文件 |
Stub/Skeleton lib | O/R mapping | 將class和Table或者數據模型進行映射 |
四、幾種常見的序列化和反序列化協議
互聯網早期的序列化協議主要有COM和CORBA。
COM主要用於Windows平臺,並沒有真正實現跨平臺,另外COM的序列化的原理利用了編譯器中虛表,使得其學習成本巨大(想一下這個場景, 工程師需要是簡單的序列化協議,但卻要先掌握語言編譯器)。由於序列化的數據與編譯器緊耦合,擴展屬性非常麻煩。
CORBA是早期比較好的實現了跨平臺,跨語言的序列化協議。COBRA的主要問題是參與方過多帶來的版本過多,版本之間兼容性較差,以及使用複雜晦澀。這些政治經濟,技術實現以及早期設計不成熟的問題,最終導致COBRA的漸漸消亡。J2SE 1.3之後的版本提供了基於CORBA協議的RMI-IIOP技術,這使得Java開發者可以採用純粹的Java語言進行CORBA的開發。
這裏主要介紹和對比幾種當下比較流行的序列化協議,包括XML、JSON、Protobuf、Thrift和Avro。
一個例子
如前所述,序列化和反序列化的出現往往晦澀而隱蔽,與其他概念之間往往相互包容。爲了更好了讓大家理解序列化和反序列化的相關概念在每種協議裏面的具體實現,我們將一個例子穿插在各種序列化協議講解中。在該例子中,我們希望將一個用戶信息在多個系統裏面進行傳遞;在應用層,如果採用Java語言,所面對的類對象如下所示:
class Address
{
private String city;
private String postcode;
private String street;
}
public class UserInfo
{
private Integer userid;
private String name;
private List<Address> address;
}
XML&SOAP
XML是一種常用的序列化和反序列化協議,具有跨機器,跨語言等優點。 XML歷史悠久,其1.0版本早在1998年就形成標準,並被廣泛使用至今。XML的最初產生目標是對互聯網文檔(Document)進行標記,所以它的設計理念中就包含了對於人和機器都具備可讀性。 但是,當這種標記文檔的設計被用來序列化對象的時候,就顯得冗長而複雜(Verbose and Complex)。 XML本質上是一種描述語言,並且具有自我描述(Self-describing)的屬性,所以XML自身就被用於XML序列化的IDL。 標準的XML描述格式有兩種:DTD(Document Type Definition)和XSD(XML Schema Definition)。作爲一種人眼可讀(Human-readable)的描述語言,XML被廣泛使用在配置文件中,例如O/R mapping、 Spring Bean Configuration File 等。
SOAP(Simple Object Access protocol) 是一種被廣泛應用的,基於XML爲序列化和反序列化協議的結構化消息傳遞協議。SOAP在互聯網影響如此大,以至於我們給基於SOAP的解決方案一個特定的名稱–Web service。SOAP雖然可以支持多種傳輸層協議,不過SOAP最常見的使用方式還是XML+HTTP。SOAP協議的主要接口描述語言(IDL)是WSDL(Web Service Description Language)。SOAP具有安全、可擴展、跨語言、跨平臺並支持多種傳輸層協議。如果不考慮跨平臺和跨語言的需求,XML的在某些語言裏面具有非常簡單易用的序列化使用方法,無需IDL文件和第三方編譯器, 例如Java+XStream。
自我描述與遞歸
SOAP是一種採用XML進行序列化和反序列化的協議,它的IDL是WSDL. 而WSDL的描述文件是XSD,而XSD自身是一種XML文件。 這裏產生了一種有趣的在數學上稱之爲“遞歸”的問題,這種現象往往發生在一些具有自我屬性(Self-description)的事物上。
IDL文件舉例
採用WSDL描述上述用戶基本信息的例子如下:
<xsd:complexType name='Address'>
<xsd:attribute name='city' type='xsd:string' />
<xsd:attribute name='postcode' type='xsd:string' />
<xsd:attribute name='street' type='xsd:string' />
</xsd:complexType>
<xsd:complexType name='UserInfo'>
<xsd:sequence>
<xsd:element name='address' type='tns:Address'/>
<xsd:element name='address1' type='tns:Address'/>
</xsd:sequence>
<xsd:attribute name='userid' type='xsd:int' />
<xsd:attribute name='name' type='xsd:string' />
</xsd:complexType>
典型應用場景和非應用場景
SOAP協議具有廣泛的羣衆基礎,基於HTTP的傳輸協議使得其在穿越防火牆時具有良好安全特性,XML所具有的人眼可讀(Human-readable)特性使得其具有出衆的可調試性,互聯網帶寬的日益劇增也大大彌補了其空間開銷大(Verbose)的缺點。對於在公司之間傳輸數據量相對小或者實時性要求相對低(例如秒級別)的服務是一個好的選擇。
由於XML的額外空間開銷大,序列化之後的數據量劇增,對於數據量巨大序列持久化應用常景,這意味着巨大的內存和磁盤開銷,不太適合XML。另外,XML的序列化和反序列化的空間和時間開銷都比較大,對於對性能要求在ms級別的服務,不推薦使用。WSDL雖然具備了描述對象的能力,SOAP的S代表的也是simple,但是SOAP的使用絕對不簡單。對於習慣於面向對象編程的用戶,WSDL文件不直觀。
JSON(Javascript Object Notation)
JSON起源於弱類型語言Javascript, 它的產生來自於一種稱之爲”Associative array”的概念,其本質是就是採用”Attribute-value”的方式來描述對象。實際上在Javascript和PHP等弱類型語言中,類的描述方式就是Associative array。JSON的如下優點,使得它快速成爲最廣泛使用的序列化協議之一:
1、這種Associative array格式非常符合工程師對對象的理解。
2、它保持了XML的人眼可讀(Human-readable)的優點。
3、相對於XML而言,序列化後的數據更加簡潔。 來自於的以下鏈接的研究表明:XML所產生序列化之後文件的大小接近JSON的兩倍。http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
4、它具備Javascript的先天性支持,所以被廣泛應用於Web browser的應用常景中,是Ajax的事實標準協議。
5、與XML相比,其協議比較簡單,解析速度比較快。
6、鬆散的Associative array使得其具有良好的可擴展性和兼容性。
IDL悖論
JSON實在是太簡單了,或者說太像各種語言裏面的類了,所以採用JSON進行序列化不需要IDL。這實在是太神奇了,存在一種天然的序列化協議,自身就實現了跨語言和跨平臺。然而事實沒有那麼神奇,之所以產生這種假象,來自於兩個原因:
第一、Associative array在弱類型語言裏面就是類的概念,在PHP和Javascript裏面Associative array就是其class的實際實現方式,所以在這些弱類型語言裏面,JSON得到了非常良好的支持。
第二、IDL的目的是撰寫IDL文件,而IDL文件被IDL Compiler編譯後能夠產生一些代碼(Stub/Skeleton),而這些代碼是真正負責相應的序列化和反序列化工作的組件。 但是由於Associative array和一般語言裏面的class太像了,他們之間形成了一一對應關係,這就使得我們可以採用一套標準的代碼進行相應的轉化。對於自身支持Associative array的弱類型語言,語言自身就具備操作JSON序列化後的數據的能力;對於Java這強類型語言,可以採用反射的方式統一解決,例如Google提供的Gson。
典型應用場景和非應用場景
JSON在很多應用場景中可以替代XML,更簡潔並且解析速度更快。典型應用場景包括:
1、公司之間傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。
2、基於Web browser的Ajax請求。
3、由於JSON具有非常強的前後兼容性,對於接口經常發生變化,並對可調式性要求高的場景,例如Mobile app與服務端的通訊。
4、由於JSON的典型應用場景是JSON+HTTP,適合跨防火牆訪問。
總的來說,採用JSON進行序列化的額外空間開銷比較大,對於大數據量服務或持久化,這意味着巨大的內存和磁盤開銷,這種場景不適合。沒有統一可用的IDL降低了對參與方的約束,實際操作中往往只能採用文檔方式來進行約定,這可能會給調試帶來一些不便,延長開發週期。 由於JSON在一些語言中的序列化和反序列化需要採用反射機制,所以在性能要求爲ms級別,不建議使用。
IDL文件舉例
以下是UserInfo序列化之後的一個例子:
{"userid":1,"name":"messi","address":[{"city":"北京","postcode":"1000000","street":"wangjingdonglu"}]}
Thrift
Thrift是Facebook開源提供的一個高性能,輕量級RPC服務框架,其產生正是爲了滿足當前大數據量、分佈式、跨語言、跨平臺數據通訊的需求。 但是,Thrift並不僅僅是序列化協議,而是一個RPC框架。相對於JSON和XML而言,Thrift在空間開銷和解析性能上有了比較大的提升,對於對性能要求比較高的分佈式系統,它是一個優秀的RPC解決方案;但是由於Thrift的序列化被嵌入到Thrift框架裏面,Thrift框架本身並沒有透出序列化和反序列化接口,這導致其很難和其他傳輸層協議共同使用(例如HTTP)。
典型應用場景和非應用場景
對於需求爲高性能,分佈式的RPC服務,Thrift是一個優秀的解決方案。它支持衆多語言和豐富的數據類型,並對於數據字段的增刪具有較強的兼容性。所以非常適用於作爲公司內部的面向服務構建(SOA)的標準RPC框架。
不過Thrift的文檔相對比較缺乏,目前使用的羣衆基礎相對較少。另外由於其Server是基於自身的Socket服務,所以在跨防火牆訪問時,安全是一個顧慮,所以在公司間進行通訊時需要謹慎。 另外Thrift序列化之後的數據是Binary數組,不具有可讀性,調試代碼時相對困難。最後,由於Thrift的序列化和框架緊耦合,無法支持向持久層直接讀寫數據,所以不適合做數據持久化序列化協議。
IDL文件舉例
struct Address
{
1: required string city;
2: optional string postcode;
3: optional string street;
}
struct UserInfo
{
1: required string userid;
2: required i32 name;
3: optional list<Address> address;
}
Protobuf
Protobuf具備了優秀的序列化協議的所需的衆多典型特徵:
1、標準的IDL和IDL編譯器,這使得其對工程師非常友好。
2、序列化數據非常簡潔,緊湊,與XML相比,其序列化之後的數據量約爲1/3到1/10。
3、解析速度非常快,比對應的XML快約20-100倍。
4、提供了非常友好的動態庫,使用非常簡介,反序列化只需要一行代碼。
Protobuf是一個純粹的展示層協議,可以和各種傳輸層協議一起使用;Protobuf的文檔也非常完善。 但是由於Protobuf產生於Google,所以目前其僅僅支持Java、C++、Python三種語言。另外Protobuf支持的數據類型相對較少,不支持常量類型。由於其設計的理念是純粹的展現層協議(Presentation Layer),目前並沒有一個專門支持Protobuf的RPC框架。
典型應用場景和非應用場景
Protobuf具有廣泛的用戶基礎,空間開銷小以及高解析性能是其亮點,非常適合於公司內部的對性能要求高的RPC調用。由於Protobuf提供了標準的IDL以及對應的編譯器,其IDL文件是參與各方的非常強的業務約束,另外,Protobuf與傳輸層無關,採用HTTP具有良好的跨防火牆的訪問屬性,所以Protobuf也適用於公司間對性能要求比較高的場景。由於其解析性能高,序列化後數據量相對少,非常適合應用層對象的持久化場景。
它的主要問題在於其所支持的語言相對較少,另外由於沒有綁定的標準底層傳輸層協議,在公司間進行傳輸層協議的調試工作相對麻煩。
IDL文件舉例
message Address
{
required string city=1;
optional string postcode=2;
optional string street=3;
}
message UserInfo
{
required string userid=1;
required string name=2;
repeated Address address=3;
}
Avro
Avro的產生解決了JSON的冗長和沒有IDL的問題,Avro屬於Apache Hadoop的一個子項目。 Avro提供兩種序列化格式:JSON格式或者Binary格式。Binary格式在空間開銷和解析性能方面可以和Protobuf媲美,JSON格式方便測試階段的調試。 Avro支持的數據類型非常豐富,包括C++語言裏面的union類型。Avro支持JSON格式的IDL和類似於Thrift和Protobuf的IDL(實驗階段),這兩者之間可以互轉。Schema可以在傳輸數據的同時發送,加上JSON的自我描述屬性,這使得Avro非常適合動態類型語言。 Avro在做文件持久化的時候,一般會和Schema一起存儲,所以Avro序列化文件自身具有自我描述屬性,所以非常適合於做Hive、Pig和MapReduce的持久化數據格式。對於不同版本的Schema,在進行RPC調用的時候,服務端和客戶端可以在握手階段對Schema進行互相確認,大大提高了最終的數據解析速度。
典型應用場景和非應用場景
Avro解析性能高並且序列化之後的數據非常簡潔,比較適合於高性能的序列化服務。
由於Avro目前非JSON格式的IDL處於實驗階段,而JSON格式的IDL對於習慣於靜態類型語言的工程師來說不直觀。
IDL文件舉例
protocol Userservice {
record Address {
string city;
string postcode;
string street;
}
record UserInfo {
string name;
int userid;
array<Address> address = [];
}
}
所對應的JSON Schema格式如下:
{
"protocol" : "Userservice",
"namespace" : "org.apache.avro.ipc.specific",
"version" : "1.0.5",
"types" : [ {
"type" : "record",
"name" : "Address",
"fields" : [ {
"name" : "city",
"type" : "string"
}, {
"name" : "postcode",
"type" : "string"
}, {
"name" : "street",
"type" : "string"
} ]
}, {
"type" : "record",
"name" : "UserInfo",
"fields" : [ {
"name" : "name",
"type" : "string"
}, {
"name" : "userid",
"type" : "int"
}, {
"name" : "address",
"type" : {
"type" : "array",
"items" : "Address"
},
"default" : [ ]
} ]
} ],
"messages" : { }
}
五、Benchmark以及選型建議
Benchmark
以下數據來自https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
解析性能
序列化之空間開銷
從上圖可得出如下結論:
1、XML序列化(Xstream)無論在性能和簡潔性上比較差。
2、Thrift與Protobuf相比在時空開銷方面都有一定的劣勢。
3、Protobuf和Avro在兩方面表現都非常優越。
選型建議
以上描述的五種序列化和反序列化協議都各自具有相應的特點,適用於不同的場景:
1、對於公司間的系統調用,如果性能要求在100ms以上的服務,基於XML的SOAP協議是一個值得考慮的方案。
2、基於Web browser的Ajax,以及Mobile app與服務端之間的通訊,JSON協議是首選。對於性能要求不太高,或者以動態類型語言爲主,或者傳輸數據載荷很小的的運用場景,JSON也是非常不錯的選擇。
3、對於調試環境比較惡劣的場景,採用JSON或XML能夠極大的提高調試效率,降低系統開發成本。
4、當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具有一定的競爭關係。
5、對於T級別的數據的持久化應用場景,Protobuf和Avro是首要選擇。如果持久化後的數據存儲在Hadoop子項目裏,Avro會是更好的選擇。
6、由於Avro的設計理念偏向於動態類型語言,對於動態語言爲主的應用場景,Avro是更好的選擇。
7、對於持久層非Hadoop項目,以靜態類型語言爲主的應用場景,Protobuf會更符合靜態類型語言工程師的開發習慣。
8、如果需要提供一個完整的RPC解決方案,Thrift是一個好的選擇。
9、如果序列化之後需要支持不同的傳輸層協議,或者需要跨防火牆訪問的高性能場景,Protobuf可以優先考慮。
參考文獻:
http://www.codeproject.com/Articles/604720/JSON-vs-XML-Some-hard-numbers-about-verbosity
https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
http://en.wikipedia.org/wiki/Serialization
http://en.wikipedia.org/wiki/Soap
http://en.wikipedia.org/wiki/XML
http://en.wikipedia.org/wiki/JSON
http://avro.apache.org/
http://www.oracle.com/technetwork/java/rmi-iiop-139743.html