我们看到是没有任何问题的,然后再来使用 Python 的客户端。Python 的服务端使用异步,但是对于客户端来说,其实使不使用异步是无所谓的。我们先来看看之前同步的 grpc 是否可以访问(其实肯定可以,Go 都可以更不用说 Python 了):
import grpc import sys from pathlib import Path sys.path.append(str(Path(sys.prefix) / "Lib/site-packages/grpc_tools/_proto")) pb2, pb2_grpc = grpc.protos_and_services("hello.proto") channel = grpc.insecure_channel("127.0.0.1:22222") client = pb2_grpc.HelloStub(channel=channel) response = client.SayHello( pb2.HelloRequest() ) print(response.info) # {\'name\': \'凑阿库娅\', \'age\': \'5\'}因为是 grpc,所以我们没有使用 grpclib 对应的两个文件,总之调用是没有问题的。那么下面来看看如何使用 grpclib 的客户端:
import asyncio from grpclib.client import Channel import hello_pb2 as pb2 import hello_grpc as pb2_grpc # 采用 grpc 的命名习惯 async def main(): async with Channel("127.0.0.1", 22222) as channel: hello = pb2_grpc.HelloStub(channel) # 没有参数,传递一个空的过去即可 res = await hello.SayHello(pb2.HelloRequest()) return res.info if __name__ == \'__main__\': print(asyncio.run(main())) # {\'name\': \'凑阿库娅\', \'age\': \'5\'}我们看到也是可以的,当然一些注释就没写了,相信此时已经不需要再写了。
因此客户端可以使用 asyncio 发起连接,服务端可以使用 asyncio 维护大量连接,而且也不会影响彼此之间的调用。比如:Go 使用 grpc 编写的服务端,那么 Python 使用 grpc 和 grpclib 客户端都是可以使用的;Python 使用 grpclib 编写的服务端,Go 的 grpc 客户端、Python 的 grpc 客户端也是可以调用的;Python grpc 编写的服务端,Go 的 grpc 客户端、Python 的 grpclib 客户端同样是可以调用的。因此不用担心使用 asyncio 之后会出现调用问题,它们之间的互相调用是不受影响的。
如果你希望使用 Python 编写 rpc 服务,但是对并发量要求不高,那么可以使用 grpc 框架;如果你希望支持高并发,那么就是用 grpclib 来编写 rpc 服务端,使用 asyncio 来维护上万个连接是很轻松的。但如果是线程池,想要维护上万个线程是根本不可能的。
gRPC 的 metadata 机制gRPC 可以让我们像本地调用一样实现远程调用,对于每一次的 RPC 调用,都会通过 metadata 来传递一些额外的信息。正如 HTTP 请求一样,我们在发送数据的时候,服务端接收到的难道只有数据吗?显然不是的,除了我们发送的数据之外还有请求头,而我们发送的数据叫做请求头,它们会组合起来形成 HTTP 报文发给服务端;服务端返回的时候也不仅仅只是数据,还有响应头。之所以要存在头部信息,是为了 client 和 server 能够为彼此提供一些信息,比如:cookie。而 RPC 调用也是如此,存在一个 metadata,它的作用和 http header 是类似的。而且生命周期也一样,http 中的 header 的生命周期是一次 HTTP 请求,而 metadata 的生命周期就是一次 RPC 调用。
通过 HTTP 的 header,我们应该很好理解 RPC 的 metadata,因为有些数据不一定非要放到 request 里面。
metadata 是以 key-value 的形式存储数据的,其中 key 是字符串,value 是字符串组成的数组(切片、列表)。
下面我们就来操作一波,还以之前的 hello.proto 为例,尽量把业务逻辑搞简单一点
syntax = "proto3"; option go_package = ".;yoyoyo"; // 对 Python 无影响 message HelloRequest { } message HelloResponse { map<string, string> info = 1; } service Hello { rpc SayHello(HelloRequest) returns (HelloResponse) {} }这次我们先来编写客户端:
package main import ( "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "matsuri/yoyoyo" ) func main() { conn, err := grpc.Dial("127.0.0.1:33333", grpc.WithInsecure()) if err != nil { fmt.Println(err) return } defer func() { _ = conn.Close() }() client := yoyoyo.NewHelloClient(conn) // 这里创建 metadata,接收一个 map[string]string 返回一个 map[string][]string // 这里就随便写了 md := metadata.New(map[string]string{"Content-Type": "Json", "Cookie": "SessionID"}) // 或者我们还可以这么创建,通过 metadata.Pairs,里面的参数个数必须是偶数个,会两两组合 // md = metadata.Pairs("Content-Type", "Json", "Cookie", "SessionID") // 以上两个方式都会自动将 key 转成小写 // 创建 Context 对象,看到这个 Context,你应该明白了我们后面在服务端要怎么获取了,就是通过之前那个一直没有使用的 context 参数 ctx := metadata.NewOutgoingContext(context.Background(), md) // 这里把 ctx 传进去即可,之前用的是 context.Background() response, _ := client.SayHello(ctx, &yoyoyo.HelloRequest{}) // 我们在服务端将 metadata 里面的内容设置在返回值中 fmt.Println(response.Info["content-type"]) // application/grpc 来自服务端 fmt.Println(response.Info["cookie"]) // SessionID 来自服务端 }