这里或许是互联网从业者的最后一片净土,随客社区期待您的加入!
您需要 登录 才可以下载或查看,没有账号?立即注册
×
本帖最后由 mrkong 于 2025-4-23 14:33 编辑
现在大家做系统开发,都喜欢搞“微服务架构”——简单说就是把一个大系统拆成很多小服务,这样更灵活也更容易扩展。那这些服务之间怎么沟通呢?就得靠一种技术叫 RPC(远程过程调用)。今天我们就来聊聊它的“进化版”:gRPC,看看它和传统的 RPC 到底有啥不一样。
一、先搞懂几个概念什么是 RPC?可以把它理解成“跨机器调用函数”的方式。就像你在本地调用一个函数一样,但其实它是在另一台服务器上运行的。传统 RPC 有很多种实现,比如 XML-RPC、JSON-RPC、SOAP 等,数据格式多是 XML 或 JSON。 那 gRPC 是啥?Google 出品的一个更高效的 RPC 框架,基于 HTTP/2 协议,数据格式使用 Protocol Buffers(Protobuf)。性能好、效率高,还能自动生成代码,听起来就很香对吧?
二、gRPC 和传统 RPC 的几大区别(白话版)[td]对比点 | 传统 RPC | gRPC | 传输协议 | HTTP/1 或 TCP | HTTP/2,多路复用、速度快 | 数据格式 | XML/JSON,可读但体积大 | Protobuf,体积小、解析快 | 代码生成 | 多为手写 | 支持自动生成客户端/服务端代码 | 流式处理 | 一般不支持 | 支持四种调用模式,支持双向流 | 跨语言支持 | 较麻烦 | 官方支持多语言(Go、Python、Java、C++等) | 错误处理 | HTTP 状态码处理 | 标准错误码机制 + 详细描述 |
三、举个例子更直观用传统 JSON-RPC 调接口(Go 示例)
- package main
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
- )
- func main() {
- payload := map[string]interface{}{
- "jsonrpc": "2.0",
- "method": "getUserProfile",
- "params": map[string]interface{}{
- "userId": 123,
- "includeDetails": true,
- },
- "id": 1,
- }
- data, _ := json.Marshal(payload)
- resp, _ := http.Post("http://localhost:8000", "application/json", bytes.NewReader(data))
- defer resp.Body.Close()
- var result map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&result)
- fmt.Printf("结果: %+v\n", result)
- }
复制代码
用 gRPC + Protobuf(Go 示例)Protobuf 协议定义: - 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;
- }
复制代码Go 客户端调用: - conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
- defer conn.Close()
- client := pb.NewUserServiceClient(conn)
- req := &pb.UserRequest{UserId: 123, IncludeDetails: true}
- res, _ := client.GetUserProfile(context.Background(), req)
- fmt.Printf("用户名: %s\n", res.Username)
复制代码结构更清晰、体积更小、传输效率更高。
四、请求处理方式对比
传统 RPC 的调用方式(Go 示例)
- client, _ := xmlrpc.NewClient("http://localhost:8000", nil)
- var result string
- _ = client.Call("get_user_info", []interface{}{123}, &result)
- fmt.Println("用户信息:", result)
- // 再调另一个方法,又要重新连接
- var product string
- _ = client.Call("get_product_details", []interface{}{456}, &product)
- fmt.Println("商品信息:", product)
复制代码 每次调用都像重新打一次电话,很慢。
gRPC 的调用方式(Go 示例)
- conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
- defer conn.Close()
- client := pb.NewUserServiceClient(conn)
- // 同一个连接可以复用多次调用
- userRes, _ := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: 123})
- productRes, _ := client.GetProduct(context.Background(), &pb.GetProductRequest{ProductId: 456})
- fmt.Println("用户:", userRes.Username)
- fmt.Println("商品:", productRes.Name)
- // 流式接收商品列表
- stream, _ := client.ListProducts(context.Background(), &pb.ListProductsRequest{Category: "手机"})
- for {
- product, err := stream.Recv()
- if err == io.EOF {
- break
- }
- fmt.Printf("产品: %s, 价格: %.2f\n", product.Name, product.Price)
- }
复制代码 gRPC 就像是拉了一条专线,调用高效还支持流式处理。
五、性能差距有多大?REST(HTTP/1 + JSON)版本(Go 示例)- start := time.Now()
- users := []map[string]interface{}{}
- for i := 0; i < 1000; i++ {
- resp, _ := http.Get(fmt.Sprintf("http://api.example.com/users/%d", i))
- var user map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&user)
- users = append(users, user)
- resp.Body.Close()
- }
- fmt.Printf("REST API: 获取了 %d 个用户,耗时 %.2f 秒\n", len(users), time.Since(start).Seconds())
复制代码 gRPC 版本(Go 示例)
- conn, _ := grpc.Dial("api.example.com:50051", grpc.WithInsecure())
- defer conn.Close()
- client := pb.NewUserServiceClient(conn)
- start := time.Now()
- res, _ := client.GetUsers(context.Background(), &pb.GetUsersRequest{Limit: 1000})
- fmt.Printf("gRPC: 获取了 %d 个用户,耗时 %.2f 秒\n", len(res.Users), time.Since(start).Seconds())
复制代码
gRPC 更快的原因: 支持连接复用 使用 Protobuf,数据轻量 支持流式、批量处理
六、错误处理方式对比REST 错误处理(Go 示例)
- resp, _ := http.Get("http://api.example.com/users/12345")
- if resp.StatusCode != http.StatusOK {
- var errResp map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&errResp)
- fmt.Printf("错误: %v\n", errResp["error"])
- }
复制代码 靠 HTTP 状态码和自定义结构,客户端处理比较麻烦。
gRPC 错误处理(Go 示例)服务端代码: - func (s *UserServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserProfile, error) {
- user := db.FindUser(req.UserId)
- if user == nil {
- return nil, status.Errorf(codes.NotFound, "找不到用户 %d", req.UserId)
- }
- return user, nil
- }
复制代码客户端代码: - res, err := client.GetUser(context.Background(), &pb.GetUserRequest{UserId: 12345})
- if err != nil {
- st, _ := status.FromError(err)
- if st.Code() == codes.NotFound {
- fmt.Println("错误: 用户不存在 -", st.Message())
- } else {
- fmt.Println("RPC错误:", st.Code(), "-", st.Message())
- }
- }
复制代码gRPC 错误处理就像捕获本地异常一样自然。
七、实际应用场景选择用 REST API 的情况: 用 gRPC 的情况:
Go 示例:
- func (s *StockServer) PriceStream(req *pb.StockRequest, stream pb.StockService_PriceStreamServer) error {
- for {
- price := getPrice(req.Symbol)
- stream.Send(&pb.StockPrice{
- Symbol: req.Symbol,
- Price: price,
- Timestamp: time.Now().Unix(),
- })
- time.Sleep(1 * time.Second)
- }
- }
复制代码
八、总结REST API 就像普通话,大家都能听懂;gRPC 像高速公路,虽然门槛高些,但速度飞快! |