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.go
et mettez-le dans notre module.
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 tidy
pour 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
.
package grpc_gateway_example
3 ./api_pb/api.go
, ./api_pb/api.gw.go
./api.swager.json
.

, :
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
.
, .
.
BlockchainServiceServerpackage 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.gopackage 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