[java] Questions about Non-Blocking Sockets

Started by
26 comments, last by Son of Cain 18 years, 10 months ago
I'm trying to learn Java's New I/O for asynchronous socket programming, and I would appreciate it if several of my questions were answered: 1. Where are some good tutorials on nonblocking sockets? The ones I've googled are very short and uninstructive - none of them explain the architecture of nonblocking sockets very well. 2. How are attachments used? Can attachments be sent over the network? What are they for? Which objects have attachments (SelectionKey, I know so far). 3. When OP_READ is "selected", how does one know that the complete message was read? In some implementations I've seen it was checked via the "\n" delimiter, but I'm sure that's not the way. 4. How come some tutorials use OP_WRITE while some don't - they write immediately when OP_READ is selected. 5. Can I write a NIO server and write a regular blocking socket-using client? How would that work? Is ByteBuffer compatible with DataInputStream? 6. How do you know if a client has disconnected? I wrote a simplechat server/client, and when I Ctrl-C'd the client, the server started releasing exceptions like mad - something like "client forcibly terminated connection" 7. How do you know if the server has disconnected (from the client standpoint of view) 8. Can you use the same selector for accepting/reading/writing (from a server standpoint) 9. Can you use the same selector for connecting/reading (from a client standpoint) 10. What's the best way of implementing a client in NIO? First, you create the selector/SocketChannel, connect. once a OP_CONNECT comes in, what next? Do I create another SocketChannel? The Java Docs are merely API references, and they don't shed much light on how NIO fits as a whole. Thank you very much, Cipher3D [Edited by - Cipher3D on June 12, 2005 7:34:57 PM]
I eat heart attacks
Advertisement
1) "Good" tutorials don't exist. At least not from what I've seen. I'm sure Sun has stuff online (although probably little or nothing about how to use it all in a real application), but you can try:

http://grexengine.com/sections/externalgames/articles/Adam%20Martin-Java%20NIO%20Networking%20for%20Games-2.html

Or search for the other similar threads on this forum for other links..

2) Attachments? --Do you mean keys?

3) Good question, I believe you keep calling socketChannel.read(buffer) until it returns zero (which means zero bytes read)...(could be wrong). If you mean how do you split up different messages--what I've done is use a delimiter character(s) such as "&&" or whatever. (Need to make sure the actual data can't contain the same characters of course..)

4) Probably because of the fact that the tutorials I've seen suck. I don't know. Even a book I have here doesn't even explain this correctly from what I've seen so far.

5) That's easy -- you just need to call configureBlocking(true) on the SelectableChannel you're using.

6) Good question again-- I believe there's an "undocumented feature" (from what I've read, could be inaccurate)...if a ReadableByteChannel's read() returns less than zero you've reached the "end of stream". (This is probably the case for any type of channel..)

Interface ReadableByteChannel
read(ByteBuffer dst)
returns:
"The number of bytes read, possibly zero, or -1 if the channel has reached end-of-stream "

ex.

int numBytesRead = readableByteChannel.read(inputBuffer);

if( numBytesRead < 0 )
{
//Client has disconnected
}

Of course, it also throws a ClosedChannelException, so you could catch that I suppose. But it seems a little contrary to what exceptions are used for--error handling...to me a client simply disconnecting is hardly an "error".

I still have yet to answer this one completely for myself.

7) Probably a similar method

8/9) I think, probably, but I'm not sure... I do sortof recall seeing a mention of two selectors somewhere.

10) Yay...something else you rarely see tutorials for... :(

"The Java Docs are merely API references, and they don't shed much light on how NIO fits as a whole."

--Yep

I'm no expert on the topic and I'm trying to get answers some of these questions myself (well, I was until I became preoccupied with other stuff). I have "working" network code but it's nothing I'm proud of yet. I even have a book or two on the topic, and one of them has what basically amounts to EVERY other example on the web-- an "echo server". (useless) And the other, I'm not sure about yet, but I don't think it's much better. I agree that the information out there on the subject really blows. Books are helpful in this case, but so far for me not 100% yet.

Maybe we need to find a specific "Game Networking" type book, MAYBE that would help. A very simple working game example would sure as hell be better than the damn "echo server" pieces of crap all over the place. :)
Hi,

