gRPC 和传统 RPC 有啥不一样?一篇讲清楚!

笔记哥 / 04-18 / 43点赞 / 0评论 / 598阅读
现在大家做系统开发,都喜欢搞"微服务架构"——简单说就是把一个大系统拆成很多小服务,这样更灵活也更容易扩展。那这些服务之间怎么沟通呢?就得靠一种技术叫 **RPC(远程过程调用)**。今天我们就来聊聊它的"进化版":**gRPC**,看看它和传统的 RPC 到底有啥不一样。 ## 一、先搞懂几个概念 ### 什么是 RPC? 可以把它理解成"跨机器调用函数"的方式。就像你在本地调用一个函数一样,但其实它是在另一台服务器上运行的。传统 RPC 有很多种实现,比如 XML-RPC、JSON-RPC、SOAP 等,数据格式多是 XML 或 JSON。 ### 那 gRPC 是啥? Google 出品的一个更高效的 RPC 框架,基于 HTTP/2 协议,数据格式用的是 Protocol Buffers(简称 Protobuf)。性能好、效率高,还能自动生成代码,听起来就很香对吧? ## 二、gRPC 和传统 RPC 的几大区别(白话版) | 对比点 | 传统 RPC | gRPC | | --- | --- | --- | | 传输协议 | 通常用 HTTP/1 或 TCP | HTTP/2,支持多路复用,速度快 | | 数据格式 | XML/JSON,可读但体积大 | Protobuf,体积小,解析快 | | 代码生成 | 通常手动写 | 支持自动生成客户端/服务端代码 | | 流式处理 | 一般不支持 | 支持四种调用模式,支持双向流 | | 跨语言支持 | 有点费劲 | 官方支持多语言(Go、Python 等) | | 错误处理 | 用 HTTP 状态码处理 | 用标准错误码机制,支持详细描述 | ## 三、举个例子更直观 ### 用传统 JSON-RPC 调接口 ```csharp { "jsonrpc": "2.0", "method": "getUserProfile", "params": { "userId": 123, "includeDetails": true }, "id": 1 } ``` 人类能看懂,但数据量大,解析速度也慢。 ### 用 gRPC + Protobuf 首先定义协议: ```csharp syntax = "proto3"; ​ service UserService { rpc GetUserProfile(UserRequest) returns (UserProfile) {} } ​ message UserRequest { int32 user_id = 1; bool include_details = 2; } ​ message UserProfile { int32 user_id = 1; string username = 2; string email = 3; } ``` 然后就可以这样调用: ```csharp request = user_pb2.UserRequest(user_id=123, include_details=True) response = stub.GetUserProfile(request) print(f"用户名: {response.username}") ``` 结构更清晰、体积更小、传输效率更高。 ## 四、请求处理方式对比 ### 传统RPC的调用方式 ```csharp # XML-RPC示例 import xmlrpc.client ​ # 创建客户端 server = xmlrpc.client.ServerProxy("http://localhost:8000") ​ # 每次调用都会建立新连接 result = server.get_user_info(user_id=123) print(f"用户信息: {result}") ​ # 又得重新连接 another_result = server.get_product_details(product_id=456) ``` 就像每次打电话都要重新拨号一样,费时间! ### gRPC的调用方式 ```csharp import grpc import user_service_pb2 import user_service_pb2_grpc ​ # 创建一个连接通道 with grpc.insecure_channel('localhost:50051') as channel: # 创建调用对象 stub = user_service_pb2_grpc.UserServiceStub(channel) # 同一个连接可以调用多个方法 response1 = stub.GetUser(user_service_pb2.GetUserRequest(user_id=123)) response2 = stub.GetProduct(user_service_pb2.GetProductRequest(product_id=456)) # 还能做流式调用,像看视频一样一点点接收数据 for product in stub.ListProducts(user_service_pb2.ListProductsRequest(category="手机")): print(f"产品: {product.name}, 价格: {product.price}") ``` 就像建立一条专线,通话不断,还能边说边听,太方便了! ## 五、性能差距有多大? 场景:获取 1000 个用户信息 ### 传统 REST(HTTP/1 + JSON)版本: ```csharp import requests import time ​ start_time = time.time() users = [] ​ # 发送1000个独立的HTTP请求,每次都要建连接 for i in range(1000): response = requests.get(f"http://api.example.com/users/{i}") users.append(response.json()) ​ duration = time.time() - start_time print(f"REST API: 获取了{len(users)}个用户,耗时{duration:.2f}秒") # 输出: REST API: 获取了1000个用户,耗时10.45秒 ``` ### gRPC 版本: ```csharp import grpc import user_pb2 import user_pb2_grpc import time ​ start_time = time.time() ​ with grpc.insecure_channel('api.example.com:50051') as channel: stub = user_pb2_grpc.UserServiceStub(channel) # 一次请求获取所有用户,批量处理 users = list(stub.GetUsers(user_pb2.GetUsersRequest(limit=1000))) ​ duration = time.time() - start_time print(f"gRPC: 获取了{len(users)}个用户,耗时{duration:.2f}秒") # 输出: gRPC: 获取了1000个用户,耗时1.23秒 ``` **总结**:gRPC 更快,因为它: - 支持连接复用(不用每次都重新连) - 使用 Protobuf,数据更轻更快 - 流式处理,批量效率高 ## 六、错误处理方式对比 ### REST 错误处理: 服务端返回的错误: ```csharp { "error": { "code": 404, "message": "User not found", "details": "The user with ID 12345 does not exist" } } ``` 客户端处理: ```csharp fetch('/api/users/12345') .then(response => { if (!response.ok) { return response.json().then(err => { throw new Error(`${err.error.message}: ${err.error.details}`); }); } return response.json(); }) .catch(error => console.error('错误:', error)); ``` 靠 HTTP 状态码,但格式不统一,需要手动解析。 ### gRPC 错误处理: 服务端定义错误: ```csharp def GetUser(self, request, context): user = database.find_user(request.user_id) if not user: context.set_code(grpc.StatusCode.NOT_FOUND) context.set_details(f"找不到用户 {request.user_id}") return user_pb2.UserProfile() # 返回空对象 return user ``` 客户端处理错误: ```csharp try: response = stub.GetUser(request) print(f"用户信息: {response}") except grpc.RpcError as e: if e.code() == grpc.StatusCode.NOT_FOUND: print(f"错误: 用户不存在 - {e.details()}") else: print(f"RPC错误: {e.code()} - {e.details()}") ``` 标准的错误码 + 描述,客户端可以直接 catch。像处理本地异常一样方便! ## 七、实际应用场景选择 ### 什么时候用传统REST API? 1. **前端直接调API** ```csharp // 浏览器调REST API就很方便 fetch('/api/products') .then(res => res.json()) .then(products => console.log(products)); ``` 2. **接第三方平台** 比如接微信支付、支付宝API,人家都是REST的,你也得跟着来 3. **简单系统** 小项目不追求性能,REST开发速度快 ### 什么时候用gRPC? 1. **微服务内部通信** 服务多了,内部调用频繁,用gRPC又快又稳 2. **实时数据应用** ```csharp // 股票价格实时推送 func (s *StockServer) PriceStream(request *pb.StockRequest, stream pb.StockService_PriceStreamServer) error { for { price := getLatestPrice(request.Symbol) stream.Send(&pb.StockPrice{ Symbol: request.Symbol, Price: price, Timestamp: time.Now().Unix(), }) time.Sleep(1 * time.Second) } } ``` 3. **移动端应用** 手机流量金贵,gRPC数据小,省流量 4. **多语言系统** Python服务调Go服务,Java服务调C#服务,都不是问题 ## 八、总结一句话 **REST API就像普通话,大家都听得懂;gRPC像高速公路,虽然有门槛,但一旦上了路就飞快!** 如果你在做面向普通用户的接口,或者简单系统,REST API足够了。 但如果你在构建微服务、需要高性能、多语言、流式处理能力,那就果断上gRPC!