搞懂 gRPC 支持 HTTP 進行雙協議通信

gRPC 是一種高性能、跨語言的 RPC 框架,其核心優勢包括:

1)基於 HTTP/2 協議實現多路複用和低延遲通信,顯著提升傳輸效率;

2)通過 Protocol Buffers 提供強類型接口定義和高效的二進制序列化,減少數據體積;

3)支持 雙向流式通信(如客戶端 / 服務端流),靈活應對複雜交互場景;

4)自動生成多語言客戶端和服務端代碼,簡化開發並保障一致性;

5)內置 認證、負載均衡、重試和超時機制,天然適配微服務架構,是構建高併發、分佈式系統的理想選擇。

爲什麼需要將 gRPC 以 HTTP 形式提供接口?

在微服務架構中,gRPC 憑藉其高性能、強類型接口和雙向流式通信等特性,成爲服務間內部通信的首選協議。然而,直接對外暴露 gRPC 接口往往面臨挑戰,尤其是在需要與瀏覽器、移動端或第三方系統交互時。此時,**同時支持 HTTP 協議(如 RESTful API)**成爲關鍵需求,將 gRPC 服務通過 HTTP(如 RESTful API)對外提供,主要有以下便利性:

1)跨平臺兼容性:HTTP/1.1 + JSON 是 Web、移動端、IoT 設備的通用標準,瀏覽器原生支持,無需引入 gRPC 客戶端庫。

2)簡化客戶端調用:前端開發者可直接用 fetch 或 axios 調用接口,無需生成 gRPC 客戶端代碼。

3)生態集成:兼容現有工具鏈(如 API 網關、監控、日誌、Postman 調試)。

4)漸進式遷移:允許舊系統逐步遷移到 gRPC,無需一次性重構。

如何實現雙協議支持?

將 gRPC 服務同時暴露爲 HTTP 接口,本質是通過協議轉換層代碼生成工具實現兩種協議之間的映射。常見的有以下幾種典型實現方式:

方式一:協議轉換層(反向代理)

通過中間代理將 HTTP 請求轉換爲 gRPC 調用,常見工具有 gRPC-Gateway 和 Envoy Proxy。核心流程:

1)在 Protobuf 文件中通過註解定義 HTTP 路由(如 RESTful 路徑、方法)。

2)生成反向代理代碼,監聽 HTTP 請求並轉發至 gRPC 服務。

3)代理層處理協議差異(如 JSON ↔ Protobuf 的編解碼)。

示例(gRPC-Gateway)

// 定義 gRPC 服務時添加 HTTP 註解
service UserService {
  rpc GetUser(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{user_id}"
    };
  }
}

message GetUserRequest { string user_id = 1; }
message User { string name = 1; uint32 age = 2; }

生成代理代碼後,HTTP 請求 GET /v1/users/123 會被轉換爲 GetUser(user_id="123") 的 gRPC 調用。

方式二:雙協議服務端

部分框架(如 go-zero、.NET Core gRPC-HTTP API)允許服務端同時監聽 gRPC 和 HTTP 端口,並自動處理協議轉換。核心流程:

1)使用同一套接口定義(Protobuf 或代碼優先)。

2)框架生成兩種協議的處理邏輯,共享業務實現。

3)服務端並行處理 gRPC 和 HTTP 請求。

示例(go-zero)

// 定義 REST 路由和 gRPC 服務
// greet.api 文件
service greet {
  @handler GreetHandler
  get /greet (Request) returns (Response)
}

// 自動生成 gRPC 和 HTTP 服務代碼
goctl api go -api greet.api -dir .
方式三:基礎設施層轉換

通過 API 網關(如 KongEnvoy)動態轉換協議,無需修改服務代碼。核心流程:

1)網關接收 HTTP 請求,根據配置路由到 gRPC 服務。

2)網關處理協議轉換(如 JSON → Protobuf)和負載均衡。

示例(Envoy gRPC-JSON Transcoding)

# Envoy 配置
http_filters:
- name: envoy.filters.http.grpc_json_transcoder
  config:
    proto_descriptor: "path/to/descriptor.pb"
    services: ["user.UserService"]

gRPC 支持 HTTP 協議實戰

這篇文章我們就來分享一下使用 gRPC API Gateway 插件,通過反向代理實現雙協議的支持,大致會分爲以下幾個步驟:

1)定義 RPC 接口:引入 gRPC API Gateway 模塊定義 RPC 和 HTTP 接口

2)編譯中間文件:使用 buf 工具編譯 protobuf 文件

3)編碼實現接口:編寫 Go 代碼實現 RPC 接口,並同步支持 HTTP

定義 RPC 接口

首先我們按照 buf 工具的約定定義配置文件,名爲 buf.yaml:

version: v1
deps:
  - "buf.build/meshapi/grpc-api-gateway"

接下來定義 protobuf 文件,與常規 gRPC 接口定義不同的地方主要有兩處:

第一是引入了三方的 protobuf 文件:

import "meshapi/gateway/annotations.proto";

第二是利用引入的 protobuf 文件定義 HTTP 接口,具體如下:

syntax = "proto3";

package main;

import "meshapi/gateway/annotations.proto";

option go_package = "/main";

