RPC是Remote Procedure Call的简称,中文叫远程过程调用。
RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP或者HTTP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程,使得开发网络分布式程序在内的应用程序更加容易。
说的白话一点,可以这么理解:现在有两台服务器A和B。部署在A服务器上的应用,想调用部署在B服务器上的另一个应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来达到调用的效果。
现在,我们在A服务的一个本地方法中封装调用B的逻辑,然后只需要在本地使用这个方法,就达到了调用B的效果。对使用者来说,屏蔽了细节。你只需要知道调用这个方法返回的结果,而无需关注底层逻辑。
那,从封装的那个方法角度来看,调用B之前我们需要知道什么?
当然是一些约定。比如:
1、调用的语义,也可以理解为接口规范。(比如RESTful)
2、网络传输协议。 (比如HTTP)
3、数据序列化反序列化规范(比如JSON)。
有了这些约定,我就知道如何给你发数据,发什么样的数据,你返回给我的又是什么样的数据。
从上图中可以看出,RPC是一种客户端-服务端(Client/Server)模式。
HTTP 和 RPC 有什么区别?
首先这个问题本身不太严谨。HTTP只是一个通信协议,工作在OSI第七层。而RPC是一个完整的远程调用方案。它包含了:接口规范、传输协议、数据序列化反序列化规范。
这样看,RPC和 HTTP的关系只可能是包含关系。为什么是可能?因为RPC传输协议那块我可以不基于HTTP,可以基于TCP或UDP。
所以这个问题应该改成:基于HTTP的远程调用方案 (如:HTTP+RESTful+JSON) 和直接使用RPC远程调用方案有什么区别。
业界主流的 RPC 框架整体上分为三类:
gRPC,则是RPC的一种,它是免费且开源的,由谷歌出品,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。
gRPC的典型特征就是使用protobuf(全称protocol buffers)作为其接口定义语言(Interface Definition Language,缩写IDL),同时底层的消息交换格式也是使用protobuf。
gRPC要求client存放一个stub(存根:提供与服务器相同的方法和功能),stub由gRPC框架自动生成。有了stub后,通过直接调用stub里的方法,开发人员只需要关心具体的业务逻辑,而不需要关心网络通信相关实现原理。
client的stub是由编译.protoc文件后gRPC这个框架自动生成的,可以利用插件快速将.proto文件生成gRPC需要的stub。
参考文:
《什么是 gRPC ?什么是 RPC?》:大白话,首先解释了rpc,再解释rpc和gprc的关系,解释得很直白与明白。
《初识gRPC》:很好的解释了存根
《gRPC详解》:小demo和参数说明
《技术实践:教你用Python搭建gRPC服务》
《使用Python实现gRPC通信》
《gRPC 入门》
多线程python
《Python 中的并发编程和异步编程》
《Python threading实现多线程 基础篇》
安装grpc
pip install grpcio
安装gRPC tools
pip install grpcio-tools
注:gRPC tools包含了protobuf的编译器protoc,以及编译插件grpc_python_out(后面编译会用到)。且里面已经自动安装了protobuf库,不用再单独 pip install protobuf
安装了。
python实现grpc需要以下步骤:
1、编写.proto文件,即定义接口和数据类型规范;
2、通过编译.proto文件,生成2份存根文件;
3、编写服务端代码。实现第一步定义的接口并启动,这些接口的定义在存根文件里面;
4、编写客户端代码。客户端借助存根文件调用服务端的函数,虽然客户端调用的函数是由服务端实现的,但是调用起来就像是本地函数一样。
目录结构如下:
apt-get install tree
tree ./
关于具体的proto常见语法可参考:《gRPC之proto语法》
// helloworld.proto:定义接口和数据类型规范
// 限定该文件使用的是proto3的语法
syntax = "proto3";
// 可以为proto文件指定包名,防止消息命名冲突
package helloworld;
// 基础Demo
service Greeter {
//一个服务中可以定义多个接口,也就是多个函数功能。注意return加了个s
// 方法名 方法参数 返回值
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 请求的参数
message HelloRequest {
string name = 1; //数字1是参数的位置顺序,并不是对参数赋值
}
// 返回的对象
message HelloResponse {
string message = 1;
}
利用编译工具把 .proto文件转化成 .py文件,直接在当前文件目录下运行下方代码即可。
cd test0_zct
python3 -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. proto/helloworld.proto
参数说明:
- -m 指定通过protoc工具自动生成 .py文件(即存根文件)
- -I 指定 .proto所在目录
- –python_out指定生成.py文件的输出路径
- hello.proto 输入的.proto文件
编译后会生成如下图所示,helloworld_pb2.py和helloworld_pb2_grpc.py两份存根文件(stub)。大致可以理解为:_pb2中定义了数据结构,_pb2_grpc中定义了相关的方法。
vim hello_server.py
代码内容如下:
# -*- coding: utf-8 -*-
# hello_server.py
'''
服务器端代码
'''
import grpc
from concurrent import futures
import helloworld_pb2
import helloworld_pb2_grpc
class Hello(helloworld_pb2_grpc.GreeterServicer):
'''
实现在helloworld.proto中'service'定义的SayHello方法。说明:
1、定义一个类,要继承父类GreeterServicer。
这里为何是GreeterServicer?因为helloworld_pb2_grpc.py是通过helloworld.proto编译后自动生成的,
而在helloworld.proto文件中定义的'service'名为'Greeter',编译后会自动生成'名+Servcier',
继承'GreeterServicer'就相当于是helloworld.proto的service Greeter。
2、初始化
3、具体实现SayHello方法。注意该方法的参数和返回值,要和helloworld.proto中'service'定义的SayHello方法相对应。
此方法虽存在服务器端,但是通过rpc,客户端可以实现远程调用。
具体实现细节是客户端可以调用本地的存根文件里的方法。存根文件来源于编译.proto文件后,grpc自动生成的。
'''
def __init__(self):
pass
def SayHello(self, request, context):
'''
这里具体实现之前定义的SayHello()
:param request: request是在.proto文件中定义的HelloRequest消息类型
:param context: context是保留字段,这里不用管
:return: .proto文件中定义的HelloResponse类型
'''
# message = 'Hello {msg}'.format(msg=request.name)
# return helloworld_pb2.HelloResponse(message)
return helloworld_pb2.HelloResponse(message='Hello {msg}'.format(msg=request.name))
def serve():
'''
模拟服务启动
:return:
'''
# 这里通过thread pool来并发处理server的任务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 将对应的任务处理函数添加到rpc server中,第一个参数为上面定义的类名
helloworld_pb2_grpc.add_GreeterServicer_to_server(Hello(), server)
# 添加端口
server.add_insecure_port('[::]:50054')
# 开始监听
server.start()
print('gRPC 服务端已开启,端口为50054...')
# 堵塞监测本地50054端口,等待接收数据
server.wait_for_termination()
# try:
# while True:
# time.sleep(_ONE_DAY_IN_SECONDS)
# except KeyboardInterrupt:
# server.stop(0)
if __name__ == '__main__':
serve()
vim hello_client.py
代码内容如下:
# -*- coding: utf-8 -*-
# hello_client.py
'''
客户端代码
'''
import grpc
import helloworld_pb2, helloworld_pb2_grpc
def run():
# 本次不使用SSL,所以channel是不安全的
channel = grpc.insecure_channel('localhost:50054')
# 客户端实例
stub = helloworld_pb2_grpc.GreeterStub(channel)
# 调用服务端方法,这里其实是通过调用client本地的存根文件中方法实现的
response = stub.SayHello(helloworld_pb2.HelloRequest(name='World'))
print("Greeter client received: " + response.message)
if __name__ == '__main__':
run()
一个终端运行hello_server.py
python3 hello_server.py
另一个终端运行hello_client.py
python3 hello_client.py
运行结果如下
// helloworld.proto:定义接口和数据类型
// 限定该文件使用的是proto3的语法
syntax = "proto3";
// 可以为proto文件指定包名,防止消息命名冲突
package helloworld;
// 进阶Demo
service Greeter {
// 获取部门员工信息接口
// 方法名 方法参数 返回值
rpc GetDeptUser (GetDeptUserRequest) returns (GetDeptUserResponse) {}
}
// 请求的参数
// 数字1,2,3是参数的位置顺序,并不是对参数赋值
message GetDeptUserRequest {
uint32 dept_id = 1; // 部门
string dept_name = 2; // 部门名称
repeated uint32 uid_list = 3; // 用户id列表
}
// 返回的对象
message GetDeptUserResponse {
repeated BasicUser user_list = 1; // 用户列表
map<uint32, BasicUser> user_map = 2; // 用户哈希表,即字典
}
// 用户基本信息
message BasicUser {
uint32 id = 1;
string name = 2;
}
cd test1_zct
python3 -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. proto/helloworld.proto
# -*- coding: utf-8 -*-
import grpc
import random
from concurrent import futures
import helloworld_pb2
import helloworld_pb2_grpc
# 实现.protow文件中定义的方法
class Greeter(helloworld_pb2_grpc.GreeterServicer):
def __init__(self):
pass
def GetDeptUser(self, request, context):
'''
这里具体实现之前定义的GetDeptUser()
:param request: request是在.proto文件中定义的GetInfoRequest消息类型
:param context: context是保留字段,这里不用管
:return: .proto文件中定义的GetDeptUserResponse类型
'''
# 字段使用点号获取
dept_id = request.dept_id
dept_name = request.dept_name
uid_list = request.uid_list
if dept_id <= 0 or dept_name == '' or len(uid_list) <= 0:
return helloworld_pb2.GetDeptUserResponse()
print('dept_id is {0}, dept_name is {1}'.format(dept_id, dept_name))
user_list = []
user_map = {}
for id_ in uid_list:
uid = id_ + random.randint(0, 1000)
letters = 'qwertyuiopasdfghjklzxcvbnm'
name = "".join(random.sample(letters, 10))
user = helloworld_pb2.BasicUser()
user.id = uid
user.name = name
user_list.append(user) # 与正常的添加操作差不多
user_map[uid] = user
return helloworld_pb2.GetDeptUserResponse(user_list=user_list, user_map=user_map)
def serve():
'''
模拟服务启动
:return:
'''
# 这里通过thread pool来并发处理server的任务
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
# 将对应的任务处理函数添加到rpc server中,第一个参数为上面定义的类名
helloworld_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
# 添加端口
server.add_insecure_port('[::]:50054')
# 开始监听
server.start()
print('gRPC 服务端已开启,端口为50054...')
# 堵塞监测本地50054端口,等待接收数据
server.wait_for_termination()
if __name__ == '__main__':
serve()
# -*- coding: utf-8 -*-
import grpc
import helloworld_pb2, helloworld_pb2_grpc
def run():
# 本次不使用SSL,所以channel是不安全的
channel = grpc.insecure_channel('localhost:50054')
# 客户端实例
stub = helloworld_pb2_grpc.GreeterStub(channel)
# 调用服务端方法,这里其实是通过client本地的存根文件实现的
response = stub.GetDeptUser(helloworld_pb2.etDeptUserRequeGst(dept_id=1, dept_name='dd', uid_list=[1, 2, 3]))
print(response.user_list)
print(response.user_map)
if __name__ == '__main__':
run()