Python下的rpc开发


1、什么是rpc

  • RPC(Remote Procedure Call)远程过程调用,简而言之,一个节点请求另一个节点提供的服务。
  • 对应rpc的是本地过程调用,函数调用就是最常见的本地过程调用。
  • 将本地过程调用变成远程调用会面临各种问题。

1.1 本地过程调用

def add(a, b):
    total = a + b
    return total


print(add(1, 2))

# 函数调用过程:
# 1.将1和2压入add函数的栈
# 2.进入add函数,从栈中取出1和2分别赋给a和b
# 3.执行a + b将结果赋值给局部变量total并压入栈
# 4.将栈中的值取出来赋值给全局total

1.2 远程过程面临的问题

1.2.1 Call ID映射问题

在本地调用中,函数体是直接通过函数指针来指定的,我们调用函数,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。

1.2.2 序列化与反序列化

客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。

1.2.3 网络传输

远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。

1.3 rpc调用过程

Python转做RPA python开发rpa_客户端

2、rpc、http和restful之间的区别

2.1 rpc和http的区别

http协议只是我们实现rpc的一种选择而已,你可以选择或不选择,两者并不是竞争关系、

2.2 rpc和restful的区别

两者并不是互斥的,一般我们的服务想要对外提供服务的时候一般采取的是http请求,但是我们这么多接口按照什么规范放出去呢,这就是restful,其本质就是一种规范而已。rpc一般是系统内部调用,其中rpc的协议灵活性会使我们的传输性能更高。

3、通过httpserver实现rpc

明确两点:

  1. 一定会发起一个网络请求
  2. 一定会有网络连接(tcp/udp)

3.1 利用Python自带的httpServer启动服务

server:remote_add.py

from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qsl
import json

host = ('', 8003)


class AddHandle(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_url = urlparse(self.path)
        qs = dict(parse_qsl(parsed_url.query))
        a = int(qs.get("a", 0))
        b = int(qs.get("b", 0))
        self.send_response(200)
        self.send_header('Content-type', "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(
            {
                "result": a + b
            }
        ).encode("utf-8"))


if __name__ == '__main__':
    server = HTTPServer(host, AddHandle)
    print("Starting server, listen at: %s:%s" % host)
    server.serve_forever()

3.2 自定义Client,发起请求

提前下载request包,用于发起http请求
命令:pip install requests -i https://pypi.douban.com/simple

client:rpc_client_1.py

import requests

rsp = requests.get("http://127.0.0.1:8003?a=1&b=2")

print(rsp.text)

Python转做RPA python开发rpa_微服务_02


client:rpc_client_2.py(改进版本)

import requests
import json


class Client:
    def __init__(self, url):
        self.url = url

    def add(self, a, b):
        rsp = requests.get(f"{self.url}?a={a}&b={b}")
        return json.loads(rsp.text).get("result")


client = Client("http://127.0.0.1:8003")
print(client.add(1, 2))
print(client.add(2, 3))

Python转做RPA python开发rpa_python_03

4、rpc开发的要素分析

4.1 rpc开发的四大要素:

  • 客户端(Client):服务调用发起方,也称为服务消费者
  • 客户端存根(Client stub):该程序运行在客户端所在的计算机器上,主要用来调用服务器地址。另外,该程序还负责将客户端请求远程服务器程序的数据信息法宝成数据包,通过网络发送给服务端Stub,并解析返回给客户端
  • 服务端(Server):远端的计算机上运行的程序,其中有客户端需调用的方法
  • 服务端存根(Server Stub):接收客户端Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务器执行调用的结果进行数据处理打包发送给客户端Stub程序

4.2 rpc原理图

Python转做RPA python开发rpa_rpc_04

5、基于xml的rpc的调用

xml_rpc_server.py

from xmlrpc.server import SimpleXMLRPCServer


class Calculate:
    def add(self, x, y):
        return x + y

    def multiply(self, x, y):
        return x * y

    def subtract(self, x, y):
        return abs(x - y)

    def divide(self, x, y):
        return x / y


obj = Calculate()
server = SimpleXMLRPCServer(("localhost", 8088))
# 将实例注册给rpc server
server.register_instance(obj)
print("Listening on port 8088")
server.serve_forever()

xml_rpc_client.py

from xmlrpc import client

server = client.ServerProxy("http://127.0.0.1:8088")
print(server.add(1, 2))
print(server.multiply(2, 3))

Python转做RPA python开发rpa_rpc_05

6、基于json实现rpc的调用


6.1 安装

pip install jsonrpclib-pelix -i https://pypi.douban.com/simple

6.2 测试

json_rpc_server.py

from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer


def add(a, b):
    return a + b


# 实例化server
server = SimpleJSONRPCServer(('localhost', 8080))
# 将函数注册到server中
server.register_function(add)
# 启动server服务
server.serve_forever()

json_rpc_client.py

import jsonrpclib

server = jsonrpclib.ServerProxy('http://localhost:8080')
print(server.add(5, 6))

Python转做RPA python开发rpa_微服务_06

7、基于zerorpc实现rpc的调用


7.1 简介说明

  • zerorpc 是利用 zeroMQ消息队列 + msgpack 消息序列化(二进制) 来实现类似 grpc 的功能,跨语言远程调用。
  • 主要使用到 zeroMQ 的通信模式是 ROUTER–DEALER,模拟 grpc 的 请求-响应式 和 应答流式 RPC
  • zerorpc 还支持 PUB-SUB 通信模式的远程调用
  • zerorpc实际上会依赖msgpack-python, pyzmq, future, greenlet, gevent

7.2 安装

pip install zerorpc

7.3 一元调用

zerorpc_one_server.py

import zerorpc


class HelloRPC(object):
    def hello(self, name):
        return "Hello, %s" % name


# 1.实例化server
s = zerorpc.Server(HelloRPC())
# 绑定业务代码到server中
s.bind("tcp://0.0.0.0:4242")
# 启动服务
s.run()

zerprpc_one_client.py

import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")
print(c.hello("jason"))

7.4 流式响应

zerorpc_stream_server.py

import zerorpc


class StreamingRPC(object):
    @zerorpc.stream  # @zerorpc.stream这里的函数修饰是必须的,否则会有异常,如TypeError: can’t serialize
    def streaming_range(self, fr, to, step):
        return range(fr, to, step)


s = zerorpc.Server(StreamingRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()

zerorpc_stream_client.py

import zerorpc

c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")

for item in c.streaming_range(10, 20, 2):
    print(item)

Python转做RPA python开发rpa_微服务_07

7.5 zerorpc调用流程

Python转做RPA python开发rpa_python_08