Всем привет!
Данная статья является гайдом по построению REST прокси поверх существующих gRPC сервисов. После прочтения данного материала можно будет вызывать любой из существующих gRPC сервисов используя стандартный REST API, а так же получить полную документацию в swagger формате.
Для полного понимания данного материала необходимо:
Уметь писать на go
Понимать основные принципы gRPC (proto, кодогенерация, типы ...)
Мотивация для создания REST шлюза поверх существующего gRPC API может быть разная (альтернативный доступ, удобство тестирования и др.), однако как показывает практика gRPC не всегда хватает.
Дополнить существующий go gRPC сервис можно довольно быстро (от ~10 минут до суток) в зависимости от сложности существующего API, и степени проработанности REST прокси, однако в базовом формате это можно сделать относительно (сравнивая с ручным написанием сериализаторов и сервиса `перевызывающего gRPC`) быстро.
Для нетерпеливых есть репозиторий, в котором есть все необходимые материалы для подключения прокси. Далее идет пошаговый гайд как добавить прокси к существующему сервису.
1 - Устанавливаем необходимые инструменты
В данном блоке указаны пакеты которые необходимо установить (используя go) для генерации gateway кода в дополнение к коду gRPC
// +build tools
package tools
import (
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
_ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
_ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
_ "google.golang.org/protobuf/cmd/protoc-gen-go"
)
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
В случае проблем с установкой можно поискать решение здесь.
2 - Опционально (для swagger) - добавляем rest опции в proto файл
Важный нюанс - google/api/annotaitons.proto - это файл который не отгружается по умолчанию вметсе с инструментами для генерации прокси, его нужно скачать отдельно и положить в рабочую директорию с проектом с исходным proto файлом.
В репозитории есть необходмые файлы и их можно взять там. Для генерации документации и присвоения кастомных путей нужны следующие файлы:
Вот ссылки:
Далее если надо добавить опции в proto файл в следующем формате:
syntax = "proto3";
package pb;
option go_package = "/pb";
import "google/api/annotations.proto";
service Gateway {
rpc PostExample(Message) returns (Message) {
option (google.api.http) = {
post: "/post"
body: "*"
};
}
rpc GetExample(Message) returns (Message) {
option (google.api.http) = {
get: "/get/{id}"
};
}
rpc DeleteExample(Message) returns (Message) {
option (google.api.http) = {
delete: "/delete/{id}"
};
}
rpc PutExample(Message) returns (Message) {
option (google.api.http) = {
put: "/put"
body: "*"
};
}
rpc PatchExample(Message) returns (Message) {
option (google.api.http) = {
patch: "/patch"
body: "*"
};
}
}
message Message {
uint64 id = 1;
}
К существующим rpc методам мы добавили опции для получения `пути` в ссылках.
3 - Генерируем новый код с учетом rest proxy
Вот один из примеров:
protoc --go_out=. --go-grpc_out=. --grpc-gateway_out=. --grpc-gateway_opt generate_unbound_methods=true --openapiv2_out . api.proto
Другие примеры генерации кода можно найти тут.
4 - Пишем функцию которая запустит прокси сервер
Данная функция запускает rest сервер который будет обращаться к gRPC серверу и использовать его для обработки сообщений:
func runRest() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := pb.RegisterGatewayHandlerFromEndpoint(ctx, mux, "localhost:12201", opts)
if err != nil {
panic(err)
}
log.Printf("server listening at 8081")
if err := http.ListenAndServe(":8081", mux); err != nil {
panic(err)
}
}
5 - Запускаем сервер в отдельной горутине и проверяем что всё работает
Необходимо добавить:
func main() {
go runRest()
runGrpc()
}
6 - Опционально - проверяем документацию в swagger
Берем сгенерированный файл api.swagger.json и вставляем в swagger editor.
В качестве результата мы должны получить хорошую документацию в формате openapi.
Заключение
Таким образом мы получили дополнительный способ доступа к нашему gRPC сервиса малой кровью (проще чем писать собственный код и сериализаторы), что может быть полезно в ряде случаев.
Использованный инструмент: grpc-gateway
Репозиторий с примером: gateway
Полный код сервера на go:
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"gateway/pb"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type server struct {
pb.UnimplementedGatewayServer
}
func (s *server) PostExample(ctx context.Context, in *pb.Message) (*pb.Message, error) {
fmt.Println(in)
return &pb.Message{Id: in.Id}, nil
}
func (s *server) GetExample(ctx context.Context, in *pb.Message) (*pb.Message, error) {
fmt.Println(in)
return &pb.Message{Id: in.Id}, nil
}
func (s *server) DeleteExample(ctx context.Context, in *pb.Message) (*pb.Message, error) {
fmt.Println(in)
return &pb.Message{Id: in.Id}, nil
}
func (s *server) PutExample(ctx context.Context, in *pb.Message) (*pb.Message, error) {
fmt.Println(in)
return &pb.Message{Id: in.Id}, nil
}
func (s *server) PatchExample(ctx context.Context, in *pb.Message) (*pb.Message, error) {
fmt.Println(in)
return &pb.Message{Id: in.Id}, nil
}
func runRest() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := pb.RegisterGatewayHandlerFromEndpoint(ctx, mux, "localhost:12201", opts)
if err != nil {
panic(err)
}
log.Printf("server listening at 8081")
if err := http.ListenAndServe(":8081", mux); err != nil {
panic(err)
}
}
func runGrpc() {
lis, err := net.Listen("tcp", ":12201")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGatewayServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
panic(err)
}
}
func main() {
go runRest()
runGrpc()
}