一、为什么使用gRPC

1. 性能优势

        传统的前后端交互方式如下:

# server
from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route("/user/<int:user_id>", methods=["GET"])
def get_user(user_id):
    return jsonify({
        "id": user_id,
        "name": f"User {user_id}"
    })

if __name__ == "__main__":
    app.run(port=5000)

-----------------------------------------------------

# client
import requests

resp = requests.get("http://localhost:5000/user/1")
print(resp.json())  # 输出: {'id': 1, 'name': 'User 1'}

        但是,在构建大型分布式系统、微服务架构或跨语言通信的系统时,传统的 RESTful 接口和 JSON 传输方式在某些方面逐渐暴露出性能与扩展性的瓶颈。

        比如,一个服务需要每秒处理 1 万个请求,每个请求传输一个结构化的订单对象(比如嵌套字段较多)。那么,就会遇到以下问题:

  • JSON 是文本格式,体积大,解析慢;

  • 每次都需要进行字符串解析和对象映射(反序列化),消耗 CPU;

  • 嵌套结构多时,反序列化容易出错、调试困难。

       而 gRPC 使用 Protobuf,二进制格式,解析速度远快于 JSON,CPU 占用更低。 

        再比如,服务 A 需要同时调用服务 B 和服务 C,然后聚合数据返回前端。那么又会产生如下问题:

  • REST 基于 HTTP/1.1,请求需要串行排队,无法并发复用 TCP;

  • 每个请求都要建立 HTTP 连接(三次握手),慢;

  • 请求头巨大(冗余),比如每次都发送 User-AgentAccept 等;

        gRPC 基于 HTTP/2,支持多路复用(multiplexing),一个连接上同时发送多个请求不阻塞,极大减少延迟。 

 2. 接口协议的统一

        传统项目开发中,多个微服务分别由不同团队维护,使用 REST 风格的 API,有的写 Swagger,有的直接靠 README 文档说明。那么就会有以下几个问题:

  • 没有统一的接口标准;

  • 接口定义冗余、重复、错误;

  • 需要手动维护文档,代码和接口易不一致;

  • 新人不清楚应该怎么调某个服务,需要找人问或抓包。

        gRPC 使用 .proto 统一定义接口 + 数据结构,并自动生成服务端与客户端代码,接口一致性有保障。 

3. 跨语言沟通

         如果服务 A 使用 Java,服务 B 使用 Go,服务 C 使用 Python。它们之间通过 REST 接口通信。以下问题必须解决。

  • 需要各自解析 JSON,接口字段不一致可能导致错误;

  • 字段拼写错误无法被编译器发现;

  • 需要写大量重复的 HTTP 请求封装代码;

  • 零散维护,没有统一规范和 SDK。

        gRPC 支持多语言,使用同一个 .proto 文件就能自动生成多语言 SDK,省时省力。        

        因此,在小项目中,REST 是简单直接的解决方案;但在大型项目中,性能、接口规范、多语言支持和通信模式等方面的挑战,往往让 REST 力不从心。gRPC 就是为这些“高级场景”而生的。 

二、如何使用gRPC

1. 一个简单的例子

1.1 定义 proto 文件(user.proto)
syntax = "proto3";

service UserService {
  rpc GetUser (UserRequest) returns (UserReply);
}

message UserRequest {
  int32 id = 1;
}

message UserReply {
  int32 id = 1;
  string name = 2;
}

        保存为 user.proto,然后使用 protoc 生成代码: 

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto

        接下来会生成两个 python 源码文件,分别是:

  • user_pb2.py

    • 包含 Protobuf 消息定义(如 UserRequestUserReply);

    • 提供序列化、反序列化等方法;

    • --python_out=. 参数有关。

  • user_pb2_grpc.py

    • 包含 gRPC 的服务接口定义和 stub 实现

    • 自动生成服务端基类(如 UserServiceServicer)和客户端 stub(如 UserServiceStub);

    • --grpc_python_out=. 参数有关。

 

        比如,user_pb2_grpc.py生成以后可能长这样,

