Set lengkap gRPC, API JSON SISA, WS dan Swagger dari satu file proto. Dari pengantar nuansa dan seluk-beluk grpc-gateway

Pada artikel ini, saya akan menjelaskan proses membuat server dengan gRPC dan API JSON yang tenang pada saat bersamaan dan dokumentasi Swagger untuknya.


Artikel ini merupakan kelanjutan dari analisis berbagai cara menerapkan server API Golang dengan kode dan dokumentasi yang dibuat secara otomatis . Di sana saya berjanji untuk memikirkan pendekatan ini secara lebih rinci.


grpc-gateway adalah plugin protoc . Bunyinya definisi layanan gRPC dan menghasilkan proksi terbalik yang menerjemahkan API JSON yang tenang menjadi gRPC. Server ini dibuat sesuai dengan parameter pengguna dalam definisi gRPC Anda.


Ini terlihat seperti ini:



Instalasi


Pertama kita perlu menginstal protoc .


Dan kita membutuhkan 3 perpustakaan dieksekusi pada Go protoc-gen-go, protoc-gen-swagger, protoc-gen-grpc-gateway.


Karena kita semua beralih ke modul sejak lama, mari kita perbaiki dependensi sesuai dengan instruksi ini.


Buat file tools.godan letakkan di modul kami.


// +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"
)

Kami menelepon go mod tidyuntuk mengunduh versi paket yang diperlukan. Dan instal:


$ 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: trueenum , ;
  • 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