Как создать приложение на Go с gRPC



Книга Как создать приложение на Go с gRPC

В архитектуре REST («передача состояния представления»)  —  основной технологии построения веб-приложений и микросервисов  —  есть несколько способов создания API. Обычно используется протокол HTTP.


API считается RESTful, когда он:


  • имеет единый интерфейс и клиент-серверную независимость;
  • не сохраняет состояние;
  • допускает кеширование;
  • многоуровневый.

REST был призван устранить недостатки SOAPлегковесным RPC под капотом), но возвращал много метаданных и поэтому не смог его заменить. Это привело к появлению новых или улучшенных технологий типа gRPC и GraphQL.


gRPC  —  это высокопроизводительный универсальный фреймворк RPC с открытым исходным кодом для скоростного обмена данными между микросервисами.


По умолчанию в gRPC есть Protobuf (буферы протокола), которые форматируют или сериализовывают сообщения в определенный формат с плотноупакованными высокоэффективными данными.


Очевидно, что легковесный RPC типа gRPC будет идеальным в ряде сценариев, например при создании приложения на Go  —  популярном языке для микросервисов.


Что такое RPC?


RPC (удаленный вызов процедур)  —  одна из старейших архитектур. Позволяет вызывать функцию на удаленном сервере и получать ответ в том же формате.


Концепции RPC API и REST API чем-то похожи. Интерфейсы RPC определяют правила и методы, с которыми взаимодействует клиент. Чтобы вызывать эти методы, он отправляет вызовы с аргументами, которые находятся в строке запроса. У вызова RPC POST /deletePokemon будет такая строка запроса: {“id”: 2 }, а у типичного REST  —  DELETE /pokemon/delete/2.


Что такое gRPC?


Это легковесный преемник RPC. Разработан для обмена данными между микросервисами и другими взаимодействующими системами.


Преимущества gRPC:


  • буферы протокола (Protobuf) вместо JSON;
  • HTTP 2 вместо HTTP 1.1;
  • встроенная генерация кода;
  • высокая производительность;
  •  SSL-защита.

Также gRPC улучшает дизайн приложения: в отличие от REST, он ориентирован на API, а не на ресурсы.


А еще gRPC асинхронен по умолчанию: не блокирует поток при поступлении запроса и обслуживает миллионы запросов параллельно, обеспечивая высокую масштабируемость.


В gRPC есть четыре типа API:


  • Унарный.
    Очень похож на традиционный API (REST API): клиент делает запрос, а сервер отправляет ответ.


  • Потоковой передачи данных с сервера.
    Здесь клиент делает один запрос, а сервер отправляет несколько или поток данных.


  • Потоковой передачи данных от клиента.
    То же, что предыдущий, но наоборот: клиент отправляет поток данных, а сервер  —  один ответ.


  • Двунаправленной потоковой передачи.
    Здесь клиент и сервер отправляют потоки данных.


Что особенного в буферах протокола?


Буферы протокола  —  это расширяемый механизм сериализации данных с открытым исходным кодом. Они используются для языка описания интерфейсов и для формата обмена сообщениями.


В отличие от JSON (нотация объектов JavaScript), буферы протокола не зависят от языка, меньше по размеру полезных данных, быстрее, проще и производительнее.


В gRPC они применяются для определения:


  • сообщений (данные, запрос и ответ);
  • сервиса (имя сервиса и конечные точки RPC).

Остается лишь определить структурированные данные, и компилятор protoc буфера протокола сгенерирует код на основе выбранного языка. Последний protoc (версия 3) поддерживает C#, C++, Dart, Go, Java, Kotlin, Node, Objective-C, PHP, Python и Ruby.


Когда стоит использовать gRPC?


gRPC идеально подойдет для внутренних задач:


  • связывания микросервисов;
  • потоковой передачи данных в режиме реального времени;
  • многоязычных систем.

gRPC  —  это будущее для API микросервисов и мобильных серверов. Является частью Cloud Native Computing Foundation и технологических гигантов типа Google, Netflix, Square и CoackroachDB.


Приступаем к работе с gRPC


Создадим небольшое приложение на Golang с gRPC потоковой передачи данных с сервера. Для этого с помощью gRPC/protoc (компилятора буферов протокола) определим сообщения и сервисы. Потом сгенерируем для них интерфейс и добавим в него функциональность, например MongoDB (для уровня базы данных), чтобы постоянного хранить данные.


Необходимые условия


  • Последняя версия Go (1.0 или новее).
  • Последняя версия protoc (protoc 3).

Сначала установим компилятор protoc и добавим его в пути доступа из любого места операционной системы. Процесс установки для своей ОС см. здесь.


Инициализируем проект, создав каталог и запустив эти команды:


go mod init github.com/username/grpc-pokemon

go get -u google.golang.org/grpc
go get -u google.golang.org/protobuf/protoc-gen-go

Первой командой создается файл go.mod для отслеживания зависимостей кода. А двумя другими  —  устанавливаются зависимости для grpc и protoc-gen-go в Golang, которые понадобятся в файле .proto.


Перейдем к самому интересному  —  созданию файлов .proto, в которых определяется сервис и соответствующие функции/сообщения. А с помощью компилятора protoc здесь генерируются необходимые файлы буферов протокола.


В каталоге pokemon создаем этот файл proto:


syntax = "proto3";

package pokemon;

option go_package = "github.com/TRomesh/grpc-pokemon;pokemonpc";

message Pokemon {
string id = 1;
string pid = 2;
string name = 3;
string power = 4;
string description = 5;
}

message CreatePokemonRequest {
Pokemon pokemon = 1;
}

message CreatePokemonResponse {
Pokemon pokemon = 1; // будет иметь идентификатор покемона
}

message ReadPokemonRequest {
string pid = 1;
}

message ReadPokemonResponse {
Pokemon pokemon = 1;
}

message UpdatePokemonRequest {
Pokemon pokemon = 1;
}

message UpdatePokemonResponse {
Pokemon pokemon = 1;
}

message DeletePokemonRequest {
string pid = 1;
}

message DeletePokemonResponse {
string pid = 1;
}

message ListPokemonRequest {

}

message ListPokemonResponse {
Pokemon pokemon = 1;
}

service PokemonService {
rpc CreatePokemon (CreatePokemonRequest) returns (CreatePokemonResponse);
rpc ReadPokemon (ReadPokemonRequest) returns (ReadPokemonResponse); // возвращает NOT_FOUND, если не найдено
rpc UpdatePokemon (UpdatePokemonRequest) returns (UpdatePokemonResponse); // возвращает NOT_FOUND, если не найдено
rpc DeletePokemon (DeletePokemonRequest) returns (DeletePokemonResponse); // возвращает NOT_FOUND, если не найдено
rpc ListPokemon (ListPokemonRequest) returns (stream ListPokemonResponse); // Для потоковой передачи с сервера
}

С помощью компилятора protoc. сгенерируем код для Golang, выполнив следующую команду из корневого каталога проекта:


protoc — go_out=. — go_opt=paths=source_relative — go-grpc_out=.
— go-grpc_opt=paths=source_relative pokemon/pokemon.proto

Внимание: при компилировании используем относительные пути и задаем путь к файлу pokemon.proto.


Этой командой генерируются два файла: pokemon.pb.go для сериализации сообщений с помощью буферов протокола и pokemon_grpc.pb.go, состоящий из кода для клиента gRPC и серверного кода, где они потом будут реализовываться:


Файлы Go, сгенерированные компилятором protoc

В файле pokemon_grpc.pb.go для клиент-серверной реализации генерируются структуры и интерфейсы.


Создание сервера и клиента


Реализацию сервера начнем с создания файла server.go. Для постоянного хранения данных установим зависимость MongoDB:


go get go.mongodb.org/mongo-driver/mongo

Создаем сервер с подключением MongoDB. Сначала импортируем определения буферов протокола и создаем структуру с помощью PokemonServiceServer, а затем реализуем функции в файле .proto (createPokemon, ReadPokemon и т. д.):


import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"

pokemonpc "github.com/TRomesh/grpc-pokemon/pokemon"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
)

var collection *mongo.Collection

type server struct {
pokemonpc.PokemonServiceServer
}

type pokemonItem struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Pid string `bson:"pid"`
Name string `bson:"name"`
Power string `bson:"power"`
Description string `bson:"description"`
}

func getPokemonData(data *pokemonItem) *pokemonpc.Pokemon {
return &pokemonpc.Pokemon{
Id: data.ID.Hex(),
Pid: data.Pid,
Name: data.Name,
Power: data.Power,
Description: data.Description,
}
}

func (*server) CreatePokemon(ctx context.Context, req *pokemonpc.CreatePokemonRequest) (*pokemonpc.CreatePokemonResponse, error) {
fmt.Println("Create Pokemon")
pokemon := req.GetPokemon()
data := pokemonItem{
Pid: pokemon.GetPid(),
Name: pokemon.GetName(),
Power: pokemon.GetPower(),
Description: pokemon.GetDescription(),
}

res, err := collection.InsertOne(ctx, data)

if err != nil {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Internal error: %v", err),
)
}

oid, ok := res.InsertedID.(primitive.ObjectID)
if !ok {
return nil, status.Errorf(
codes.Internal,
fmt.Sprintf("Cannot convert to OID"),
)
}

