1. Введение
В этой лабораторной работе вы будете использовать gRPC-Java для создания клиента и сервера, которые составят основу приложения для сопоставления маршрутов, написанного на Java.
К концу руководства у вас будет клиент, который подключается к удаленному серверу с помощью gRPC для получения информации об объектах на маршруте клиента, создания сводки маршрута клиента и обмена информацией о маршруте, такой как обновления трафика, с сервером и другими клиентами.
Служба определена в файле Protocol Buffers, который будет использоваться для генерации шаблонного кода для клиента и сервера, чтобы они могли взаимодействовать друг с другом, экономя ваше время и усилия при реализации этой функциональности.
Сгенерированный код учитывает не только сложности взаимодействия между сервером и клиентом, но также сериализацию и десериализацию данных.
Чему вы научитесь
- Как использовать Protocol Buffers для определения API сервиса.
- Как создать клиент и сервер на основе gRPC из определения Protocol Buffers с использованием автоматической генерации кода.
- Понимание потокового взаимодействия клиент-сервер с помощью gRPC.
Эта практическая работа предназначена для разработчиков Java, впервые использующих gRPC, желающих освежить свои знания, а также для всех, кто интересуется разработкой распределённых систем. Опыт работы с gRPC не требуется.
2. Прежде чем начать
Предпосылки
- JDK версии 24.
Получить код
Чтобы вам не пришлось начинать всё с нуля, эта лабораторная работа предоставляет вам заготовку исходного кода приложения. Следующие шаги покажут вам, как завершить приложение, включая использование плагинов компилятора буфера протокола для генерации шаблонного кода gRPC.
Сначала создайте рабочий каталог codelab и перейдите в него:
mkdir streaming-grpc-java-getting-started && cd streaming-grpc-java-getting-started
Загрузите и распакуйте кодовую лабораторию:
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-streaming/start_here
Кроме того, вы можете загрузить .zip-файл, содержащий только каталог codelab, и вручную распаковать его.
Если вы не хотите вводить реализацию вручную, готовый исходный код доступен на GitHub .
3. Определите сообщения и услуги
Первым шагом будет определение службы gRPC приложения, её метода RPC и типов сообщений запросов и ответов с помощью Protocol Buffers . Ваша служба будет предоставлять:
- Методы RPC, называемые
ListFeatures
,RecordRoute
иRouteChat
, которые реализует сервер и вызывает клиент. - Типы сообщений
Point
,Feature
,Rectangle
,RouteNote
иRouteSummary
представляют собой структуры данных, которыми обмениваются клиент и сервер при вызове методов, указанных выше.
Буферы протоколов обычно называются protobuf. Подробнее о терминологии gRPC см. в разделе «Основные концепции, архитектура и жизненный цикл gRPC».
Этот метод RPC и его типы сообщений будут определены в файле proto/routeguide/route_guide.proto
предоставленного исходного кода.
Давайте создадим файл route_guide.proto
.
Поскольку в этом примере мы генерируем код Java, мы указали параметр файла java_package
в нашем .proto
:
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
Определить типы сообщений
В файле proto/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;
}
Чтобы передать клиенту несколько точек в пределах области, вам понадобится сообщение Rectangle
, представляющее прямоугольник широты и долготы, представленный в виде двух диагонально противоположных точек lo
и hi
:
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
Также сообщение RouteNote
, представляющее собой сообщение, отправленное в заданной точке:
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
Наконец, вам понадобится сообщение RouteSummary
. Это сообщение приходит в ответ на RPC-запрос RecordRoute
, который описан в следующем разделе. Оно содержит количество полученных отдельных точек, количество обнаруженных объектов и общее пройденное расстояние, представляющее собой сумму расстояний между точками.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
Определить методы обслуживания
Чтобы определить службу, укажите её название в файле .proto
. Файл route_guide.proto
содержит структуру service
RouteGuide
, которая определяет один или несколько методов, предоставляемых службой приложения.
При определении методов RPC
в определении сервиса вы указываете их типы запросов и ответов. В этом разделе практикума давайте определим:
СписокОсобенности
Получает объекты Feature
доступные в заданном Rectangle
. Результаты передаются потоком, а не возвращаются сразу, поскольку прямоугольник может охватывать большую площадь и содержать огромное количество объектов.
В этом приложении вы будете использовать серверный потоковый RPC-вызов: клиент отправляет запрос серверу и получает поток для чтения последовательности сообщений. Клиент читает данные из возвращаемого потока до тех пор, пока сообщения не закончатся. Как видно из нашего примера, вы указываете серверный потоковый метод, добавляя ключевое слово stream перед типом ответа.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
RecordRoute
Принимает поток точек на проходимом маршруте и возвращает RouteSummary
после завершения обхода.
В этом случае целесообразно использовать потоковый RPC-вызов на стороне клиента : клиент записывает последовательность сообщений и отправляет их на сервер, снова используя предоставленный поток. После завершения записи сообщений клиент ожидает, пока сервер прочитает их все и вернет ответ. Для указания метода потоковой передачи на стороне клиента необходимо добавить ключевое слово stream перед типом запроса.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
RouteChat
Принимает поток RouteNotes
отправленных во время прохождения маршрута, одновременно получая другие RouteNotes
(например, от других пользователей).
Именно такой вариант использования используется для двунаправленной потоковой передачи . Двунаправленный потоковый RPC-вызов, при котором обе стороны отправляют последовательность сообщений, используя поток чтения-записи. Два потока работают независимо, поэтому клиенты и серверы могут читать и писать в любом порядке: например, сервер может дождаться получения всех сообщений от клиентов, прежде чем отправлять свои ответы, или поочередно читать сообщение, а затем записывать его, или использовать другую комбинацию чтения и записи. Порядок сообщений в каждом потоке сохраняется. Этот тип метода указывается ключевым словом stream перед запросом и ответом.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
4. Генерация клиентского и серверного кода
Далее нам нужно сгенерировать клиентский и серверный интерфейсы gRPC из определения сервиса .proto
. Для этого мы используем компилятор буфера протокола protoc
со специальным плагином gRPC для Java. Для генерации сервисов gRPC вам понадобится компилятор proto3 (поддерживающий синтаксис proto2 и proto3).
При использовании Gradle или Maven плагин сборки protoc может сгенерировать необходимый код в процессе сборки. Инструкции по генерации кода из собственных файлов .proto
см. в файле README grpc-java .
Мы предоставили конфигурацию Gradle.
Из каталога streaming-grpc-java-getting-started
введите
$ chmod +x gradlew $ ./gradlew generateProto
Следующие классы генерируются из нашего определения службы (в build/generated/sources/proto/main/java
):
- По одному для каждого типа сообщения:
Feature.java
,Rectangle.java, ...
, которые содержат весь код буфера протокола для заполнения, сериализации и извлечения наших типов сообщений-запросов и ответов. -
RouteGuideGrpc.java
, который содержит (вместе с другим полезным кодом) базовый класс для реализации серверамиRouteGuide
,RouteGuideGrpc.RouteGuideImplBase
, со всеми методами, определенными в службеRouteGuide
, и классами-заглушками для использования клиентами.
5. Реализуйте услугу
Сначала давайте рассмотрим, как создать сервер RouteGuide
. Чтобы наш сервис RouteGuide
заработал, нужно выполнить два шага:
- Реализация интерфейса сервиса, созданного на основе определения нашего сервиса: выполнение фактической «работы» нашего сервиса.
- Запуск сервера gRPC для прослушивания запросов от клиентов и отправки их в нужную реализацию службы.
Реализовать RouteGuide
Мы реализуем класс RouteGuideService
, расширяющий сгенерированный класс RouteGuideGrpc.RouteGuideImplBase. Вот как будет выглядеть реализация.
public void listFeatures(Rectangle request, StreamObserver<Feature> responseObserver) {
...
}
public StreamObserver<Point> recordRoute(final StreamObserver<RouteSummary> responseObserver) {
...
}
public StreamObserver<RouteNote> routeChat(final StreamObserver<RouteNote> responseObserver) {
...
}
Давайте подробно рассмотрим каждую реализацию RPC.
Потоковая передача RPC на стороне сервера
Далее рассмотрим один из наших потоковых RPC-вызовов. ListFeatures
— это серверный потоковый RPC-вызов, поэтому нам нужно отправить несколько Features
обратно нашему клиенту.
private final Collection<Feature> features;
@Override
public void listFeatures(Rectangle request, StreamObserver<Feature> responseObserver) {
int left = min(request.getLo().getLongitude(), request.getHi().getLongitude());
int right = max(request.getLo().getLongitude(), request.getHi().getLongitude());
int top = max(request.getLo().getLatitude(), request.getHi().getLatitude());
int bottom = min(request.getLo().getLatitude(), request.getHi().getLatitude());
for (Feature feature : features) {
if (!RouteGuideUtil.exists(feature)) {
continue;
}
int lat = feature.getLocation().getLatitude();
int lon = feature.getLocation().getLongitude();
if (lon >= left && lon <= right && lat >= bottom && lat <= top) {
responseObserver.onNext(feature);
}
}
responseObserver.onCompleted();
}
Как и простой RPC, этот метод получает объект запроса ( Rectangle
, в котором наш клиент хочет найти Features
) и наблюдатель ответа StreamObserver
.
На этот раз мы получаем столько объектов Feature
, сколько необходимо для возврата клиенту (в данном случае мы выбираем их из коллекции объектов Feature сервиса в зависимости от того, находятся ли они внутри Rectangle
нашего запроса), и по очереди записываем их в наблюдатель ответов с помощью его метода onNext()
. Наконец, как и в нашем простом RPC, мы используем метод onCompleted()
наблюдателя ответов, чтобы сообщить gRPC о завершении записи ответов.
Клиентская потоковая передача RPC
Теперь давайте рассмотрим что-то более сложное: клиентский потоковый метод RecordRoute()
, где мы получаем поток Points
от клиента и возвращаем один RouteSummary
с информацией о его поездке.
@Override
public StreamObserver<Point> recordRoute(final StreamObserver<RouteSummary> responseObserver) {
return new StreamObserver<Point>() {
int pointCount;
int featureCount;
int distance;
Point previous;
long startTime = System.nanoTime();
@Override
public void onNext(Point point) {
pointCount++;
if (RouteGuideUtil.exists(checkFeature(point))) {
featureCount++;
}
// For each point after the first, add the incremental distance from the previous point
// to the total distance value.
if (previous != null) {
distance += calcDistance(previous, point);
}
previous = point;
}
@Override
public void onError(Throwable t) {
logger.log(Level.WARNING, "Encountered error in recordRoute", t);
}
@Override
public void onCompleted() {
long seconds = NANOSECONDS.toSeconds(System.nanoTime() - startTime);
responseObserver.onNext(RouteSummary.newBuilder().setPointCount(pointCount)
.setFeatureCount(featureCount).setDistance(distance)
.setElapsedTime((int) seconds).build());
responseObserver.onCompleted();
}
};
}
Как вы можете видеть, как и предыдущие типы методов, наш метод получает параметр responseObserver
StreamObserver
, но на этот раз он возвращает StreamObserver
, чтобы клиент мог записать свои Points
.
В теле метода мы создаем для возврата экземпляр анонимного StreamObserver
, в котором мы:
- Переопределите метод
onNext()
, чтобы получать функции и другую информацию каждый раз, когда клиент записываетPoint
в поток сообщений. - Переопределяем метод
onCompleted()
(вызываемый после того, как клиент завершил отправку сообщений) для заполнения и построенияRouteSummary
. Затем мы вызываем методonNext()
наблюдателя ответа нашего метода с нашимRouteSummary
, а затем вызываем его методonCompleted()
для завершения вызова со стороны сервера.
Двунаправленный потоковый RPC
Наконец, давайте рассмотрим наш двунаправленный потоковый RPC RouteChat()
.
@Override
public StreamObserver<RouteNote> routeChat(final StreamObserver<RouteNote> responseObserver) {
return new StreamObserver<RouteNote>() {
@Override
public void onNext(RouteNote note) {
List<RouteNote> notes = getOrCreateNotes(note.getLocation());
// Respond with all previous notes at this location.
for (RouteNote prevNote : notes.toArray(new RouteNote[0])) {
responseObserver.onNext(prevNote);
}
// Now add the new note to the list
notes.add(note);
}
@Override
public void onError(Throwable t) {
logger.log(Level.WARNING, "Encountered error in routeChat", t);
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
}
Как и в нашем примере потоковой передачи на стороне клиента, мы получаем и возвращаем StreamObserver
, но на этот раз мы возвращаем значения через наблюдатель ответа нашего метода, пока клиент продолжает записывать сообщения в свой поток сообщений. Синтаксис чтения и записи здесь точно такой же, как для наших методов потоковой передачи на стороне клиента и сервера. Хотя каждая сторона всегда получает сообщения другой стороны в порядке их записи, и клиент, и сервер могут читать и записывать в любом порядке — потоки работают совершенно независимо.
Запустить сервер
После реализации всех наших методов нам также необходимо запустить gRPC-сервер, чтобы клиенты могли использовать наш сервис. Следующий фрагмент кода показывает, как это сделать для нашего сервиса RouteGuide
:
public RouteGuideServer(int port, URL featureFile) throws IOException {
this(ServerBuilder.forPort(port), port, RouteGuideUtil.parseFeatures(featureFile));
}
/** 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();
}
public void start() throws IOException {
server.start();
logger.info("Server started, listening on " + port);
}
Как вы можете видеть, мы создаем и запускаем наш сервер с помощью ServerBuilder
.
Для этого мы:
- Укажите адрес и порт, которые мы хотим использовать для прослушивания клиентских запросов с помощью метода
forPort()
конструктора. - Создайте экземпляр класса реализации нашей службы
RouteGuideService
и передайте его методуaddService()
конструктора. - Вызовите
build()
иstart()
в конструкторе, чтобы создать и запустить RPC-сервер для нашей службы.
Поскольку ServerBuilder уже включает порт, единственная причина, по которой мы передаем порт, — это его использование для ведения журнала.
6. Создайте клиента
В этом разделе мы рассмотрим создание клиента для нашего сервиса RouteGuide
. Полный пример кода клиента можно найти в файле ../complete/src/main/java/io/grpc/complete/routeguide/
RouteGuideClient.java
.
Создать заглушку
Для вызова методов сервиса нам сначала нужно создать заглушку , а точнее, две заглушки:
- блокирующая/синхронная заглушка: это означает, что вызов RPC ждет ответа сервера и либо вернет ответ, либо вызовет исключение.
- Неблокирующая/асинхронная заглушка, которая выполняет неблокирующие вызовы к серверу, возвращая ответ асинхронно. Некоторые типы потоковых вызовов можно выполнять только с помощью асинхронной заглушки.
Сначала нам нужно создать канал gRPC для нашей заглушки, указав адрес сервера и порт, к которому мы хотим подключиться:
public static void main(String[] args) throws InterruptedException {
String target = "localhost:8980";
if (args.length > 0) {
if ("--help".equals(args[0])) {
System.err.println("Usage: [target]");
System.err.println("");
System.err.println(" target The server to connect to. Defaults to " + target);
System.exit(1);
}
target = args[0];
}
List<Feature> features;
try {
features = RouteGuideUtil.parseFeatures(RouteGuideUtil.getDefaultFeaturesFile());
} catch (IOException ex) {
ex.printStackTrace();
return;
}
ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
.build();
try {
RouteGuideClient client = new RouteGuideClient(channel);
// Looking for features between 40, -75 and 42, -73.
client.listFeatures(400000000, -750000000, 420000000, -730000000);
// Record a few randomly selected points from the features file.
client.recordRoute(features, 10);
// Send and receive some notes.
CountDownLatch finishLatch = client.routeChat();
if (!finishLatch.await(1, TimeUnit.MINUTES)) {
client.warning("routeChat did not finish within 1 minutes");
}
} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
}
Для создания канала мы используем ManagedChannelBuilder
.
Теперь мы можем использовать канал для создания наших заглушек с помощью методов newStub
и newBlockingStub
, предоставленных в классе RouteGuideGrpc
, который мы сгенерировали из нашего .proto
.
public RouteGuideClient(Channel channel) {
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
}
Помните, если это не блокировка, это асинхронность.
Методы обслуживания вызовов
Теперь давайте посмотрим, как мы вызываем наши сервисные методы. Обратите внимание, что любые RPC-вызовы, созданные из блокирующей заглушки, будут работать в блокирующем/синхронном режиме, то есть RPC-вызов ожидает ответа сервера и либо возвращает ответ, либо ошибку.
Потоковая передача RPC на стороне сервера
Далее рассмотрим потоковый вызов ListFeatures
на стороне сервера, который возвращает поток географических Feature
:
Rectangle request = Rectangle.newBuilder()
.setLo(Point.newBuilder().setLatitude(lowLat).setLongitude(lowLon).build())
.setHi(Point.newBuilder().setLatitude(hiLat).setLongitude(hiLon).build()).build();
Iterator<Feature> features;
try {
features = blockingStub.listFeatures(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
Как вы можете видеть, это очень похоже на простой унарный RPC, который мы рассматривали в кодовой лаборатории Getting_Started_With_gRPC_Java, за исключением того, что вместо возврата одного Feature
метод возвращает Iterator
, который клиент может использовать для чтения всех возвращенных Features
.
Клиентская потоковая передача RPC
Теперь немного посложнее: клиентский потоковый метод RecordRoute
, в котором мы отправляем поток Points
на сервер и получаем обратно один RouteSummary
. Для этого метода нам понадобится асинхронная заглушка. Если вы уже читали раздел «Создание сервера» , некоторые моменты могут показаться вам очень знакомыми — асинхронные потоковые RPC-вызовы реализованы одинаково на обеих сторонах.
public void recordRoute(List<Feature> features, int numPoints) throws InterruptedException {
info("*** RecordRoute");
final CountDownLatch finishLatch = new CountDownLatch(1);
StreamObserver<RouteSummary> responseObserver = new StreamObserver<RouteSummary>() {
@Override
public void onNext(RouteSummary summary) {
info("Finished trip with {0} points. Passed {1} features. "
+ "Travelled {2} meters. It took {3} seconds.", summary.getPointCount(),
summary.getFeatureCount(), summary.getDistance(), summary.getElapsedTime());
}
@Override
public void onError(Throwable t) {
Status status = Status.fromThrowable(t);
logger.log(Level.WARNING, "RecordRoute Failed: {0}", status);
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("Finished RecordRoute");
finishLatch.countDown();
}
};
StreamObserver<Point> requestObserver = asyncStub.recordRoute(responseObserver);
try {
// Send numPoints points randomly selected from the features list.
Random rand = new Random();
for (int i = 0; i < numPoints; ++i) {
int index = rand.nextInt(features.size());
Point point = features.get(index).getLocation();
info("Visiting point {0}, {1}", RouteGuideUtil.getLatitude(point),
RouteGuideUtil.getLongitude(point));
requestObserver.onNext(point);
// Sleep for a bit before sending the next one.
Thread.sleep(rand.nextInt(1000) + 500);
if (finishLatch.getCount() == 0) {
// RPC completed or errored before we finished sending.
// Sending further requests won't error, but they will just be thrown away.
return;
}
}
} catch (RuntimeException e) {
// Cancel RPC
requestObserver.onError(e);
throw e;
}
// Mark the end of requests
requestObserver.onCompleted();
// Receiving happens asynchronously
finishLatch.await(1, TimeUnit.MINUTES);
}
Как видите, для вызова этого метода нам нужно создать StreamObserver
, реализующий специальный интерфейс, к которому сервер будет обращаться с ответом RouteSummary
. В нашем StreamObserver
мы:
- Переопределите метод
onNext()
, чтобы вывести возвращаемую информацию, когда сервер записываетRouteSummary
в поток сообщений. - Переопределите метод
onCompleted()
(вызываемый, когда сервер завершил вызов на своей стороне), чтобы уменьшитьCountDownLatch
и иметь возможность проверить, завершил ли сервер запись.
Затем мы передаём StreamObserver
методу recordRoute()
асинхронной заглушки и получаем обратно наш собственный обозреватель запросов StreamObserver
для записи Points
для отправки на сервер. После завершения записи точек мы используем метод onCompleted()
обозревателя запросов, чтобы сообщить gRPC о завершении записи на стороне клиента. После этого мы проверяем CountDownLatch
, чтобы убедиться, что сервер завершил запись.
Двунаправленный потоковый RPC
Наконец, давайте рассмотрим наш двунаправленный потоковый RPC RouteChat()
.
public CountDownLatch routeChat() {
info("*** RouteChat");
final CountDownLatch finishLatch = new CountDownLatch(1);
StreamObserver<RouteNote> requestObserver =
asyncStub.routeChat(new StreamObserver<RouteNote>() {
@Override
public void onNext(RouteNote note) {
info("Got message \"{0}\" at {1}, {2}", note.getMessage(), note.getLocation()
.getLatitude(), note.getLocation().getLongitude());
}
@Override
public void onError(Throwable t) {
warning("RouteChat Failed: {0}", Status.fromThrowable(t));
finishLatch.countDown();
}
@Override
public void onCompleted() {
info("Finished RouteChat");
finishLatch.countDown();
}
});
try {
RouteNote[] requests =
{newNote("First message", 0, 0), newNote("Second message", 0, 10_000_000),
newNote("Third message", 10_000_000, 0), newNote("Fourth message", 10_000_000, 10_000_000)};
for (RouteNote request : requests) {
info("Sending message \"{0}\" at {1}, {2}", request.getMessage(), request.getLocation()
.getLatitude(), request.getLocation().getLongitude());
requestObserver.onNext(request);
}
} catch (RuntimeException e) {
// Cancel RPC
requestObserver.onError(e);
throw e;
}
// Mark the end of requests
requestObserver.onCompleted();
// return the latch while receiving happens asynchronously
return finishLatch;
}
Как и в нашем примере потоковой передачи на стороне клиента, мы получаем и возвращаем наблюдатель ответа StreamObserver
, но на этот раз мы отправляем значения через наблюдатель ответа нашего метода, пока сервер всё ещё записывает сообщения в свой поток сообщений. Синтаксис чтения и записи здесь точно такой же, как и для нашего метода потоковой передачи на стороне клиента. Хотя каждая сторона всегда получает сообщения другой стороны в том порядке, в котором они были записаны, и клиент, и сервер могут читать и записывать данные в любом порядке — потоки работают совершенно независимо.
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
8. Что дальше?
- Узнайте, как работает gRPC, изучив раздел «Введение в gRPC» и «Основные концепции».
- Проработайте базовый учебник
- Изучите справочник API .