Java NIO Strategy

M

mearvk

So basically if I want to demux concurrent (multiple overlapping
readFromSocketChannel calls) incoming flows on my client, the best way
is to do it is to create multiple SocketChannels initially? I wouldn't
be against this but I want a sanity check on this thinking...

To illustrate more clearly:

The client requests 3 files from the server using a single
SocketChannel. These files are large and therefore require several
trips each to readFromSocketChannel. Now since the SocketChannel has
only one associated SelectionKey how do I match the bytes to the proper
ByteBuffers (and eventually to their File objects)? Unless I preface
each Server write with a header like (or some such scheme):

"RequestID:123456789"

and use these to reference into a Vector<ByteBuffer> (starting to get
complicated!) how am I to demux on the client sans multiple
SocketChannels?

Thanks,

Mearvk
 
M

mearvk

Does this make sense? If you explicitly tell your program to re-insert
the SelectionKey back into the key set and we know it has more data to
be read (per the premise), then unless your server is completely
pegged, wouldn't it *always* return to a ready-operation state?


Only if more data arrives.

So calling interestOps 'resets' the SocketChannel's awareness of its
buffer? I'm a bit confused on this point. If we omit the interestOps
call do we have a working system? Putting a partially-read
SocketChannel back into the Selector's awareness *should* cause the
Selector.select() call to return at least 1, even without new data
arriving, should it not?

(b) When I've had enough read events and read enough data to constitute

a complete request (an interesting problem in itself) I deregister for
OP_READ and pass the request off for processing. If I get EOF instead I

close the channel, physically and logically.


(c) When I get the response back from wherever it was processed I
attempt a write. If this doesn't succeed completely I register
OP_WRITE.
Any time I get OP_WRITE and the write succeeds completely I
deregister
OP_WRITE and register OP_READ.

So basically what I'd like to know is how you actually implemented
this? If you implement it in the readFromSocketChannel call, then your
whole program blocks while waiting for the processing call to return,
right? So do you start a new thread and use a notification system, or
what?

Anyways, thanks again.


Mearvk
 
E

EJP

mearvk said:
So basically if I want to demux concurrent (multiple overlapping
readFromSocketChannel calls) incoming flows on my client, the best way
is to do it is to create multiple SocketChannels initially? I wouldn't
be against this but I want a sanity check on this thinking...

To illustrate more clearly:

The client requests 3 files from the server using a single
SocketChannel. These files are large and therefore require several
trips each to readFromSocketChannel. Now since the SocketChannel has
only one associated SelectionKey how do I match the bytes to the proper
ByteBuffers (and eventually to their File objects)? Unless I preface
each Server write with a header like (or some such scheme):

"RequestID:123456789"

Yep, that's what you would have to do. You'll also have to write length
words into each chunk that you transmit so you can pick apart the
responses at the receiving end.

I would definitely do this via multiple sockets myself.
 
E

EJP

mearvk said:
So calling interestOps 'resets' the SocketChannel's awareness of its
buffer?

No. I was assuming a complete read of the available data. I should have
said something like 'only if and when more data is or becomes available
in the socket receive buffer'.

However the situation we are both quoting from in the blog is completely
meaningless because you *can't* 'know it has more data to be read'.
I'm a bit confused on this point. If we omit the interestOps
call do we have a working system? Putting a partially-read
SocketChannel back into the Selector's awareness *should* cause the
Selector.select() call to return at least 1, even without new data
arriving, should it not?

Yes, putting a partially-read socket channel back into the Selector's
awareness does indeed cause it to be selected again. But so would
leaving it alone, unless the first thing the read code does is
deregister the key for OP_READ - but why would it do that?

The entire situation is bogus. You accept a connection; you register it
for OP_READ. OP_READ fires for the channel; you do a read; you get a
read count; you look at the buffer. If it now contains a complete
request you process it. Otherwise you just return to the
ready-key-processing loop, process more keys, and eventually you return
to the select(); eventually, or immediately, or never, it fires another
OP_READ for this channel, so you do another read, appending to the same
buffer.

You don't have to register/re-register for this second OP_READ because
you're *already* registered for OP_READ: that's why you did the *first*
read.

It's all much simpler than presented in that futile blog. *When you have
the complete request* is the time to deregister for OP_READ. The less
you fiddle with things unnecessarily the better, surely?
So basically what I'd like to know is how you actually implemented
this? If you implement it in the readFromSocketChannel call, then your
whole program blocks while waiting for the processing call to return,
right?

Of course not. That would be just as ridiculous as the original
suggestion to use a second selector and block eveybody else on that. NIO
is for concurrency and scalability, and blocking on anything except a
select() is strictly out.
So do you start a new thread and use a notification system ...?

Of course. In systems where executing the transaction can block (e.g.
because of database contention), you have worker threads and all that
jazz. However I do have one system where my server is the only user of
the database and it all fits into memory, so I just do the database
operations in line in the selecting thread. This was kind of forced on
me and not my ideal choice, but in practice it seems to work well enough.
 
