土法炼钢兴趣小组的算法知识备份

【网络工程】协议选型决策树:REST vs gRPC vs GraphQL vs WebSocket

文章导航

分类入口
network
标签入口
#protocol#rest#grpc#graphql#websocket#architecture

目录

“用什么协议”是每个新项目启动时必须回答的问题。REST 是默认选择,但 gRPC 号称性能碾压 REST,GraphQL 宣称解决了 Over-fetching,WebSocket 提供实时双向通信。

选型不是比功能列表——而是在你的具体场景下,哪个协议的工程权衡最有利。延迟敏感的内部 RPC 和面向移动端的公开 API 是完全不同的决策空间。盲目追新协议的代价往往比性能提升更大。

一、四种协议的本质定位

在比较之前,先厘清每种协议要解决的核心问题:

协议       │ 设计目标                    │ 通信模式
───────────┼────────────────────────────┼──────────────────
REST       │ 资源导向的统一接口          │ 请求/响应
gRPC       │ 高性能的服务间 RPC          │ 请求/响应 + 流式
GraphQL    │ 客户端驱动的灵活查询        │ 请求/响应
WebSocket  │ 全双工实时通信              │ 双向消息流

1.1 REST:资源与超媒体

REST(Representational State Transfer)不是协议,是架构风格。它的核心约束:

REST 的六个约束:

  1. 客户端-服务端分离
  2. 无状态: 每个请求包含所有必要信息
  3. 可缓存: 响应明确标记是否可缓存
  4. 统一接口: 资源用 URI 标识,操作用 HTTP 方法
  5. 分层系统: 允许中间层(代理、缓存、网关)
  6. 按需代码(可选): 服务端可下发可执行代码

工程现实:
  大多数"REST API"只满足 1-4,严格的 HATEOAS 几乎没人做。
  但这不影响它的工程价值——
  统一接口 + 无状态 + 可缓存 = 极其友好的基础设施生态。

1.2 gRPC:强类型 RPC 框架

gRPC 基于 HTTP/2 + Protocol Buffers,为服务间通信设计:

gRPC 核心特征:

  1. IDL(Interface Definition Language)优先
     用 .proto 文件定义服务接口
     自动生成多语言客户端/服务端代码

  2. 四种通信模式
     Unary: 普通请求/响应
     Server Streaming: 服务端推流
     Client Streaming: 客户端推流
     Bidirectional Streaming: 双向流

  3. 基于 HTTP/2
     多路复用、头部压缩、流控
     但对浏览器不友好(需要 gRPC-Web 代理)

  4. Protocol Buffers 编码
     二进制序列化,比 JSON 小 3-10 倍
     强类型,编译时检查
     前后向兼容的 Schema 演进

1.3 GraphQL:查询语言

GraphQL 是 Facebook 为移动端开发的查询语言:

GraphQL 核心特征:

  1. 客户端指定返回字段
     query {
       user(id: "123") {
         name
         email
         posts(first: 5) { title }
       }
     }
     一次请求获取精确需要的数据

  2. 单一端点
     所有查询发送到 POST /graphql
     不需要为不同资源设计不同 URL

  3. 强类型 Schema
     type User {
       id: ID!
       name: String!
       email: String
       posts: [Post!]!
     }

  4. 内省(Introspection)
     客户端可以查询 Schema 本身
     自动生成文档和代码

1.4 WebSocket:全双工通道

WebSocket 在 TCP 上提供全双工消息通道:

WebSocket 核心特征:

  1. 全双工: 服务端和客户端随时发送消息
  2. 低延迟: 建立连接后无需重复握手
  3. 长连接: 一次握手,持续通信
  4. 消息帧: 支持文本和二进制帧

  注意: WebSocket 只提供传输通道,
  应用层协议(消息格式、路由、确认)需要自己设计。

二、性能对比

2.1 延迟

延迟分解(单次请求,已建立连接):

REST (HTTP/1.1 + JSON):
  请求序列化:    ~0.1 ms (JSON)
  网络传输:      ~RTT
  响应反序列化:  ~0.1 ms
  总额外开销:    ~0.2 ms + 文本传输开销

