搞懂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-GatewayEnvoy Proxy。核心流程:

1)在 Protobuf 文件中通过注解定义 HTTP 路由(如 RESTful 路径、方法)。

2)生成反向代理代码,监听 HTTP 请求并转发至 gRPC 服务。

3)代理层处理协议差异(如 JSON ↔ Protobuf 的编解码)。

示例(gRPC-Gateway)

代码语言:javascript代码运行次数:0运行复制
// 定义 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)

代码语言:javascript代码运行次数:0运行复制
// 定义 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)

代码语言:javascript代码运行次数:0运行复制
# 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:

代码语言:javascript代码运行次数:0运行复制
version: v1
deps:
  - "buf.build/meshapi/grpc-api-gateway"

接下来定义protobuf文件,与常规gRPC接口定义不同的地方主要有两处:

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

代码语言:javascript代码运行次数:0运行复制
import "meshapi/gateway/annotations.proto";

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

代码语言:javascript代码运行次数:0运行复制
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 文件的开发、维护和协作流程。

下载和安装方式参考:/docs/cli/installation/

优势对比传统 Protobuf 工具:

传统 Protobuf 流程

Buf 工具

手动管理 protoc 版本和插件

统一 CLI 工具,简化安装和版本管理

繁琐的命令行参数生成代码

声明式配置,一键生成多语言代码

依赖需手动下载或拷贝 .proto 文件

声明式依赖,自动从远程仓库拉取

缺乏代码规范检查

内置 Linting 和格式化,提升代码质量

无版本化模块管理

支持模块发布、版本控制和安全扫描

编译命令

更新模块依赖:

代码语言:javascript代码运行次数:0运行复制
buf mod update

编译protobuf文件:

代码语言:javascript代码运行次数:0运行复制
buf generate

编译后我们会看见项目目录会增加很多相关文件。

编写Go代码实现RPC接口

在Go代码中我们定义HelloService结构体,实现SayHello方法,用于处理gRPC请求,SayHello方法接收一个HelloRequest请求,返回包含问候信息的HelloResponse。

主函数启动gRPC服务器,在40000端口监听TCP连接,创建gRPC服务器实例,注册HelloService服务,在goroutine中启动gRPC服务器,创建新的HTTP ServeMux,注册HTTP到gRPC的转换处理器,在4000端口启动HTTP网关服务器,具体代码如下:

代码语言:javascript代码运行次数:0运行复制
package main

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

"github/meshapi/grpc-api-gateway/gateway"
"google.golang/grpc"
"google.golang/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)
 }
}

调用接口

代码语言:javascript代码运行次数:0运行复制
package main

import (
"context"
"google.golang/grpc"
"google.golang/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 接口。这一方案尤其适合需要兼顾微服务效率与开放生态的场景,例如:

  • 对外提供开放 API 的 SaaS 平台;
  • 需要与浏览器、移动端深度交互的应用;
  • 渐进式迁移至云原生架构的传统系统。

未来,随着 HTTP/3 和 gRPC-Web 的普及,跨协议支持将更加高效,但“双协议适配”仍是微服务设计中的重要模式。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-16,如有侵权请联系 cloudcommunity@tencent 删除httpgrpc接口通信协议