代码地址:https://github.com/brandon-rhodes/fopnp/tree/m/

本书在kindle显示为乱码。

背景:

https://github.com/brandon-rhodes/fopnp/tree/m/playground

以下每台机器可以用docker容器实现。

 

 

 



RPC



RPC简介

远程过程调用(Remote Procedure Call,RPC)是IPC(inter-process communication)的一种,该协议允许运行程序调用另一地址空间(通常是共享网络的另一计算机)的子程序,就像调用本地过程一样。面向对象中常称为远程方法调用RMI(remote method invocation)或远程方法调用。 多用于分布式计算。调用通常比本地慢、可靠性也差些。

 

RPC的作用:

  • 不改变代码进行水平扩展
  • 获取网络上磁盘或主机特定的信息。

最初rpc主要用c语言开发,基于二进制传输,类似python的struct,需要知道定义的头文件,且容易发生错误和crash。随着硬件和网络的加速,逐渐改为可读性更好的格式。

老协议发送的数据可能如下:

0, 0, 0, 1, 64, 36, 0, 0, 0, 0, 0, 0

新协议不需要头文件:

<params>
<param><value><i4>41</i4></value></param>
<param><value><double>10.</double></value></param>
</params>

JSON则更加简化。

[1, 10.0]

 

 

RPC协议特殊的地方:

  • 弱语义:调用的语义不强,含义由API自己确定。基础数据类型:整型、浮点型、字符串、列表
  • 调用方法,但是不对方法进行定义。
  • 调用和普通方法或函数类似。

RPC的特点如下:

  • 传递的数据类型有限。只支持位置参数,因为需要跨语言。 一般只支持数值和字符串、序列或列表、结构体或联合数组。 操作系统相关的文件,live socket、shared memory通常不能传递。
  • 异常处理:一般要定义错误信息,而不是简单地打印堆栈。 因为客户端未必有服务器端的代码,打印出这些堆栈意义不大,反而有可能暴露安全问题。
  • 有些内省支持,以查看支持的的调用及参数。python因为是动态类型,内省支持较差。
  • 有些有寻址支持
  • 有些有认证、访问控制 ,不过大多还是基于HTTP。

 



XML-RPC

通常不会使用XML-RPC,因为它比较笨重。因为它是首批基于HTTP的RPC协议,python提供了内置支持。

标准参见:http://xmlrpc.scripting.com/spec.html。
数据类型: int ;  float ;  unicode ;  list ;  dict with  unicode keys; with nonstandard extensions,  datetime
and  None
库:xmlrpclib ,  SimpleXMLRPCServer ,  DocXMLRPCServer

xmlrpc_server.py


#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_server.py
# XML-RPC server

import operator, math
from xmlrpc.server import SimpleXMLRPCServer
from functools import reduce

def main():
    server = SimpleXMLRPCServer(('127.0.0.1', 7001))
    server.register_introspection_functions()
    server.register_multicall_functions()
    server.register_function(addtogether)
    server.register_function(quadratic)
    server.register_function(remote_repr)
    print("Server ready")
    server.serve_forever()

def addtogether(*things):
    """Add together everything in the list `things`."""
    return reduce(operator.add, things)