gRPC (HTTP/2 + Protobuf):
  请求序列化:    ~0.01 ms (Protobuf)
  网络传输:      ~RTT(二进制更紧凑)
  响应反序列化:  ~0.01 ms
  总额外开销:    ~0.02 ms
  多路复用:      多个并发请求共用一个连接

GraphQL (HTTP/1.1 或 /2 + JSON):
  请求解析:      ~0.5 ms(查询语法解析)
  执行 Resolver: ~N ms(取决于查询复杂度)
  响应序列化:    ~0.1 ms
  总额外开销:    ~0.6 ms + Resolver 开销

WebSocket (已连接):
  消息序列化:    ~0.05 ms
  网络传输:      ~RTT
  无连接建立开销
  总额外开销:    ~0.05 ms(最低)

延迟排序(单次请求):
  WebSocket < gRPC < REST ≈ GraphQL
  但真正的瓶颈往往在业务逻辑和数据库,不在协议层。

2.2 吞吐量

吞吐量影响因素:

  协议      │ 编码开销  │ 连接复用  │ 压缩效率  │ 元数据开销
  ──────────┼──────────┼──────────┼──────────┼───────────
  REST/JSON │ 中       │ Keep-Alive│ gzip/br  │ HTTP 头部大
  gRPC      │ 低       │ HTTP/2 流 │ 内置      │ HPACK 压缩
  GraphQL   │ 中       │ 同 REST  │ gzip/br  │ 查询语句本身
  WebSocket │ 最低     │ 长连接   │ permsg   │ 2-14 字节帧头

典型吞吐量对比(Go 实现,同等硬件):

  场景: 获取用户信息(~200 字节响应)
  ┌──────────┬────────────────┬────────────┐
  │ 协议     │ 请求/秒 (QPS) │ P99 延迟    │
  ├──────────┼────────────────┼────────────┤
  │ REST     │ ~50,000        │ ~2 ms      │
  │ gRPC     │ ~120,000       │ ~0.8 ms    │
  │ GraphQL  │ ~30,000        │ ~5 ms      │
  │ WebSocket│ ~200,000       │ ~0.3 ms    │
  └──────────┴────────────────┴────────────┘

  注:
  - WebSocket 的 QPS 高因为省去了 HTTP 请求/响应开销
  - GraphQL 的 QPS 低因为查询解析有固定开销
  - gRPC 的优势在大量小消息场景中最明显
  - 实际生产中,数据库查询通常是瓶颈,协议差异被稀释

2.3 带宽效率

同一个用户信息的传输大小对比:

REST (JSON):
  请求: GET /api/users/123 HTTP/1.1
        Host: api.example.com
        Accept: application/json
        Authorization: Bearer eyJ...
        ~350 字节(含头部)

  响应: HTTP/1.1 200 OK
        Content-Type: application/json
        {"id":123,"name":"张三","email":"zhang@example.com",
         "created_at":"2024-01-15T10:30:00Z","role":"admin"}
        ~400 字节(含头部)

gRPC (Protobuf):
  请求: ~20 字节(Protobuf 编码的 ID)
        + HTTP/2 帧头和 HPACK 压缩头
        ~60 字节

  响应: ~45 字节(Protobuf 编码)
        + HTTP/2 帧头
        ~80 字节

GraphQL (JSON):
  请求: POST /graphql
        {"query":"{ user(id:123) { name email } }"}
        ~300 字节(含头部 + 查询语句)

  响应: {"data":{"user":{"name":"张三","email":"zhang@example.com"}}}
        ~350 字节(只返回请求的字段,但有 data 包装)

带宽节省:
  gRPC 比 REST 节省 ~80% 带宽
  GraphQL 在简单查询时与 REST 相当
  GraphQL 在避免 Over-fetching 时节省带宽(不返回不需要的字段)

三、开发体验对比

3.1 学习曲线与上手成本

REST:
  学习曲线:  ★☆☆☆☆ (最低)
  上手成本:  curl 即可测试,任何 HTTP 客户端都能调用
  文档:      Swagger / OpenAPI 生成文档
  调试:      浏览器、curl、Postman 直接查看
  痛点:      API 设计缺乏约束,容易不一致

