10

I have got a server and client architecture that exchange information. I want to return from the server the number of connected channels. I want to return the message of the server to the clients using promise. My code is:

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");
}

When I run one client it is always receiving the message The number if the connected clients is not two and the returning number is always one. When I run a second client it is receiving always as a returning value two, however, the first client still is receiving one. I cannot find which is the correct way to update the promise for the case of the first client.

EDIT: Client Server:

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);
  }
} 

The code from the client handler storing the message received from server to the promise.

konstantin
  • 853
  • 4
  • 16
  • 50

1 Answers1

7

With the Netty framework, a Promise and a Future are write-once objects, this principle makes them easier to use in a multithreaded environment.

Since a Promise doesn't do what you want, we need to see if other technologies are fit for your conditions, your conditions basically boil down to:

  • Read from multiple threads
  • Write from a single thread only (as inside a Netty channel the read method can only be executed by 1 thread at the same time, unless the channel is marked shareable)

For these requirements, the best fitting match is a volatile variable, as this is thread-safe for reading, and can safely be updated by 1 thread without worrying about the write order.

To update your code for usage with a volatile variable, it requires some modifications, as we cannot easily pass the reference link to the variable inside your function, but we must pass a function that updates the backend variable.

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 the approach above should work, we quickly see that the while loop inside the main class uses a large share of CPU time, and this may affect other parts of your local client system, luckily, this problem is also solvable if we add other parts to the system, namely synchronization. By leaving the initial read of the connectedClients outside the synchronization block, we can still profit from the quick reads in the case of the "true" case, and in case of the "false' case, we can safe important CPU cycles that can be used in other parts of your system.

To tackle this problem, we use the following steps when reading:

  1. Store the value of connectedClients in a separate variable
  2. Compare this variable with the target value
  3. If it's true, then break early out of the loop
  4. If false, go inside a synchronized block
  5. start a while true loop
  6. Read out the variable again, since the value might be changed now
  7. Check the condition, and break if condition is correct now
  8. If not, wait for a change in the value

And the following when writing:

  1. synchronize
  2. Update the value
  3. Wake up all other threads waiting for this value

This can be implemented in code as the following:

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 );
}

Server side changes

However, not all of your problems are related to the client side.

SInce you posted a link to your github repository, you never send a request from the server back to the old clients when a new person has joined. Because this is not done, the client is never notified about the change, make sure to do this as well.