I'll try to give in what I've learned so far.

Attachments: They are instances of a class that is used to control how much data has been read into that socket channel. You can use a StringBuilder attachment, and everytime you check the SelectionKey for a reading, you append something to it. A common approach is to create your own Attachment class, with a ByteBuffer to read data from the SocketChannel. Add to this class methods to check when you have complete data on the buffer, so that you can retrieve it and clear the buffer, to start reading into it again;

Selector: Selector is the class that allows you to do multiplexing. So yes, you might need two or more selectors to perform operations separately. Say you have a class that deals with reading data from all your clients - you will need a selector here, and you will accept the connections and register them with for read.

To check for an end-of-stream:
long nbytes = channel.read(myAttachment.getInputBuffer());if (nbytes == -1) {    System.err.println("end-of-stream");    channel.close();}


Now, about reading complete messages, it depends on your implementation. If you're sending ByteBuffers in, you probably know how they were written - meaning, you can have a HEADER for your data. A small example:

// Event Header// int: event type// int: client ID// int: size of your payload// Assume the use of an Event class, that has a write(ByteBuffer bb)// method used to write the Event data on itByteBuffer bb = ByteBuffer.allocate(1024);bb.putInt(evt.getType());bb.putInt(evt.getClientID());int sizePos = writeBuffer.position();bb.putInt(0); // here we set the payload size// write the event on the ByteBuffer// It must return the position of the ByteBuffer after the writting// So that we know what is header info, and what's dataint payloadSize = event.write(bb); bb.putInt(sizePos, payloadSize); // Place the size, after writting the event bb.flip(); // ready for writting (sending) to the server)


Then, on the attachment, you keep looping and checking if you have a full event from your client. Once you have it, you grab an Event instance, and process it on the server-side. Note that both client and server must be aware of the event architecture, so you might want to put that on a small framework of your own.

Hope I have helped. ;)

Son Of Cain

[edit: bad english of mine =]
a.k.a javabeats at yahoo.ca
Thanks a lot for the excellent replies.

It seems that there isn't much on NIO out there. Now for another question:

I'm making a chat network similar to AIM's. Now, of course I won't be dealing with thousands/millions of users, but I would at least like to create a server that's scalable to at least one hundred concurrent users. Would the regular socket implementation - with one thread per client, work very well? Keep in mind I won't have access to first class hardware - my computer, for example is going to be a server.

Is NIO really worth the benefit?

I've written a chat program in NIO (server NIO, client regular sockets) and it's pretty laggy, I don't know why. Simply typing a message to a server located on localhost takes time - a noticeable delay. Where could my code be lagging?
I eat heart attacks
Since you'd end up with potentially 100 threads (although I guess it depends on your implementation, I'm assuming a big client/server thing), I'd say NIO would be worth learning. The need for lots of threads was one of the major shortcomings of the old Java networking code, and NIO eliminates that.