gRPC:
  学习曲线:  ★★★☆☆ (中等)
  上手成本:  需要学习 Protobuf、安装代码生成工具链
  文档:      .proto 文件即文档
  调试:      需要专用工具(grpcurl、BloomRPC、Postman 新版)
  痛点:      浏览器不直接支持,需要 gRPC-Web 或 Connect

GraphQL:
  学习曲线:  ★★★★☆ (较高)
  上手成本:  需要学习查询语言、Schema 设计、Resolver 实现
  文档:      Schema 内省自动生成(GraphiQL / Apollo Studio)
  调试:      GraphiQL 交互式查询
  痛点:      N+1 查询、复杂查询性能、缓存难度

WebSocket:
  学习曲线:  ★★☆☆☆ (低)
  上手成本:  连接 API 简单,但应用协议需自行设计
  文档:      无标准工具,需自行维护
  调试:      浏览器 DevTools WS 面板、wscat
  痛点:      状态管理复杂、没有标准错误处理

3.2 类型安全与代码生成

类型安全对比:

REST + OpenAPI:
  定义:    YAML/JSON 格式的 API 描述文件
  生成:    openapi-generator 生成客户端(多语言)
  验证:    运行时验证(靠中间件/库)
  演进:    版本号在 URL 或 Header 中(/v1/users → /v2/users)
  痛点:    OpenAPI 描述和实际实现容易不一致

gRPC + Protobuf:
  定义:    .proto 文件(IDL)
  生成:    protoc 生成强类型客户端/服务端代码
  验证:    编译时类型检查
  演进:    字段编号保证前后兼容
  优势:    接口定义即代码,不可能不一致

GraphQL:
  定义:    GraphQL Schema Definition Language (SDL)
  生成:    codegen 生成类型定义(TypeScript / Swift / Kotlin)
  验证:    Schema 层面的编译时检查
  演进:    @deprecated 标记废弃字段
  优势:    客户端精确知道可用字段和类型

WebSocket:
  定义:    无标准(自行设计消息格式)
  生成:    无标准代码生成
  验证:    完全依赖运行时
  演进:    无标准机制
  痛点:    消息格式容易客户端/服务端不一致

四、工程权衡矩阵

4.1 综合对比表

维度                │ REST        │ gRPC        │ GraphQL     │ WebSocket
────────────────────┼────────────┼────────────┼────────────┼────────────
序列化格式          │ JSON/XML   │ Protobuf   │ JSON       │ 自定义
传输协议            │ HTTP/1.1+  │ HTTP/2     │ HTTP/1.1+  │ TCP(升级)
通信模式            │ 请求/响应  │ 四种模式   │ 请求/响应  │ 双向消息
浏览器支持          │ ✅ 原生    │ ❌ 需代理  │ ✅ 原生    │ ✅ 原生
流式传输            │ ❌(SSE替代)│ ✅ 原生   │ ❌(订阅替代)│ ✅ 原生
HTTP 缓存           │ ✅ 完整    │ ❌         │ ❌(GET可以)│ ❌
强类型接口          │ 可选(OAS)  │ ✅ 强制    │ ✅ Schema  │ ❌
代码生成            │ 可选       │ ✅ 成熟    │ ✅ 成熟    │ ❌
错误处理标准        │ HTTP状态码 │ gRPC状态码 │ errors数组 │ 自定义
中间件/代理兼容     │ ✅ 最好    │ ⚠️ 需L7   │ ✅         │ ⚠️ 需支持
CDN 缓存            │ ✅         │ ❌         │ ❌(复杂) │ ❌
版本管理            │ URL/Header │ 字段编号   │ @deprecated│ 自定义
负载均衡            │ ✅ L4/L7  │ ⚠️ 需L7   │ ✅ L4/L7  │ ⚠️ 粘性
调试难度            │ 低         │ 中         │ 中         │ 高
学习曲线            │ 最低       │ 中         │ 较高       │ 低(协议设计高)
社区生态            │ 最成熟     │ 成熟       │ 活跃       │ 成熟

4.2 场景适配评分

