cool. you got to the nitty gritty of networking.
one or more threads per connection is the “old” way to implement it. new “new” way is using just one single thread to handle all trafic.
NIO is not as hard as it might look on first glance. it’s pretty much this :
new Thread(new Runnable()
{
@Override public void run()
{
try
{
selector = Selector.open();
while(true) // server main loop
{
selector.select(); // block
Iterator<SelectionKey> it;
try
{
it = selector.selectedKeys().iterator();
}
catch(final ClosedSelectorException ex)
{
// handle error
return;
}
while(it.hasNext())
{
final SelectionKey key = it.next();
it.remove();
try
{
// actual work the server does.
if(!key.isValid()) key.cancel(); // broken key
else if(key.isWritable()) write(key); // outgoing data
else if(key.isReadable()) read(key); // incoming data
else if(key.isConnectable()) connect(key); // outgoing connection
else if(key.isAcceptable()) accept(key); // incoming connection
}
catch(final CancelledKeyException ex)
{
// could be ignored
}
}
}
}
catch(IOException e)
{
// handle error
return;
}
}
}).start();
interesting part is that [icode]selector.select()[/icode] blocks until it has something to do. you can poke the selector by [icode]selector.wakeup()[/icode] which will simply unblock it. you might want that if you know you have key to be selected or whatever else is in your main loop.
how to setup a Selector, ServerSocketChannel, SocketChannel is not complicated :
http://docs.oracle.com/javase/8/docs/technotes/guides/io/example/
clients :
SocketChannel clientSocket = SocketChannel.open();
clientSocket.configureBlocking(false);
clientSocket.connect(new InetSocketAddress([...]));
// this will make the server main loop unblock and fall into key.isConnectable()
SelectionKey key = clientSocket.register(selector, SelectionKey.OP_CONNECT);
the twist here is [icode]clientSocket.register()[/icode] has a 3rd argument which is a user-data object. you can attach anything you want to the key. that object can be accessed by the server main loop later, [icode]connect(key)[/icode] in this example. personally i stick the whole socket-abstraction-object in so the server/selector-thread doesn’t starve from missing information.
server is pretty similar :
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.configureBlocking(false);
serverSocket.socket().bind(new InetSocketAddress(port_number));
// allow main loop to fall into key.isAcceptable() if something connects to the port
SelectionKey key = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
the two other SelectionKey options are
[icode]SelectionKey.OP_READ[/icode] - which we want to use with the incoming-socket-channel, after a connection is “accepted”. such connection is not really different to a “outgoing” connection.
private void accept(SelectionKey key) throws IOException
{
SocketChannel socketChannel = ( (ServerSocketChannel)key.channel() ).accept();
// every time the new connection is sending data to us the selector-thread main loop
// will fall into key.isReadable() and may pull the received bytes
socketChannel.register(selector,SelectionKey.OP_READ);
Object attachment = key.attachment();
// twist here is, if this attachment is the server which registered itself with SelectionKey.OP_ACCEPT
// we could notify this server about the new connection here.
}
interesting about this is it means that we can setup as many different servers (listening to different ports) as we want, still using only one thread.
private void read(SelectionKey key) throws IOException
{
SocketChannel socketChannel = (SocketChannel)key.channel();
// up to your "logic"
Object attachment = key.attachment();
if(!socketChannel.isConnected())
{
// error
return;
}
// selector-thread read cache.
// a simple ByteBuffer big enough for a TCP package in this example. ~8kb
readBuffer.clear();
int numRead = socketChannel.read(readBuffer);
if(numRead == -1)
{
// connection closed
key.cancel();
return;
}
readBuffer.flip();
// buffer contains all recieved bytes up to its limit()
// since we do not care what those bytes are, we pass it to the client, the attachment in this example.
attachment.pull(readBuffer);
}
twist here is, the pull method would stall the selector-thread when packed with too much logic. another idea would be to have the attachment/client-abstraction provide the read-buffer - so we could read into it and just notify the “client” and do the work somewhere else.
last one is [icode]SelectionKey.OP_WRITE[/icode] - which i wont explain too much. there is alot of controversy about it. it’s basically used for all outgoing-data when desired to write to sockets from the selector-thread.
the twist is, sending data is utterly thread-safe. the NIC is doing that for us anyway. means, sending data is possible from any thread at any time. i use it only in a few situations when i know i have outgoing-data stalled. using OP_WRITE also mean that we would need to copy all outgoing bytes into some cache/ring-buffer which is purged by the selector-thread - which introduces latency.
o/