来源1
来源2
RPC,全称为Remote Procedure Call,即远程过程调用,它是一个计算机通信协议。它允许像调用本地服务一样调用远程服务。它可以有不同的实现方式。如RMI(远程方法调用)、Hessian、Http invoker等。另外,RPC是与语言无关的。
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行时,我们实际上执行了以下操作:
l_times_r
以上5步就是执行本地调用的过程。
在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,Multiply是在另一个进程中执行的。这就带来了几个新问题:
{函数 <--> Call ID}
的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
所以,要实现一个RPC框架,其实只需要把以上三点实现了就基本完成了。Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的。网络传输库可以自己写socket,或者用asio,ZeroMQ,Netty之类。
原作者实现的一个RPC框架tinyrpc
rpc框架做的最重要的一件事情就是封装,调用者和被调用者的通讯细节,客户端代理负责向调用方法的方法名参数返回值包等信息根据通信协议组织成报文发送给服务端,服务端解析报文,根据客户端传递的信息执行对应的方法,然后将返回值安装协议组织成报文发送给客户端,客户端再解析出来。RPC屏蔽了底层的实现细节,让调用者无需关注网络通信,数据传输等细节。
RPC的核心原理:RPC能够让本地应用简单、高效地调用服务器中的过程(服务)。它主要应用在分布式系统。如Hadoop中的IPC组件。但怎样实现一个RPC框架呢?
从下面几个方面思考,仅供参考:
来源:一个简单RPC框架是如何炼成的
试图通过简单的编程来模拟和解释RPC的原理和过程,不是真实的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消息已经初具雏形,不过
既然是要带参数,那只能扩展原来的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
实际的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
(原作者写的不全,现在还没把完整的还原出来代码,先放在这里,以后再补)
接下来处理数据传输。实际应用场景一般都是基于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()
总结,引入传输层的意义在于
接下来处理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': >, 'whoAreYou': >, '_kaos': >}}
>
详细说明 一下原理,利用了类似的反射的技术。有兴趣的同学可以先去了解一下inspect
register_class
表示自动搜索一个类对象中的成员方法,并将其作为server端的rpc方法注册进去。__init__
, __kaos
,_kaos
之类的系统固有方法,或者私有方法,会自动剔除。if not (hasattr(obj, '__class__') and inspect.isclass(obj.__class__)):
return False
类实例的特点就是,包含__class__
成员,而且__class__
成员的值就是该类的类对象。inspect.isclass就是检测是不是类对象
inspect.getmembers()
返回的是类对象的所有成员,包括系统固有方法以及私有方法register_fucntion
, register_method
与普通的服务注册基本一样。就是添加(key,value)对。总结:
来源:
下面使用比较原始的方案实现RPC框架,采用Socket通信、动态代理与反射与Java原生的序列化。
RPC架构分为三部分:
服务提供者接口定义与实现,代码如下:
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
RPC本质为消息处理模型,RPC屏蔽了底层不同主机间的通信细节,让进程调用远程的服务就像是本地的服务一样。
这里实现的简单RPC框架是使用Java语言开发,与Java语言高度耦合,并且通信方式采用的Socket是基于BIO实现的,IO效率不高,还有Java原生的序列化机制占内存太多,运行效率也不高。可以考虑从下面几种方法改进。
另外可参考:
简单的RPC java实现
自定义的RPC的Java实现
三百行代码完成一个简单的rpc框架
远程调用一般分为两种,远程过程调用(RPC)和远程方法调用(RMI)。
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看一下这期间传递的数据是什么样子的,请求的数据:
<methodCall>
<methodName>
add
methodName>
<params>
<param>
<value>
<int> 3 int>
value>
param>
<param>
<value>
<int> 4 int>
value>
param>
params>
methodCall>
响应的数据:
<methodResponse>
<params>
<param>
<value>
<int> 7 int>
value>
param>
params>
methodResponse>
其他python实现的RPC服务例子:
python 简单RPC示例
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.。
Netty入门教程——认识Netty
Netty入门(一):零基础“HelloWorld”详细图文步骤
Netty 4.x User Guide 中文翻译《Netty 4.x 用户指南》
Essential Netty in Action 《Netty 实战(精髓)》
Netty官网
Jetty官方文档翻译
Jetty:The Definitive Reference
补充知识
来源:allwefantasy/ServiceFramework
来源
接口文档
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
java.io.ObjectOutputStream:表示对象输出流
它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream:表示对象输入流
它的readObject()方法从输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。
只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常。
假定一个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)的方法进行反序列化。
步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));
需要一个文件来存放序列化的数据
步骤二:通过对象输出流的writeObject()方法写对象:
out.writeObject(“Hello”);
out.writeObject(new Date());
步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\\objectfile.obj”));
步骤二:通过对象输出流的readObject()方法读取对象:
String obj1 = (String)in.readObject();
Date obj2 = (Date)in.readObject();
说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。
为了更好地理解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模式中很是有用;二是可以对象数据的远程通信。
模块讲解----pickle模块(只在python用的序列化与反序列化)
特点
pickle库的使用详解
Python数据存储:pickle模块的使用讲解
#序列化和反序列化
来源:刘丁 ·2015-02-26 10:00
##摘要
序列化和反序列化几乎是工程师们每天都要面对的事情,但是要精确掌握这两个概念并不容易:一方面,它们往往作为框架的一部分出现而湮没在框架之中;另一方面,它们会以其他更容易理解的概念出现,例如加密、持久化。然而,序列化和反序列化的选型却是系统设计或重构一个重要的环节,在分布式、大数据量系统设计里面更为显著。恰当的序列化协议不仅可以提高系统的通用性、强健性、安全性、优化系统性能,而且会让系统更加易于调试、便于扩展。本文从多个角度去分析和讲解“序列化和反序列化”,并对比了当前流行的几种序列化协议,期望对读者做序列化选型有所帮助。
##简介
文章作者服务于美团推荐与个性化组,该组致力于为美团用户提供每天billion级别的高质量个性化推荐以及排序服务。从Terabyte级别的用户行为数据,到Gigabyte级别的Deal/Poi数据;从对实时性要求毫秒以内的用户实时地理位置数据,到定期后台job数据,推荐与重排序系统需要多种类型的数据服务。推荐与重排序系统客户包括各种内部服务、美团客户端、美团网站。为了提供高质量的数据服务,为了实现与上下游各系统进行良好的对接,序列化和反序列化的选型往往是我们做系统设计的一个重要考虑因素。
本文内容按如下方式组织:
##一、定义以及相关概念
互联网的产生带来了机器间通讯的需求,而互联通讯的双方需要采用约定的协议,序列化和反序列化属于通讯协议的一部分。通讯协议往往采用分层模型,不同模型每层的功能定义以及颗粒度不同,例如: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 | 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;
}
###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描述上述用户基本信息的例子如下:
典型应用场景和非应用场景
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;
}
###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 = [];
}
}
所对应的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