- PVSM.RU - https://www.pvsm.ru -

Полный набор gRPC, RESTful JSON API, WS и Swagger из одного proto файла. От введения до нюансов и тонкостей grpc-gateway

В этой статье я опишу процесс создания сервера с gRPC и RESTful JSON API одновременно и Swagger документацию к нему.

Эта статья — продолжение разбора различных способов реализаций API-сервера на Golang с автогенерацией кода и документации [1]. Там я обещал более подробно остановиться на этом подходе.

grpc-gateway [2] — это плагин protoc [3]. Он читает определение сервиса gRPC и генерирует обратный прокси-сервер, который переводит RESTful JSON API в gRPC. Этот сервер создается в соответствии с пользовательскими параметрами в вашем определении gRPC.

Это выглядит вот так:

Полный набор gRPC, RESTful JSON API, WS и Swagger из одного proto файла. От введения до нюансов и тонкостей grpc-gateway - 1

Установка

Для начала нам нужно установить protoc [4].

И еще нам понадобятся 3 исполняемых библиотеки на Go protoc-gen-go, protoc-gen-swagger, protoc-gen-grpc-gateway.

Так как мы все уже давно перешли на модули, то давайте зафиксируем зависимости [5] по этой инструкции.

Создадим файлик tools.go и положим его в наш модуль.

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

Вызовем go mod tidy для загрузки нужных версий пакетов. И установим их:

$ 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, то этот код вам может показаться знакомым. Если же нет, то документацию можно почитать здесь [6].

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 [7].

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.

Полный набор gRPC, RESTful JSON API, WS и Swagger из одного proto файла. От введения до нюансов и тонкостей grpc-gateway - 2

А вместе с ними и интерфейс сервера, который нам надо реализовать:

// 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 [8] — это просто прототипное представление объекта JSON. Любое сообщение proto3 может быть механически преобразовано в JSON и встроено в поле этого типа. Это очень гибкий тип и дает преимущества динамической типизации для protobuf.

Тип google.protobuf.Any [9] встраивает двоичный сериализованный protobuf вместе с информацией о типе в поле другого protobuf. Внутри это просто байтовый массив с сериализацией протокольного формата встроенного сообщения и строкой, содержащей тип URL. URL-адрес типа — это, по сути, строка, содержащая имя типа в форме type.googleapis.com/packagename.messagename.

Хотя эти типы похожи, они имеют некоторые различия [10].

Про более простые типы можно почитать здесь [11].

Инициализация сервера

Пример реализации интерфейса 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
        }
    }
}

Про использование, форматирование и коды ошибок здесь [12], здесь [13] и здесь [14].

Пример 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 [15] использует json-кодирование с разделителями строк.

Для prometheus метрики [16] и middleware [17] есть отдельные репозитории с документацией на странице github [18].

Советы по написанию 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. Подробнее здесь [19], здесь [20] и здесь [21].

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'ов можно посмотреть здесь [22]

Что бы описать дополнительные параметры 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 пользовательскими заголовками [23], например это полезно для выгрузки файлов. Больше деталей и особенностей вы найдете в разделе Features [24]

Вывод

Возможность grpc-gateway генерировать обратный HTTP прокси к gRPC и файлы swagger документации — это замечательная функция, которая помогает быстро создать сервер и красивую документацию, для тестирования вашего приложения.

Прочитав этот пост, я надеюсь, что вам будет удобнее пользоваться библиотекой. Если у вас есть комментарии или предложения, пишите в разделе комментариев.

P.S. Все файлы из статьи можно найти в репозитории https://github.com/klim0v/grpc-gateway-example [25]

Автор: Сергей Климов

Источник [26]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/api/352081

Ссылки в тексте:

[1] способов реализаций API-сервера на Golang с автогенерацией кода и документации: https://habr.com/ru/post/496098/#grpc-gateway

[2] grpc-gateway: https://grpc-ecosystem.github.io/grpc-gateway/

[3] protoc: https://github.com/protocolbuffers/protobuf

[4] protoc: https://github.com/protocolbuffers/protobuf/releases

[5] зафиксируем зависимости: https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module

[6] здесь: https://grpc.io/docs/quickstart/go/

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

[8] google.protobuf.Struct: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#struct

[9] google.protobuf.Any: https://developers.google.com/protocol-buffers/docs/proto3#any

[10] некоторые различия: https://blog.envoyproxy.io/dynamic-extensibility-and-protocol-buffers-dcd0bf0b8801

[11] здесь: https://developers.google.com/protocol-buffers/docs/proto3#simple

[12] здесь: https://cloud.google.com/apis/design/errors

[13] здесь: https://cloud.google.com/service-infrastructure/docs/service-management/reference/rpc/google.rpc#google.rpc.Code

[14] здесь: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto

[15] wsproxy: https://github.com/tmc/grpc-websocket-proxy

[16] prometheus метрики: https://github.com/grpc-ecosystem/go-grpc-prometheus

[17] middleware: https://github.com/grpc-ecosystem/go-grpc-middleware

[18] github: https://github.com/grpc-ecosystem

[19] здесь: https://github.com/grpc-ecosystem/grpc-gateway/blob/master/docs/_docs/patch.md

[20] здесь: https://pkg.go.dev/google.golang.org/genproto/protobuf/field_mask?tab=doc#FieldMask

[21] здесь: https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/field_mask.proto

[22] здесь: https://github.com/grpc-ecosystem/grpc-gateway/blob/master/third_party/googleapis/google/api/http.proto

[23] ответа от сервера c пользовательскими заголовками: https://github.com/grpc-ecosystem/grpc-gateway/blob/master/docs/_docs/httpbody.md

[24] Features: https://github.com/grpc-ecosystem/grpc-gateway#features

[25] https://github.com/klim0v/grpc-gateway-example: https://github.com/klim0v/grpc-gateway-example

[26] Источник: https://habr.com/ru/post/496574/?utm_source=habrahabr&utm_medium=rss&utm_campaign=496574