场景                    │ REST │ gRPC │ GraphQL │ WebSocket
────────────────────────┼──────┼──────┼─────────┼──────────
公开 API(第三方接入)   │ ★★★★★│ ★★   │ ★★★★   │ ★
微服务内部通信           │ ★★★  │ ★★★★★│ ★★     │ ★★
移动端 BFF              │ ★★★  │ ★★★  │ ★★★★★  │ ★★
实时通知/推送            │ ★    │ ★★★  │ ★★     │ ★★★★★
IoT 设备通信            │ ★★   │ ★★   │ ★      │ ★★★
在线协作/聊天           │ ★    │ ★    │ ★      │ ★★★★★
管理后台 CRUD           │ ★★★★★│ ★★   │ ★★★★   │ ★
文件上传/下载           │ ★★★★ │ ★★★  │ ★★     │ ★★
高吞吐数据管道          │ ★★   │ ★★★★ │ ★      │ ★★★

评分说明:
  ★★★★★ = 最佳选择
  ★★★★  = 很好
  ★★★   = 可以用,但有更好选择
  ★★    = 勉强可用
  ★     = 不推荐

五、选型决策树

5.1 第一步:通信模式

你的场景需要什么通信模式?

  ┌────────────────────────────┐
  │ 需要服务端主动推送消息吗? │
  └──────────┬─────────────────┘
             │
     ┌───────┴───────┐
     │ 是            │ 否
     ▼               ▼
  ┌──────────────┐ ┌────────────────────┐
  │ 推送频率?   │ │ 走请求/响应路线    │
  └──────┬───────┘ │ REST / gRPC /      │
         │         │ GraphQL            │
  ┌──────┴──────┐  └────────────────────┘
  │             │
 高频          低频
(>1次/秒)    (<1次/秒)
  │             │
  ▼             ▼
WebSocket     SSE / 长轮询
(双向)        (单向推送足够)

需要双向实时通信 → WebSocket
服务端单向推送   → SSE(简单)或 WebSocket(如果还需要客户端上行)

5.2 第二步:面向谁

你的 API 面向谁?

  ┌──────────────────────────┐
  │ API 的消费者是谁?       │
  └──────────┬───────────────┘
             │
  ┌──────────┼──────────┐
  │          │          │
内部服务   移动/Web   第三方
  │        前端        开发者
  │          │          │
  ▼          ▼          ▼
gRPC      GraphQL     REST
(首选)    (首选)      (首选)

内部服务间通信:
  → gRPC: 强类型、高性能、代码生成
  → REST 也可以,但 gRPC 在延迟和吞吐上有优势

面向前端(特别是多端/多种数据需求):
  → GraphQL: 客户端按需取数据,减少接口数量
  → REST: 如果数据模型简单、不需要灵活查询

面向第三方开发者:
  → REST: 学习成本最低、文档生态最好、任何语言/工具可用
  → GraphQL 也可以(GitHub、Shopify 的公开 API 用 GraphQL)

5.3 第三步:基础设施约束

你的基础设施支持什么?

  约束检查清单:

  □ 浏览器直接调用?
    → 排除原生 gRPC(需要 gRPC-Web 代理或 Connect 协议)

  □ 需要经过 CDN 缓存?
    → REST 最友好(GET 请求天然可缓存)
    → GraphQL 的 POST 请求不缓存(需要 Persisted Queries)
    → gRPC 和 WebSocket 不走 CDN 缓存

  □ 负载均衡器只支持 L4?
    → gRPC 的 HTTP/2 长连接导致 L4 LB 负载不均
    → 需要 L7 LB(Envoy/Nginx)或客户端 LB

  □ 防火墙/代理限制?
    → 有些企业代理不支持 HTTP/2(gRPC 受阻)
    → WebSocket 升级可能被中间代理阻断
    → REST 穿透性最好

  □ 现有 API 网关支持?
    → 多数网关原生支持 REST
    → gRPC 支持需要网关版本较新(Kong 3.x+, APISIX)
    → GraphQL 可能需要专用网关(Apollo Router)

