1. Введение
В этом практическом занятии вы будете использовать gRPC-Java для создания клиента и сервера, которые составляют основу приложения для сопоставления маршрутов, написанного на Java.
К концу этого урока у вас будет клиент, который подключается к удалённому серверу с помощью gRPC , чтобы получить имя или почтовый адрес объекта, расположенного по определённым координатам на карте. Полноценное приложение может использовать эту клиент-серверную архитектуру для перечисления или обобщения точек интереса вдоль маршрута.
API сервера определяется в файле Protocol Buffers, который будет использоваться для генерации шаблонного кода для клиента и сервера, чтобы они могли взаимодействовать друг с другом, экономя вам время и усилия на реализации этой функциональности.
Сгенерированный код учитывает не только сложности взаимодействия между сервером и клиентом, но и сериализацию и десериализацию данных.
Что вы узнаете
- Как использовать Protocol Buffers для определения API сервиса.
- Как создать клиент и сервер на основе gRPC из определения Protocol Buffers с помощью автоматической генерации кода.
- Понимание взаимодействия клиент-сервер с использованием gRPC.
Данный практический семинар предназначен для Java-разработчиков, которые только начинают работать с gRPC или хотят освежить свои знания gRPC, а также для всех, кто заинтересован в создании распределенных систем. Предварительный опыт работы с gRPC не требуется.
2. Прежде чем начать
Предварительные требования
- Версия JDK 8 или выше
Получите код
Чтобы вам не пришлось начинать с нуля, в этом практическом руководстве представлен шаблон исходного кода приложения, который вы сможете доработать. Следующие шаги покажут вам, как завершить приложение, включая использование плагинов компилятора Protocol Buffer для генерации шаблонного кода gRPC.
Сначала создайте рабочую директорию codelab и перейдите в неё с помощью команды cd:
mkdir grpc-java-getting-started && cd grpc-java-getting-started
Скачайте и распакуйте CodeLab:
curl -sL https://github.com/grpc-ecosystem/grpc-codelabs/archive/refs/heads/v1.tar.gz \
| tar xvz --strip-components=4 \
grpc-codelabs-1/codelabs/grpc-java-getting-started/start_here
В качестве альтернативы вы можете скачать ZIP-архив, содержащий только папку codelab, и распаковать его вручную.
Полный исходный код доступен на GitHub, если вы хотите обойтись без ввода кода вручную.
3. Определите услугу.
Первым шагом является определение gRPC-сервиса приложения, его RPC-метода, а также типов сообщений запроса и ответа с помощью Protocol Buffers . Ваш сервис будет предоставлять:
- Метод RPC под названием
GetFeature, который реализует сервер, а клиент вызывает. - Типы сообщений
PointиFeatureпредставляют собой структуры данных, которыми обмениваются клиент и сервер при использовании методаGetFeature. Клиент предоставляет координаты карты в видеPointв своем запросеGetFeatureк серверу, а сервер отвечает соответствующимFeature, описывающим то, что находится по этим координатам.
Данный RPC-метод и типы сообщений будут определены в файле src/main/proto/routeguide/route_guide.proto предоставленного исходного кода.
Протоколы Protocol Buffers обычно называются protobufs. Для получения дополнительной информации о терминологии gRPC см. раздел «Основные концепции, архитектура и жизненный цикл gRPC».
Поскольку в этом примере мы генерируем Java-код, мы указали параметр файла java_package и имя для Java-класса в нашем .proto :
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
Типы сообщений
В файле routeguide/route_guide.proto исходного кода сначала определите тип сообщения Point . Point представляет собой пару координат широты и долготы на карте. Для этого практического задания используйте целые числа для координат:
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
Цифры 1 и 2 — это уникальные идентификационные номера для каждого поля в структуре message .
Далее определите тип сообщения Feature . В Feature используется string поле для имени или почтового адреса объекта, расположенного в точке Point :
message Feature {
// The name or address of the feature.
string name = 1;
// The point where the feature is located.
Point location = 2;
}
Метод обслуживания
В файле route_guide.proto содержится структура service с именем RouteGuide , которая определяет один или несколько методов, предоставляемых сервисом приложения.
Добавьте rpc метод GetFeature в определение RouteGuide . Как объяснялось ранее, этот метод будет искать название или адрес местоположения по заданному набору координат, поэтому пусть GetFeature возвращает Feature для заданной Point :
service RouteGuide {
// Definition of the service goes here
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
}
Это унарный RPC-метод: простой RPC-вызов , при котором клиент отправляет запрос на сервер и ожидает ответа, подобно вызову локальной функции.
4. Сгенерируйте код клиента и сервера.
Далее нам необходимо сгенерировать интерфейсы gRPC-клиента и сервера из определения сервиса в файле .proto . Мы делаем это с помощью компилятора Protocol Buffer protoc со специальным плагином gRPC для Java. Для генерации gRPC-сервисов необходимо использовать компилятор proto3 (который поддерживает синтаксис как proto2, так и proto3).
При использовании Gradle или Maven плагин сборки protoc может генерировать необходимый код в процессе сборки. Инструкции по генерации кода из собственных .proto файлов можно найти в файле README плагина grpc-java .
В исходном коде учебного пособия мы предоставили среду Gradle и конфигурацию для сборки этого проекта.
Внутри каталога grpc-java-getting-started выполните следующую команду:
$ chmod +x gradlew $ ./gradlew generateProto
Следующие классы генерируются на основе определения нашего сервиса:
-
Feature.java,Point.javaи другие содержат весь код протокола Protocol Buffer для заполнения, сериализации и получения типов сообщений запроса и ответа. - Файл
RouteGuideGrpc.javaсодержит (а также некоторый другой полезный код) базовый класс для реализации серверамиRouteGuide,RouteGuideGrpc.RouteGuideImplBase, со всеми методами, определенными в сервисеRouteGuide, и заглушками для использования клиентами.
5. Реализация сервера
Для начала давайте рассмотрим, как создать сервер RouteGuide . Для того, чтобы наш сервис RouteGuide выполнял свою работу, необходимо выполнить две задачи:
- Реализация сервисного интерфейса, сгенерированного на основе определения нашего сервиса, который выполняет фактическую «работу» нашего сервиса.
- Запуск gRPC-сервера для приема запросов от клиентов и их перенаправления к соответствующей реализации сервиса.
Внедрить RouteGuide
Как видите, на нашем сервере есть класс RouteGuideService , который расширяет сгенерированный абстрактный класс RouteGuideGrpc.RouteGuideImplBase :
private static class RouteGuideService extends RouteGuideGrpc.RouteGuideImplBase {
...
}
Для инициализации вашего сервера с необходимыми функциями мы предоставили следующие 2 файла:
./src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java
./src/main/resources/io/grpc/examples/routeguide/route_guide_db.json
Давайте подробно рассмотрим простую реализацию RPC.
Унарный РПК
RouteGuideService реализует все методы нашего сервиса. В данном случае это просто GetFeature() , который принимает сообщение Point от клиента и возвращает в сообщении Feature соответствующую информацию о местоположении из списка известных мест.
@Override
public void getFeature(Point request, StreamObserver<Feature> responseObserver) {
responseObserver.onNext(checkFeature(request));
responseObserver.onCompleted();
}
Метод getFeature() принимает два параметра:
-
Point: запрос. -
StreamObserver<Feature>: наблюдатель ответа, представляющий собой специальный интерфейс, который сервер может вызывать для передачи своего ответа.
Чтобы отправить ответ клиенту и завершить звонок:
- Мы создаём и заполняем объект ответа
Featureдля возврата клиенту, как указано в определении нашего сервиса. В этом примере мы делаем это в отдельном приватном методеcheckFeature(). - Для возврата
Featureмы используем методonNext()наблюдателя ответа. - Мы используем метод
onCompleted()наблюдателя ответа, чтобы указать, что мы завершили обработку RPC-запроса.
Запустите сервер
После реализации всех методов сервиса нам необходимо запустить gRPC-сервер, чтобы клиенты могли фактически использовать наш сервис. В наш шаблон мы включаем создание объекта ServerBuilder:
ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile)
Мы создаём сервис в конструкторе:
- Укажите порт, который мы хотим использовать для прослушивания клиентских запросов, с помощью метода
forPort()конструктора (он будет использовать адрес с подстановочным знаком). - Создайте экземпляр класса реализации нашего сервиса
RouteGuideServiceи передайте его методуaddService()построителя. - Вызовите
build()в построителе, чтобы создать RPC-сервер для нашего сервиса.
Следующий фрагмент кода показывает, как мы создаём объект ServerBuilder .
/** Create a RouteGuide server listening on {@code port} using {@code featureFile} database. */
public RouteGuideServer(int port, URL featureFile) throws IOException {
this(Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create()),
port, RouteGuideUtil.parseFeatures(featureFile));
}
Следующий фрагмент кода показывает, как мы создаём объект сервера для нашего сервиса RouteGuide .
/** Create a RouteGuide server using serverBuilder as a base and features as data. */
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
this.port = port;
server = serverBuilder.addService(new RouteGuideService(features))
.build();
}
Реализуйте метод `start`, который вызывает start на созданном нами выше сервере.
public void start() throws IOException {
server.start();
logger.info("Server started, listening on " + port);
}
Реализуйте метод, который будет ждать завершения работы сервера, чтобы он не завершал работу немедленно.
/** Await termination on the main thread since the grpc library uses daemon threads. */
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
Как видите, мы создаём и запускаем наш сервер с помощью ServerBuilder .
В основном методе мы:
- Создайте экземпляр
RouteGuideServer. - Вызовите
start()для активации RPC-сервера для нашего сервиса. - Дождитесь остановки службы, вызвав
blockUntilShutdown().
public static void main(String[] args) throws Exception {
RouteGuideServer server = new RouteGuideServer(8980);
server.start();
server.blockUntilShutdown();
}
6. Создайте клиента.
В этом разделе мы рассмотрим создание клиента для нашего сервиса RouteGuide .
Создать экземпляр заглушки
Для вызова методов сервиса нам сначала нужно создать заглушку . Существует два типа заглушек, но для этой практической работы нам нужен только блокирующий тип. Два типа заглушек:
- Блокирующий/синхронный заглушка, которая выполняет вызов RPC и ожидает ответа от сервера, после чего либо возвращает ответ, либо генерирует исключение.
- Асинхронный/неблокирующий заглушка, выполняющий неблокирующие вызовы к серверу, при этом ответ возвращается асинхронно. Некоторые типы потоковых вызовов можно выполнять только с помощью асинхронной заглушки.
Сначала нам нужно создать gRPC- канал , а затем использовать этот канал для создания нашего заглушки.
Мы могли бы использовать ManagedChannelBuilder напрямую для создания канала.
ManagedChannelBuilder.forAddress(host, port).usePlaintext().build
Но давайте воспользуемся вспомогательным методом, который принимает строку, содержащую hostname:port .
Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();
Теперь мы можем использовать канал для создания блокирующего заглушки. Для этой практической работы у нас есть только блокирующие RPC-вызовы, поэтому мы используем метод newBlockingStub , предоставленный в классе RouteGuideGrpc который мы сгенерировали из нашего .proto .
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
Методы вызова сервиса
Теперь давайте посмотрим, как мы вызываем методы нашего сервиса.
Простой RPC
Вызов простого RPC- GetFeature почти так же прост, как вызов локального метода.
Мы создаём и заполняем объект буфера протокола запроса (в нашем случае Point ), передаём его методу getFeature() нашего блокирующего объекта-заглушки и получаем в ответ Feature .
В случае возникновения ошибки она кодируется как Status , который мы можем получить из StatusRuntimeException .
Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();
Feature feature;
try {
feature = blockingStub.getFeature(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
Стандартный код регистрирует сообщение, содержащее информацию о наличии или отсутствии объекта в указанной точке.
7. Попробуйте!
- Внутри каталога
start_hereвыполните следующую команду:
$ ./gradlew installDist
Это скомпилирует ваш код, упакует его в JAR-файл и создаст скрипты для запуска примера. Они будут созданы в каталоге build/install/start_here/bin/ . Скрипты называются: route-guide-server и route-guide-client .
Перед запуском клиента необходимо, чтобы сервер был запущен.
- Запустите сервер:
$ ./build/install/start_here/bin/route-guide-server
- Запустите клиент:
$ ./build/install/start_here/bin/route-guide-client
В результате вы увидите примерно такой вывод, при этом временные метки для наглядности опущены:
INFO: *** GetFeature: lat=409,146,138 lon=-746,188,906 INFO: Found feature called "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" at 40.915, -74.619 INFO: *** GetFeature: lat=0 lon=0 INFO: Found no feature at 0, 0
8. Что дальше?
- Узнайте, как работает gRPC, в разделе «Введение в gRPC и основные концепции».
- Пройдите базовый курс.
- Изучите справочник API .