In diesem Artikel werde ich den Prozess des gleichzeitigen Erstellens eines Servers mit gRPC und der RESTful JSON-API sowie die entsprechende Swagger-Dokumentation beschreiben.
Dieser Artikel ist eine Fortsetzung der Analyse verschiedener Möglichkeiten zur Implementierung des Golang-API-Servers mit automatisch generiertem Code und Dokumentation . Dort habe ich versprochen, näher auf diesen Ansatz einzugehen.
grpc-gateway ist ein Protoc- Plugin . Es liest die gRPC-Dienstdefinition und generiert einen Reverse-Proxy, der die RESTful JSON-API in gRPC übersetzt. Dieser Server wird gemäß den Benutzerparametern in Ihrer gRPC-Definition erstellt.
Es sieht aus wie das:

Installation
Zuerst müssen wir protoc installieren .
Und wir brauchen die drei ausführbare Bibliothek auf der Go protoc-gen-go
, protoc-gen-swagger
, protoc-gen-grpc-gateway
.
Da wir alle vor langer Zeit zu Modulen gewechselt sind, beheben wir die Abhängigkeiten gemäß dieser Anweisung.
Erstellen Sie eine Datei tools.go
und fügen Sie sie in unser Modul ein.
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"
)
Wir rufen go mod tidy
an, um die erforderlichen Versionen der Pakete herunterzuladen. Und installieren Sie sie:
$ 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