gRPC для абсолютных новичков на языке Go


Введение

За последние два десятилетия Интернет сильно эволюционировал. HTTP/1.1 было недостаточно, поэтому сейчас у нас есть HTTP2. Спецификация, которую мы использовали для передачи данных между клиентом и сервером, также эволюционировала. От XML до JSON, теперь у нас есть Protocol Buffer, который является двоичной спецификацией. Давайте погрузимся глубже.

На рисунке ниже из Википедии показано, как экспоненциально растут данные.

Предварительные условия

  • Перейти
  • HTTP2

Достаточно HTTP2, чтобы знать gRPC

В первые дни существования Интернета у нас был только статический контент, едва ли текстовые файлы и файлы HTML. Затем веб немного вырос, и веб-сервер и клиенты начали иметь дело с богатыми медиа, такими как богатые тексты, изображения и видео. Серверы также начали предоставлять динамическое содержимое, основанное на запросах клиентов.

Это было началом эры RPC. Клиент обращался к определенной конечной точке с определенными данными, а работа сервера заключалась в том, чтобы ответить на этот запрос. В начале эпохи RPC широко использовался XML. Мы до сих пор используем XML в некоторых системах. Но большая часть мира перешла на JSON в качестве формата обмена данными между сервером и клиентом.

С течением времени все развивалось. Одной из вещей, которые развивались, является спецификация HTTP. Теперь у нас есть HTTP2. HTTP2 является основой gRPC. Определенные свойства делают HTTP2 питательной средой для gRPC.

HTTP/1.1 HTTP/2
Заголовок является открытым текстом и не сжимается Заголовок сжатый и двоичный
Создает новое TCP-соединение при каждом запросе использует уже существующее TCP-соединение
TLS не требуется Требуется TLS по умолчанию, повышенная безопасность

Давайте сначала узнаем о буфере протокола

Мы видели, как HTTP2 необходим для gRPC, теперь давайте посмотрим, что представляет собой Protocol Buffer.

Хотя вы можете использовать gRPC с JSON, Protocol Buffer привносит новые моменты. Мы видели, что долгое время JSON использовался для передачи данных туда и обратно между серверами и клиентами. Теперь произошло то, что мы сделали еще один шаг к переходу от JSON к его преемнику.

Буферы протоколов являются строительным блоком gRPC и заменой JSON. Буфер протокола многое унаследовал от HTTP/2.

JSON Буфер протокола
Обычный текст Двоичный
Большая полезная нагрузка по проводу Меньшая полезная нагрузка по проводу
Напишите свой сервер Файлы .proto позволяют создать заглушку сервера
  • Как мы уже знаем, HTTP/2 является бинарным. Так же как и буферы протокола. Теперь вам не нужно передавать дату как строку в JSON. Вы можете передавать BSON по проводам.
  • Это удобно в сети, так как мы используем только то пространство, которое нам нужно. Скажем, int32 использует только 4 байта данных. Те же данные в JSON (строка) могли бы использовать несколько байт для более длинного целочисленного значения.
  • Мы пишем файлы .proto. Компилятор proto генерирует файлы-заглушки, которые можно использовать для написания как сервера, так и клиента. Ключевым моментом здесь является то, что мы можем использовать одни и те же файлы proto для создания клиентов и серверов на разных языках.
  • Protocol Buffers не зависит от языка, на котором вы работаете для разработки сервера или клиента. Файлы .proto имеют свой синтаксис и типы данных, которые преобразуются в определенные типы данных в целевом языке программирования. Это моя любимая причина использовать gRPC в своих проектах.

Я рекомендую вам ознакомиться с синтаксисом, ключевыми словами и типами данных proto здесь: https://developers.google.com/protocol-buffers/docs/proto3.

Что такое gRPC?

Вы когда-нибудь слышали о RPC? Это означает удаленный вызов процедур. Это старый способ запуска удаленной процедуры на удаленной машине. Позвольте мне сделать его немного знакомым для вас. Когда вы обращаетесь к конечной точке с фронтенда на бэкенд, вы делаете удаленный вызов или удаленный вызов процедуры.