def quadratic(a, b, c):
    """Determine `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 ]))

def remote_repr(arg):
    """Return the `repr()` rendering of the supplied `arg`."""
    return arg

if __name__ == '__main__':
    main()


客户端使用内省:

 

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_introspect.py
# XML-RPC client

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('{0}({1})'.format(method_name, signature))
        else:
            print('{0}(...)'.format(method_name,))

        method_help = proxy.system.methodHelp(method_name)
        if method_help:
            print('  ', method_help)

if __name__ == '__main__':
    main()


客户端调用:


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_client.py
# XML-RPC client

import xmlrpc.client

def main():
    proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001')
    print(proxy.addtogether('x', 'ÿ', '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()


执行结果:


# python3 xmlrpc_client.py
x?z
55
[0.0, 8.0]
[-1.0]
[1, 2.0, 'three']
[1, 2.0, 'three']
{'name': 'Arthur', 'data': {'age': 42, 'sex': 'M'}}
Traceback (most recent call last):
  File "xmlrpc_client.py", line 22, in <module>
    main()
  File "xmlrpc_client.py", line 19, in main
    print(proxy.quadratic(1, 0, 1))
  File "/opt/python3.5/lib/python3.5/xmlrpc/client.py", line 1091, in __call__
    return self.__send(self.__name, args)
  File "/opt/python3.5/lib/python3.5/xmlrpc/client.py", line 1431, in __request
    verbose=self.__verbose
  File "/opt/python3.5/lib/python3.5/xmlrpc/client.py", line 1133, in request
    return self.single_request(host, handler, request_body, verbose)
  File "/opt/python3.5/lib/python3.5/xmlrpc/client.py", line 1149, in single_request
    return self.parse_response(resp)
  File "/opt/python3.5/lib/python3.5/xmlrpc/client.py", line 1321, in parse_response
    return u.close()
  File "/opt/python3.5/lib/python3.5/xmlrpc/client.py", line 654, in close
    raise Fault(**self._stack[0])
xmlrpc.client.Fault: <Fault 1: "<class 'ValueError'>:math domain error">

一次传递多个调用

 


#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_multicall.py
# XML-RPC client performing a multicall

import xmlrpc.client

def main():
    proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001')
    multicall = xmlrpc.client.MultiCall(proxy)
    multicall.addtogether('a', 'b', 'c')
    multicall.quadratic(2, -4, 0)
    multicall.remote_repr([1, 2.0, 'three'])
    for answer in multicall():
        print(answer)

if __name__ == '__main__':
    main()

可以在服务器端的日志看到只有一条记录。SimpleXMLRPCServer不支持认证和TLS安全机制等。

实际往返的数据,对 quadratic()而言如下:

 

<?xml version='1.0'?>
<methodCall>
    <methodName>quadratic</methodName>
    <params>
        <param>
            <value><int>2</int></value>
        </param>
        <param>
            <value><int>-4</int></value>
        </param>
        <param>
            <value><int>0</int></value>
        </param>
    </params>
</methodCall>


<?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>

 



JSON-RPC

标准参见:  http://json-rpc.org/wiki/specification
数据类型: int ;  float ;  unicode ;  list ;  dict with  unicode keys;  None
库:many third-party, including  jsonrpclib

python标准库不支持jsonrpc。

jsonrpclib是流行的 jsonrpc库之一,在python3,名为jsonrpclib-pelix。

服务器端:jsonrpc_server.py

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/jsonrpc_server.py
# JSON-RPC server needing "pip install jsonrpclib-pelix"

from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer

def lengths(*args):
    """Measure the length of each input argument.

    Given N arguments, this function returns a list of N smaller
    lists of the form [len(arg), arg] that each state the length of
    an input argument and also echo back the argument itself.

    """
    results = []
    for arg in args:
        try:
            arglen = len(arg)
        except TypeError:
            arglen = None
        results.append((arglen, arg))
    return results

def main():
    server = SimpleJSONRPCServer(('localhost', 7002))
    server.register_function(lengths)
    print("Starting server")
    server.serve_forever()

if __name__ == '__main__':
    main()

客户端:jsonrpc_client.py


#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/jsonrpc_client.py
# JSON-RPC client needing "pip install jsonrpclib-pelix"

from jsonrpclib import Server

def main():
    proxy = Server('http://localhost:7002')
    print(proxy.lengths((1,2,3), 27, {'Sirius': -1.46, 'Rigel': 0.12}))

if __name__ == '__main__':
    main()

wireshark的抓包结果:


{"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}]]

 JSON-RPC还可以给每个请求增加id,这样可以同时发送多个请求。