Ferrybig
  • 18,194
  • 6
  • 57
  • 79
  • When am trying to add the connectedClients to the initChannels (new ClientHandler(i -> {connectedClients = i;}) am receiving incompatible types it expects int while it receiving a lambda parameter. – konstantin Nov 02 '17 at 10:21
  • @konstantin Did you notice that I adjusted the `ClientHandler`, I changed the parameter from `Promise` to `IntConsumer`. If you have an parameter that has the type `IntConsumer`, you can use a lamba block that accepts 1 int as input, and this is the method I use to propagate the results to the other class. – Ferrybig Nov 02 '17 at 10:24
  • Ok I wrote correctly the code. I run two clients, however in both of them I received the following message: Nov 02, 2017 11:43:09 AM io.netty.channel.DefaultChannelPipeline onUnhandledInboundException WARNING: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception. java.lang.ClassCastException: chat.ResponseData cannot be cast to java.lang.Integer – konstantin Nov 02 '17 at 10:44
  • The updated code can be found here https://github.com/kristosh/netty-nio-Client-Server – konstantin Nov 02 '17 at 10:57
  • That was probably something I missed from your original code, inside `CLientHandler`, instead of `update.accept(Integer.parseInt(msg));`, do `update.accept(((RequestData)msg).getIntValue());`, this extracts the size directly from your request data object – Ferrybig Nov 02 '17 at 11:01
  • I changed it and I got the say message which points that line. – konstantin Nov 02 '17 at 11:03
  • I updated the channelRead by converting correctly the object msg to integer using the following code: update.accept(Integer.parseInt(msg.toString())); When, I run the problem the number of connected channels in the first client remains always one (int connected = connectedClients;) – konstantin Nov 02 '17 at 12:50
  • Are you sure the server actually sends the correct number, add a `new LoggingHandler(LogLevel.INFO)` to your pipeline (the list that comes after `ch.pipeline().addLast(` is your pipeline) – Ferrybig Nov 02 '17 at 13:41
  • Also inside your `ProcessingHandler` file in your repository you never send a `2` back – Ferrybig Nov 02 '17 at 13:42
  • Hmm the code in github wasnt properly updated. As a matter of fact the processing handler returns the number of channels (the tested code not the one in github). What is happening is that the first clients is receiiving the following messagess: – konstantin Nov 02 '17 at 13:57
  • The number if the connected clients is not two before locking The number if the connected clients is not two 1 The number if the connected clients is not two – konstantin Nov 02 '17 at 13:57
  • While the second client receives the following: The number if the connected clients is not two before locking The number if the connected clients is not two 1 The number if the connected clients is not two – konstantin Nov 02 '17 at 13:58
  • Everytime you run a new client the returned number of connected channels is correct, the problem is that it doesnt updated in the previous clients so the result of the first client is always one (the number of connected channels) in the second client two, in the third three and so so. – konstantin Nov 02 '17 at 14:08
  • Why the int connected = connectedClients; is always one for the first client? – konstantin Nov 07 '17 at 09:28
  • Because inside your server's `ProcessingHandler`, you only write to ctx (instead of `channels1.writeandflush`), so the client never receives a message saying something other than 1 – Ferrybig Nov 07 '17 at 09:59
  • So I need something like that:for (Channel ch : channels1) { ChannelFuture future = ch.writeAndFlush(responseData); //future.addListener(ChannelFutureListener.CLOSE); } – konstantin Nov 07 '17 at 11:31
  • You need `ChannelFuture future = channels1.writeAndFlush(responseData);` – Ferrybig Nov 07 '17 at 11:34
  • Please can you posted it as a solution? Indeed it worked. – konstantin Nov 07 '17 at 12:26
  • Actually channels1 is a list of channels, so i cannot do what you propose. What i can do is to loop through channel1 and send the message. – konstantin Nov 07 '17 at 12:31
  • I thought your list was a [`ChannelGroup`](https://netty.io/4.0/api/io/netty/channel/group/ChannelGroup.html), this has a option for `writeAndFlush` – Ferrybig Nov 07 '17 at 13:05
  • Yes its a channelGroup but when i try to write directly there I got an error. Actually the error is Incompatible error: Required ChannelFuture, Found ChannelGroupFuture. – konstantin Nov 07 '17 at 13:28
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/158426/discussion-between-konstantin-and-ferrybig). – konstantin Nov 07 '17 at 14:29
  • I am going to accept your solution, thanks a lot for the help, however, have got one more question. How can I send the message from server to client (or from client to the server) but using a string from the main server or main client method? – konstantin Nov 07 '17 at 14:42
  • At the moment, you have your data structure called "RequestData" that you can use for this purpose, your data structure accepts both a number (`int`) using `setIntValue`, and a string value using `setStringValue`. Just call `setStringValue` from your code after you called the `setIntValue` to send a custom string. This can then be read out in the client side using `getStringValue` and requires the same `volatile` variable trick – Ferrybig Nov 07 '17 at 14:45
  • The things is that now am using the writeAndFlush and requestData from the client and server handlers. How can i do it in the main? – konstantin Nov 07 '17 at 14:48
  • In order to send messages back, it would be the easiest to pass the `ctx` variable from the handler back towards your main method, and use that when you need to call `writeAndFLush`. A better solution would be trying to get a reference to the `Channel` object. FOr this, inside your `callback()` function, add a `return f.get()`, and let it (and main) throw any of the exceptions. After that you can use the reference of your channel to call WriteAndFLushh – Ferrybig Nov 07 '17 at 14:57
  • So make also sense to create an object of the handler in my main function and process the object? – konstantin Nov 07 '17 at 14:59
  • That would also be fine, just play around a bit and try to get access to either the `Channel` object, or the `ctx` object, so you can send a message back – Ferrybig Nov 07 '17 at 15:01
  • Inisde the callBack function I am doing some synchronization for the ClientHandler do i need to the same for the object that i ll create in the main function? – konstantin Nov 07 '17 at 15:09
  • Normally in handler where I am sending messages from client to server or the opposite am retrieving the channels using the following: channelActive(ChannelHandlerContext ctx) a method with parameter the ChannelHandlerContext ctx, however, how can I call from main this ChannelHandlerContext ctx and initialize it without being null? – konstantin Nov 08 '17 at 10:00
  • The best way would be using the Channel reference actually. Change `public static void callBack () throws Exception{` to `public static Channel callBack () throws Exception{`, then `ChannelFuture f = b.connect(host, port).sync();` to `return ChannelFuture f = b.connect(host, port).sync();`. Then inside your main function change `callBack();` to `Channel ctx = callBack();`. Then you can use `ctx.writeAndFlush()` inside your main method. – Ferrybig Nov 08 '17 at 10:24
  • The f from the b.connect returs a ChannelFuture, how can convert it to channel or to ChannelHandlerContext? What probably can do is to return an ChannelFuture object for instance obj and then in obj.channel to writeAndFlush. However, have no idea if it is the same. – konstantin Nov 08 '17 at 10:28
  • Oops, forgot a `.channel()` there, calling `.channel()` on a `ChannelFuture` returns a `Channel` object – Ferrybig Nov 08 '17 at 10:32
  • Could you add a listener to the result of `writeandFlush`? Example here: https://stackoverflow.com/a/40862161/1542723 This shows if there is any error during the sending of the packet – Ferrybig Nov 08 '17 at 10:39
  • Yes it works i add it in the channel, however, need to understand what is the difference between the channel and the ChannelHandlerContext. – konstantin Nov 08 '17 at 12:41
  • @konstantin The main difference between these functions is that for every handler in the pipeline, a new `ChannelHandlerContext` is created, so your `packetDecoder` sees a different `ctx` than your main handler. Because every CTX is basicly bound towards a handler, it means it knows its own position in the pipeline, and therefore your protocol knowns that it should pass through the packet handler. When you invoke write on the main channel object, it will pass through all the handlers inside the pipeline, making sure its passes the all handlers, see: https://stackoverflow.com/q/32514803/1542723 – Ferrybig Nov 08 '17 at 12:50
  • really thanks you deserve more than 150 of a bonus reputation. – konstantin Nov 08 '17 at 13:49