目录
1 RPC Basics
PRC(Remote Procedure Call,远程过程调用):是一个计算机通信协议。该协议允许client的程序通过internet调用server,就像在本地调用server一样。
RPC区别与HTTP或者SMTP有以下3个方面:
-
RPC的调用目的是宽泛的,由用户定义。而HTTP调用目的是获取documents,SMTP是发送messgage。
-
RPC的函数是宽泛的,由用户定义。而HTTP预定义了一些基本函数,如GET,PUT等;SMTP如EHLO,MAIL等。
-
RPC的客户端和服务端的代码和本地函数调用无异。
RPC有以下5个特点:
-
RPC调用目的的宽泛性使得它能支持的数据格式是严格的。对于OOP语言,object有时候也可以在RPC上先序列化后反序列化来传输。但是实时的client的object,像file,socket,就不能传输。
-
RPC的server可以将异常发回给client。
-
RPC提供introspection机制。client和server在连接初始会交换双方共同支持的函数即其参数。
-
RPC提供寻址机制。有些client的远程调用会自动寻找server。
-
RPC提供身份认证,权限控制。不同用户可以在server上有不同的函数和data access。
1.1 XML-RPC
1.1.1 Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import operator, math
from xmlrpc.server import SimpleXMLRPCServer
from functools import reduce
def main():
server = SimpleXMLRPCServer(('127.0.0.1',7001)) #create ROC Server
server.register_introspection_functions() #enable introspection
server.register_multicall_functions() #enable mutlcall
server.register_function(addtogether) #register 3 functions
server.register_function(quadratic)
server.register_function(remote_repr)
print("Server ready")
server.serve_forever() #server run forever
def addtogether(*things):
'''Add together everying in the list `things`.'''
return reduce(operator.add,things)
def quadratic(a,b,c):
'''Determing `x` values satisfying `a` *x*x + `b`*x + c == 0 '''
b24ac = math.sqrt(b*b-4.0*a*c)
return list(set([(-b-b24ac)/2.0*a, (-b+b24ac)/2.0*a])) #RPC function can only return 1 argument or list
def remote_repr(arg):
'''Return the `repr()` rendering of the supplied `arg`.'''
return arg
if __name__ == '__main__':
main()
1.1.2 Client
1.1.2.1 列举Server函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import xmlrpc.client
def main():
proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001')
print('Here are the functions supported by this server:')
for method_name in proxy.system.listMethods():
if method_name.startswith('system.'):
continue
signatures = proxy.system.methodSignature(method_name)
if isinstance(signatures,list) and signatures:
for signature in signatures:
print('%s(%s)' % (method_name,signature))
else:
print('%s(...)' % (method_name,))
method_help = proxy.system.methodHelp(method_name)
if method_help:
print(' ',method_help)
if __name__ == '__main__':
main()
1.1.2.2 调用server函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xmlrpc.client
def main():
proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001')
print(proxy.addtogether('x','y','z'))
print(proxy.addtogether(20,30,4,1))
print(proxy.quadratic(2,-4,0))
print(proxy.quadratic(1,2,1))
print(proxy.remote_repr((1,2.0,'three')))
print(proxy.remote_repr([1,2.0,'three']))
print(proxy.remote_repr({'name': 'Arthur',
'data':{'age':42,'sex':'M'}}))
print(proxy.quadratic(1,0,1))
if __name__ == '__main__':
main()
通过上面代码,可见RPC函数有以下几个特点:
-
函数参数类型没有限制。可以是number或者string,个数可以变化。
-
函数返回参数个数只能是1。
-
输入和返回参数支持的sequence类型只有1种,即list。
-
复杂数据结构可以是递归的,例如上面的dict。
-
server的error可以传递回client。
1.1.2.3 调用server函数,一次性打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xmlrpc.client
def main():
proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001')
multicall = xmlrpc.client.MultiCall(proxy)
multicall.addtogether('x','y','z')
multicall.addtogether(20,30,4,1)
multicall.quadratic(2,-4,0)
multicall.remote_repr({'name': 'Arthur',
'data':{'age':42,'sex':'M'}})
for answer in multicall():
print(answer)
if __name__ == '__main__':
main()
当将多个client的function call打包成1个时,我们可以看到server端只收到1此http连接。
1
localhost - - [04/Oct/2010 00:16:19] "POST /RPC2 HTTP/1.0" 200 -
1.1.3 XML Based Request & Response
对于xml-RPC的Client的请求和Server的reponse到底是什么样子,我们可以通过下面的例子一窥究竟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# the first call to quadratic() that the sample client program makes:
<?xml version='1.0'?>
<methodCall>
<methodName>quadratic</methodName>
338
<params>
<param>
<value><int>2</int></value>
</param>
<param>
<value><int>-4</int></value>
</param>
<param>
<value><int>0</int></value>
</param>
</params>
</methodCall>
#The response to the preceding call looks like this:
<?xml version='1.0'?>
<methodResponse>
<params>
<param>
<value><array><data>
<value><double>0.0</double></value>
<value><double>8.0</double></value>
</data></array></value>
</param>
</params>
</methodResponse>
1.2 JSON-RPC
上面xML-RPC传输的数据看起来太过臃肿,而JSON-RPC就显得简洁多了。
1.2.1 Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
def main():
server = SimpleJSONRPCServer(('127.0.0.1',7001)) #create JSON Server
server.register_introspection_functions() #enable introspection
server.register_function(lengths)
print('server in running......')
server.serve_forever() #run forever
def lengths(*args):
'''Measure the length of each argument'''
results = []
for arg in args:
try:
arglen = len(arg)
except TypeError:
arglen = None
results.append((arg,arglen))
return results
if __name__ == '__main__':
main()
1.2.2 Client
1
2
3
4
5
6
7
8
from jsonrpclib import Server
def main():
proxy = Server('http://127.0.0.1:7001')
print(proxy.lengths(*['hi',27,'itsgoodday']))
if __name__ == '__main__':
main()
1.2.3 JSON Based Request & Response
用wireshark可以嗅探到Request和Response如下,比XML简洁多了。
1
2
3
4
5
{"version": "1.1",
"params": [[1, 2, 3], 27, {"Rigel": 0.12, "Sirius": -1.46}],
"method": "lengths"}
{"result": [[3, [1, 2, 3]], [null, 27],
[2, {"Rigel": 0.12, "Sirius": -1.46}]]}
1.3 Self-Documenting Data
略
2 RPC Advanced
在RPC上传递Object比较复杂和tricky:
-
Object在不同语言里(Client和Server在不同语言下运行的情况)的语义不一样(方法,属性),难以统一。
-
Object的数据传递多少是个问题。全部传递开销太大;部分传递,如何选择?
关于在RPC上传递Object,业内有两个主导方法SOAP和CORBA。两者都比较复杂,需要一整本书来讨论。
但是对于Client和Server都是Python语言来说,却有两个简单的方法。
-
Pyro,建立在pickle模块上,任何pickle-able的object都可以在client和server里传(unpickle-able的object也可以通过实现具体方法变成pickle-able);
-
RPyc,这个更像CORBA,比Pyro复杂。在client和server间传递的object不是object本身,而是其ID。当然这种情况下网络traffic和security的要求就更高。
2.1 Talking About Objects: Pyro and RPyC
2.1.1 RPyC Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import rpyc
def main():
from rpyc.utils.server import ThreadedServer
t = ThreadedServer(MyService, port = 18861)
t.start()
class MyService(rpyc.Service):
def exposed_line_counter(self, fileobj, function): # let client call the exposed_method
print('Client has invoked exposed_line_counter()') # pass the filobj ID back to client to call the function.
for linenum, line in enumerate(fileobj.readlines()):
function(line)
return linenum+1
if __name__ == '__main__':
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import rpyc
def main():
config = {'allow_public_attrs': True}
proxy = rpyc.connect('localhost',18861,config=config)
fileobj = open('testfile.txt')
linecount = proxy.root.line_counter(fileobj,noisy)
print('The number of lines in the file was', linecount)
def noisy(string):
print('Noisy:',repr(string))
if __name__ == '__main__':
main()
2.2 RPC, Web Frameworks, and Message Queues
Python中的xmlrpc模块实际上对于想要使用RPC的开发者来说很少使用到。通常他们将RPC服务集成到大型网站而不是运行1个单独的port。下面介绍3种方法集成到网站的方法:
-
在WSGI安装RPC插件。
-
某些web framework已经实现了RPC库。
-
Message Queuqe。
2.3 Recovering From Netowrk Errors
网络问题是RPC比较复杂的1个问题,有可能函数已完成调用,server数据库已更改,但是网络问题导致client以为request fail,重复1遍request。因此我们通常将request设成幂等的,也就是说多次操作对server影响一样,比如“perform transaction 312312 to remove ¥10”就比“remove ¥10”安全(前者幂等,后者会多次扣除¥10)。同时将根据子任务来使用try:except
比根据函数来使用更有指导性。