初识gRPC

本来有个不错的机会可以跟着某大厂的训练营进行学习,但或许是名额有限,又或者是本人太菜,错失良机。不过,既然没法跟着学,那就自己动手,丰衣足食,自学成才吧!这gRPC正是其中的一环……

概述

学一个新东西,需要先了解定义。

gRPC (gRPC Remote Procedure Calls[1]) 是Google发起的一个开源远程过程调用 (Remote procedure call) 系统。该系统基于 HTTP/2 协议传输,使用Protocol Buffers 作为接口描述语言。[2] ——wiki

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services. ——google

简单来说,gRPC是Google开发的一个跨语言跨平台的远程调用框架。值得一提,其基于Http2实现,支持基于流的传输,相交基于Http1.1的Rest更为高效,且支持Protocol Buffer序列化,信息熵更高。

工作流程

了解完定义、特性,已经可以向外行夸耀几句了,但是想真正投入应用,还得参考一个完整的工作流程。

简单而言,一个gRPC调用的应用分为以下几步:

  1. 编写proto文件
  2. 分别生成pb2与pb2_grpc代码
  3. 编写client与server端

编写proto文件

proto文件即protocol buffers的核心组成,对编码较为抽象的protocol buffers报文进行解释——通过生成代码对proto报文进行处理。

怎么个抽象法呢?

这是Json

1
2
3
4
{
msg: Logres
code: 11
}

这是protocol buffer

1
126Logres21211

protocol buffer报文的内容前三位分别表示字段序号、数据类型(eg. String类型编号为2)、消息长度。

但仅仅这三位附属数据仅能从语法上区分字段,想要从语义上进行解读还需要别的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";

service WindServer {
rpc wind_predict(Request) returns (Response) {}
}

message Request {
string content = 1;
}

message Response {
string msg = 1;
int32 code = 2;
}

上述文件中的Response就对字段序号为1、2的消息进行解释,当然,此处仅给出了简单类型,复杂类型protocol也提供了相应支持,此处就暂且忽略。

编译生成pb2 与 pb2_grpc代码

前面提到,protocol提供报文解析的方式是生成相应代码。

对于上述proto文件,我们以python为例,可以使用grpc工具生成代码。

1
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. wind.proto

代码内容:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

wind_pb2.py

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: wind.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwind.proto\"\x1a\n\x07Request\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\"%\n\x08Response\x12\x0b\n\x03msg\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\x05\x32\x33\n\nWindServer\x12%\n\x0cwind_predict\x12\x08.Request\x1a\t.Response\"\x00\x62\x06proto3')



_REQUEST = DESCRIPTOR.message_types_by_name['Request']
_RESPONSE = DESCRIPTOR.message_types_by_name['Response']
Request = _reflection.GeneratedProtocolMessageType('Request', (_message.Message,), {
'DESCRIPTOR' : _REQUEST,
'__module__' : 'wind_pb2'
# @@protoc_insertion_point(class_scope:Request)
})
_sym_db.RegisterMessage(Request)

Response = _reflection.GeneratedProtocolMessageType('Response', (_message.Message,), {
'DESCRIPTOR' : _RESPONSE,
'__module__' : 'wind_pb2'
# @@protoc_insertion_point(class_scope:Response)
})
_sym_db.RegisterMessage(Response)

_WINDSERVER = DESCRIPTOR.services_by_name['WindServer']
if _descriptor._USE_C_DESCRIPTORS == False:

DESCRIPTOR._options = None
_REQUEST._serialized_start=14
_REQUEST._serialized_end=40
_RESPONSE._serialized_start=42
_RESPONSE._serialized_end=79
_WINDSERVER._serialized_start=81
_WINDSERVER._serialized_end=132
# @@protoc_insertion_point(module_scope)

尽是一些看不懂的东西,不过可以看出跟序列化的参数有关,是个类似配置模块的东西。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

wind_pb2_grpc.py

# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

import wind_pb2 as wind__pb2


