Ensemble complet de gRPC, API JSON RESTful, WS et Swagger à partir d'un fichier proto. De l'introduction aux nuances et subtilités de grpc-gateway

Dans cet article, je vais décrire le processus de création d'un serveur avec gRPC et l'API JSON RESTful en même temps et la documentation Swagger correspondante.


Cet article est une continuation de l'analyse des différentes manières d'implémenter le serveur API Golang avec du code et de la documentation générés automatiquement . Là, j'ai promis de m'attarder sur cette approche plus en détail.


grpc-gateway est un plugin de protocole . Il lit la définition du service gRPC et génère un proxy inverse qui traduit l'API JSON RESTful en gRPC. Ce serveur est créé en fonction des paramètres utilisateur dans votre définition gRPC.


Cela ressemble Ă  ceci:



Installation


Nous devons d'abord installer le protocole .


Et nous avons besoin de 3 bibliothèque exécutable sur le Go protoc-gen-go, protoc-gen-swagger, protoc-gen-grpc-gateway.


Puisque nous sommes tous passés aux modules il y a longtemps, corrigeons les dépendances selon cette instruction.


Créez un fichier tools.goet mettez-le dans notre module.


// +build tools

package tools

import (
    _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway"
    _ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger"
    _ "github.com/golang/protobuf/protoc-gen-go"
)

Nous appelons go mod tidypour télécharger les versions nécessaires des packages. Et installez-les:


$ go install \
    github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway \
    github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger \
    github.com/golang/protobuf/protoc-gen-go

, , .



gRPC, . , .


syntax = "proto3";
package api_pb;
message AddressRequest {
    string address = 1;
    uint64 height = 2;
}
message AddressResponse {
    map<string, string> balance = 1;
    string transactions_count = 2;
}

service BlockchainService {
    rpc Address (AddressRequest) returns (AddressResponse);
}

google.api.http.


syntax = "proto3";
package api_pb;
import "google/api/annotations.proto";

message AddressRequest {
    string address = 1;
    uint64 height = 2;
}
message AddressResponse {
    map<string, string> balance = 1;
    string transactions_count = 2;
}

service BlockchainService {
    rpc Address (AddressRequest) returns (AddressResponse) {
        option (google.api.http) = {
            get: "/address/{address}"
        };
    }
}

web-socket endpoint.


syntax = "proto3";
package api_pb;
import "google/api/annotations.proto";
import "google/protobuf/struct.proto";

message AddressRequest {
    string address = 1;
    uint64 height = 2;
}
message AddressResponse {
    map<string, string> balance = 1;
    string transactions_count = 2;
}
message SubscribeRequest {
    string query = 1;
}
message SubscribeResponse {
    string query = 1;
    google.protobuf.Struct data = 2;
    message Event {
        string key = 1;
        repeated string events = 2;
    }
    repeated Event events = 3;
}

service BlockchainService {
    rpc Address (AddressRequest) returns (AddressResponse) {
        option (google.api.http) = {
            get: "/address/{address}"
        };
    }
    rpc Subscribe (SubscribeRequest) returns (stream SubscribeResponse) {
        option (google.api.http) = {
            get: "/subscribe"
        };
    }
}

Makefile .