5.4 完整决策流程图

                    ┌───────────────────────────┐
                    │ 新 API 选型               │
                    └─────────┬─────────────────┘
                              │
                    ┌─────────▼─────────────────┐
                    │ 需要实时双向通信?         │
                    └─────────┬─────────────────┘
                         是 ↙     ↘ 否
                    ┌────────┐   ┌──────────────────────┐
                    │WebSocket│   │ 服务间 or 面向用户? │
                    └────────┘   └─────┬────────────────┘
                              内部 ↙       ↘ 外部
                        ┌──────────┐   ┌──────────────────┐
                        │ gRPC     │   │ 多端多种数据需求?│
                        │ (首选)   │   └────┬─────────────┘
                        └──────────┘    是 ↙     ↘ 否
                                  ┌──────────┐ ┌──────────┐
                                  │ GraphQL  │ │ REST     │
                                  │ (首选)   │ │ (首选)   │
                                  └──────────┘ └──────────┘

六、各协议的工程陷阱

6.1 REST 陷阱

陷阱 1: Over-fetching / Under-fetching
  问题: /api/users/123 返回 50 个字段,前端只需要 3 个
  或者: 需要用户+订单+地址,要调 3 个接口
  解法: 字段选择参数(?fields=name,email)
        或 BFF 层聚合

陷阱 2: 版本管理混乱
  /v1/users 和 /v2/users 同时存在
  v1 谁来维护?什么时候下线?
  解法: 用 Header 版本(Accept: application/vnd.api+json;version=2)
        设置明确的版本废弃策略

陷阱 3: 缺乏标准错误格式
  有的返回 {"error": "..."}, 有的返回 {"message": "...", "code": 1001}
  解法: 采用 RFC 7807 Problem Details 标准
  {
    "type": "https://example.com/errors/not-found",
    "title": "User Not Found",
    "status": 404,
    "detail": "User with ID 123 does not exist"
  }

陷阱 4: REST 不等于 CRUD
  不是所有操作都能映射到 GET/POST/PUT/DELETE
  "转账""审批""发送验证码"—— 动作型接口设计困难
  解法: 使用子资源动词(POST /orders/123/cancel)
        或接受部分 RPC 风格

6.2 gRPC 陷阱

陷阱 1: 浏览器不直接支持
  浏览器无法发送 HTTP/2 帧级别的请求
  必须通过 gRPC-Web 代理(Envoy)或 Connect 协议
  增加了部署复杂度

陷阱 2: HTTP/2 + L4 LB 负载不均
  HTTP/2 的长连接导致所有请求走同一条 TCP 连接
  L4 负载均衡器按连接分配,导致负载倾斜
  解法: 使用 L7 LB 或客户端负载均衡

陷阱 3: Protobuf 调试不直观
  抓包看到的是二进制,不像 JSON 一眼看出内容
  解法: grpcurl 命令行工具
        Wireshark 的 Protobuf 解析插件
        服务端打印 Protobuf 的 Text Format 日志

陷阱 4: 跨语言代码生成版本不一致
  protoc 版本、插件版本、生成代码版本不一致
  导致编译错误或运行时不兼容
  解法: 使用 Buf 工具链统一管理
        CI 中锁定 protoc 版本
        用 buf.lock 锁定依赖

陷阱 5: 错误处理的惯例差异
  gRPC 状态码(16 个)与 HTTP 状态码不一样
  团队成员容易按 HTTP 惯例使用 gRPC 状态码
  解法: 明确 gRPC 状态码使用规范
        NOT_FOUND vs INVALID_ARGUMENT vs FAILED_PRECONDITION 的边界

6.3 GraphQL 陷阱

陷阱 1: N+1 查询
  query { users { posts { comments { author { name } } } } }
  天真的 Resolver 实现: 每个 user 查一次 posts,
  每个 post 查一次 comments, 每个 comment 查一次 author
  100 个 user → 数千次数据库查询

  解法: DataLoader 批量加载
  const userLoader = new DataLoader(async (ids) => {
      const users = await db.users.findMany({ where: { id: { in: ids } } });
      return ids.map(id => users.find(u => u.id === id));
  });

陷阱 2: 复杂查询的 DoS 攻击
  恶意客户端发送深层嵌套查询:
  { a { b { c { d { e { f { ... } } } } } } }
  消耗服务端大量资源

  解法: 查询深度限制(max depth: 10)
        查询复杂度计算(每个字段权重)
        查询白名单(Persisted Queries)

陷阱 3: 缓存困难
  REST 的 GET /users/123 天然可以 HTTP 缓存
  GraphQL 的 POST /graphql 默认不缓存
  解法: Persisted Queries(查询哈希作为 GET 参数)
        Apollo Client 的 Normalized Cache
        CDN 层面的 GraphQL 缓存(如 Stellate)

