刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • rpc

  • grpc

    • 【grpc】1.初识
    • 【grpc】2.上下文 metadata
    • 【grpc】3.健康检查
    • 【grpc】 4.keepalive
    • 【grpc】 5.命名解析
    • 【grpc】 6.中间件&拦截器
    • 【grpc】 7.负载均衡
    • 【grpc】 8.身份认证
    • 【grpc】 9.超时重试
    • 【grpc】 10.链路追踪
    • 【grpc】11.grpc-gw将gRPC 转 RESTful api
      • 应用场景
      • 版本信息
      • 定义api
      • 生成stub
      • 服务启动
        • 1. http和grpc服务
        • 2. http和grpc共用一个端口
      • 生成swagger文档
      • 常见问题
        • 1. 自定义返回body
        • 1.1 方案一
        • 1.2 方案二
        • 2. 跨域问题解决
        • 3. 空字段不返回问题
    • 【grpc】12.grpc-gw自定义选项
  • protobuf

  • rpc+grpc
  • grpc
bigox
2023-05-02
目录

【grpc】11.grpc-gw将gRPC 转 RESTful api

# 应用场景

gRPC is great – it generates API clients and server stubs in many programming languages, it is fast, easy-to-use, bandwidth-efficient and its design is combat-proven by Google. However, you might still want to provide a traditional RESTful API as well. Reasons can range from maintaining backwards-compatibility, supporting languages or clients not well supported by gRPC to simply maintaining the aesthetics and tooling involved with a RESTful architecture.

gRPC非常棒--它可以用许多编程语言生成API客户端和服务器存根,它速度快、易于使用、带宽效率高,而且它的设计经过了Google的实战验证。然而,你可能仍然想提供一个传统的RESTful API。原因可能包括保持向后兼容,支持gRPC不支持的语言或客户端,以及简单地保持RESTful架构所涉及的美学和工具。

# 版本信息

  • go:1.18.0
  • grpc:1.54.0
  • protoc:3.19.4
  • protoc-gen-go: 1.28.0
  • protoc-gen-grpc-gateway: go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2

# 定义api

https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L46

  • 项目目录

    grpc_2_http
    └── proto
        └── helloworld
            └── hello_world.proto
    
    1
    2
    3
    4
  • googleapi仓库

    /Users/liusaisai/third_party
    |__google/
    └── googleapis/
    
    // git clone https://github.com/googleapis/googleapis.git
    
    1
    2
    3
    4
    5
  • hello.proto

    syntax = "proto3";
    
    package proto;
    option go_package = ".;proto";
    
    import "google/api/annotations.proto";  // 必须引入
    
    
    service Messaging {
      rpc GetMessage(GetMessageRequest) returns (Message) {
        option (google.api.http) = {
          get: "/v1/{name=messages/*}"
        };
      }
    }
    message GetMessageRequest {
      string name = 1; // Mapped to URL path.
    }
    message Message {
      string text = 1; // The resource content.
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

# 生成stub

  • 生成grpc和gw stub 命令

    protoc -I=proto  -I=/Users/liusaisai/third_party \
       --go_out=proto --go_opt=paths=source_relative \
       --go-grpc_out=proto --go-grpc_opt=paths=source_relative \
       --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative \
       helloworld/hello_world.proto
    
    1
    2
    3
    4
    5

# 服务启动

# 1. http和grpc服务

package main

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

	helloworldpb "bigox-rpc/proto/helloworld"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // 注意v2版本
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

type Server struct {
	helloworldpb.UnimplementedMessagingServer
}

func NewServer() *Server {
	return &Server{}
}

func (s *Server) GetMessage(ctx context.Context, in *helloworldpb.GetMessageRequest) (*helloworldpb.Message, error) {
	return &helloworldpb.Message{Text: in.Name + " world"}, nil
}

func main() {
	// Create a listener on TCP port
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	// 创建gRPC server
	s := grpc.NewServer()
	// 注册Message service到server
	helloworldpb.RegisterMessagingServer(s, &Server{})
	// 8080端口启动gRPC Server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	go func() {
		log.Fatalln(s.Serve(lis))
	}()

	// 创建一个连接到我们刚刚启动的 gRPC 服务器的客户端连接
	// gRPC-Gateway 就是通过它来代理请求(将HTTP请求转为RPC请求)
	conn, err := grpc.DialContext(
		context.Background(),
		"0.0.0.0:8080",
		grpc.WithBlock(),
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalln("Failed to dial server:", err)
	}

	gwMux := runtime.NewServeMux()
	// 注册Greeter
	err = helloworldpb.RegisterMessagingHandler(context.Background(), gwMux, conn)
	if err != nil {
		log.Fatalln("Failed to register gateway:", err)
	}

	gwServer := &http.Server{
		Addr:    ":8090",
		Handler: gwMux,
	}
	// 8090端口提供gRPC-Gateway服务
	log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
	log.Fatalln(gwServer.ListenAndServe())
}

