Go+Python双语言混合开发 第三部分 grpc入门和进阶 Go开发学习 第1章 python下的rpc开发 学习笔记

981 阅读19分钟

Go+Python双语言混合开发 第三部分 grpc入门和进阶 Go开发学习 第1章 python下的rpc开发 学习笔记

第三部分 grpc入门和进阶

最后附上视频下载地址

第1章 python下的rpc开发

python下的rpc开发

1.1 什么是rpc

远程过程调用带来的新问题

在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,add是在另一个进程中执行的。这就带来了几个新问题:

  1. Call ID映射。我们怎么告诉远程机器我们要调用add,而不是sub或者Foo呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用add,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <--> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
  2. 序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
  3. 网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。

解决了上面三个机制,就能实现RPC了,具体过程如下:

client端解决的问题:

1. 将这个调用映射为Call ID。这里假设用最简单的字符串当Call ID的方法
2. 将Call ID,a和b序列化。可以直接将它们的值以二进制形式打包
3. 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
4. 等待服务器返回结果
4. 如果服务器调用成功,那么就将结果反序列化,并赋给total

server端解决的问题

1. 在本地维护一个Call ID到函数指针的映射call_id_map,可以用dict完成
2. 等待请求,包括多线程的并发处理能力
3. 得到一个请求后,将其数据包反序列化,得到Call ID
4. 通过在call_id_map中查找,得到相应的函数指针
5. 将a和rb反序列化后,在本地调用add函数,得到结果
6. 将结果序列化后通过网络返回给Client

在上面的整个流程中,估计有部分同学看到了熟悉的计算机网络的流程和web服务器的定义。

所以要实现一个RPC框架,其实只需要按以上流程实现就基本完成了。

其中:

  • Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。
  • 序列化反序列化可以自己写,也可以使用Protobuf或者FlatBuffers之类的。
  • 网络传输库可以自己写socket,或者用asio,ZeroMQ,Netty之类。

实际上真正的开发过程中,除了上面的基本功能以外还需要更多的细节:网络错误、流量控制、超时和重试等。

最后提一个问题: 如何将远程的这些过程写出本地函数调用的感觉来?

1.2 rpc、http以及restful 之间的区别

1.2.1 RPC 和 REST 区别是什么?

你一定会觉得这个问题很奇怪,是的,包括我,但是你在网络上一搜,会发现类似对比的文章比比皆是,我在想可能很多初学者由于基础不牢固,才会将不相干的二者拿出来对比吧。既然是这样,那为了让你更加了解陌生的RPC,就从你熟悉得不能再熟悉的 REST 入手吧。

REST,是Representational State Transfer 的简写,中文描述表述性状态传递(是指某个瞬间状态的资源数据的快照,包括资源数据的内容、表述格式(XML、JSON)等信息。)

REST 是一种软件架构风格。这种风格的典型应用,就是HTTP。其因为简单、扩展性强的特点而广受开发者的青睐。

而RPC 呢,是 Remote Procedure Call Protocol 的简写,中文描述是远程过程调用,它可以实现客户端像调用本地服务(方法)一样调用服务器的服务(方法)。

而 RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输的,按理说它和REST不是一个层面意义上的东西,不应该放在一起讨论,但是谁让REST这么流行呢,它是目前最流行的一套互联网应用程序的API设计标准,某种意义下,我们说 REST 可以其实就是指代 HTTP 协议。

1.2.2 使用方式不同

从使用上来看,HTTP 接口只关注服务提供方,对于客户端怎么调用并不关心。接口只要保证有客户端调用时,返回对应的数据就行了。而RPC则要求客户端接口保持和服务端的一致。

REST 是服务端把方法写好,客户端并不知道具体方法。客户端只想获取资源,所以发起HTTP请求,而服务端接收到请求后根据URI经过一系列的路由才定位到方法上面去RPC是服务端提供好方法给客户端调用,客户端需要知道服务端的具体类,具体方法,然后像调用本地方法一样直接调用它。

1.2.3 面向对象不同

从设计上来看,RPC,所谓的远程过程调用 ,是面向方法的 ,REST:所谓的 Representational state transfer ,是面向资源的,除此之外,还有一种叫做 SOA,所谓的面向服务的架构,它是面向消息的,这个接触不多,就不多说了。

1.2.4 序列化协议不同

接口调用通常包含两个部分,序列化和通信协议。

通信协议,上面已经提及了,REST 是 基于 HTTP 协议,而 RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输的。

常见的序列化协议,有:json、xml、hession、protobuf、thrift、text、bytes等,REST 通常使用的是 JSON或者XML,而 RPC 使用的是 JSON-RPC,或者 XML-RPC。

通过以上几点,我们知道了 REST 和 RPC 之间有很明显的差异。

然后第二个问题:为什么要采用RPC呢?

那到底为何要使用 RPC,单纯的依靠RESTful API不可以吗?为什么要搞这么多复杂的协议,渣渣表示真的学不过来了。

关于这一点,以下几点仅是我的个人猜想,仅供交流哈:

