【gRPC-gateway】.yaml配置文件定义http规则以及网关上传文件,go案例

发布于:2025-02-14 ⋅ 阅读:(34) ⋅ 点赞:(0)

用 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"

关键字段说明

  1. type:
  • 固定为 google.api.Service,表示这是一个 gRPC-Gateway 配置文件。
  1. config_version:
  • 固定为 3,表示配置文件的版本。
  1. http:
  • 定义 HTTP 路由规则的核心字段。
  • 包含一个 rules 列表,每个规则将 HTTP 请求映射到 gRPC 方法。
  1. 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>: 可以是 getpostputdelete 等。
  • <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()启动)。
  • 信号处理:捕获中断信号以优雅关闭服务。
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}

https://github.com/0voice