// curl http://localhost:8090/v1/messages/133
// res {"text":"messages/133 world"}%
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

# 2. http和grpc共用一个端口

package main

import (
	hellopb "bigox-rpc/proto/helloworld"
	"context"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/reflection"
	"log"
	"net/http"
	"strings"
)

const addr = "0.0.0.0:8080"

type HelloServer struct {
	hellopb.UnimplementedMessagingServer
}

func (h *HelloServer) GetMessage(ctx context.Context, req *hellopb.GetMessageRequest) (*hellopb.Message, error) {
	return &hellopb.Message{Text: req.Name + "++++++"}, nil
}

func main() {
	// http server
	httpMux := http.NewServeMux()
	// grpc server
	grpcSvc := grpc.NewServer()
	hellopb.RegisterMessagingServer(grpcSvc, &HelloServer{})
	reflection.Register(grpcSvc)

	// gateway
	gwMux := runtime.NewServeMux()
	options := []grpc.DialOption{
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	}
	err := hellopb.RegisterMessagingHandlerFromEndpoint(context.Background(), gwMux, addr, options)
	if err != nil {
		log.Fatal(err)
	}
	httpMux.Handle("/", gwMux)
	err = http.ListenAndServe(addr, grpcHandlerFunc(grpcSvc, httpMux))
	if err != nil {
		log.Fatal(err)
	}

}

func grpcHandlerFunc(grpcServer *grpc.Server, httpHandler http.Handler) http.Handler {
	return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, r)
		} else {
			httpHandler.ServeHTTP(w, r)
		}
	}), &http2.Server{})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# 生成swagger文档

  • cmd

    # swagger doc
    protoc -I=./proto  -I=/Users/liusaisai/third_party \
        --swagger_out ./proto/ \
        --swagger_out=logtostderr=true:. \
        helloworld/hello_world.proto
    
    1
    2
    3
    4
    5

# 常见问题

# 1. 自定义返回body

  • grpc-gw 出错时默认返回格式

    {
      "code":5, 
      "message":"Not Found", 
      "details":[]
    }
    
    1
    2
    3
    4
    5

