В прошлой статье мы разобрали, как писать сервер на gRPC. И протестировали его с помощью BloomRPC. Теперь, давайте разберем как пишется клиент. И попробуем отправлять запросы с клиента на сервер. Это будет наш первый шаг к созданию микросервисов на грпц.
В моем случае я использую многомодульный проект, но можно создать отдельный проект. Структура проекта будет примерно следующей:
Мы реализуем основные методы, которые были описаны в .proto. Обратите внимание, что serverStream разобьется на 2 метода, о чем я расскажу дальше.
Как и в прошлой статье нам необходимо добавить плагин для генерации .proto файлов. Если возникли сложности, можно посмотреть код примера в репозитории.
Для того, чтобы отправлять вызовы на удаленный сервер, нам необходимо создать клиента. gRPC предоставляет несколько возможностей создания клиента - это BlockingStub и Stub. В чем отличие этих “заглушек”? BlockingStub - создает новую блокирующую заглушку, которая поддерживает вызовы унарного и потокового вывода (unary and streaming output calls, офф. док.), Stub - cоздает новую асинхронную заглушку, которая поддерживает все типы вызовов. Мы рассмотрим оба варианта заглушек. Для создании заглушки нам необходим Channel. Channel - если коротко, то это виртуальное соединение с концептуальной конечной точкой для выполнения RPC. Для создания Channel нам потребуется хост и порт.
// Access a service running on the local machine on port 7777
String target = "localhost:7777";
// Create a communication channel to the server, known as a Channel. Channels are thread-safe
// and reusable. It is common to create channels at the beginning of your application and reuse
// them until the application shuts down.
ManagedChannel channel = ManagedChannelBuilder.forTarget(target)
// Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
// needing certificates.
.usePlaintext()
.build();
Затем мы можем создать newBlockingStub и newStub (создаем через Channel и ManagedChannel для примера)
// 'channel' here is a Channel, not a ManagedChannel,
// so it is not this code's responsibility to
// shut it down.
// Passing Channels to code makes code easier to test
// and makes it easier to reuse Channels.
blockingStub = ProfileServiceGrpc.newBlockingStub(channel);
asyncStub = ProfileServiceGrpc.newStub(ManagedChannelBuilder.forAddress(HOST, PORT).usePlaintext().build());
Вся подготовительная работа закончена. Теперь можно реализовать методы интерфейса. Начнем с самого простого - getCurrentProfile. Для вызова сервера, нам нужно лишь вызвать метод заглушки и передать в нее необходимые параметры.
Я использую стандартный main метод, где создаю объект интерфейса и вызываю необходимые методы. В целом ничего не мешает написать тесты, но для меня быстрее и проще написать main.
Теперь если вызвать getCurrentProfile, то мы получим ответ профиля с именем “test” (сервер из прошлой статьи).
Отлично, с базовым методом разобрались. Давайте теперь рассмотрим clientStream. Отличия от предыдущего метода минимальные. Нам лишь нужно передавать сообщения на сервер не единожды, как было в прошлом примере, а постоянно. Если обратиться к логам сервера, то можно заметить, что он получает сообщения от клиента и логирует их.
Server Stream. Для реализации получения потоковых данных с сервера, можно использовать различные заглушки. Если использовать блокирующую заглушку, то мы дождемся всех ответов сервера и затем обработаем результат. Если использовать асинхронную заглушку, то мы будем обрабатывать результат сразу после того как сервер нам ответит.
// Блокирующая и асинхронная заглушки
private final ProfileServiceGrpc.ProfileServiceBlockingStub blockingStub;
private final ProfileServiceGrpc.ProfileServiceStub asyncStub;
// Создание заглушек в конструкторе
public GrpcProfileClientImpl() {
// Create a communication channel to the server, known as a Channel. Channels are thread-safe
// and reusable. It is common to create channels at the beginning of your application and reuse
// them until the application shuts down.
ManagedChannel channel = ManagedChannelBuilder.forTarget(TARGET)
// Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
// needing certificates.
.usePlaintext()
.build();
blockingStub = ProfileServiceGrpc.newBlockingStub(channel);
asyncStub = ProfileServiceGrpc.newStub(channel);
}
Для принятия ответов, используя BlockingStub нам потребуется Iterator, куда сервер будет складывать ответы. И потом, пробежавшись по нему, мы увидим то, что нам прислал сервер.
А вот для не блокирующего принятия сообщений нам потребуется передать StreamObserver, в который сервер будет передавать ответы.
Так как у нас получение происходит асинхронно, я добавляю CountDownLatch с await, для того что бы текущий поток подождал ответов от сервера.
И последний вариант клиент серверного взаимодействия - biDirectionalStream. Нам снова потребуется реализовать StreamObserver в который клиент будет передавать сообщения и получать ответы от сервера.
Если все запустить и проверить, то по логам клиента и сервера можно заметить, что сообщение передаются и наш клиент работает корректно.
Подведем итоги.
Мы написали клиент, для рассмотренного в прошлой статье сервера на gRPC. Рассмотрели и реализовали основные варианты клиент серверного взаимодействия с использованием gRPC. А вы используете gRPC для клиент серверного общения? Почему вы выбрали gRPC?