Как стать автором
Обновить

Готовим rest шлюз для gRPC сервисов на го — gRPC-gateway

Go *API *IT-инфраструктура *
Tutorial

Всем привет!

Данная статья является гайдом по построению 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()
}
Теги:
Хабы:
Всего голосов 1: ↑0 и ↓1 -1
Просмотры 39
Комментарии Комментировать