class WindServerStub(object):
"""Missing associated documentation comment in .proto file."""

def __init__(self, channel):
"""Constructor.

Args:
channel: A grpc.Channel.
"""
self.wind_predict = channel.unary_unary(
'/WindServer/wind_predict',
request_serializer=wind__pb2.Request.SerializeToString,
response_deserializer=wind__pb2.Response.FromString,
)


class WindServerServicer(object):
"""Missing associated documentation comment in .proto file."""

def wind_predict(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_WindServerServicer_to_server(servicer, server):
rpc_method_handlers = {
'wind_predict': grpc.unary_unary_rpc_method_handler(
servicer.wind_predict,
request_deserializer=wind__pb2.Request.FromString,
response_serializer=wind__pb2.Response.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'WindServer', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))


# This class is part of an EXPERIMENTAL API.
class WindServer(object):
"""Missing associated documentation comment in .proto file."""

@staticmethod
def wind_predict(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/WindServer/wind_predict',
wind__pb2.Request.SerializeToString,
wind__pb2.Response.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)

可以看到pb2_grpc中定义了3个类与1个方法:

  1. class *Stub: 为客户端提供了一个桩,绑定到某个远程服务的类上。
  2. class *Servicer: 服务端提供的类,其方法供客户端调用,服务端具体实现需要继承该类。
  3. class *Server: 实验性API,包含了一个静态方法,作用未知~
  4. method add_WindServerServicer_to_server:使用该方法能够将服务端的具体实现绑定到一个gRPC server上。

到此,proto buffer与围绕其生成的用于解读报文/流的类就解读完了。

编写客户端与服务端

服务端

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import grpc
import wind_pb2
import wind_pb2_grpc

from concurrent import futures

class WindPredictSrv(wind_pb2_grpc.WindServerServicer):

def wind_predict(self, request, context):
print(request.content)
return wind_pb2.Response(msg="%s!" % request.content)

def server():
option = [('grpc.keepalive_timeout_ms', 10000)]
grpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10),options=option)
wind_pb2_grpc.add_WindServerServicer_to_server(WindPredictSrv(),grpc_server)
grpc_server.add_insecure_port("[::]:50051")
grpc_server.start()
grpc_server.wait_for_termination()

if __name__ == "__main__":
server()

在服务端代码中,我们通过WindPredictSrv继承前面生成pb2_grpc.py代码中的WindServerServicer类,并重写了wind_predict方法。

然后,生成一个grpc_server,并通过add_WindServerServicer_to_server将其加入其中,并在所有IP的50051端口监听。

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

import wind_pb2 as wind_pb2
import wind_pb2_grpc as wind_pb2_grpc
import grpc


def run():
with grpc.insecure_channel(target = "localhost:50051") as channel:
stub = wind_pb2_grpc.WindServerStub(channel)
response = stub.wind_predict(wind_pb2.Request(content="hellp grpc"))
print("Res:"+response.msg)

if __name__ == "__main__":
run()

客户端较为简单,首先打开服务端对应端口的channel,然后使用WindServerStub绑定这个channel,之后就能将stub作为一个WindPredictSrv实例使用了。

一些补充

What’s Channel

在编写服务端与客户端代码时,我们并未直接指定一个server或stub的地址与端口,而是通过添加一个Channel来实现这件事。

所以,Channel是什么呢?

我们知道,Http1.0是最为经典的客户端与服务端一问一答架构,发出Request,返回Response。

为了避免过多的TCP握手,Http1.1中将Http涉及的TCP连接改成了长连接。但是,客户端还是需要等服务端返回上一条Request的Response才能发出下一条Request。

最后,在Http2中,实现了流式传输,通讯双方可以连续发送报文。

Channel正是gRpc中对这种长的流式连接的一种抽象,将提供具体调用的Server与管理连接的Channel分离,保持业务的纯正性,其间可以形成多对多联系,即多个Server共用一个Channel,或一个Server注册于多个Channel。

实现了软件工程的关注点分离原则,利好开发。