- Введение
- Предварительные условия
- Достаточно HTTP2, чтобы знать gRPC
- Давайте сначала узнаем о буфере протокола
- Что такое gRPC?
- Типы сервисных методов в gRPC
- Разработка с использованием gRPC
- Напишем файл proto
- calculator/calculator.proto
- Пояснение
- Генерация кода из файла proto
- Реализация сервера калькулятора и клиента
- Написание сервера калькулятора gRPC
- Написание клиента калькулятора gRPC
- Демонстрация нашего сервера и клиента
- Заключение
- Внешние ссылки
Введение
За последние два десятилетия Интернет сильно эволюционировал. 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 вида определений методов.
-
Unary – Unary похож на обычный вызов REST. Клиент инициирует TCP-соединение, отправляет сообщение, ждет ответа сервера, и, наконец, сервер отвечает.
-
Server Streaming – серверный потоковый RPC, когда клиент посылает запрос на сервер и получает в ответ поток для чтения последовательности сообщений. Например, вы искали ключевое слово. Вместо статической страницы Twitter возвращает поток твитов, включая все, что публикуется в твиттере в реальном времени.
-
Потоковый клиент – потоковый RPC, при котором клиент пишет последовательность сообщений и отправляет их на сервер. После того, как клиент закончил писать сообщения, он ждет, пока сервер прочитает их и вернет свой ответ. И снова gRPC гарантирует упорядочивание сообщений в рамках отдельного вызова RPC. Примером может служить IoT-устройство (например, автомобиль), передающее данные о местоположении своего устройства на центральный сервер (например, Uber).
-
Двунаправленная потоковая передача – Двунаправленная потоковая передача 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
Если эта команда будет успешной, то будет сгенерировано еще 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