gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 协议传输、使用 Protocol Buffers(Protobuf)作为接口定义语言和序列化格式。它在微服务架构中被广泛使用,提供了强类型接口、双向流、内置认证等能力。
但 gRPC 不是”用了就快”的银弹。HTTP/2 的连接复用模型与传统负载均衡器的冲突、Protobuf 的版本兼容性陷阱、流控与超时的微妙交互——这些工程细节决定了 gRPC 服务能否在生产环境中稳定运行。
一、gRPC 的四种通信模式
gRPC 支持四种通信模式,覆盖了 RPC 的所有交互场景:
1.1 Unary RPC(一元调用)
// 最常见的模式:一个请求,一个响应
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
string user_id = 1;
}
message GetUserResponse {
string user_id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}// Go 客户端调用
conn, err := grpc.Dial("user-service:50051", grpc.WithInsecure())
defer conn.Close()
client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "u-123"})
if err != nil {
st, ok := status.FromError(err)
if ok {
log.Printf("gRPC error: code=%s, message=%s", st.Code(), st.Message())
}
return
}
log.Printf("User: %s (%s)", resp.Name, resp.Email)1.2 Server Streaming RPC(服务端流)
// 服务端持续推送多条响应
service OrderService {
rpc ListOrders(ListOrdersRequest) returns (stream Order);
}// 客户端接收流
stream, err := client.ListOrders(ctx, &pb.ListOrdersRequest{UserId: "u-123"})
if err != nil {
log.Fatal(err)
}
for {
order, err := stream.Recv()
if err == io.EOF {
break // 流结束
}
if err != nil {
log.Fatal(err)
}
log.Printf("Order: %s, amount: %d", order.OrderId, order.Amount)
}1.3 Client Streaming RPC(客户端流)
// 客户端持续发送,服务端最终返回一个响应
service UploadService {
rpc UploadFile(stream FileChunk) returns (UploadResult);
}// 客户端发送流
stream, err := client.UploadFile(ctx)
if err != nil {
log.Fatal(err)
}
// 分块发送文件
buf := make([]byte, 64*1024)
for {
n, err := file.Read(buf)
if err == io.EOF {
break
}
if err := stream.Send(&pb.FileChunk{Data: buf[:n]}); err != nil {
log.Fatal(err)
}
}
result, err := stream.CloseAndRecv()
log.Printf("Upload result: %s, size: %d", result.FileId, result.TotalSize)1.4 Bidirectional Streaming RPC(双向流)
// 双方同时发送和接收
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}// 双向流:发送和接收在不同 goroutine 中
stream, err := client.Chat(ctx)
// 发送 goroutine
go func() {
for _, msg := range messages {
if err := stream.Send(msg); err != nil {
return
}
}
stream.CloseSend()
}()
// 接收 goroutine(主线程)
for {
msg, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Printf("Received: %s", msg.Content)
}四种模式的选型:
模式 │ 场景 │ HTTP/2 流行为
──────────────┼─────────────────────────────┼──────────────────
Unary │ 简单查询/写入 │ 一个请求帧 + 一个响应帧
Server Stream │ 列表查询、实时推送 │ 一个请求帧 + 多个响应帧
Client Stream │ 文件上传、批量导入 │ 多个请求帧 + 一个响应帧
Bidi Stream │ 聊天、实时协作、流式处理 │ 多个请求帧 + 多个响应帧
二、gRPC 在 HTTP/2 上的映射
gRPC 不是独立的传输协议——它完全构建在 HTTP/2 之上。理解这个映射关系对调试和性能优化至关重要。
2.1 请求映射
gRPC 调用映射到 HTTP/2:
一个 gRPC 调用 = 一个 HTTP/2 stream
请求(HEADERS 帧):
:method POST
:scheme https
:path /package.ServiceName/MethodName
:authority server.example.com
content-type application/grpc
te trailers ← gRPC 必须的
grpc-timeout 5S ← 超时时间
grpc-encoding gzip ← 压缩方式(可选)
请求体(DATA 帧):
┌──────────────────────────────────────┐
│ Compressed Flag (1 byte) │
│ Message Length (4 bytes, big-endian) │
│ Message Data (protobuf 编码) │
└──────────────────────────────────────┘
响应(HEADERS 帧):
:status 200
content-type application/grpc
grpc-encoding gzip
响应尾部(HEADERS 帧,带 END_STREAM):
grpc-status 0 ← gRPC 状态码
grpc-message OK ← 状态消息
2.2 gRPC 消息帧格式
gRPC 在 HTTP/2 DATA 帧中使用自己的消息分帧:
┌─────────────────┬──────────────────┬─────────────────────┐
│ Compressed (1B) │ Length (4B) │ Message (N bytes) │
│ 0 or 1 │ big-endian uint32 │ Protobuf 编码的消息 │
└─────────────────┴──────────────────┴─────────────────────┘
为什么需要这个额外分帧?
HTTP/2 的 DATA 帧可能被拆分或合并
gRPC 需要知道一条消息的边界在哪里
这 5 字节的头就是消息的"长度前缀"(Length-Prefixed Message)
流式 RPC 的消息排列:
[1B + 4B + Msg1] [1B + 4B + Msg2] [1B + 4B + Msg3] ...
每个消息独立编码,串行排列在同一个 HTTP/2 stream 的 DATA 帧中
2.3 gRPC 状态码
gRPC 状态码(grpc-status)与 HTTP 状态码是两个体系:
gRPC Code │ 名称 │ HTTP 码 │ 常见场景
──────────┼────────────────────┼─────────┼───────────────────────
0 │ OK │ 200 │ 成功
1 │ CANCELLED │ 499 │ 客户端取消
2 │ UNKNOWN │ 500 │ 未知错误
3 │ INVALID_ARGUMENT │ 400 │ 参数校验失败
4 │ DEADLINE_EXCEEDED │ 504 │ 超时
5 │ NOT_FOUND │ 404 │ 资源不存在
6 │ ALREADY_EXISTS │ 409 │ 资源已存在
7 │ PERMISSION_DENIED │ 403 │ 权限不足
8 │ RESOURCE_EXHAUSTED │ 429 │ 资源耗尽(限流)
9 │ FAILED_PRECONDITION│ 400 │ 前置条件不满足
10 │ ABORTED │ 409 │ 事务冲突
11 │ OUT_OF_RANGE │ 400 │ 范围越界
12 │ UNIMPLEMENTED │ 501 │ 方法未实现
13 │ INTERNAL │ 500 │ 服务端内部错误
14 │ UNAVAILABLE │ 503 │ 服务不可用(应重试)
15 │ DATA_LOSS │ 500 │ 数据丢失
16 │ UNAUTHENTICATED │ 401 │ 未认证
注意: gRPC 的 HTTP 状态码几乎总是 200
真正的错误信息在 grpc-status trailer 中
很多监控工具只看 HTTP 200 会误认为全部成功
三、Protobuf 编码效率
Protobuf 是 gRPC 的默认序列化格式。它的紧凑二进制编码是 gRPC 高性能的关键原因之一。
3.1 编码原理
Protobuf Wire Format:
message User {
string name = 1; // field number = 1
int32 age = 2; // field number = 2
}
编码 User{name: "Alice", age: 30}:
字节流: 0a 05 41 6c 69 63 65 10 1e
拆解:
0a = field 1, wire type 2 (length-delimited)
(1 << 3) | 2 = 0x0a
05 = 长度 5 字节
41 6c 69 63 65 = "Alice" 的 UTF-8 编码
10 = field 2, wire type 0 (varint)
(2 << 3) | 0 = 0x10
1e = 30 的 varint 编码
Wire Types:
0 — Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
1 — 64-bit (fixed64, sfixed64, double)
2 — Length-delimited (string, bytes, embedded messages, repeated)
5 — 32-bit (fixed32, sfixed32, float)
3.2 Protobuf vs JSON 对比
同一条数据的序列化大小对比:
数据: {name: "Alice", age: 30, email: "alice@example.com", active: true}
格式 │ 大小 │ 编码时间 │ 解码时间
────────────┼──────────┼───────────┼──────────
JSON │ 73 bytes │ 1.0x │ 1.0x
Protobuf │ 35 bytes │ 0.15x │ 0.12x
MessagePack │ 55 bytes │ 0.5x │ 0.4x
Protobuf 的优势:
- 大小约为 JSON 的 40-60%
- 编码/解码速度约为 JSON 的 6-10 倍
- 强类型: 编译时检查,不需要运行时反射
- 向前/向后兼容: 增加字段不破坏旧客户端
Protobuf 的劣势:
- 不可读: 二进制格式需要 .proto 文件才能解码
- 调试困难: 不能直接用文本工具查看
- 动态性差: 不适合 Schema 频繁变化的场景
3.3 Protobuf 版本兼容性
安全的 Schema 演进规则:
✅ 安全操作:
- 添加新字段(使用新的 field number)
- 删除字段(保留 field number,不重用)
- 重命名字段(只要 field number 不变)
- int32 → int64(兼容扩大)
❌ 危险操作:
- 修改字段的 field number → 数据解析错乱
- 修改字段类型(string → int32)→ 解码失败
- 重用已删除的 field number → 数据解析为旧类型
reserved 关键字防止误用:
message User {
reserved 3, 5, 9 to 11; // 保留已删除的 field number
reserved "old_name", "legacy"; // 保留已删除的字段名
string name = 1;
int32 age = 2;
// field 3 曾是 address,已删除,不可重用
}
四、gRPC 负载均衡
gRPC 基于 HTTP/2,一个 TCP 连接可以承载多个并发 RPC 调用。这与传统 L4/L7 负载均衡器的模型有根本冲突。
4.1 问题:L4 负载均衡无效
传统 HTTP/1.1 负载均衡:
客户端 → L4 LB → 后端 A # 请求 1
客户端 → L4 LB → 后端 B # 请求 2(新连接,新后端)
客户端 → L4 LB → 后端 C # 请求 3(新连接,新后端)
gRPC/HTTP/2 负载均衡:
客户端 → L4 LB → 后端 A # 请求 1(建立连接)
客户端 → L4 LB → 后端 A # 请求 2(复用连接,同一后端!)
客户端 → L4 LB → 后端 A # 请求 3(复用连接,同一后端!)
问题: HTTP/2 连接复用使得所有 RPC 都走同一个后端
L4 负载均衡器只在连接建立时做决策
连接建立后的所有请求都固定在同一个后端
4.2 解决方案
方案 1: L7 负载均衡(代理模式)
客户端 → L7 LB → 后端 A # 请求 1
客户端 → L7 LB → 后端 B # 请求 2(LB 解析 HTTP/2 帧,按 stream 分发)
客户端 → L7 LB → 后端 C # 请求 3
实现:
- Envoy: 原生支持 gRPC L7 负载均衡
- Nginx: 1.13.10+ 支持 gRPC 代理
- Linkerd/Istio: Service Mesh 的 sidecar 代理
配置示例(Envoy):
clusters:
- name: grpc_service
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {} # 启用 HTTP/2 上游
load_assignment:
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: grpc-backend
port_value: 50051
方案 2: 客户端负载均衡
客户端维护多个后端连接,自己做负载分发:
客户端 ──→ 后端 A # 请求 1(直连)
├──→ 后端 B # 请求 2(直连)
└──→ 后端 C # 请求 3(直连)
Go 实现:
import "google.golang.org/grpc/balancer/roundrobin"
conn, err := grpc.Dial(
"dns:///grpc-service.default.svc.cluster.local:50051",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithInsecure(),
)
// gRPC 支持的内置策略:
// pick_first: 使用第一个可用地址(默认)
// round_robin: 轮询所有地址
服务发现集成:
- DNS: 返回多个 A/AAAA 记录
- Consul / etcd: 自定义 Resolver
- Kubernetes: Headless Service(返回所有 Pod IP)
方案对比:
方案 │ 优点 │ 缺点
──────────┼───────────────────────┼──────────────────────
L7 代理 │ 客户端无感知 │ 额外延迟和资源开销
│ 统一管理 │ 代理成为瓶颈
客户端 LB │ 无额外延迟 │ 客户端复杂度增加
│ 无中间代理 │ 需要服务发现集成
4.3 Kubernetes 中的 gRPC 负载均衡
Kubernetes 默认的 ClusterIP Service 是 L4(iptables/IPVS)
对 gRPC 来说,负载均衡基本无效
解决方案:
方案 A: Headless Service + 客户端 LB
apiVersion: v1
kind: Service
metadata:
name: grpc-service
spec:
clusterIP: None # Headless: DNS 返回所有 Pod IP
selector:
app: grpc-service
ports:
- port: 50051
客户端连接: dns:///grpc-service.default.svc.cluster.local:50051
gRPC 客户端轮询所有 Pod IP
方案 B: Service Mesh(Istio/Linkerd)
Sidecar proxy 自动处理 gRPC L7 负载均衡
对应用代码完全透明
方案 C: Ingress/Gateway(如 Envoy Gateway)
对外暴露 gRPC 服务时使用
五、gRPC 连接管理
5.1 连接状态
gRPC 连接状态机:
IDLE ──→ CONNECTING ──→ READY ──→ IDLE(空闲超时)
│ │ │
│ ↓ ↓
│ TRANSIENT_FAILURE SHUTDOWN
│ │
│ ↓
└───→ (指数退避重连)
状态说明:
IDLE: 连接未建立或已空闲关闭
CONNECTING: 正在建立 TCP/TLS 连接
READY: 连接可用,可以发送 RPC
TRANSIENT_FAILURE: 连接失败,正在指数退避重连
SHUTDOWN: 连接已关闭,不再重试
客户端行为:
- gRPC 默认维护连接池,自动重连
- 连接空闲超过一定时间自动关闭
- TRANSIENT_FAILURE 状态下的 RPC 会排队等待
5.2 Keepalive 配置
// 服务端 Keepalive 参数
server := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 5 * time.Minute, // 空闲连接最大时长
MaxConnectionAge: 30 * time.Minute, // 连接最大存活时间
MaxConnectionAgeGrace: 10 * time.Second, // 优雅关闭的等待时间
Time: 2 * time.Minute, // Keepalive Ping 间隔
Timeout: 20 * time.Second, // Ping 超时
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 30 * time.Second, // 客户端 Ping 最小间隔
PermitWithoutStream: true, // 允许无活跃流时发送 Ping
}),
)
// 客户端 Keepalive 参数
conn, err := grpc.Dial(target,
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 30 * time.Second, // 空闲时发送 Ping 的间隔
Timeout: 10 * time.Second, // Ping 超时
PermitWithoutStream: true, // 无活跃流时也发送 Ping
}),
)
// MaxConnectionAge 的工程价值:
// 定期断开并重新建立连接
// 使得客户端有机会连接到新的后端实例
// 这对负载均衡和滚动发布非常重要六、拦截器(Interceptor)
gRPC 拦截器类似于 HTTP 中间件,用于在 RPC 调用前后注入通用逻辑。
6.1 Unary 拦截器
// 日志 + 监控拦截器
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// 调用实际处理器
resp, err := handler(ctx, req)
// 记录日志和指标
duration := time.Since(start)
code := status.Code(err)
log.Printf("method=%s duration=%v code=%s",
info.FullMethod, duration, code)
// Prometheus 指标
grpcRequestDuration.WithLabelValues(
info.FullMethod, code.String(),
).Observe(duration.Seconds())
grpcRequestTotal.WithLabelValues(
info.FullMethod, code.String(),
).Inc()
return resp, err
}
// 注册拦截器(可链式注册多个)
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
recoveryInterceptor, // panic 恢复(最外层)
loggingInterceptor, // 日志记录
authInterceptor, // 认证
validationInterceptor, // 参数校验
),
)6.2 认证拦截器
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// 跳过不需要认证的方法
if info.FullMethod == "/grpc.health.v1.Health/Check" {
return handler(ctx, req)
}
// 从 metadata 中提取 token
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
// 验证 token
claims, err := validateToken(tokens[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
// 将用户信息注入 context
ctx = context.WithValue(ctx, "user", claims)
return handler(ctx, req)
}七、gRPC 错误处理
7.1 结构化错误
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
// 返回带详细信息的错误
func (s *server) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
// 参数校验
if req.Amount <= 0 {
st := status.New(codes.InvalidArgument, "invalid order amount")
// 添加结构化的错误详情
details, _ := st.WithDetails(
&errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{
Field: "amount",
Description: "Amount must be positive",
},
},
},
)
return nil, details.Err()
}
// 资源限流
if s.rateLimiter.IsExceeded(ctx) {
st := status.New(codes.ResourceExhausted, "rate limit exceeded")
details, _ := st.WithDetails(
&errdetails.RetryInfo{
RetryDelay: durationpb.New(5 * time.Second),
},
)
return nil, details.Err()
}
return &pb.Order{OrderId: "ord-001"}, nil
}7.2 超时与取消
// 超时传播: 客户端设置的 deadline 自动传递到服务端
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.ProcessOrder(ctx, req)
if err != nil {
st := status.Convert(err)
switch st.Code() {
case codes.DeadlineExceeded:
// 超时: 可能需要查询订单状态确认是否已处理
log.Println("Request timed out, checking order status...")
case codes.Canceled:
// 取消: 客户端主动取消
log.Println("Request cancelled")
case codes.Unavailable:
// 不可用: 应该重试
log.Println("Service unavailable, retrying...")
}
}
// 服务端检查 deadline
func (s *server) ProcessOrder(ctx context.Context, req *pb.OrderRequest) (*pb.OrderResponse, error) {
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
if remaining < 100*time.Millisecond {
return nil, status.Error(codes.DeadlineExceeded, "insufficient time remaining")
}
}
// 长时间操作中检查 context
select {
case <-ctx.Done():
return nil, status.FromContextError(ctx.Err()).Err()
case result := <-s.processAsync(req):
return result, nil
}
}八、gRPC 性能优化
8.1 连接池
// 默认 gRPC 客户端使用单连接
// 对于高并发场景,单连接可能成为瓶颈
// 方案: 手动管理连接池
type ConnPool struct {
conns []*grpc.ClientConn
idx uint64
}
func NewConnPool(target string, size int) (*ConnPool, error) {
pool := &ConnPool{conns: make([]*grpc.ClientConn, size)}
for i := 0; i < size; i++ {
conn, err := grpc.Dial(target, grpc.WithInsecure())
if err != nil {
return nil, err
}
pool.conns[i] = conn
}
return pool, nil
}
func (p *ConnPool) Get() *grpc.ClientConn {
idx := atomic.AddUint64(&p.idx, 1)
return p.conns[idx%uint64(len(p.conns))]
}
// 连接池大小建议:
// CPU 核数 × 2(经验值)
// 通过压测确定最优值8.2 消息压缩
import "google.golang.org/grpc/encoding/gzip"
// 客户端启用压缩
resp, err := client.GetLargeData(ctx, req,
grpc.UseCompressor(gzip.Name))
// 服务端自动支持解压(无需额外配置)
// 压缩效果参考:
// JSON 文本: 压缩率 60-80%
// Protobuf 二进制: 压缩率 20-40%(已经很紧凑)
// 小消息(<100B): 压缩后可能更大(压缩头开销)
// 建议:
// 消息 > 1KB 时考虑压缩
// 小消息不要压缩(CPU 开销大于网络节省)8.3 性能基准对比
gRPC vs REST/JSON 性能对比(典型微服务场景):
指标 │ gRPC + Protobuf │ REST + JSON
────────────────────┼──────────────────┼──────────────
序列化速度 │ ~1.2μs │ ~8.5μs
反序列化速度 │ ~0.9μs │ ~12.3μs
消息大小(小对象) │ ~35 bytes │ ~120 bytes
消息大小(大列表) │ ~4.2 KB │ ~11.8 KB
请求延迟 (P50) │ ~0.5ms │ ~1.2ms
请求延迟 (P99) │ ~2.1ms │ ~5.8ms
吞吐量 (单连接) │ ~45,000 req/s │ ~12,000 req/s
gRPC 的性能优势来自:
1. Protobuf 二进制编码(vs JSON 文本)
2. HTTP/2 连接复用(vs HTTP/1.1 每请求一连接)
3. HTTP/2 头压缩(HPACK)
4. 编译时生成代码(vs 运行时反射)
九、gRPC-Web 与浏览器限制
浏览器无法直接使用 gRPC,因为浏览器的 HTTP/2 实现不暴露帧级别的控制能力。gRPC-Web 是解决这个问题的方案。
gRPC-Web 的工作方式:
浏览器 ──→ gRPC-Web Proxy ──→ gRPC Server
(Envoy)
浏览器发送: HTTP/1.1 或 HTTP/2 + application/grpc-web
Proxy 转换: application/grpc-web → application/grpc
Server 响应: 标准 gRPC 响应
Proxy 转换: application/grpc → application/grpc-web
浏览器接收: gRPC-Web 格式的响应
gRPC-Web 的限制:
❌ 不支持 Client Streaming(浏览器 Fetch API 限制)
❌ 不支持 Bidirectional Streaming
✅ 支持 Unary RPC
✅ 支持 Server Streaming(通过 chunked transfer)
如果需要浏览器双向流,使用 WebSocket 或 SSE 更合适
Envoy gRPC-Web 配置:
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
替代方案: Connect Protocol (buf.build)
- 支持标准 HTTP/1.1 JSON 和 gRPC 双模式
- 浏览器可直接使用,无需 proxy
- 兼容现有 gRPC 服务端
十、总结
gRPC 是微服务通信的高效选择,但它的工程复杂度不可忽视。
四种通信模式选对场景。大部分场景 Unary 足够。Server Streaming 适合列表查询和事件推送。双向流只在真正需要实时双向通信时使用。
负载均衡是最大的工程挑战。HTTP/2 连接复用让 L4 负载均衡失效。生产环境必须使用 L7 代理(Envoy)或客户端负载均衡。
Protobuf 的版本兼容要谨慎。永远不要重用 field number,删除字段用 reserved 标记。
错误处理用 gRPC 状态码而不是 HTTP 状态码。监控系统必须解析 grpc-status trailer,否则所有请求都显示 HTTP 200。
连接管理很重要。MaxConnectionAge 配合客户端负载均衡可以实现渐进式的流量迁移。Keepalive 参数需要客户端和服务端协调。
gRPC-Web 有局限。浏览器场景如果需要双向流,用 WebSocket。如果只需要 Unary + Server Streaming,gRPC-Web 是好选择。Connect Protocol 是更现代的替代方案。
参考文献
- gRPC Core Concepts (grpc.io/docs/what-is-grpc/core-concepts/)
- gRPC over HTTP/2 (github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md)
- Protocol Buffers Language Guide (protobuf.dev)
- Envoy gRPC Configuration (envoyproxy.io)
- Connect: A better gRPC (buf.build/docs/connect)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】HTTP/2 完整解剖:流、帧、HPACK 与 Server Push
深入剖析 HTTP/2 的二进制帧格式、流多路复用、HPACK 头部压缩与 Server Push 的工程实践。从协议设计到 Nginx/Go 配置,覆盖 HTTP/2 的工程挑战与性能优化。
【网络工程】协议选型决策树:REST vs gRPC vs GraphQL vs WebSocket
从延迟、吞吐、开发效率、生态成熟度四个维度对比 REST、gRPC、GraphQL、WebSocket,给出微服务内部与面向客户端的选型决策树,讨论混合架构模式与迁移路径。
HTTP/3 实战:从 QUIC 到 H3 的完整请求链路
QUIC 解决了传输层的问题,但 HTTP 怎么跑在上面?HTTP/3 不是简单地把 HTTP/2 搬到 QUIC 上——帧格式变了,头部压缩换了,流控删了。这篇从 QPACK 压缩到完整请求链路,把 HTTP/3 拆干净。
网络工程索引
汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。