用 YAML (google.api.Service) 定义 HTTP 映射规则
区别于直接直接在 .proto 文件里写 google.api.http 映射规则
.yaml文件:
- 作用是将 gRPC 服务的 RPC 方法映射到 HTTP 请求的路径和方法上,以便可以通过 HTTP 请求来调用这些 RPC 方法。
- 它的格式是一个 YAML 文件,其中包含一个或多个
http
配置项,每个配置项对应一个 RPC 方法。
直观对比
对比项 | .proto 方式 |
YAML (google.api.Service ) 方式 |
---|---|---|
定义位置 | .proto 文件 |
yaml 配置文件 |
耦合性 | 和 gRPC 代码紧密结合 | 代码与 HTTP 规则分离 |
适用范围 | 仅适用于 grpc-gateway |
适用于 grpc-gateway 、Envoy、Google Cloud Endpoints |
修改方式 | 需要重新编译 .proto |
可以独立修改 YAML,无需修改 .proto |
维护成本 | 低(所有定义在一个文件) | 略高(需要同步管理 .proto 和 YAML) |
1. 配置文件的基本结构
一个典型的 .yaml
配置文件结构如下:
type: google.api.Service
config_version: 3
http:
rules:
- selector: package.Service.Method
get: /path/to/endpoint
body: "*"
additional_bindings:
- post: /another/path
body: "field_name"
关键字段说明
type
:
- 固定为
google.api.Service
,表示这是一个 gRPC-Gateway 配置文件。
config_version
:
- 固定为
3
,表示配置文件的版本。
http
:
- 定义 HTTP 路由规则的核心字段。
- 包含一个
rules
列表,每个规则将 HTTP 请求映射到 gRPC 方法。
rules
:
- 定义具体的 HTTP 路由规则。
- 每个规则包含以下字段:
selector
: 指定 gRPC 方法的完整名称,格式为package.Service.Method
。get/post/put/delete
: 定义 HTTP 方法和路径。body
: 指定 HTTP 请求体如何映射到 gRPC 请求的字段。additional_bindings
: 为同一个 gRPC 方法定义多个 HTTP 路由。
2. 关键字段的详细说明
2.1 selector
- 作用: 指定要映射的 gRPC 方法。
- 格式:
package.Service.Method
。 package
: Protobuf 文件中定义的包名。package后面的内容Service
: 服务名称。Method
: 方法名称。- 示例:
selector: echo.EchoService.Echo
2.2 HTTP 方法 (get/post/put/delete
)
- 作用: 定义 HTTP 请求的方法和路径。
- 格式:
<http_method>: <path>
。 <http_method>
: 可以是get
、post
、put
、delete
等。<path>
: HTTP 路径,支持路径参数(如{param}
)。- 示例:
get: /v1/echo/{message}
post: /v1/echo
2.3 body
- 作用: 指定 HTTP 请求体如何映射到 gRPC 请求的字段。
- 取值:
"*"
: 将整个 HTTP 请求体映射到 gRPC 请求的对应字段。"field_name"
: 将 HTTP 请求体映射到 gRPC 请求的指定字段。- 空值(不填): 表示不映射请求体。
- 示例:
body: "*" # 映射整个请求体
body: "message" # 映射到 gRPC 请求的 `message` 字段
2.4 additional_bindings
- 作用: 为同一个 gRPC 方法定义多个 HTTP 路由。
- 格式: 一个包含多个路由规则的列表。
- 示例:
additional_bindings:
- post: /v1/echo/stream
body: "*"
- get: /v1/echo/{message}
yaml文件生成grpc-gateway代码
- grpc_api_configuration=proto/user.yaml 指明yaml文件路径
- –grpc-gateway_out . 输出路径
- –grpc-gateway_opt paths=source_relative 以proto文件相对路径输出,意味着生成的输出文件(如服务端、客户端代码等)中的路径信息将基于
.proto
文件的位置而不是绝对路径来定义。
protoc -I . --grpc-gateway_out . --grpc-gateway_opt paths=source_relative --grpc-gateway_opt grpc_api_configuration=proto/user.yaml proto/user.proto
案例
代码结构分析
1. user.proto
文件
syntax = "proto3";
package user;
option go_package = "user.yaml/proto";
message Member {
int64 id = 1;
string userName = 2[json_name = "user_name"];
int32 age = 3;
string phone = 4;
Addr addr = 5;
}
message Addr {
string province = 1;
string city = 2;
string county = 3;
}
message UploadRequest {
int64 size = 1;
bytes content = 2;
}
message UploadResponse {
string filePath = 1[json_name = "file_path"];
}
service User {
rpc Get(Member) returns (Member) {}
rpc AddOrUpdate(Member) returns (Member) {}
rpc Delete(Member) returns (Member) {}
rpc Upload(stream UploadRequest) returns (UploadResponse) {}
}
- 定义了消息类型:
Member
,Addr
,UploadRequest
,UploadResponse
。 - 定义了服务:
User
,包含四个RPC方法:Get
,AddOrUpdate
,Delete
, 和Upload
。其中Upload
是一个流式上传接口。
2. main.go
文件
package main
import (
"context"
"golang19-grpc-gateway/user/proto"
"golang19-grpc-gateway/user/user-server/gateway"
"golang19-grpc-gateway/user/user-server/server"
"google.golang.org/grpc"
"log"
"net"
"os"
"os/signal"
"time"
)
func main() {
go func() {
if err := run(); err != nil {
log.Fatal(err)
}
}()
time.Sleep(time.Second)
go func() {
if err := gateway.Run(); err != nil {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer stop()
<-ctx.Done()
}
func run() error {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
userServiceServer := server.NewServer()
proto.RegisterUserServer(s, userServiceServer)
return s.Serve(lis)
}
- 启动了两个服务:
- gRPC服务器(监听在
50051
端口),注册了User
服务的实现。 - HTTP网关(通过
gateway.Run()
启动)。
- gRPC服务器(监听在
- 信号处理:捕获中断信号以优雅关闭服务。
3. server/server.go
文件
package server
import (
"context"
"errors"
"fmt"
"golang19-grpc-gateway/user/proto"
"google.golang.org/grpc/metadata"
"io"
"log"
"os"
)
// userServer 结构体实现了 proto.UserServer 接口,提供用户相关服务
type userServer struct {
proto.UnimplementedUserServer
}
// NewServer 创建并返回一个新的 userServer 实例
func NewServer() proto.UserServer {
return &userServer{}
}
// Get 处理用户获取请求,返回相同的用户信息
func (s *userServer) Get(ctx context.Context, in *proto.Member) (*proto.Member, error) {
fmt.Printf("%+v\n", in)
return in, nil
}
// AddOrUpdate 处理用户添加或更新请求,返回相同的用户信息
func (s *userServer) AddOrUpdate(ctx context.Context, in *proto.Member) (*proto.Member, error) {
fmt.Printf("%+v\n", in)
return in, nil
}
// Delete 处理用户删除请求,返回相同的用户信息
func (s *userServer) Delete(ctx context.Context, in *proto.Member) (*proto.Member, error) {
fmt.Printf("%+v\n", in)
return in, nil
}
// Upload 处理文件上传流,将接收到的文件数据保存到服务器
// Upload 实现了 proto.User_UploadServer 接口,用于处理文件上传请求。
// 该函数接收一个流对象,该流对象包含了上下文和上传的文件数据。
func (s *userServer) Upload(stream proto.User_UploadServer) error {
// 从上下文中获取文件名,如果获取失败或文件名为空,则返回错误。
md, ok := metadata.FromIncomingContext(stream.Context())
if !ok || len(md["file_name"]) <= 0 {
log.Println("metadata get failed")
return errors.New("文件名获取失败")
}
// 获取文件名,并打印到控制台。
filename := md["file_name"][0]
fmt.Println("server recv " + filename)
// 根据文件名创建或打开一个文件,用于存储上传的数据。
filePath := "user-server/upload-file/" + filename
dst, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer dst.Close()
// 循环读取上传流中的数据,直到遇到EOF,表示数据传输完成。
for {
req, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Println(err)
return err
}
// 将接收到的数据写入到文件中。
dst.Write(req.Content[:req.Size])
}
// 使用 SendAndClose 发送最终的响应,包含文件路径,确保客户端知道上传已完成。
// 这一步是必要的,否则客户端会因为没有接收到最终响应而阻塞。
stream.SendAndClose(&proto.UploadResponse{
FilePath: filePath,
})
return nil
}
- 实现了
User
服务的各个方法:Get
,AddOrUpdate
,Delete
方法简单返回传入的参数。Upload
方法处理文件上传,从上下文中获取文件名,并将接收到的数据写入文件,最后返回文件路径。
4. gateway/gateway.go
文件
package gateway
import (
"context"
"flag"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
gw "golang19-grpc-gateway/user/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"net/http"
)
var (
// grpc服务器端点
grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:50051", "gRPC server endpoint")
)
// Run 启动一个 HTTP 服务器,用于将 HTTP 请求转发到 gRPC 服务器。
// 该函数配置了一个 HTTP 多路复用器以处理不同的 HTTP 路径,并将这些路径与 gRPC 方法关联起来。
// 它还设置了一个不安全的 gRPC 连接选项,仅适用于开发环境。
// 返回值: 如果 HTTP 服务器启动失败或在处理请求时遇到错误,则返回错误。
func Run() error {
// 初始化一个上下文对象,用于取消操作和传递请求范围的值。
ctx := context.Background()
// 创建一个可取消的上下文,以便在函数退出时取消可能的挂起操作。
ctx, cancel := context.WithCancel(ctx)
// 确保在函数退出时取消上下文。
defer cancel()
// 创建一个 HTTP 处理多路复用器,用于将 HTTP 请求分发到不同的处理程序。
mux := runtime.NewServeMux()
// 为 "/upload" 路径添加一个处理程序,处理 POST 请求。
mux.HandlePath("POST", "/upload", uploadHandler)
// 配置一个不安全的 gRPC 连接选项,这仅适用于开发环境。
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
// 将 gRPC 方法映射为 HTTP 请求,使 HTTP 客户端可以与 gRPC 服务器通信。
err := gw.RegisterUserHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
// 如果注册处理程序时发生错误,返回该错误。
if err != nil {
return err
}
// 启动 HTTP 服务器,监听端口 8081,使用配置好的多路复用器处理请求。
return http.ListenAndServe(":8081", mux)
}
- 配置并启动HTTP网关:
- 使用
grpc-gateway
将gRPC服务映射为HTTP API。 - 注册自定义的
uploadHandler
处理文件上传请求。 - 监听
8081
端口提供HTTP服务。
- 使用
5. gateway/upload.go
文件
package gateway
import (
"context"
"fmt"
"github.com/golang/protobuf/jsonpb"
"golang19-grpc-gateway/user/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"io"
"net/http"
)
// 处理http文件上传请求,并将其转换为 gRPC 流式传输到服务器。
// uploadHandler: 这是一个HTTP请求处理函数,用于处理文件上传请求。它接收三个参数:
// w http.ResponseWriter: 用于向客户端发送HTTP响应。
// r *http.Request: 包含客户端发送的HTTP请求信息。
// pathParams map[string]string: 包含URL路径参数
func uploadHandler(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
err := r.ParseForm() // 解析表单数据
if err != nil {
http.Error(w, fmt.Sprintf("上传失败1:%s", err.Error()), http.StatusInternalServerError)
return
}
// 获取上传的文件
f, header, err := r.FormFile("attachment")
if err != nil {
http.Error(w, fmt.Sprintf("上传失败2:%s", err.Error()), http.StatusInternalServerError)
return
}
defer f.Close()
// 建立gRPC连接
conn, err := grpc.Dial(*grpcServerEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
http.Error(w, fmt.Sprintf("上传失败3:%s", err.Error()), http.StatusInternalServerError)
return
}
defer conn.Close()
// 创建gRPC客户端
c := proto.NewUserClient(conn)
// 添加上下文元数据
ctx := context.Background()
// metadata.NewOutgoingContext: 在上下文中添加元数据,这里将文件名作为元数据传递给服务端。
ctx = metadata.NewOutgoingContext(ctx, metadata.New(map[string]string{"file_name": header.Filename}))
// 创建gRPC流用于上传文件数据
stream, err := c.Upload(ctx)
if err != nil {
http.Error(w, fmt.Sprintf("上传失败4:%s", err.Error()), http.StatusInternalServerError)
return
}
// 读取并发送文件数据
buf := make([]byte, 1024)
for {
n, err := f.Read(buf)
if err != nil && err != io.EOF {
http.Error(w, fmt.Sprintf("上传失败5:%s", err.Error()), http.StatusInternalServerError)
return
}
if n == 0 {
break
}
stream.Send(&proto.UploadRequest{
Content: buf[:n],
Size: int64(n),
})
}
// 将 gRPC 的响应转换为 JSON 格式返回给客户端。
// 关闭 gRPC 流并接收最终的响应结果。CloseAndRecv() 会发送一个结束信号给服务器,并等待服务器的最终响应。
res, err := stream.CloseAndRecv() // // CloseAndRecv 适用场景:客户端流模式。
// 作用:关闭客户端的流(即发送完成)。同时阻塞等待服务器返回一个最终响应。
// 当客户端发送完数据后,需要告诉服务器 "我发完了",并等待服务器处理所有收到的数据后返回一个最终响应。
if err != nil {
http.Error(w, fmt.Sprintf("上传失败6:%s", err.Error()), http.StatusInternalServerError)
return
}
// 用于将 Protobuf 消息转换为 JSON 字符串。
m := jsonpb.Marshaler{}
str, err := m.MarshalToString(res) // 将 gRPC 响应 rs 转换为 JSON 字符串。
if err != nil {
http.Error(w, fmt.Sprintf("上传失败7:%s", err.Error()), http.StatusInternalServerError)
return
}
// 设置 HTTP 响应头,指定返回的内容类型为 JSON。
w.Header().Add("Content-Type", "application/json")
// 将 JSON 字符串写入 HTTP 响应体
fmt.Fprint(w, str)
}
- 处理HTTP文件上传请求:
- 解析表单数据并读取上传文件。
- 连接到gRPC服务器并调用
Upload
方法进行文件上传。 - 将gRPC响应转换为JSON格式并返回给客户端。
6. user.yaml
文件
type: google.api.Service
config_version: 3
http:
rules:
- selector: user.User.Get
get: "/user/{id}"
- selector: user.User.AddOrUpdate
post: "/user"
body: "*"
additional_bindings:
- put: "/user"
body: "*"
- patch: "/user"
body: "addr"
- selector: user.User.Delete
delete: "/user/{id}"
- 定义了HTTP到gRPC的映射规则:
Get
方法映射为HTTP GET请求/user/{id}
。AddOrUpdate
方法映射为HTTP POST、PUT和PATCH请求/user
。Delete
方法映射为HTTP DELETE请求/user/{id}
。