RPC 和 REST 两者的定位不同,REST 面向资源,更注重接口的规范,因为要保证通用性更强,所以对外最好通过 REST。而 RPC 面向方法,主要用于函数方法的调用,可以适合更复杂通信需求的场景。RESTful API客户端与服务端之间采用的是同步机制,当发送HTTP请求时,客户端需要等待服务端的响应。当然对于这一点是可以通过一些技术来实现异步的机制的。采用RESTful API,客户端与服务端之间虽然可以独立开发,但还是存在耦合。比如,客户端在发送请求的时,必须知道服务器的地址,且必须保证服务器正常工作。而 rpc + ralbbimq中间件可以实现低耦合的分布式集群架构。说了这么多,我们该如何选择这两者呢?我总结了如下两点,供你参考:

REST 接口更加规范,通用适配性要求高,建议对外的接口都统一成 REST。而组件内部的各个模块,可以选择 RPC,一个是不用耗费太多精力去开发和维护多套的HTTP接口,一个RPC的调用性能更高(见下条)从性能角度看,由于HTTP本身提供了丰富的状态功能与扩展功能,但也正由于HTTP提供的功能过多,导致在网络传输时,需要携带的信息更多,从性能角度上讲,较为低效。而RPC服务网络传输上仅传输与业务内容相关的数据,传输数据更小,性能更高。

1.2.5 为什么一定要rpc,不能只学http协议和restful协议吗?

  1. rpc可以基于tcp直接开发自己的协议,这个是可以保持长连接的,tcp的传输效率高,并且可以一直维持链接

  2. 自定义协议可以优化数据的传输

如果我们只是开发web网站或者一些服务的使用者, 那么我们用restful看起来已经足够了,但是rpc的这种模式在大量的服务中都有,比如redis协议, rabbitmq的AMQP协议, 聊天软件的协议,也就是说我们想要开发一个redis的客户端,我们只需要用我们喜欢的语言实现redis定义的协议就行了,这对于开发服务来说非常有用,一般这种协议的价值在于我们自己开发的服务之间需要通信的时候 - 那你会问了,自己开发的组件之间协作,直接调用函数不就行了吗? - 对了,有些人已经反映过来了 -- 分布式系统,分布式系统中非常常用, 比如openstack中。 还有就是微服务!

所以掌握rpc开发,对于进阶和分布式开发就变得非常重要。

http协议1.x一般情况下一个来回就关闭连接,虽然提供了keep-alive可以保持长连接,但是依然不方便,所以就出现了http2.0, http2.0基本上可以当做tcp协议使用了。所以后面讲解到的grpc就会使用http2.0开发

1.3 通过httpserver实现rpc

首先一点需要明确:一定会发起一个网络请求,一定会有个网络连接(tcp/udp)

    1. 把远程的函数变成一个http请求
from http.server import HTTPServer, BaseHTTPRequestHandler

host = ('', 8003)

class TodoHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        from urllib.parse import urlparse, parse_qsl
        import json
        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, TodoHandler)
    print("Starting server, listen at: %s:%s" % host)
    server.serve_forever()

本地调用

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

print(add(1+2))

总结说来,本地程序调用的过程大致可以分为几个步骤和阶段:

  • 开发者开发好的程序,并进行编译,编译成机器认可的可执行文件。
  • 运行可执行文件,调用对应的功能方法,期间会读取可执行文件中的机器指令,进行入栈,出栈赋值等操作。此时,计算机由可执行程序所在的进程控制。
  • 调用结束,所有的内存数据出栈,程序执行结束。计算机继续由操作系统进行控制。

远程过程调用是在两台或者多台不同的物理机器上实现的调用,其间要跨越网络进行调用。因此,我们再想通过前文本地方法调用的形式完成功能调用,就无法实现了,因为编译器无法通过编译的可执行文件来调用远程机器上的程序方法。因此需要采用RPC的方式来实现远端服务器上的程序方法的调用。

RPC技术内部原理是通过两种技术的组合来实现的:本地方法调用 和 网络通信技术。

如果想要做到不用url来确定调用方法和参数怎么办?

改进一下client的代码

import requests

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

    def add(self, a, b):
        payload = {
            "method": "add",
            "params": [a, b],
            "jsonrpc": "2.0",
            "id": 0,
        }
        response = requests.post(self.url, json=payload).json()
        print(response)
        return response["result"]

cli = Client("http://localhost:8001/jsonrpc")
print(cli.add(1,2))

从上面的代码我们关注几点:

  1. 通信使用了json,所以json中的内容应该如何写就成为了一种协议
  2. 要想实现远程调用必须要要有网络连接
  3. 上面通过http连接,可以发现客户端和服务器端之间可以独立,实际上除了通过json以外还可以通过xml作为数据格式
  4. rpc不应该和http拿来比较
  5. rpc也不应该和restful拿来比较,我们完全可以把上述代码通过client和server端的封装将rpc编程基于restful来实现
  6. rpc可以理解为一种调用风格,具体实现可以随意写,至于底层是走tcp协议还是http协议看需求

1.4 rpc开发的要素分析

rpc开发的四大要素

