Введение в RSocket

Введение в RSocket

1. Вступление

В этом руководстве мы сначала рассмотримRSocket и то, как он обеспечивает связь клиент-сервер.

2. Что такое RSocket?

RSocket is a binary, point-to-point communication protocol предназначен для использования в распределенных приложениях. В этом смысле он предоставляет альтернативу другим протоколам, таким как HTTP.

Полное сравнение между RSocket и другими протоколами выходит за рамки этой статьи. Вместо этого мы сосредоточимся на ключевой особенности RSocket: его моделях взаимодействия.

RSocket provides four interaction models. Имея это в виду, мы рассмотрим каждый из них на примере.

3. Maven Зависимости

Для наших примеров RSocket нужны только две прямые зависимости:


    io.rsocket
    rsocket-core
    0.11.13


    io.rsocket
    rsocket-transport-netty
    0.11.13

Зависимостиrsocket-core иrsocket-transport-netty доступны в Maven Central.

An important note is that the RSocket library makes frequent use of reactive streams. В этой статье используются классыFlux иMono, поэтому будет полезно их базовое понимание.

4. Настройка сервера

Сначала давайте создадим классServer:

public class Server {
    private final Disposable server;

    public Server() {
        this.server = RSocketFactory.receive()
          .acceptor((setupPayload, reactiveSocket) -> Mono.just(new RSocketImpl()))
          .transport(TcpServerTransport.create("localhost", TCP_PORT))
          .start()
          .subscribe();
    }

    public void dispose() {
        this.server.dispose();
    }

    private class RSocketImpl extends AbstractRSocket {}
}

Here we use the RSocketFactory to set up and listen to a TCP socket. Мы передаем наш собственныйRSocketImpl для обработки запросов от клиентов. Мы будем добавлять методы вRSocketImpl по мере продвижения.

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

Server server = new Server();

A single server instance can handle multiple connections. В результате всего один экземпляр сервера будет поддерживать все наши примеры.

Когда мы закончим, методdispose остановит сервер и освободит TCP-порт.

4. Модели взаимодействия

4.1. Request/Response

RSocket предоставляет модель запрос / ответ - каждый запрос получает один ответ.

Для этой модели мы создадим простой сервис, который возвращает сообщение клиенту.

Начнем с добавления метода к нашему расширениюAbstractRSocket, RSocketImpl:

@Override
public Mono requestResponse(Payload payload) {
    try {
        return Mono.just(payload); // reflect the payload back to the sender
    } catch (Exception x) {
        return Mono.error(x);
    }
}

The requestResponse method returns a single result for each request, как видно по типу ответаMono<Payload>.

Payload is the class that contains message content and metadata. Он используется всеми моделями взаимодействия. Содержимое полезных данных является двоичным, но есть удобные методы, поддерживающие содержимое на основеString.

Далее мы можем создать наш класс клиента:

public class ReqResClient {

    private final RSocket socket;

    public ReqResClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();
    }

    public String callBlocking(String string) {
        return socket
          .requestResponse(DefaultPayload.create(string))
          .map(Payload::getDataUtf8)
          .block();
    }

    public void dispose() {
        this.socket.dispose();
    }
}

Клиент использует методRSocketFactory.connect(), чтобы инициировать соединение сокета с сервером. We use the requestResponse method on the socket to send a payload to the server.

Наша полезная нагрузка содержитString, переданные клиенту. When the Mono<Payload> ответ приходит, мы можем использовать методgetDataUtf8()  для доступа к содержаниюString ответа.

Наконец, мы можем запустить интеграционный тест, чтобы увидеть запрос / ответ в действии. Мы отправим на серверString и проверим, что возвращается тот жеString:

@Test
public void whenSendingAString_thenRevceiveTheSameString() {
    ReqResClient client = new ReqResClient();
    String string = "Hello RSocket";

    assertEquals(string, client.callBlocking(string));

    client.dispose();
}

4.2. Головка самонаведения

В модели «запустил и забыл»the client will receive no response from the server.

В этом примере клиент будет отправлять смоделированные измерения на сервер с интервалами 50 мс. Сервер опубликует измерения.

Давайте добавим на наш сервер обработчик включения и выключения в классеRSocketImpl:

@Override
public Mono fireAndForget(Payload payload) {
    try {
        dataPublisher.publish(payload); // forward the payload
        return Mono.empty();
    } catch (Exception x) {
        return Mono.error(x);
    }
}

Этот обработчик выглядит очень похоже на обработчик запроса / ответа. ОднакоfireAndForget returns Mono<Void> вместоMono<Payload>.

dataPublisher является экземпляромorg.reactivestreams.Publisher. Таким образом, это делает полезную нагрузку доступной для подписчиков. Мы воспользуемся этим в примере запроса / потока.

Затем мы создадим клиента «запустил и забыл»:

public class FireNForgetClient {
    private final RSocket socket;
    private final List data;

    public FireNForgetClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();
    }

    /** Send binary velocity (float) every 50ms */
    public void sendData() {
        data = Collections.unmodifiableList(generateData());
        Flux.interval(Duration.ofMillis(50))
          .take(data.size())
          .map(this::createFloatPayload)
          .flatMap(socket::fireAndForget)
          .blockLast();
    }

    // ...
}

Настройка сокета точно такая же, как и раньше.

МетодsendData() использует потокFlux для отправки нескольких сообщений. For each message, we invoke socket::fireAndForget.

We need to subscribe to the Mono<Void> response for each message. Если мы забудем подписаться,socket::fireAndForget не выполнится.

ОператорflatMap гарантирует, что ответыVoid передаются подписчику, а операторblockLast действует как подписчик.