service HelloService {
    rpc SayHello(HelloRequest) returns (HelloResponse) {
        option (meshapi.gateway.http) = {
            post: "/hello",
            body: "*"
        };
    }
}

message HelloRequest {
    string msg = 1;
}

message HelloResponse {
    string msg = 1;
}

使用 buf 工具編譯 protobuf 文件

buf 工具 是一個專爲 Protocol Buffers(Protobuf) 設計的現代化開發工具鏈,旨在優化 Protobuf 生態系統的開發體驗。它通過提供代碼生成、依賴管理、代碼質量檢查(Linting)、格式化、版本控制等功能,簡化了 Protobuf 文件的開發、維護和協作流程。

下載和安裝方式參考:https://buf.build/docs/cli/installation/

優勢對比傳統 Protobuf 工具:

I0yiXQ

編譯命令

更新模塊依賴:

buf mod update

編譯 protobuf 文件:

buf generate

編譯後我們會看見項目目錄會增加很多相關文件。

編寫 Go 代碼實現 RPC 接口

在 Go 代碼中我們定義 HelloService 結構體,實現 SayHello 方法,用於處理 gRPC 請求,SayHello 方法接收一個 HelloRequest 請求,返回包含問候信息的 HelloResponse。

主函數啓動 gRPC 服務器,在 40000 端口監聽 TCP 連接,創建 gRPC 服務器實例,註冊 HelloService 服務,在 goroutine 中啓動 gRPC 服務器,創建新的 HTTP ServeMux,註冊 HTTP 到 gRPC 的轉換處理器,在 4000 端口啓動 HTTP 網關服務器,具體代碼如下:

package main

import (
"context"
"fmt"
"log"
"net"
"net/http"

"github.com/meshapi/grpc-api-gateway/gateway"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

type HelloService struct {
 UnimplementedHelloServiceServer
}

func (u *HelloService) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
 log.Printf("Received request: %+v", req)
return &HelloResponse{Msg: fmt.Sprintf("Hi %s", req.GetMsg())}, nil
}

func main() {
// Start the gRPC server
 listener, err := net.Listen("tcp", ":40000")
if err != nil {
  log.Fatalf("Failed to bind: %s", err)
 }

 server := grpc.NewServer()
 RegisterHelloServiceServer(server, &HelloService{})

gofunc() {
  log.Printf("Starting gRPC server on port 40000...")
if err := server.Serve(listener); err != nil {
   log.Fatalf("Failed to start gRPC server: %s", err)
  }
 }()

// Set up the HTTP gateway
 connection, err := grpc.NewClient(":40000", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
  log.Fatalf("Failed to dial gRPC server: %s", err)
 }

 restGateway := gateway.NewServeMux()
 RegisterHelloServiceHandlerClient(context.Background(), restGateway, NewHelloServiceClient(connection))

 log.Printf("Starting HTTP gateway on port 4000...")
if err := http.ListenAndServe(":4000", restGateway); err != nil {
  log.Fatalf("Failed to start HTTP gateway: %s", err)
 }
}

調用接口

package main

import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"io"
"log"
"net/http"
"strings"
"testing"
)

func TestCallRpcApi(t *testing.T) {
 connection, err := grpc.NewClient(":40000", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
  log.Fatalf("Failed to dial gRPC server: %s", err)
 }

 client := NewHelloServiceClient(connection)
 resp, err := client.SayHello(context.TODO(), &HelloRequest{Msg: "YanTongXue"})
if err != nil {
  log.Fatalf("Failed to call SayHello: %s", err)
 }
 log.Printf("Response: %s", resp.GetMsg())
}

func TestCallHttpApi(t *testing.T) {
// 創建HTTP客戶端
 client := &http.Client{}

// 創建請求體
 requestBody := strings.NewReader(`{"msg":"YanTongXue"}`)

// 創建HTTP請求
 req, err := http.NewRequest("POST", "http://localhost:4000/hello", requestBody)
if err != nil {
  log.Fatalf("Failed to create HTTP request: %s", err)
 }
 req.Header.Set("Content-Type", "application/json")

// 發送請求
 resp, err := client.Do(req)
if err != nil {
  log.Fatalf("Failed to send HTTP request: %s", err)
 }
defer resp.Body.Close()

// 讀取響應
 body, err := io.ReadAll(resp.Body)
if err != nil {
  log.Fatalf("Failed to read response body: %s", err)
 }

// 打印響應
 log.Printf("HTTP Response: %s", string(body))
}

啓動服務後運行兩個 Test 函數就能成功調用 RPC 接口和 HTTP 接口了。

小總結

爲 gRPC 接口同時支持 HTTP 協議,本質上是在高性能與廣泛兼容性之間尋求平衡。通過協議轉換層、雙協議框架或基礎設施網關,開發者可以在保留 gRPC 內部通信優勢的同時,對外提供易用的 HTTP 接口。這一方案尤其適合需要兼顧微服務效率與開放生態的場景,例如:

未來,隨着 HTTP/3 和 gRPC-Web 的普及,跨協議支持將更加高效,但 “雙協議適配” 仍是微服務設計中的重要模式。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eqxw9WliuxwXr2GnGZDKIw