import grpc
import user_pb2 as user__pb2

class UserServiceStub(object):
    def __init__(self, channel):
        self.GetUser = channel.unary_unary(
            '/UserService/GetUser',
            request_serializer=user__pb2.UserRequest.SerializeToString,
            response_deserializer=user__pb2.UserReply.FromString,
        )

class UserServiceServicer(object):
    def GetUser(self, request, context):
        # 用户应实现此方法
        raise NotImplementedError('Method not implemented!')

def add_UserServiceServicer_to_server(servicer, server):
    rpc_method_handlers = {
        'GetUser': grpc.unary_unary_rpc_method_handler(
            servicer.GetUser,
            request_deserializer=user__pb2.UserRequest.FromString,
            response_serializer=user__pb2.UserReply.SerializeToString,
        ),
    }
    generic_handler = grpc.method_handlers_generic_handler(
        'UserService', rpc_method_handlers)
    server.add_generic_rpc_handlers((generic_handler,))
1.2 定义gRPC 服务端
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc

class UserService(user_pb2_grpc.UserServiceServicer):
    def GetUser(self, request, context):
        return user_pb2.UserReply(id=request.id, name=f"User {request.id}")

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    print("gRPC server started on port 50051")
    server.wait_for_termination()

if __name__ == "__main__":
    serve()
1.3 定义gRPC 客户端
import grpc
import user_pb2
import user_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = user_pb2_grpc.UserServiceStub(channel)
    response = stub.GetUser(user_pb2.UserRequest(id=1))
    print(response)  # 输出: id: 1 name: "User 1"

if __name__ == "__main__":
    run()

 2. 请求流程

        首选,浏览器发起请求,后端框架会把这个请求路由到 handler。然后在处理业务逻辑时,会调用 gRPC 客户端(gRPC连接池)。然后,gRPC客户端发起调用:

response = stub.GetUser(UserRequest(id=1))

        然后,gRPC框架内部(由proto生成的user_pb2_grpc.py)执行如下逻辑,此时,会进行这样的转化:UserRequest(id=1) 对象 → .SerializeToString() → 二进制数据。最后,数据通过 HTTP/2 发送给 gRPC 服务端。

# user_pb2_grpc.py 中 Stub 定义了:
self.GetUser = channel.unary_unary(
    '/UserService/GetUser',  # 服务路径
    request_serializer=UserRequest.SerializeToString,  # 序列化函数
    response_deserializer=UserReply.FromString,  # 反序列化函数
)

        然后,gRPC 服务端接收到二进制数据后。使用 UserRequest.FromString 自动反序列化 →  UserRequest(id=1) 对象。然后调用gRPC服务端所写的代码:

class MyUserService(UserServiceServicer):
    def GetUser(self, request, context):
        # request 是反序列化后的 UserRequest 实例
        return UserReply(id=request.id, name=f"User {request.id}")

         最后,使用 UserReply.SerializeToString → 序列化成二进制,发回客户端。客户端接收响应后,使用 UserReply.FromString → 得到 UserReply 对象,作为函数返回值返回给调用者。

rpc_method_handlers = {
    'GetUser': grpc.unary_unary_rpc_method_handler(
        servicer.GetUser,
        request_deserializer=user__pb2.UserRequest.FromString,
        response_serializer=user__pb2.UserReply.SerializeToString,
     ),
}

3. 总结

        gRPC 客户端请求时会将 Python 中的请求对象通过 request_serializer(如 .SerializeToString())进行序列化,发送给服务端,服务端接收后反序列化调用你实现的 UserServiceServicer.GetUser() 方法并返回响应对象,再通过 response_serializer 序列化后传回客户端,客户端用 response_deserializer(如 .FromString())反序列化恢复为 Python 对象。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