SOAP и REST являются примерами RPC. Вы можете отправить данные в теле и получить API на другом конце. Так это происходило с самого начала.

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

Типы сервисных методов в gRPC

Мы практически увидим, что такое сервисные методы, когда будем работать с кодом. Но сейчас я хочу уточнить, что в gRPC есть 4 вида определений методов.

  1. Unary – Unary похож на обычный вызов REST. Клиент инициирует TCP-соединение, отправляет сообщение, ждет ответа сервера, и, наконец, сервер отвечает.

  2. Server Streaming – серверный потоковый RPC, когда клиент посылает запрос на сервер и получает в ответ поток для чтения последовательности сообщений. Например, вы искали ключевое слово. Вместо статической страницы Twitter возвращает поток твитов, включая все, что публикуется в твиттере в реальном времени.

  3. Потоковый клиент – потоковый RPC, при котором клиент пишет последовательность сообщений и отправляет их на сервер. После того, как клиент закончил писать сообщения, он ждет, пока сервер прочитает их и вернет свой ответ. И снова gRPC гарантирует упорядочивание сообщений в рамках отдельного вызова RPC. Примером может служить IoT-устройство (например, автомобиль), передающее данные о местоположении своего устройства на центральный сервер (например, Uber).

  4. Двунаправленная потоковая передача – Двунаправленная потоковая передача RPC, когда обе стороны отправляют последовательность сообщений, используя поток чтения-записи. Эти два потока работают независимо, поэтому клиенты и серверы могут читать и писать в любом порядке. Примером может служить приложение для чата. Но я знаю, что в природе существуют и более сложные варианты использования. Пожалуйста, дайте мне знать в комментариях, если вы это сделаете.

Для подробного объяснения того, что происходит, когда клиент gRPC вызывает метод сервера gRPC, пожалуйста, прочитайте статью Жизненный цикл RPC.

Разработка с использованием gRPC

С нас достаточно теорий. Давайте разработаем немного кода, чтобы увидеть эту штуку в действии.

Мы рассмотрим пример на языке Go. Хотя, как мы уже знаем, у файлов proto есть свой собственный язык, который используется для генерации кода на нескольких языках.

Мы напишем простое приложение-калькулятор.

Напишем файл proto

Файлы .proto – это контракты между сервером и клиентом. Это похоже на REST API, с которым вы уже сталкивались.

Прежде чем я продолжу, я хотел бы сообщить вам, что наличие всего кода в модуле облегчит вам жизнь. По этой причине я создал модуль go в корне каталога с именем github.com/santosh/example.

calculator/calculator.proto

syntax = "proto3";

option go_package = "github.com/santosh/example/calculator";

package calculator;

// The calculator service definition.
service Calculator {
    // Adds two number
    rpc Add(Input) returns (Output);
}

// Input message containing two operands
message Input {
    int32 Operand1 = 1;
    int32 Operand2 = 2;
}