all:
        mkdir -p "api_pb"
        protoc -I/usr/local/include -I. \
            -I${GOPATH}/src \
            -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
            -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway \
            --grpc-gateway_out=logtostderr=true:./api_pb \
            --swagger_out=allow_merge=true,merge_file_name=api:. \
            --go_out=plugins=grpc:./api_pb ./*.proto

gen.go .


//go:generate make

package grpc_gateway_example

3 ./api_pb/api.go, ./api_pb/api.gw.go ./api.swager.json.



, :


// BlockchainServiceServer is the server API for BlockchainService service.
type BlockchainServiceServer interface {
    Address(context.Context, *AddressRequest) (*AddressResponse, error)
    Subscribe(*SubscribeRequest, BlockchainService_SubscribeServer) error
}


protobuf. google.protobuf.Struct — JSON. proto3 JSON . protobuf.


google.protobuf.Any protobuf protobuf. , URL. URL- — , , , type.googleapis.com/packagename.messagename.


, .


.



BlockchainServiceServer
package service

import (
    "bytes"
    "context"
    "encoding/json"
    "github.com/golang/protobuf/jsonpb"
    _struct "github.com/golang/protobuf/ptypes/struct"
    "github.com/klim0v/grpc-gateway-example/api_pb"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    "time"
)

type BlockchainServer struct {
    eventBus <-chan interface{}
}

func NewBlockchainServer(eventBus <-chan interface{}) *BlockchainServer {
    return &BlockchainServer{eventBus: eventBus}
}

func (b *BlockchainServer) Address(_ context.Context, req *api_pb.AddressRequest) (*api_pb.AddressResponse, error) {
    if req.Address != "Mxb9a117e772a965a3fddddf83398fd8d71bf57ff6" {
        return &api_pb.AddressResponse{}, status.Error(codes.FailedPrecondition, "wallet not found")
    }
    return &api_pb.AddressResponse{
        Balance: map[string]string{
            "BIP": "12345678987654321",
        },
        TransactionsCount: "120",
    }, nil
}

func (b *BlockchainServer) Subscribe(req *api_pb.SubscribeRequest, stream api_pb.BlockchainService_SubscribeServer) error {
    for {
        select {
        case <-stream.Context().Done():
            return stream.Context().Err()
        case event := <-b.eventBus:
            byteData, err := json.Marshal(event)
            if err != nil {
                return err
            }
            var bb bytes.Buffer
            bb.Write(byteData)
            data := &_struct.Struct{Fields: make(map[string]*_struct.Value)}
            if err := (&jsonpb.Unmarshaler{}).Unmarshal(&bb, data); err != nil {
                return err
            }

            if err := stream.Send(&api_pb.SubscribeResponse{
                Query: req.Query,
                Data:  data,
                Events: []*api_pb.SubscribeResponse_Event{
                    {
                        Key:    "tx.hash",
                        Events: []string{"01EFD8EEF507A5BFC4A7D57ECA6F61B96B7CDFF559698639A6733D25E2553539"},
                    },
                },
            }); err != nil {
                return err
            }
        case <-time.After(5 * time.Second):
            return nil
        }
    }
}

, , .


main.go
package main

import (
    "context"
    "flag"
    "github.com/golang/glog"
    grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    gw "github.com/klim0v/grpc-gateway-example/api_pb"
    "github.com/klim0v/grpc-gateway-example/service"
    "github.com/tmc/grpc-websocket-proxy/wsproxy"
    "golang.org/x/sync/errgroup"
    "google.golang.org/grpc"
    "net"
    "net/http"
    "time"
)

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    lis, err := net.Listen("tcp", ":8842")
    if err != nil {
        return err
    }

    grpcServer := grpc.NewServer(
        grpc.StreamInterceptor(grpc_prometheus.StreamServerInterceptor),
        grpc.UnaryInterceptor(grpc_prometheus.UnaryServerInterceptor),
    )
    eventBus := make(chan interface{})
    gw.RegisterBlockchainServiceServer(grpcServer, service.NewBlockchainServer(eventBus))
    grpc_prometheus.Register(grpcServer)

    var group errgroup.Group

    group.Go(func() error {
        return grpcServer.Serve(lis)
    })

    mux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{OrigName: true, EmitDefaults: true}))
    opts := []grpc.DialOption{
        grpc.WithInsecure(),
        grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(50000000)),
    }

    group.Go(func() error {
        return gw.RegisterBlockchainServiceHandlerFromEndpoint(ctx, mux, ":8842", opts)
    })
    group.Go(func() error {
        return http.ListenAndServe(":8843", wsproxy.WebsocketProxy(mux))
    })
        group.Go(func() error {
        return http.ListenAndServe(":2662", promhttp.Handler())
    })
    group.Go(func() error {
        for i := 0; i < 100; i++ {
            eventBus <- struct {
                Type             byte
                Coin             string
                Value            int
                TransactionCount int
                Timestamp        time.Time
            }{
                Type:             1,
                Coin:             "BIP",
                TransactionCount: i,
                Timestamp:        time.Now(),
            }
        }
        return nil
    })

    return group.Wait()
}

func main() {
    flag.Parse()
    defer glog.Flush()

    if err := run(); err != nil {
        glog.Fatal(err)
    }
}

jsonpb.Marshaler :


  • EmitDefaults: true — , 0 int, "" string;
  • EnumsAsInts: true — enum , ;
  • OrigName: true — json', .proto .

( ) grpc.MaxCallRecvMsgSize(50000000).


web-sokets, handler wsproxy.WebsocketProxy(mux). wsproxy json- .


prometheus middleware github.


protobuf


JSON .proto .
Swagger grpc-gateway Swagger. snake_case, json_name protobuf .


message AwesomeName {
    uint32 id = 1;
    string awesome_name = 2 [json_name = "awesome_name"];
}

. , , . // Output only. protobuf .


message AwesomeName {
    // Output only.
    uint32 id = 1;
    string awesome_name = 2;
}

URL protobuf
, , . REST , URL.


service AwesomeService {
    rpc UpdateAppointment (UpdateAwesomeNameRequest) returns (AwesomeName) {
        option (google.api.http) = {
            put: "/v1/awesome-name/{awesome_name.id}"
            body: "awesome_name"
        };
    };
}
message UpdateAwesomeNameRequest {
    AwesomeName awesome_name = 1;
}

. , , grpc-gateway . protobuf update_mask , PATCH. , .


message UpdateAwesomeNameRequest {
    AwesomeName awesome_name = 1;
    google.protobuf.FieldMask update_mask = 2; // This field will be automatically populated by grpc-gateway.
}

HTTP- RPC, additional_bindings. endpoint'


Swagger option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger).


syntax = "proto3";
import "protoc-gen-swagger/options/annotations.proto";
package awesome.service;
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    info: {
        title: "My Habr Example Service"
        version: "1.0"
        contact: {
            name: "Klimov Sergey"
            url: "https://github.com/klim0v"
            email: "klim0v-sergey@yandex.ru"
        };
    };
    schemes: [HTTP,HTTPS]
    consumes: "application/json"
    produces: "application/json"
    responses: {
        key: "404"
        value: {
            description: "Returned when the resource does not exist."
            schema: {
                json_schema: {
                    type: STRING
                };
            };
        };
    };
};

c . .


import "google/api/httpbody.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
service HttpBodyExampleService {
    rpc HelloWorld(google.protobuf.Empty) returns (google.api.HttpBody) {
        option (google.api.http) = {
            get: "/helloworld"
        };
    }   
}

func (*HttpBodyExampleService) Helloworld(ctx context.Context, in *empty.Empty) (*httpbody.HttpBody, error) {
    return &httpbody.HttpBody{
        ContentType: "text/html",
        Data:        []byte("Hello World"),
    }, nil
}

Features



grpc-gateway HTTP gRPC swagger — , , .


, , . , .


P.S. github OpenAPI github-pages


All Articles