# 1.1 方案一

  • 将返回的内容定义为每条消息中回复的一部分

    message HelloWorldWrapper {
      int code = 1;
      HelloWorldResponse data = 2;
      string error = 3;
    }
    
    message HelloWorldResponse {
      string key = 1;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • 缺点是大量的proto api需要冗余定义

# 1.2 方案二

  • 核心函数

    • runtime.WithForwardResponseOption只会在正常响应的时候调用
    • runtime.WithErrorHandler该函数只要请求就会调用,且在正常请求runtime.WithForwardResponseOption之后调用
  • 代码示例

    • custom res
    
    type StandardResp struct {
    	Code  int         `json:"code"`
    	Data  interface{} `json:"data"`
    	Error string      `json:"error"`
    }
    
    const (
    	proxyFlag = "__success__"
    )
    
    func HttpSuccessHandler(ctx context.Context, w http.ResponseWriter, p proto.Message) error {
    	fmt.Print("111111111111111")
    	resp := StandardResp{
    		Code:  0,
    		Data:  p,
    		Error: "",
    	}
    	bs, _ := json.Marshal(&resp)
    	return errors.New(proxyFlag + string(bs))
    }
    
    func HttpErrorHandler(ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
    	fmt.Print("00000000000000")
    	w.Header().Set("Content-Type", "application/json")
    
    	// success proxy
    	raw := err.Error()
    	if strings.HasPrefix(raw, proxyFlag) {
    		raw = raw[len(proxyFlag):]
    		w.Write([]byte(raw))
    		return
    	}
    
    	// normal error
    	s, ok := status.FromError(err)
    	if !ok {
    		s = status.New(codes.Unknown, err.Error())
    	}
    	resp := StandardResp{
    		Code:  1,
    		Data:  nil,
    		Error: s.Message(),
    	}
    	bs, _ := json.Marshal(&resp)
    	w.Write(bs)
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    • mux

      	// gateway
      	gwMux := runtime.NewServeMux(
      		runtime.WithForwardResponseOption(HttpSuccessHandler),
      		runtime.WithErrorHandler(HttpErrorHandler),
      	)
      
      1
      2
      3
      4
      5

# 2. 跨域问题解决


func grpcHandlerFunc(grpcServer *grpc.Server, httpHandler http.Handler) http.Handler {
	return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, r)
		} else {
			w.Header().Set("Access-Control-Allow-Origin", "*")
			w.Header().Set("Access-Control-Request-Method", "GET, POST, PUT, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
			httpHandler.ServeHTTP(w, r)
		}
	}), &http2.Server{})
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3. 空字段不返回问题

  • 问题原因:protobuf 生成的 pb.go 中 struct 字段都是用 json:",omitempty" 修饰,这会导致在 gateway 转发返回时 json marshal 空的字段(初始值,0,空 slice 等)不返回。

  • 解决办法:是使用 jsonpb (opens new window) marshal,jsonpb 提供了 EmitDefaults 选项来控制是否解析 omitempty 字段。https://stackoverflow.com/questions/34716238/golang-protobuf-remove-omitempty-tag-from-generated-json-tags

    1. grpc-gateway

      • runtime v1

        gwMux := runtime.NewServeMux(
          runtime.WithMarshalerOption(
            runtime.MIMEWildcard, 
            &runtime.JSONPb{OrigName: true, EmitDefaults: true},
          )
        )
        
        1
        2
        3
        4
        5
        6
      • runtime v2

        import (
          "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
          "google.golang.org/protobuf/encoding/protojson"
        )
        
        gwMux := runtime.NewServeMux(
          runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
            MarshalOptions: protojson.MarshalOptions{
              EmitUnpopulated: true,
            },
          }),
        )
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
    2. 非grpc gateway

      • "github.com/golang/protobuf/jsonpb"

        func sendProtoMessage(resp proto.Message, w http.ResponseWriter) {
            w.Header().Set("Content-Type", "application/json; charset=utf-8")
            m := protojson.Marshaler{EmitDefaults: true}
            m.Marshal(w, resp) // You should check for errors here
        }
        
        1
        2
        3
        4
        5
      • google.golang.org/protobuf/jsonpb

        	func sendProtoMessage(resp proto.Message, w http.ResponseWriter) {
        		w.Header().Set("Content-Type", "application/json; charset=utf-8")
        		m := protojson.MarshalOptions{EmitUnpopulated: true}
        		b, err := m.Marshal(resp)
        		if err != nil {
        			// Handle error appropriately
        		}
        		w.Write(b)
        	}
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
#grpc
上次更新: 2023/05/04, 15:30:48
【grpc】 10.链路追踪
【grpc】12.grpc-gw自定义选项

← 【grpc】 10.链路追踪 【grpc】12.grpc-gw自定义选项→

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式