javaniononblockingsocketchannel

Proper way to read (write) through a SocketChannel


My questions is more generic than the following scenario, though this covers everything needed. It is for Java and the correct practices of socket programming.

Scenario:

All examples that I have found for a non-blocking I/O go something like this:

InetAddress host = InetAddress.getByName("localhost");
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(host, 1234));
serverSocketChannel.register(selector, SelectionKey. OP_ACCEPT);
while (true) {
   if (selector.select() <= 0)
       continue;
   Set<SelectionKey> selectedKeys = selector.selectedKeys();
   Iterator<SelectionKey> iterator = selectedKeys.iterator();
   while (iterator.hasNext()) {
       key = (SelectionKey) iterator.next();
       iterator.remove();
       if (key.isAcceptable()) {
           SocketChannel socketChannel = serverSocketChannel.accept();
           socketChannel.configureBlocking(false);
           socketChannel.register(selector, SelectionKey.OP_READ);
           // Do something or do nothing
       }
       if (key.isReadable()) {
           SocketChannel socketChannel = (SocketChannel) key.channel();
           ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
           socketChannel.read(buffer);
           // Something something dark side
           if (result.length() <= 0) {
               sc.close();
               // Something else
           }
        }
    }

Does the read here reads all incoming data from that particular client and that particular request, if the buffer is large enough, or do I need to have it inside a while loop? If the buffer is not large enough?

In case of a write, do I also just do socketChannel.write(buffer) and I am good to go (at least from the programs point of view)?

The doc here does not specify the case when all incoming data fit in the buffer. It also makes it a bit confusing when I have a blocking I/O:

It is guaranteed, however, that if a channel is in blocking mode and there is at least one byte remaining in the buffer then this method will block until at least one byte is read.

Does this mean that here (blocking I/O) I need to read through a while loop either way (most examples that I found does this)? What about a write operation?

So, to sum it up, my question is, what is the proper way to read and write the data in my scenario, from the point of view of the middle server (client to the second server)?


Solution

  • If you had not called configureBlocking(false), then yes, you would use a loop to fill the buffer.

    However… the point of a non-blocking socket is not to get hung up waiting on any one socket, since that would delay the reading from all the remaining sockets whose selected keys haven’t yet been processed by your Iterator. Effectively, if ten clients connect, and one of them happens to have a slow connection, some or all of the others might experience the same slowness.

    (The exact order of the selected key set is not specified. It would be unwise to look at the source of the Selector implementation class, since the lack of any guarantee of order means future versions of Java SE are permitted to change the order.)

    To avoid waiting for any one socket, you don’t try to fill up the buffer all in one go; rather, you read whatever the socket can give you without blocking, by reading only once per select() call.

    Since each ByteBuffer might hold a partial data sequence, you’ll need to remember each ByteBuffer’s progress for each Socket. Luckily, SelectionKey has a convenient way to do that: the attachment.

    You also want to remember how many bytes you’ve read from each socket. So, now you have two things you need to remember for each socket: the byte count, and the ByteBuffer.

    class ReadState {
        final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        long count;
    }
    
    while (true) {
    
        // ...
    
            if (key.isAcceptable()) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                socketChannel.configureBlocking(false);
    
                // Attach the read state for this socket
                // to its corresponding key.
                socketChannel.register(selector, SelectionKey.OP_READ,
                    new ReadState());
            }
    
            if (key.isReadable()) {
                SocketChannel socketChannel = (SocketChannel) key.channel();
                ReadState state = (ReadState) key.attachment();
                ByteBuffer buffer = state.buffer;
                state.count += socketChannel.read(buffer);
    
                if (state.count >= DATA_LENGTH) {
                    socketChannel.close();
                }
    
                buffer.flip();
    
                // Caution: The speed of this connection will limit your ability
                // to process the remaining selected keys!
                anotherServerChannel.write(buffer);
            }
    

    For a blocking channel, you can just use one write(buffer) call, but as you can see, the use of a blocking channel may limit the advantages of the primary server’s use of non-blocking channels. It may be worth making the connection to the other server a non-blocking channel as well. That will make things more complicated, so I won’t address it here unless you want me to.