return &pokemonpc.CreatePokemonResponse{
Pokemon: &pokemonpc.Pokemon{
Id: oid.Hex(),
Pid: pokemon.GetPid(),
Name: pokemon.GetName(),
Power: pokemon.GetPower(),
Description: pokemon.GetDescription(),
},
}, nil

}

PokemonServiceServer и функции, а также структуры для каждого запроса (*pokemonpc.CreatePokemonRequest) и ответа (*pokemonpc.CreatePokemonResponse) сгенерированы компилятором protoc.


Так же реализуются и остальные функции (чтения, обновления и удаления покемонов).


Переходим к клиентской части. Но это CLI-приложение, поэтому выполняем каждую функцию с одной попытки, например createPokemon, ReadPokemon, UpdatePokemon и т. д.:


package main

import (
"context"
"fmt"
"io"
"log"
"os"

pokemonpc "github.com/TRomesh/grpc-pokemon/pokemon"
"github.com/joho/godotenv"
"google.golang.org/grpc"
)

const defaultPort = "4041"

func main() {

fmt.Println("Pokemon Client")

err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading .env file")
}

port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}

opts := grpc.WithInsecure()

cc, err := grpc.Dial("localhost:4041", opts)
if err != nil {
log.Fatalf("could not connect: %v", err)
}
defer cc.Close() // Может, в отдельной функции ошибка будет обработана?

c := pokemonpc.NewPokemonServiceClient(cc)

// создаем покемона
fmt.Println("Creating the pokemon")
pokemon := &pokemonpc.Pokemon{
Pid: "Poke01",
Name: "Pikachu",
Power: "Fire",
Description: "Fluffy",
}
createPokemonRes, err := c.CreatePokemon(context.Background(), &pokemonpc.CreatePokemonRequest{Pokemon: pokemon})
if err != nil {
log.Fatalf("Unexpected error: %v", err)
}
fmt.Printf("Pokemon has been created: %v", createPokemonRes)
pokemonID := createPokemonRes.GetPokemon().GetId()

// считываем покемона
fmt.Println("Reading the pokemon")
readPokemonReq := &pokemonpc.ReadPokemonRequest{Pid: pokemonID}
readPokemonRes, readPokemonErr := c.ReadPokemon(context.Background(), readPokemonReq)
if readPokemonErr != nil {
fmt.Printf("Error happened while reading: %v
", readPokemonErr)
}

fmt.Printf("Pokemon was read: %v
", readPokemonRes)

// обновляем покемона
newPokemon := &pokemonpc.Pokemon{
Id: pokemonID,
Pid: "Poke01",
Name: "Pikachu",
Power: "Fire Fire Fire",
Description: "Fluffy",
}
updateRes, updateErr := c.UpdatePokemon(context.Background(), &pokemonpc.UpdatePokemonRequest{Pokemon: newPokemon})
if updateErr != nil {
fmt.Printf("Error happened while updating: %v
", updateErr)
}
fmt.Printf("Pokemon was updated: %v
", updateRes)

// удаляем покемона
deleteRes, deleteErr := c.DeletePokemon(context.Background(), &pokemonpc.DeletePokemonRequest{Pid: pokemonID})

if deleteErr != nil {
fmt.Printf("Error happened while deleting: %v
", deleteErr)
}
fmt.Printf("Pokemon was deleted: %v
", deleteRes)

// выводим список покемонов

stream, err := c.ListPokemon(context.Background(), &pokemonpc.ListPokemonRequest{})
if err != nil {
log.Fatalf("error while calling ListPokemon RPC: %v", err)
}
for {
res, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("Something happened: %v", err)
}
fmt.Println(res.GetPokemon())
}
}

Вот и все. Мы создали клиент и сервер, использовав интерфейсы и структуры, сгенерированные компилятором protoc.


Запустим сервер следующей командой:


$ go run server/server.go

Запуск сервера Pokemon

А теперь клиентское приложение. Оно вызовет методы, определенные на сервере для CRUD-операций с Pokemon:


Выполнение клиента

Когда клиент вызовет CRUD-операции с покемоном, со стороны сервера логи будут такие:


Журнал сервера после выполнения клиента

Полный код примера см. по ссылке.


Заключение


gRPC  —  это одна из лучших технологий создания микросервисов и служб потоковой передачи данных в реальном времени.


Буферы протокола, HTTP/2 и SSL-защита делают gRPC надежной, безопасной и более производительной архитектурой, чем REST.


Кроме того, gRPC поддерживает все самые известные языки. Дополнительная информация доступна в документации по gRPC. Спасибо за внимание.



1311   0  

Comments

    Ничего не найдено.