gRPC 与 Protobuf 高性能通信实战

🎯 面试题:gRPC 和 REST 有什么区别?Protobuf 为什么比 JSON 快?

gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 和 Protocol Buffers。微服务间通信的重要选型。


一、gRPC vs REST vs 消息队列

特性 gRPC REST 消息队列
协议 HTTP/2 HTTP/1.1 TCP
序列化 Protobuf(二进制) JSON(文本) 二进制/文本
性能 ⭐⭐⭐ ⭐⭐ ⭐⭐
流式支持 四种流 不支持 不支持
代码生成 自动(多语言) 手动或 Swagger 手动
浏览器兼容 需要 gRPC-Web ✅ 原生
适用场景 微服务间通信 对外 API 异步解耦

二、Protobuf 语法

syntax = "proto3";

package com.example.user;

// 定义消息(对应 Java 的 POJO)
message UserRequest {
  int64 user_id = 1;        // 字段编号,序列化用
  string username = 2;
  repeated string tags = 3; // 数组
  UserStatus status = 4;    // 枚举
}

message UserResponse {
  int64 user_id = 1;
  string username = 2;
  int64 created_at = 3;
  repeated Order orders = 4; // 嵌套消息
}

message Order {
  int64 order_id = 1;
  double amount = 2;
  string status = 3;
}

enum UserStatus {
  UNKNOWN = 0;   // proto3 枚举必须有默认值 0
  ACTIVE = 1;
  INACTIVE = 2;
  BANNED = 3;
}

// 定义服务
service UserService {
  // 一元调用
  rpc GetUser (UserRequest) returns (UserResponse);
  // 服务端流
  rpc StreamUsers (UserRequest) returns (stream UserResponse);
  // 客户端流
  rpc BatchCreate (stream UserRequest) returns (UserResponse);
  // 双向流
  rpc Chat (stream UserRequest) returns (stream UserResponse);
}

编译生成代码

# 生成 Java 代码
protoc --java_out=./src/main/java \
       --grpc-java_out=./src/main/java \
       user.proto

# 生成多语言
protoc --go_out=. --go-grpc_out=. user.proto    # Go
protoc --python_out=. user.proto                 # Python

三、Protobuf 编码原理

Protobuf 编码流程:
  .proto 定义 → 编译器 → 序列化(varint + 字段号 + 类型) → 二进制字节流

核心编码方式:Varint(变长整数)

Varint 编码:
  每字节 7 bit 表示数据,最高位 MSB 表示是否还有后续字节

  举例:300 的 Varint 编码
  300 = 100101100
  分组(7位一组):10 0101100
  第一字节:1_0101100 = 0xAC(MSB=1,还有后续)
  第二字节:0_0000010 = 0x02(MSB=0,结束)
  结果:[0xAC, 0x02] — 仅 2 字节(int32 固定 4 字节)

Wire Type:
  0 = Varint(int32/int64/bool/enum)
  1 = 64-bit(double/fixed64)
  2 = Length-delimited(string/bytes/嵌套消息/repeated)
  5 = 32-bit(float/fixed32)

为什么比 JSON 快?
  1. 二进制编码,体积小(JSON 有大量引号/冒号/花括号)
  2. 编解码简单,无需解析器(JSON 需要词法分析)
  3. 字段用编号引用,不需要字段名(JSON 每个字段都要带名字)
  4. Schema 预编译,运行时零反射(JSON 需要反射)

四、HTTP/2 特性

HTTP/1.1 的问题:
  - 每个请求一个 TCP 连接(队头阻塞)
  - 请求头冗余(每次都要发 Cookie/Accept 等大量头)
  - 单向通信(服务端无法主动推送)

HTTP/2 的改进:
  ┌───────────────────────────────────────────────┐
  │ 1. 多路复用:一个 TCP 连接上并行多个请求         │
  │    Stream 1: [HEADERS][DATA][DATA]             │
  │    Stream 2: [HEADERS][DATA]                   │
  │    Stream 3: [HEADERS][DATA][DATA][DATA]        │
  │    → 消除队头阻塞                              │
  │                                               │
  │ 2. 头部压缩:HPACK 算法,减少 70-90% 头部大小    │
  │    静态表 + 动态表 + 哈夫曼编码                 │
  │                                               │
  │ 3. 二进制帧:请求/响应拆分为 Frame,更高效        │
  │                                               │
  │ 4. 服务端推送:服务端可以主动推送资源             │
  │    → gRPC 的服务端流利用了这个特性               │
  │                                               │
  │ 5. 流优先级:客户端可以设置流的优先级             │
  └───────────────────────────────────────────────┘