Мы собираемся дождаться следующего раздела, чтобы запустить тест на запуск и забытие. На этом этапе мы создадим клиент запроса / потока для получения данных, которые были отправлены клиентом «запустил и забыл».

4.3. Request/Stream

В модели запрос / потокa single request may receive multiple responses. Чтобы увидеть это в действии, мы можем опираться на пример «забей и забудь». Для этого давайте запросим поток для получения измерений, которые мы отправили в предыдущем разделе.

Как и раньше, давайте начнем с добавления нового слушателя кRSocketImpl на сервере:

@Override
public Flux requestStream(Payload payload) {
    return Flux.from(dataPublisher);
}

The requestStream handler returns a Flux<Payload> stream. Как мы помним из предыдущего раздела, обработчикfireAndForget опубликовал входящие данные вdataPublisher.. Теперь мы создадим потокFlux, используя тот жеdataPublisher в качестве источника события. . При этом данные измерений будут передаваться асинхронно от нашего клиента «запомни и забудь» нашему клиенту запроса / потока.

Теперь давайте создадим клиент запроса / потока:

public class ReqStreamClient {

    private final RSocket socket;

    public ReqStreamClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();
    }

    public Flux getDataStream() {
        return socket
          .requestStream(DefaultPayload.create(DATA_STREAM_NAME))
          .map(Payload::getData)
          .map(buf -> buf.getFloat())
          .onErrorReturn(null);
    }

    public void dispose() {
        this.socket.dispose();
    }
}

Мы подключаемся к серверу так же, как и наши предыдущие клиенты.

ВgetDataStream()we use socket.requestStream() to receive a Flux<Payload> stream from the server. Из этого потока мы извлекаем значенияFloat из двоичных данных. Наконец, поток возвращается вызывающей стороне, что позволяет подписчику подписаться на него и обработать результаты.

А теперь давайте проверим. We’ll verify the round trip from fire-and-forget to request/stream.с

Мы можем утверждать, что каждое значение получено в том же порядке, в котором оно было отправлено. Затем мы можем утверждать, что получаем то же количество отправленных значений:

@Test
public void whenSendingStream_thenReceiveTheSameStream() {
    FireNForgetClient fnfClient = new FireNForgetClient();
    ReqStreamClient streamClient = new ReqStreamClient();

    List data = fnfClient.getData();
    List dataReceived = new ArrayList<>();

    Disposable subscription = streamClient.getDataStream()
      .index()
      .subscribe(
        tuple -> {
            assertEquals("Wrong value", data.get(tuple.getT1().intValue()), tuple.getT2());
            dataReceived.add(tuple.getT2());
        },
        err -> LOG.error(err.getMessage())
      );

    fnfClient.sendData();

    // ... dispose client & subscription

    assertEquals("Wrong data count received", data.size(), dataReceived.size());
}

4.4. канал

The channel model provides bidirectional communication. В этой модели потоки сообщений проходят асинхронно в обоих направлениях.

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

Сначала создадим обработчик на сервере. Как и раньше, мы добавляем кRSocketImpl:

@Override
public Flux requestChannel(Publisher payloads) {
    Flux.from(payloads)
      .subscribe(gameController::processPayload);
    return Flux.from(gameController);
}

The requestChannel handler has Payload streams for both input and output. Входной параметрPublisher<Payload> - это поток полезных данных, полученных от клиента. По мере поступления эти данные передаются функцииgameController::processPayload.

В ответ мы возвращаем клиенту другой потокFlux. Этот поток создается из нашегоgameController, который также являетсяPublisher.

Вот краткое описание классаGameController:

public class GameController implements Publisher {

    @Override
    public void subscribe(Subscriber subscriber) {
        // send Payload messages to the subscriber at random intervals
    }

    public void processPayload(Payload payload) {
        // react to messages from the other player
    }
}

КогдаGameController получает подписчика, он начинает отправлять сообщения этому подписчику.

Затем давайте создадим клиента:

public class ChannelClient {

    private final RSocket socket;
    private final GameController gameController;

    public ChannelClient() {
        this.socket = RSocketFactory.connect()
          .transport(TcpClientTransport.create("localhost", TCP_PORT))
          .start()
          .block();

        this.gameController = new GameController("Client Player");
    }

    public void playGame() {
        socket.requestChannel(Flux.from(gameController))
          .doOnNext(gameController::processPayload)
          .blockLast();
    }

    public void dispose() {
        this.socket.dispose();
    }
}

Как мы видели в наших предыдущих примерах, клиент подключается к серверу так же, как и другие клиенты.

Клиент создает свой собственный экземплярGameController.

We use socket.requestChannel() to send our Payload stream to the server. Сервер отвечает собственным потоком полезных данных.

Как данные, полученные от сервера, мы передаем их нашему обработчикуgameController::processPayload.

В нашей игровой симуляции клиент и сервер являются зеркальным отображением друг друга. То естьeach side is sending a stream of Payload and receiving a stream of Payload from the other end.

Потоки работают независимо, без синхронизации.

Наконец, давайте запустим моделирование в тесте:

@Test
public void whenRunningChannelGame_thenLogTheResults() {
    ChannelClient client = new ChannelClient();
    client.playGame();
    client.dispose();
}

5. Заключение

В этой вводной статье мы изучили модели взаимодействия, предоставляемые RSocket. Полный исходный код примеров можно найти в нашемGithub repository.

Обязательно ознакомьтесь сRSocket website для более глубокого обсуждения. В частности, документыFAQ иMotivations предоставляют хорошую справочную информацию.