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.go
dan letakkan di modul kami.
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 tidy
untuk 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
.
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