Асинхронное обновление обещаний в Netty Nio

У меня есть серверная и клиентская архитектура, которые обмениваются информацией. Я хочу вернуть с сервера количество подключенных каналов. Я хочу вернуть сообщение сервера клиентам, используя обещание. Мой код:

public static void callBack () throws Exception{

   String host = "localhost";
   int port = 8080;

   try {
       Bootstrap b = new Bootstrap();
       b.group(workerGroup);
       b.channel(NioSocketChannel.class);
       b.option(ChannelOption.SO_KEEPALIVE, true);
       b.handler(new ChannelInitializer<SocketChannel>() {
        @Override
           public void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline().addLast(new RequestDataEncoder(), new ResponseDataDecoder(), new ClientHandler(promise));
           }
       });
       ChannelFuture f = b.connect(host, port).sync();
       //f.channel().closeFuture().sync();
   }
   finally {
    //workerGroup.shutdownGracefully();
   }
}

public static void main(String[] args) throws Exception {

  callBack();
  while (true) {

    Object msg = promise.get();
    System.out.println("The number if the connected clients is not two");
    int ret = Integer.parseInt(msg.toString());
    if (ret == 2){
        break;
    }
  }
  System.out.println("The number if the connected clients is two");
}

Когда я запускаю один клиент, он всегда получает сообщение The number if the connected clients is not two, а возвращаемый номер всегда один. Когда я запускаю второго клиента, он получает всегда как возвращаемое значение два, однако первый клиент все равно получает его. Я не могу найти, какой правильный способ обновить обещание для случая первого клиента.

EDIT: Клиентский сервер:

public class ClientHandler extends ChannelInboundHandlerAdapter {
  public final Promise<Object> promise;
  public ClientHandler(Promise<Object> promise) {
      this.promise = promise;
  }

  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
      RequestData msg = new RequestData();
      msg.setIntValue(123);
      msg.setStringValue("all work and no play makes jack a dull boy");
      ctx.writeAndFlush(msg);
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      System.out.println(msg);
      promise.trySuccess(msg);
  }
} 

Код обработчика клиента, сохраняющий сообщение, полученное от сервера, к обещанию.

Ответ 1

С картой Netty, Promise и Future являются объектами с однократной записью, этот принцип упрощает их использование в многопоточной среде.

Поскольку Promise не делает то, что вы хотите, нам нужно выяснить, подходят ли другие технологии для ваших условий, ваши условия в основном сводятся к следующему:

  • Чтение из нескольких потоков
  • Запись только из одного потока (как внутри канала Netty, метод чтения может выполняться только одним потоком одновременно, если канал не отмечен как общий)

Для этих требований наилучшее соответствие соответствия - это изменчивая переменная, так как она безопасна для чтения для чтения и может безопасно обновляться одним потоком, не беспокоясь о порядке записи.

Чтобы обновить свой код для использования с изменчивой переменной, он требует некоторых изменений, поскольку мы не можем легко передать ссылку ссылки на переменную внутри вашей функции, но мы должны передать функцию, которая обновляет базовую переменную.

private static volatile int connectedClients = 0;
public static void callBack () throws Exception{
    //....
           ch.pipeline().addLast(new RequestDataEncoder(), new ResponseDataDecoder(),
                                 new ClientHandler(i -> {connectedClients = i;});
    //....
}

public static void main(String[] args) throws Exception {

  callBack();
  while (true) {
    System.out.println("The number if the connected clients is not two");
    int ret = connectedClients;
    if (ret == 2){
        break;
    }
  }
  System.out.println("The number if the connected clients is two");
}

public class ClientHandler extends ChannelInboundHandlerAdapter {
  public final IntConsumer update;
  public ClientHandler(IntConsumer update) {
      this.update = update;
  }

  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
      RequestData msg = new RequestData();
      msg.setIntValue(123);
      msg.setStringValue("all work and no play makes jack a dull boy");
      ctx.writeAndFlush(msg);
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
      System.out.println(msg);
      update.accept(Integer.parseInt(msg));
  }
} 

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

Чтобы решить эту проблему, мы используем следующие шаги при чтении:

  • Сохраните значение connectedClients в отдельной переменной
  • Сравните эту переменную с целевым значением
  • Если это правда, тогда выйдите из цикла
  • Если false, перейдите в синхронизированный блок
  • запустить цикл while
  • Снова прочтите переменную, так как теперь значение может быть изменено.
  • Проверить состояние и разорвать, если условие сейчас правильно.
  • Если нет, дождитесь изменения значения

И при написании:

  • Синхронизировать
  • Обновить значение
  • Пробудите все остальные потоки, ожидающие этого значения.

Это может быть реализовано в коде следующим образом:

private static volatile int connectedClients = 0;
private static final Object lock = new Object();
public static void callBack () throws Exception{
    //....
           ch.pipeline().addLast(new RequestDataEncoder(), new ResponseDataDecoder(),
                                 new ClientHandler(i -> {
               synchronized (lock) {
                   connectedClients = i;
                   lock.notifyAll();
               }
           });
    //....
}

public static void main(String[] args) throws Exception {

  callBack();
  int connected = connectedClients;
  if (connected != 2) {
      System.out.println("The number if the connected clients is not two before locking");
      synchronized (lock) {
          while (true) {
              connected = connectedClients;
              if (connected == 2)
                  break;
              System.out.println("The number if the connected clients is not two");
              lock.wait();
          }
      }
  }
  System.out.println("The number if the connected clients is two: " + connected );
}

Изменения на стороне сервера

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

Если вы разместили ссылку на свой репозиторий github, вы никогда не отправляете запрос с сервера обратно старым клиентам, когда новый человек присоединился. Поскольку это не сделано, клиент никогда не уведомляется об изменениях, обязательно сделайте это.