RPC技术在架构设计上有四部分组成,分别是:客户端、客户端存根、服务端、服务端存根。

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

了解完了RPC技术的组成结构我们来看一下具体是如何实现客户端到服务端的调用的。实际上,如果我们想要在网络中的任意两台计算机上实现远程调用过程,要解决很多问题,比如:

  • 两台物理机器在网络中要建立稳定可靠的通信连接。
  • 两台服务器的通信协议的定义问题,即两台服务器上的程序如何识别对方的请求和返回结果。也就是说两台计算机必须都能够识别对方发来的信息,并且能够识别出其中的请求含义和返回含义,然后才能进行处理。这其实就是通信协议所要完成的工作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D4kzOaZN-1617246033490)(./image/3/1584618604250-e8f1e0d6-93a9-4c44-8540-0b503a120096.png)]

在上述图中,通过1-10的步骤图解的形式,说明了RPC每一步的调用过程。具体描述为:

  • 1、客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
  • 2、客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
  • 3、客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
  • 4、服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
  • 5、服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
  • 6、服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
  • 7、服务端Stub程序**将程序调用结果按照约定的协议进行序列化,**并通过网络发送回客户端Stub程序。
  • 8、客户端Stub程序接收到服务端Stub发送的返回数据,**对数据进行反序列化操作,**并将调用返回的数据传递给客户端请求发起者。
  • 9、客户端请求发起者得到调用结果,整个RPC调用过程结束。

rpc需要使用到的术语

通过上文一系列的文字描述和讲解,我们已经了解了RPC的由来和RPC整个调用过程。我们可以看到RPC是一系列操作的集合,其中涉及到很多对数据的操作,以及网络通信。因此,我们对RPC中涉及到的技术做一个总结和分析:

  • 1、动态代理技术: 上文中我们提到的Client Stub和Sever Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序。

  • 2、序列化和反序列化: 在RPC调用的过程中,我们可以看到数据需要在一台机器上传输到另外一台机器上。在互联网上,所有的数据都是以字节的形式进行传输的。而我们在编程的过程中,往往都是使用数据对象,因此想要在网络上将数据对象和相关变量进行传输,就需要对数据对象做序列化和反序列化的操作。

    • **序列化:**把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
    • **反序列化:**把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。

我们常见的Json,XML等相关框架都可以对数据做序列化和反序列化编解码操作。后面我们要学习的Protobuf协议,这也是一种数据编解码的协议,在RPC框架中使用的更广泛。

1.5 基于xml的rpc调用

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()

客户端

from xmlrpc import client
server = client.ServerProxy("http://localhost:8088")
print(server.add(2, 3))

然后,我们通过 server_proxy 对象就可以远程调用之前的rpc server的函数了。

>> server.add(2, 3)
``5``
>>> server.multiply(2, 3)
``6``
>>> server.subtract(2, 3)
``1``
>>> server.divide(2, 3)``0

1.6 json实现rpc的调用

SimpleXMLRPCServer 是基于 xml-rpc 实现的远程调用,上面我们也提到 除了 xml-rpc 之外,还有 json-rpc 协议。

那 python 如何实现基于 json-rpc 协议呢?

答案是很多,很多web框架其自身都自己实现了json-rpc,但我们要独立这些框架之外,要寻求一种较为干净的解决方案,我们使用 jsonrpclib

jsonrpclib

1.6.1 安装

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

它与 Python 标准库的 SimpleXMLRPCServer 很类似(因为它的类名就叫做 SimpleJSONRPCServer ,不明真相的人真以为它们是亲兄弟)。或许可以说,jsonrpclib 就是仿照 SimpleXMLRPCServer 标准库来进行编写的。

它的导入与 SimpleXMLRPCServer 略有不同,因为SimpleJSONRPCServer分布在jsonrpclib库中。

1.6.2. 代码

2.1 服务端
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer


server = SimpleJSONRPCServer(('localhost', 8081))
server.register_function(pow)
server.register_function(lambda x,y: x+y, 'add')
server.register_function(lambda x: x, 'ping')
server.serve_forever()
2.2 客户端
import jsonrpclib

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

1.7 zerorpc实现rpc调用

zerorpc 是利用 zeroMQ消息队列 + msgpack 消息序列化(二进制) 来实现类似 grpc 的功能,跨语言远程调用。

主要使用到 zeroMQ 的通信模式是 ROUTER–DEALER,模拟 grpc 的 请求-响应式 和 应答流式 RPC :

zerorpc 还支持 PUB-SUB 通信模式的远程调用。

zerorpc实际上会依赖msgpack-python, pyzmq, future, greenlet, gevent

zerorpc

一元调用

1. 服务端
import zerorpc

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

s = zerorpc.Server(HelloRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()
2. 客户端
import zerorpc

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

流式响应

1. 服务端
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()
2. 客户端
import zerorpc

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

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

传入多个参数

1. 服务端
import zerorpc

class myRPC(object):
    def listinfo(self,message):
        return "get info : %s"%message

    def getpow(self,n,m):
        return n**m           

s = zerorpc.Server(myRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()
2. 客户端

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述 Go+Python双语言混合开发