五、gRPC 四种调用方式

1. 一元调用(Unary)

// 服务端
@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
    @Override
    public void getUser(UserRequest request, StreamObserver<UserResponse> observer) {
        User user = userService.findById(request.getUserId());
        UserResponse response = UserResponse.newBuilder()
            .setUserId(user.getId())
            .setUsername(user.getName())
            .build();
        observer.onNext(response);
        observer.onCompleted();
    }
}

// 客户端
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
UserResponse response = stub.getUser(UserRequest.newBuilder()
    .setUserId(1001)
    .build());

2. 服务端流(Server Streaming)

// 服务端:推送用户列表
@Override
public void streamUsers(UserRequest request, StreamObserver<UserResponse> observer) {
    List<User> users = userService.findAll();
    for (User user : users) {
        observer.onNext(convert(user));  // 逐条推送
    }
    observer.onCompleted();
}

// 客户端
Iterator<UserResponse> responses = stub.streamUsers(request);
while (responses.hasNext()) {
    UserResponse r = responses.next();
    System.out.println(r.getUsername());
}

3. 客户端流(Client Streaming)

// 服务端:接收批量数据
@Override
public StreamObserver<UserRequest> batchCreate(StreamObserver<UserResponse> observer) {
    return new StreamObserver<UserRequest>() {
        private int count = 0;
        @Override
        public void onNext(UserRequest request) {
            userService.create(convert(request));
            count++;
        }
        @Override
        public void onError(Throwable t) { observer.onError(t); }
        @Override
        public void onCompleted() {
            observer.onNext(UserResponse.newBuilder().setMessage("Created " + count).build());
            observer.onCompleted();
        }
    };
}

4. 双向流(Bidirectional)

// 服务端:聊天
@Override
public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessage> observer) {
    return new StreamObserver<ChatMessage>() {
        @Override
        public void onNext(ChatMessage msg) {
            // 收到消息后回复
            observer.onNext(ChatMessage.newBuilder()
                .setFrom("server").setContent("Echo: " + msg.getContent())
                .build());
        }
        @Override public void onError(Throwable t) { }
        @Override public void onCompleted() { observer.onCompleted(); }
    };
}

六、拦截器

// 服务端拦截器:鉴权
@Order(1)
public class AuthInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        String token = headers.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER));
        if (token == null || !jwtUtil.validate(token)) {
            call.close(Status.UNAUTHENTICATED.withDescription("Token invalid"), headers);
            return new ServerCall.Listener<>() {};
        }
        return next.startCall(call, headers);
    }
}

// 客户端拦截器:添加 Token
public class TokenInterceptor implements ClientInterceptor {
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
        return new ForwardingClientCall.SimpleForwardingClientCall<>(next.newCall(method, callOptions)) {
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                headers.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "Bearer " + token);
                super.start(responseListener, headers);
            }
        };
    }
}

七、面试高频题

Q1: gRPC 和 REST 有什么区别?

gRPC 基于 HTTP/2 + Protobuf,REST 基于 HTTP/1.1 + JSON。gRPC 支持四种流式调用、自动生成客户端 SDK、二进制序列化性能更高(体积小 3-10 倍、速度快 5-20 倍)。REST 更通用,浏览器原生支持。gRPC 适合微服务间通信,REST 适合对外 API。浏览器端需要 gRPC-Web 适配。

Q2: Protobuf 为什么比 JSON 快?

四个原因:① 二进制编码,没有 JSON 的引号、冒号、花括号等冗余字符,体积小 3-10 倍;② Varint 编码,小数字只占 1 字节;③ 不携带字段名,用编号引用字段;④ Schema 预编译生成代码,序列化/反序列化是直接字段赋值,无需反射和语法解析。缺点是可读性差、需要 IDL 定义。

Q3: HTTP/2 如何解决 HTTP/1.1 的队头阻塞?

HTTP/1.1 的队头阻塞是因为多个请求共用一个 TCP 连接时,前一个请求的响应未返回会阻塞后续请求。HTTP/2 通过多路复用解决:在一个 TCP 连接上建立多个 Stream(帧流),每个 Stream 独立传输,互不阻塞。但 HTTP/2 仍有 TCP 层的队头阻塞(丢包会阻塞所有 Stream),HTTP/3 用 QUIC 协议(基于 UDP)彻底解决这个问题。