陷阱 4: Schema 演进的破坏性
  删除字段会立即影响所有客户端
  不像 REST 可以用版本号隔离
  解法: @deprecated 标记 + 客户端使用情况追踪
        永远只增加字段,不删除
        用 Schema Registry 管理版本

6.4 WebSocket 陷阱

陷阱 1: 没有标准应用协议
  WebSocket 只是传输层——消息格式、路由、错误码、确认机制
  全部需要自行设计
  解法: 使用标准子协议(STOMP、WAMP)
        或设计清晰的消息格式:
        { "type": "chat.message", "id": "uuid", "payload": {...} }

陷阱 2: 连接状态管理复杂
  断线重连、消息重发、消息去重、顺序保证
  REST 的无状态简单性全部失去
  解法: 客户端维护消息队列 + 确认机制
        服务端维护连接注册表
        使用 Last-Event-ID 风格的断点续传

陷阱 3: 水平扩展困难
  WebSocket 连接是有状态的长连接
  用户 A 连在 Server 1,用户 B 连在 Server 2
  A 给 B 发消息怎么路由?
  解法: Redis Pub/Sub 跨节点广播
        或使用会话粘性(Sticky Session)

陷阱 4: 心跳与超时
  不设置心跳 → 死连接占用资源
  心跳太频繁 → 浪费带宽和电量
  解法: 30-60 秒 Ping/Pong
        应用层 heartbeat 补充协议层 Ping

七、混合架构模式

7.1 常见混合模式

实际项目中很少只用一种协议。以下是经过验证的混合架构:

模式 1: REST + WebSocket
  适用: 大多数 Web 应用
  REST:  CRUD 操作、表单提交、文件上传
  WebSocket: 实时通知、聊天、状态变更推送

  架构:
    浏览器 ─── REST ───→ API Server ───→ Database
              WebSocket ─→ WS Server ──→ Redis Pub/Sub

模式 2: REST(外部) + gRPC(内部)
  适用: 微服务架构
  REST:  面向前端/第三方的公开 API
  gRPC:  微服务之间的内部通信

  架构:
    客户端 ── REST ──→ API Gateway ── gRPC ──→ Service A
                                     gRPC ──→ Service B
                                     gRPC ──→ Service C

模式 3: GraphQL BFF + gRPC 微服务
  适用: 多端(Web/iOS/Android)+ 微服务
  GraphQL: BFF 层聚合多个微服务的数据
  gRPC:    微服务间通信

  架构:
    Web App ────→ GraphQL BFF ── gRPC ──→ User Service
    iOS App ────→              ── gRPC ──→ Order Service
    Android ────→              ── gRPC ──→ Product Service

模式 4: REST + SSE(轻量实时)
  适用: 需要服务端推送但不需要双向通信
  REST:  常规 API
  SSE:   实时事件流(通知、进度更新)
  比 WebSocket 简单得多,适合推送频率不高的场景

7.2 混合架构的接口设计

// 混合架构:REST 对外 + gRPC 对内 的网关示例

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"

    userpb "example.com/proto/user"
    orderpb "example.com/proto/order"
    "google.golang.org/grpc"
)

type Gateway struct {
    userClient  userpb.UserServiceClient
    orderClient orderpb.OrderServiceClient
}

