mrkong 发表于 2025-4-23 13:53:43

gRPC 和传统 RPC 有啥不一样?

本帖最后由 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 的几大区别(白话版)
对比点传统 RPCgRPC
传输协议HTTP/1 或 TCPHTTP/2,多路复用、速度快
数据格式XML/JSON,可读但体积大Protobuf,体积小、解析快
代码生成多为手写支持自动生成客户端/服务端代码
流式处理一般不支持支持四种调用模式,支持双向流
跨语言支持较麻烦官方支持多语言(Go、Python、Java、C++等)
错误处理HTTP 状态码处理标准错误码机制 + 详细描述

三、举个例子更直观用传统 JSON-RPC 调接口(Go 示例)
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    payload := mapinterface{}{
      "jsonrpc": "2.0",
      "method":"getUserProfile",
      "params": mapinterface{}{
            "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 mapinterface{}
    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 := []mapinterface{}{}

for i := 0; i < 1000; i++ {
    resp, _ := http.Get(fmt.Sprintf("http://api.example.com/users/%d", i))
    var user mapinterface{}
    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 mapinterface{}
    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 的情况:
[*]浏览器调用接口:如前端页面 fetch('/api/products')
[*]对接第三方平台:比如微信支付、支付宝,接口是 REST 的
[*]中小项目:追求开发效率即可,不追求性能极致
用 gRPC 的情况:
[*]微服务通信:服务与服务之间高频通信,高性能必选 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)
    }
}

[*]移动端通信:节省流量,响应更快
[*]多语言系统:Go 调 Java、Java 调 Python,gRPC 都能搞定

八、总结REST API 就像普通话,大家都能听懂;gRPC 像高速公路,虽然门槛高些,但速度飞快!
[*]如果你在做面向用户的 Web 接口,或者项目简单,REST API 足够。
[*]如果你在构建微服务、高性能系统、多语言协作,那就果断上 gRPC!
页: [1]
查看完整版本: gRPC 和传统 RPC 有啥不一样?