您现在的位置是:首页 >其他 >rpc与grpc学习记录网站首页其他

rpc与grpc学习记录

一只菜得不行的鸟 2024-06-17 11:27:56
简介rpc与grpc学习记录

1、RPC

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 框架整体上分为三类:

  • 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift;
  • 只支持特定语言的 RPC 框架,例如新浪微博的 Motan;
  • 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的
    Dubbo。《秒懂Dubbo框架(原理篇)》

2、gRPC

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实现多线程 基础篇》

3、grpc代码

3.1、安装python需要的库:

安装grpc

pip install grpcio

安装gRPC tools

pip install grpcio-tools 

注:gRPC tools包含了protobuf的编译器protoc,以及编译插件grpc_python_out(后面编译会用到)。且里面已经自动安装了protobuf库,不用再单独 pip install protobuf 安装了。


3.2、grpc编程步骤

python实现grpc需要以下步骤:

1、编写.proto文件,即定义接口和数据类型规范;
2、通过编译.proto文件,生成2份存根文件;
3、编写服务端代码。实现第一步定义的接口并启动,这些接口的定义在存根文件里面;
4、编写客户端代码。客户端借助存根文件调用服务端的函数,虽然客户端调用的函数是由服务端实现的,但是调用起来就像是本地函数一样。


3.3、Demo1

目录结构如下:

在这里插入图片描述
具体可以采用如下命令查看:

apt-get install tree

tree ./

3.3.1、编写 .proto文件,定义接口和数据类型

关于具体的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;
}

3.3.2、编译 .proto文件生成存根文件

利用编译工具把 .proto文件转化成 .py文件,直接在当前文件目录下运行下方代码即可。

cd test0_zct

python3 -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. proto/helloworld.proto

参数说明:

  1. -m 指定通过protoc工具自动生成 .py文件(即存根文件)
  2. -I 指定 .proto所在目录
  3. –python_out指定生成.py文件的输出路径
  4. hello.proto 输入的.proto文件

编译后会生成如下图所示,helloworld_pb2.py和helloworld_pb2_grpc.py两份存根文件(stub)。大致可以理解为:_pb2中定义了数据结构,_pb2_grpc中定义了相关的方法。

在这里插入图片描述

3.3.3、编写服务器端代码

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

3.3.4、编写客户端代码:

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

3.3.5、测试

一个终端运行hello_server.py

python3 hello_server.py

另一个终端运行hello_client.py

python3 hello_client.py

运行结果如下

在这里插入图片描述

3.4、Demo2

3.4.1、编写 .proto文件,定义接口和数据类型

// 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;
}

3.4.2、编译 .proto文件生成存根文件

cd test1_zct

python3 -m grpc_tools.protoc -I./proto --python_out=. --grpc_python_out=. proto/helloworld.proto

3.4.3、编写服务器端代码

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

3.4.4、编写客户端代码

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

3.4.5、测试

在这里插入图片描述

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。