// REST 端点: GET /api/users/:id/overview
// 内部调用两个 gRPC 服务聚合数据
func (g *Gateway) getUserOverview(w http.ResponseWriter, r *http.Request) {
    userID := r.PathValue("id")
    ctx := r.Context()

    // 并发调用两个 gRPC 服务
    type result struct {
        user   *userpb.User
        orders *orderpb.OrderList
        err    error
    }

    userCh := make(chan result, 1)
    orderCh := make(chan result, 1)

    go func() {
        user, err := g.userClient.GetUser(ctx,
            &userpb.GetUserRequest{Id: userID})
        userCh <- result{user: user, err: err}
    }()

    go func() {
        orders, err := g.orderClient.ListOrders(ctx,
            &orderpb.ListOrdersRequest{UserId: userID, Limit: 5})
        orderCh <- result{orders: orders, err: err}
    }()

    userRes := <-userCh
    orderRes := <-orderCh

    if userRes.err != nil {
        http.Error(w, "user service error", http.StatusBadGateway)
        return
    }

    // 聚合为前端需要的 JSON 格式
    overview := map[string]any{
        "user": map[string]any{
            "id":    userRes.user.Id,
            "name":  userRes.user.Name,
            "email": userRes.user.Email,
        },
        "recent_orders": formatOrders(orderRes.orders),
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(overview)
}

func formatOrders(orders *orderpb.OrderList) []map[string]any {
    if orders == nil {
        return []map[string]any{}
    }
    result := make([]map[string]any, 0, len(orders.Orders))
    for _, o := range orders.Orders {
        result = append(result, map[string]any{
            "id":     o.Id,
            "amount": o.Amount,
            "status": o.Status.String(),
        })
    }
    return result
}

func main() {
    userConn, _ := grpc.Dial("user-service:50051",
        grpc.WithInsecure())
    orderConn, _ := grpc.Dial("order-service:50052",
        grpc.WithInsecure())

    gw := &Gateway{
        userClient:  userpb.NewUserServiceClient(userConn),
        orderClient: orderpb.NewOrderServiceClient(orderConn),
    }

    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/users/{id}/overview",
        gw.getUserOverview)

    log.Println("Gateway listening on :8080")
    http.ListenAndServe(":8080", mux)
}

八、迁移路径

8.1 从 REST 迁移到 gRPC

迁移策略: 渐进式,不是大爆炸

阶段 1: 新服务用 gRPC
  - 新建的微服务之间用 gRPC 通信
  - 对外 API 仍然走 REST
  - 在 API Gateway 层做 REST → gRPC 转换

阶段 2: 高流量内部接口迁移
  - 识别内部调用频率最高的 REST 接口
  - 用 gRPC 替代,保留 REST 作为降级方案
  - 对比迁移前后的延迟和吞吐

阶段 3: 工具链统一
  - 用 Buf 管理所有 .proto 文件
  - CI/CD 自动生成各语言的客户端代码
  - 监控和追踪工具适配 gRPC

注意事项:
  ✅ 不要迁移面向外部开发者的公开 API
  ✅ 确保 LB 和 Service Mesh 支持 gRPC
  ✅ 保留 REST 端点作为调试入口
  ❌ 不要一次性迁移所有接口

8.2 引入 GraphQL 的正确路径

GraphQL 不应该直接替代后端 API——它最适合作为 BFF 层:

正确路径:
  1. 在前端和后端之间加一层 GraphQL BFF
  2. GraphQL BFF 调用现有 REST / gRPC 后端
  3. 前端从调 N 个 REST 接口 → 调 1 个 GraphQL 查询
  4. 后端服务保持不变

错误路径:
  ❌ 在每个微服务上都加 GraphQL
  ❌ 用 GraphQL 替代微服务间通信
  ❌ 让 GraphQL Resolver 直接操作数据库
     (变成了一个巨大的单体数据层)

引入清单:
  □ 确认有多个前端消费同一组后端 API
  □ 前端开发者抱怨 Over-fetching / Under-fetching
  □ 团队愿意维护 GraphQL Schema 和 Resolver
  □ 有 DataLoader 策略处理 N+1 问题
  □ 有查询深度/复杂度限制的安全策略

8.3 WebSocket 引入时机

什么时候需要 WebSocket:

  ✅ 应用需要 <100ms 的实时更新
  ✅ 需要双向实时通信(聊天、协作编辑)
  ✅ 服务端需要频繁主动推送(>1 次/秒)
  ✅ 需要自定义二进制协议(游戏、音视频信令)

  ❌ 不需要 WebSocket:
  通知频率 <1 次/分钟 → SSE 或长轮询更简单
  只需要服务端推送(单向)→ SSE 就够了
  偶尔的实时需求 → 长轮询足够,不值得引入连接管理复杂度

混合方案:
  大多数交互走 REST → 简单可靠
  实时部分走 WebSocket → 只在需要的地方引入复杂度
  共享认证(JWT)→ WebSocket 握手时通过 token 参数传递

九、真实案例分析