NIO done right should definitely be faster, so something must be wrong. I have NIO server code that works fine with no delays at all--(admittedly half-done thus far but it's mostly just a lack of clean shutdown procedures).

Are you sure you're using nonblocking mode? If you're calling select() with a timeout value that could do what you're describing if the timeout is too long. Or are you using selectNow()? Or maybe you're not keeping connections open, but rather re-connecting every time you send data? (I'm not sure how much the latter would slow things down but establishing connections has a reputation for not being all that fast.)

Or--in the loop that you have which calls select() or selectNow(), do you have other code that takes a long time to execute? (If so, you'd need to split off your networking code into a different thread.) Without seeing some source code snippets that's the best guesses I can come up with. :)
Quote:Original post by Cipher3D
1. Where are some good tutorials on nonblocking sockets?


The quoted article is mine, and there's an update about to be released in the very near future, with the missing extra pages too :)

I hope.

Quote:
3. When OP_READ is "selected", how does one know that the complete message was read?


You have to implement a wire-protocol, such as HTTP. No different from using InputStream's...

Quote:
4. How come some tutorials use OP_WRITE while some don't - they write immediately when OP_READ is selected.


Because the original Sun tutorials are crap, and most people were too lazy to do anything but copy them without understanding WTF they were doing.

Or, like the author of a certain famous NIO book, they wrote books or articles on NIO without ever trying most of it out themselves!

It's a dodgy trick that happens to work - but you shouldn't really do it like that. Look hard enough, and you'll find explanations by sun staff (gooogle) that explain why it works.

Quote:
6. How do you know if a client has disconnected?


Bug in the API: it is supposed to throw the exception, but in the early versions it never did. There was only the undocumented -1 return code. Basically, someone @ sun screwed up and "forgot" to throw the exception.

Later versions tend to throw the exception when they should, and apparently the docs have been updated. It's been a long time since I can recall seeing -1 returned: usually they just throw the exception as expected.

Quote:
8. Can you use the same selector for


Yes; you may get confused about what a particular key is "supposed to be doing" if you mix many different types on one selector - but use the attachment() to store an object which lets you work out what this particular key is "for" (is it for sending data to the client? Is it for accepting an incoming connection? etc)

redmilamber
Alright, thanks for the great replies again!

So let me get this straight. I'm trying to see if I understand the overall picture. The concepts in NIO are a bit foreign to me because I'm used to programming realtime, C++ DirectX games, where I'm in control of everything and I control what happens in the engine. Here, it feels like I'm working passively, waiting for messages to come and then reacting on them, which I guess is how things are done in the "real world". I believe these are called callback mechanisms or something.

Well anyways, here goes:

Server Side


You spawn a thread called Listener. Listener creates a selector and a ServerSocketChannel that registers the interest Ops OP_ACCEPT. Then upon run() it loops indefinitely, with the selector performing its select, and then checking if there are any keys in the set. If there are, then push the channels into a separate stack, say, PendingConnections class.

In the main server class, let's say, Server, it creates ANOTHER selector and ANOTHER ServerSocketChannel that registers the interest OP_READ | OP_WRITE.

If there are things in the set, then it performs some operations on them. The thing is, I'm confused. Let's say I get an OP_READ, that means some person, some SocketChannel is sending me data, right? Well, if OP_READ doesn't guarantee complete messages, how do I know which part of the message I'm receiving? I can determine who's requesting the read, right? That's via the key.channel() operation.

What if I get an OP_WRITE? What does that mean? Does it mean I have to write something? How would I know what to write?

And where would these attachments come in handy?

I know I'm missing something here because I feel like I'm asking pretty stupid questions.

Client Side

Alright, on the client side, I'll create a class called Client that opens up a SocketChannel that tries to connect to
hostname
. Upon completion I will register the socketchannel with a selector with the interestOps OP_READ | OP_WRITE.

Same questions about OP_READ and OP_WRITE, except this time I know I'm only communicating with the server.

Some more questions

Sorry if I'm not "getting it," I'm kind of slow.

I recently read some source code, and they were implementing some funky mechanism where the disabled the read/write interest ops based on whether they received a message or something (to my knowledge), and reenabled them at some other points. I'm confused - why would somebody do that? It seems like they're exerting greater control over what's "interesting" the selector (if that's the correct term for it).

Also, I'll post my SimpleChat example source code for analysis.

Thank you!
I eat heart attacks
Client source: If you have any questions regarding what I *think* I'm doing, then just post.

Chat Class:

[source language="java"]/*author: gamediaceo@yahoo.comdescription: Chat class, allows user to type in input*//*dependencies*/import java.io.*;import java.util.*;//networkimport java.net.*;public class Chat implements Runnable{	//associated thread object	Thread thread;		BufferedReader reader;		DataOutputStream out;		public Chat(DataOutputStream out) throws Exception	{		this.out = out;						reader = new BufferedReader(new InputStreamReader(System.in));				thread = new Thread(this);		thread.start();	}			public void run()	{		while (true)		{			String line;						synchronized (out)			{				try				{					line = reader.readLine();										line = line + "\n";										out.writeBytes(line);				}				catch (Exception ex)				{					ex.printStackTrace();									}				}			}				}	}



Client Class:

[source language="Java"]/*author: gamediaceo@yahoo.comdescription: The Simple Chat Client main class. runs the main loop*//*dependencies*/import java.io.*;import java.util.*;//networkimport java.net.*;public class Client{		Socket socket;		DataOutputStream out;		DataInputStream in;		Chat chat;		//constructor	public Client(String host,int port)	{		System.out.println("Connecting to " + host + ":" + port);				try		{			socket = new Socket(host,port);						out = new DataOutputStream(socket.getOutputStream());						in = new DataInputStream(socket.getInputStream());		}		catch (UnknownHostException ex)		{			System.out.println("Unknown host");						ex.printStackTrace();						System.exit(-1);		}		catch (IOException ex)		{			System.out.println("Couldn't get I/O for " + host);						ex.printStackTrace();						System.exit(-1);		}						if (socket == null)		{			System.out.println("Socket doesn't exist!");						System.exit(-1);			}				if (out == null)		{			System.out.println("OutStream doesn't exist!");						System.exit(-1);		}				if (in == null)		{			System.out.println("InStream doesn't exist!");						System.exit(-1);		}			}		//destructor	public void finalize() throws Exception	{		out.close();				in.close();				socket.close();							}		public void run()	{		String response;				try		{			chat = new Chat(out);		}		catch (Exception ex)		{			ex.printStackTrace();		}				//every time receive a message from the server, print it		while (true)		{			try			{				response = in.readLine();								if (response != null)				{					System.out.println(response);				}			}			catch (IOException ex)			{				ex.printStackTrace();			}						}			}	}



SimpleChat class
[source language="Java"]/*author: gamediaceo@yahoo.comdescription: The Simple Chat Client main class. runs the main loop*//*dependencies*/import java.io.*;import java.util.*;public class SimpleChat{	final static int CONNECT_PORT=42040;		Client client;		public SimpleChat(String host)	{		try		{			client = new Client(host,CONNECT_PORT);		}		catch (Exception ex)		{			ex.printStackTrace();		}	}		public static void main(String args[])	{		SimpleChat simpleChat = new SimpleChat(args[0]);					simpleChat.client.run();		}			}

I eat heart attacks
Server Code:

Server class:

/*author: gamediaceo@yahoo.comdescription: The Simple Chat Server main class. the actual server loop*//*dependencies*/import java.io.*;import java.util.*;//networkimport java.net.*;import java.nio.*;import java.nio.channels.*;import java.nio.channels.spi.*;import java.nio.charset.*;public class Server implements Runnable{	Thread thread;		Selector selector;		ServerSocketChannel serverChannel;		//needed for decoding/encoding	Charset asciiCharset;		CharsetDecoder asciiDecoder;		CharsetEncoder asciiEncoder;		//constructor	//start the listener server	public Server(int listenPort) throws Exception	{		//create the selector		selector = Selector.open();				//create the server socket channel		serverChannel = ServerSocketChannel.open();				//set nonblocking		serverChannel.configureBlocking(false);				//bind		InetSocketAddress address = new InetSocketAddress(listenPort);				serverChannel.socket().bind(address);				//register		serverChannel.register(selector,SelectionKey.OP_ACCEPT);				asciiCharset = Charset.forName("US-ASCII");				asciiDecoder = asciiCharset.newDecoder();				asciiEncoder = asciiCharset.newEncoder();				thread = new Thread(this);				thread.start();	}		//destructor	public void finalize() throws Exception	{		selector.close();					}		//main	public void run()	{		//flag for loop. set to false when the server wants to exit		boolean stillRunning = true;				while (stillRunning)		{			try			{								synchronized (selector)				{					//now read					int numKeys = selector.select();										if (numKeys > 0)					{						readRequests();					}				}			}			catch (Exception ex)			{				ex.printStackTrace();			}					}	}		//read the requests	public void readRequests() throws Exception	{		Set readyKeys = selector.selectedKeys();				for (Iterator i=readyKeys.iterator(); i.hasNext(); )		{			//grab the key			SelectionKey key = (SelectionKey) i.next();						//remove the it			i.remove();						if (key.isAcceptable())			{				registerPendingConnection(key);			}						if (key.isReadable())			{				((ReadWriteCallback)key.attachment()).read();							}					}	}		//establish comm-link with client	protected void registerPendingConnection(SelectionKey key) throws Exception	{		SocketChannel channel = serverChannel.accept();				//set async		channel.configureBlocking(false);					//create a callback		ReadWriteCallback rwCallback = new ReadWriteCallback(channel,this,asciiDecoder);					//register		channel.register(selector,SelectionKey.OP_READ,rwCallback);			}		//retrieve the set of callbacks	public Set getChannelCallbacks()	{		Set channelCallbacks = new HashSet(24);	//24 is the max number of clients				Set keys = selector.keys();				synchronized (keys)		{			for (Iterator i=keys.iterator(); i.hasNext(); )			{				SelectionKey key = (SelectionKey)i.next();								if (key.interestOps() != SelectionKey.OP_ACCEPT)				{					Object attachment = key.attachment();										channelCallbacks.add(attachment);				}			}		}				return channelCallbacks;	}		}



ChannelCallback (base class for callbacks) - I'm not really sure why I'm doing this, but it's loosely based off of something I read online. I think it's to generalize a system of callbacks that are attached, and they act upon specific interests such as read/write:

/*author: gamediaceo@yahoo.comdescription: Called whenever*//*dependencies*/import java.io.*;import java.util.*;//networkimport java.net.*;import java.nio.*;import java.nio.channels.*;import java.nio.channels.spi.*;import java.nio.charset.*;public abstract class ChannelCallback{	//corresponding channel	private SocketChannel channel;		//associated Server object	private Server server = null;		//constructor	public ChannelCallback(SocketChannel channel,Server server)	{		this.channel = channel;				this.server = server;	}		//retrieval functions	public SocketChannel getChannel()	{		return channel;	}		public Server getServer()	{		return server;	}	}



ReadWriteCallback: extending ChannelCallback

/*author: gamediaceo@yahoo.comdescription: Read/Write Callback. Called whenever a socket is ready for reading/writing*//*dependencies*/import java.io.*;import java.util.*;//networkimport java.net.*;import java.nio.*;import java.nio.channels.*;import java.nio.channels.spi.*;import java.nio.charset.*;public class ReadWriteCallback extends ChannelCallback{	final static int BUFFER_SIZE=1024;		//storage for incoming data	StringBuffer incoming;		CharsetDecoder asciiDecoder;		//constructor	public ReadWriteCallback(SocketChannel channel,Server server,CharsetDecoder asciiDecoder)	{		super(channel,server);					incoming = new StringBuffer();				this.asciiDecoder = asciiDecoder;	}			public void read() throws Exception	{		ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);				//get the data in the buffer		int numBytes = getChannel().read(buffer);				//see if it's finished		if (numBytes == -1)		{			getChannel().close();			return;		}				buffer.flip();				//decode		//do the actual decoding		String result = asciiDecoder.decode(buffer).toString();				incoming.append(result);				if (incoming.indexOf("\n") != -1)		{			execute();		}			}		protected void execute() throws Exception	{		//relay to the rest of the clients		Set callbacks = getServer().getChannelCallbacks();					ByteBuffer response = ByteBuffer.wrap(incoming.toString().getBytes("US-ASCII"));				System.out.print(incoming);				for (Iterator i=callbacks.iterator();i.hasNext(); )		{			ChannelCallback callback = (ChannelCallback)i.next();			callback.getChannel().write(response);			response.flip();					}				incoming.setLength(0);				}}



SimpleChat: main
/*name: SimpleChatdesc: main class@author: gamediaceo@yahoo.com*/import java.io.*;import java.util.*;public class SimpleChat{	//the port the server listens on	final static int LISTEN_PORT = 42040;		Server server = null;		public static void main(String[] args)	{		//create a new Simple Chat object		SimpleChat simpleChat = new SimpleChat();			}		//constructor	public SimpleChat()	{		try		{						server = new Server(LISTEN_PORT);					}		catch (Exception ex)		{			ex.printStackTrace();		}			}	}


Thank you very much!
Henry
I eat heart attacks
Oh, I fixed the Server going spaz whenever a client "forcibly disconnected". However whenever I close down the server the client spazzes also. Is it the same way to detect "end of stream" (btw, does that mean lane of communication ended?) with the client? That is, detect if -1 was returned upon read(buffer) and then channel.close()?

Thanks!

EDIT: I was looking at the API docs, and it seems that selectNow is the non-blocking version of select - yet why do tutorials show select being used?

[Edited by - Cipher3D on June 14, 2005 4:46:06 PM]
I eat heart attacks

This topic is closed to new replies.

Advertisement