Всем привет!

Данная статья является гайдом по построению 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 get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
go get google.golang.org/protobuf/cmd/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

В случае проблем с установкой можно поискать решение здесь.

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.prorototo

Другие примеры генерации кода можно найти тут.

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()
}

Комментарии (2)


  1. PrinceKorwin
    02.04.2022 19:33
    +5

    Вы не задумывались над тем, что поверх вашего рест'а можно сделать прокси на SOAP? Тогда документацию и клиентов можно будет генерировать из XSD.

    Можно ждать статью об этом?


    1. dancheg Автор
      02.04.2022 19:42
      +1

      В экосистеме gRPC нет gateway для SOAP (сам не вижу для этого причин), в то время как rest gateway имеет 13к звезд на гитхабе (наверное он и правда популярен, ведь для его использования есть причины).