E

EJP

To save further discussion I'll deal with this blog here in its entirety.
> if ((key.readyOps() & SelectionKey.OP_ACCEPT) ==
> SelectionKey.OP_ACCEPT){
> handleAccept(key);
> } else if ((key.readyOps() & SelectionKey.OP_READ) ==
> SelectionKey.OP_READ) {
> handleRead(key);
> }
> Next in handleRead(key), you will do:
>
> socketChannel = (SocketChannel)key.channel();
> while ( socketChannel.isOpen() &&
> (count = socketChannel.read(byteBuffer))> -1)){
> // do something
> }

So far so good. Obviously the channel must be registered for OP_READ
otherwise we would never have got here.
> Well, the scary part is the // do something.
> Gold Candidate for a Memory Leak (GCML)
> At this stage, socketChannel is ready to read bytes. Hence you invoke
> socketChannel.read(byteBuffer), and you find that you haven't read
all > the bytes from the socket

How can you possibly find that? or does he mean that you find you
haven't read the entire request?
> so you decide to register the SelectionKey back to the Selector by
> doing:
> selectionKey.interestOps(
> selectionKey.interestOps() | SelectionKey.OP_READ);

What for? OP_READ IS ALREADY REGISTERED! How else did we get here?
> and...and...and do something like:
>
> selectionKey.attach(...)
>
> Boum...the little ... is where the devil is hiding!

etc etc etc, all the stuff about how selection keys with attachments
that never fire events are memory leaks. Well, such a selection key is
*itself* a memory leak, and you have to scan for that case anyway. So
when you scan the selector's keyset for idle keys, you've just solved
the entire imaginary problem.

You also have to maintain session context anyway, and you have to hold
this somewhere, and the selection-key attachment is as good a place as
any. Better IMHO.
> How do I retrieve the SocketChannel if I don't attach it to my
framework object.

A non-problem. SelectionKey.channel().
> How do I deal with incomplete socketChannel read.
> When you do socketChannel.read(), you can never predict when all
bytes > are read from the socket buffer. Most of the time, you will have to
> register the SelectionKey back to the Selector, waiting for more bytes
> to be available.

Why? It's already registered.
> In that case, you will most probably attach the incomplete ByteBuffer
> to the SelectionKey, and continue adding bytes to it once the
> SelectionKey is ready.

I would already have attached a session object when I accepted the
connection, and the ByteBuffer would be part of that. Possibly two, if I
want to maintain a read buffer and a write buffer.
> Instead, I would recommend you register the
> SelectionKey to a temporary Selector (I will blog about this trick in
> more details):
>
> try{
> SocketChannel socketChannel = (SocketChannel)key.channel();
> while (count > 0){
> count = socketChannel.read(byteBuffer);
> }
>
> if ( byteRead == 0 ){
> readSelector = SelectorFactory.getSelector();
> tmpKey = socketChannel
> .register(readSelector,SelectionKey.OP_READ);
> tmpKey.interestOps(tmpKey.interestOps() |
SelectionKey.OP_READ);

The last line is redundant.
> int code = readSelector.select(readTimeout);

At this point the entire application, with its 10,000 connections whose
liveness we are so concerned about, has been stalled for up to 15
seconds waiting for more data from just one channel. This suggestion is
just plain stupid.
> tmpKey.interestOps(
> tmpKey.interestOps() & (~SelectionKey.OP_READ));

What's this for?

He doesn't show why this entire extra Selector isn't a memory leak in
itself. I suppose the selector factory is doing something about that, in
which case this line might be important. Or not. I can imagine the
selector factory taking care of that itself actually.

But I certainly wouldn't be doing any of this.
> With this trick, you don't need to attach anything to the SelectionKey.

With this trick you have just lost all concurrency and scalability. Your
server has just dedicated itself to a single client for 15 seconds.
> So here you gonna need to decide based on your use of NIO: do you
want a dormant ByteBuffer attached to a SelectionKey or a Thread
blocking for readTimeout.

Clearly a non-decision. We are using NIO because we want scalability.
> In Grizzly, both approaches can be configured, but by default the
thread will block for 15 seconds and cancel the SelectionKey if the
client isn't doing anything.

Remind me to avoid Grizzly in that case.
> You can configure Grizzly to attach the ByteBuffer if you really like
to use memory :) . We did try on slow network, with broken client,
etc., and blocking a Thread scale better than having a dormant ByteBuffer,

This claim is not credible.
> [he then goes on to describe an architecture where you start more
threads to handle these incomplete reads]

Once again losing scability in favour of more threads depending on load.
> Wow that one was very long. Agree, disagree?.....

I disagree completely. NIO is just not that complicated to use well.
Considering this effort comes from within Sun it is really quite
shocking that it exhibits little if any understanding of what NIO really
does or how to use it.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top