В архитектуре 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 и серверного кода, где они потом будут реализовываться:
В файле 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 \n", readPokemonErr)
}
fmt.Printf("Pokemon was read: %v \n", 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 \n", updateErr)
}
fmt.Printf("Pokemon was updated: %v\n", updateRes)
// удаляем покемона
deleteRes, deleteErr := c.DeletePokemon(context.Background(), &pokemonpc.DeletePokemonRequest{Pid: pokemonID})
if deleteErr != nil {
fmt.Printf("Error happened while deleting: %v \n", deleteErr)
}
fmt.Printf("Pokemon was deleted: %v \n", 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
А теперь клиентское приложение. Оно вызовет методы, определенные на сервере для CRUD-операций с Pokemon:
Когда клиент вызовет CRUD-операции с покемоном, со стороны сервера логи будут такие:
Полный код примера см. по ссылке.
Заключение
gRPC — это одна из лучших технологий создания микросервисов и служб потоковой передачи данных в реальном времени.
Буферы протокола, HTTP/2 и SSL-защита делают gRPC надежной, безопасной и более производительной архитектурой, чем REST.
Кроме того, gRPC поддерживает все самые известные языки. Дополнительная информация доступна в документации по gRPC. Спасибо за внимание.
Читайте также:
- Стоит ли писать код Dart на стороне сервера?
- 4 подводных камня на Go, на которые часто натыкаются
- Почему вам стоит написать свой API-шлюз с нуля
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Tharaka Romesh: Let’s “Go” and build an Application with gRPC