9.1 电商平台

场景: 中型电商平台

前端:
  Web(React)+ iOS + Android

后端:
  用户服务、商品服务、订单服务、支付服务、库存服务

选型决策:

  面向前端: GraphQL BFF
    原因: 三端数据需求差异大
    Web 商品详情页需要完整信息
    App 列表页只需要缩略图+价格+名称
    GraphQL 让每端按需取数据

  微服务间: gRPC
    原因: 强类型、高性能
    订单服务 → 库存服务(扣减库存): 延迟敏感
    支付回调 → 订单服务(更新状态): 可靠性要求高

  实时推送: WebSocket
    原因: 订单状态实时更新
    物流追踪实时位置
    库存变化实时通知管理后台

  对外开放: REST
    原因: 第三方卖家 API、物流对接 API
    学习成本最低,文档最容易写

  架构图:
    App/Web ── GraphQL ──→ BFF ── gRPC ──→ 微服务集群
                WebSocket ──→ WS Gateway ──→ Redis Pub/Sub
    第三方 ──── REST ──→ Open API Gateway ── gRPC ──→ 微服务

9.2 实时协作工具

场景: 类似 Figma 的在线设计协作工具

选型决策:

  协作编辑: WebSocket + 自定义二进制协议
    原因: 需要 <50ms 的延迟
    操作频率极高(鼠标移动、图形变换)
    自定义 CRDT 协议处理冲突

  文件管理: REST
    原因: 项目列表、文件元数据、权限管理
    标准 CRUD 操作,REST 最合适

  团队功能: REST + SSE
    原因: 评论、通知用 REST
    在线状态、评论提醒用 SSE(频率低)

  内部服务: gRPC
    原因: 渲染服务、导出服务是计算密集型
    需要传输大量二进制数据(图形数据)
    流式传输支持渐进式渲染

  不用 GraphQL 的原因:
    数据模型相对固定(设计文件结构不常变)
    前端只有 Web,没有多端差异化需求
    实时数据走 WebSocket,不走 HTTP

十、总结

协议选型的核心原则:没有最好的协议,只有最适合场景的协议

  1. 默认选 REST。它的生态最成熟、工具最丰富、团队最容易上手。除非有明确的理由不用它。

  2. 微服务内部通信考虑 gRPC。强类型、代码生成、高性能——这是它的主场。但确保基础设施(LB、网关、Service Mesh)支持 HTTP/2。

  3. 多端差异化数据需求考虑 GraphQL。作为 BFF 层聚合后端数据,让前端按需查询。但要准备好应对 N+1 查询、安全限制、缓存策略。

  4. 实时双向通信用 WebSocket。但只在真正需要的地方用——SSE 和长轮询能解决大多数”实时”需求,复杂度低得多。

  5. 混合架构是常态。大多数成功的系统都混合使用多种协议。关键是在清晰的边界处切换——API Gateway 是 REST-to-gRPC 的自然切换点,BFF 是 GraphQL 的自然位置。

  6. 选型时考虑团队。团队没有 Protobuf 经验就不要强推 gRPC,没人维护 Schema 就不要上 GraphQL。协议的工程收益必须大于学习和维护成本。


参考文献


上一篇:MQTT 工程:IoT 协议的 QoS 与 MQTT 5.0

下一篇:WebTransport 与 WebCodecs:下一代浏览器传输

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-28 · network

【网络工程】DNS 协议解剖:查询格式、记录类型与响应码

DNS 是互联网最基础的目录服务,也是最脆弱的单点之一。本文从 wire format 出发逐字段解析 DNS 报文结构,详解 A/AAAA/CNAME/MX/SRV/TXT/NS/SOA 等记录类型的工程用途,分析 EDNS0 扩展与 DNS over TCP 的触发条件,结合 dig +trace 完整实操展示 DNS 解析的真实链路。

2025-07-31 · network

【网络工程】gRPC 深度剖析:HTTP/2 上的 RPC 框架

系统剖析 gRPC 的协议设计与工程实践:四种通信模式、HTTP/2 帧映射、Protobuf 编码效率、gRPC 负载均衡挑战(L7 vs client-side)、连接管理、拦截器、错误处理、性能调优与 gRPC-Web 的限制。


By .