// Output message containing result of operation
message Output {
    int32 Result = 1;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Пояснение

Данные буфера протокола структурированы в виде сообщений, где каждое сообщение представляет собой небольшую логическую запись информации, содержащую ряд пар имя-значение, называемых полями. Строки 7-10 являются примером сообщения. Также как и 12-14. Сообщения состоят из типов данных, идентификаторов и индексных позиций.

Вы также заметите, что сообщения составляются внутри служб. Сервис – это просто то, что делает данный веб-сервис. В настоящее время у нас есть сервис Calculator из строк 3-5. Сервис Calculator состоит из метода Add. Add принимает 2 параметра, как определено во входном сообщении, и выдает выходное сообщение.

Генерация кода из файла proto

Для генерации кода из файлов proto вам понадобится компилятор protoc. Если вы работаете на базе Debian, вы можете использовать sudo apt install protobuf-compiler. Если вы работаете на любой другой ОС, пожалуйста, прочитайте раздел Установка компилятора буфера протокола.

Я собираюсь использовать Go для этого руководства, поэтому я собираюсь установить плагин Go для компилятора protoc.

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, когда все готово, я бы выполнил эту команду из корня модуля:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative calculator/calculator.proto
Enter fullscreen mode Выйти из полноэкранного режима

Если эта команда будет успешной, то будет сгенерировано еще 2 go-файла.

$ tree calculator
calculator
├── calculator_grpc.pb.go
├── calculator.pb.go
└── calculator.proto

0 directories, 3 files
Войти в полноэкранный режим Выйти из полноэкранного режима

Если вы впервые работаете с gRPC и посмотрите на сгенерированный код сейчас, вы, вероятно, запутаетесь.

Мы собираемся использовать оба файла для создания нашего сервера и клиента. Сгенерированный код начнет обретать смысл.

Реализация сервера калькулятора и клиента

Мы все знаем, что результат, сгенерированный компилятором protoc, является заглушкой. Нам нужно использовать эту заглушку как руководство для реализации/написания кода клиента и сервера. Заглушка работает как руководство для нас.

Сейчас сгенерированный код находится внутри пакета calculator в calculator.pb.go, который в основном имеет дело с частью данных (т.е. Input, Output, Operand1, Operand2, их геттеры и т.д.) и calculator_grpc.pb.go, который в основном занимается реализацией методов (т.е. Add как на сервере, так и на клиенте). Первым делом и в коде сервера, и в коде клиента нужно импортировать этот пакет.

Я буду продолжать ссылаться на файл proto по мере того, как мы будем определять код клиента/сервера.

Написание сервера калькулятора gRPC

Если вы посмотрите на calculator_grpc.pb.go, то обнаружите, что там есть структура под названием UnimplementedCalculatorServer. Эта структура представляет наш сервер. Не зря она называется Unimplemented. Если вы посмотрите на методы, присоединенные к этой структуре, вы увидите метод Add. Это тот же метод, который мы определили в нашем файле proto. Вот краткое описание:

// Adds two number
rpc Add(Input) returns (Output);
Вход в полноэкранный режим Выход из полноэкранного режима

Мы собираемся взять этот UnimplementedCalculatorServer и реализовать метод Add.

// server is used to implement calculator.CalculatorServer.
type server struct {
    pb.UnimplementedCalculatorServer
}

// Add implements calculator.CalculatorServer
func (s *server) Add(ctx context.Context, in *pb.Input) (*pb.Output, error) {
    log.Printf("Received: %v %v", in.GetOperand1(), in.GetOperand2())
    result := in.GetOperand1() + in.GetOperand2()
    return &pb.Output{Result: result}, nil
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание на сигнатуру метода. Мы принимаем Input в виде *pb.Input и возвращаем Output в виде *pb.Output. Это очень похоже на то, что мы объявили в файле proto.

Реализация Add неполна без логики Add. В строке 26 мы используем GetOperand1 и GetOperand2, которые доступны из файла calculator.pb.go. Наконец, мы используем структуру Output для возврата результата.

Реализовать метод недостаточно, нам также нужно запустить сервер gRPC и начать прослушивание.

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterCalculatorServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

В строке 32 я начинаю прослушивать TCP-соединение на заданном порту. grpc.NewServer() вызывает функцию внутренней библиотеки для создания нового сервера gRPC, этот вызов возвращает объект grpc.ServiceRegistrar. Этот объект затем передается в RegisterCalculatorServer вместе с нашими реализованными методами.

Весь код выглядит следующим образом:

package main

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"

    pb "github.com/santosh/example/calculator"
    "google.golang.org/grpc"
)

var (
    port = flag.Int("port", 50051, "The server port")
)

// server is used to implement calculator.CalculatorServer.
type server struct {
    pb.UnimplementedCalculatorServer
}

// Add implements calculator.CalculatorServer
func (s *server) Add(ctx context.Context, in *pb.Input) (*pb.Output, error) {
    log.Printf("Received: %v %v", in.GetOperand1(), in.GetOperand2())
    result := in.GetOperand1() + in.GetOperand2()
    return &pb.Output{Result: result}, nil
}

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterCalculatorServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Написание клиента калькулятора gRPC

Как и в случае с кодом сервера, мы начнем с определения флагов командной строки flag.

var (
    addr     = flag.String("addr", "localhost:50051", "the address to connect to")
    operand1 = flag.Int("op1", 2, "1st operand")
    operand2 = flag.Int("op2", 2, "2nd operand")

    operand1int32 = int32(*operand1)
    operand2int32 = int32(*operand2)
)
Войти в полноэкранный режим Выйти из полноэкранного режима

Строки 19-20 здесь являются своего рода хаком, поскольку модуль флагов не имеет возможности принимать int32, что является требованием для нашего Output.Result.

Далее следует соединение с сервером:

    // Set up a connection to the server.
    conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
Вход в полноэкранный режим Выход из полноэкранного режима

grpc.Dial принимает адрес сервера, а также другие переменные параметры. Поскольку gRPC работает через HTTP2, который по умолчанию требует TLS, мы используем небезопасные учетные данные, так как не настроили наш сервер на использование сертификатов.

В следующей строке мы создадим новый клиент gRPC:

    c := pb.NewCalculatorClient(conn)
Войти в полноэкранный режим Выход из полноэкранного режима

В предыдущих строках мы собираемся вызвать конечную точку Add:

    // Contact the server and print out its response.
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.Add(ctx, &pb.Input{Operand1: operand1int32, Operand2: operand2int32})
    if err != nil {
        log.Fatalf("could not add: %v", err)
    }
    log.Printf("Add result: %v", r.GetResult())
Войти в полноэкранный режим Выход из полноэкранного режима

В целом код выглядит следующим образом:

package main

import (
    "context"
    "flag"
    "log"
    "time"

    pb "github.com/santosh/example/calculator"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

var (
    addr     = flag.String("addr", "localhost:50051", "the address to connect to")
    operand1 = flag.Int("op1", 2, "1st operand")
    operand2 = flag.Int("op2", 2, "2nd operand")

    operand1int32 = int32(*operand1)
    operand2int32 = int32(*operand2)
)

func main() {
    flag.Parse()
    // Set up a connection to the server.
    conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewCalculatorClient(conn)

    // Contact the server and print out its response.
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.Add(ctx, &pb.Input{Operand1: operand1int32, Operand2: operand2int32})
    if err != nil {
        log.Fatalf("could not add: %v", err)
    }
    log.Printf("Add result: %v", r.GetResult())
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Давайте продолжим и протестируем наш сервис.

Демонстрация нашего сервера и клиента

Для демонстрации я записал gif.

Здесь я продемонстрировал значение флага по умолчанию, но вы можете переопределить флаги -op1 и -op2, чтобы увидеть другие результаты.

Заключение

Это было просто введение в gRPC и буфер протоколов. Что, как я обнаружил, отличается от обычного REST API, так это конечные точки. В REST у нас есть предопределенная конечная точка, к которой мы должны обратиться. Например, /calculation/add. Затем мы передаем JSON с первым и вторым операндом.

С gRPC дело обстоит иначе. Мы получаем файлы-заглушки как артефакт protoc. Единственное взаимодействие происходит оттуда.

В этом посте я рассмотрел только унарные типы сервисных методов. Потоковую передачу я оставлю для другого раза. Если вы хотите почитать и попрактиковаться в gRPC, я нашел эту серию очень полезной.

Внешние ссылки

  • Документация по протокольным буферам Proto3
  • Официальная документация GRPC
  • Что означает мультиплексирование в HTTP/2

Оцените статью
Procodings.ru
Добавить комментарий