Java meets Flash
Persistent TCP/IP Socket Connections
with Variable-Length Messages. ie,
Tuning Java for Serving a Flash 5 XMLSocket Client.
Macromedia introduced an interesting convention for
persistent sockets with its Flash 5 XMLSocket object. Once the client
connects to a server, asynchronous bidirectional communication begins
(meaning, both client and server may "talk" freely, even at the same
time).
There are some simple guidelines. Small XML documents of variable
length are passed back and forth between client and server. XML is
the assumed message format, so the ?xml? header is skipped. The EOM
(end of message) marker is a single byte with a byte value of zero. I
think they chose this because that's how you mark the end of a String
in C and C++.
Flash clients have long been able to speak http, but http can get
inefficient and slow. This is because it follows a request-response
model. In request-response, an http client always speaks first
(making a request) and then hands over the microphone to the server.
In http, once the server is done responding, it closes the connection.
Any additional exchange must happen in a new connection.
There can be a lot of overhead establishing each connection, so
keeping the line open is a very attractive new option. This is
especially true in applications where latency is an issue, like games
and chat.
On the Flash side, much of the dirty work in the XMLSocket
protocol is handled behind the scenes. This includes buffering the
data received from the server and firing off a message handler when
each EOM delimiter is encountered.
On the server side, you may have to get your hands dirty teaching
Java about this new protocol. I'll assume you're using Flash for the
client and Java on the server. Java on the server makes a lot of
sense because it has mature, easy-to-use threading and socket APIs.
From the simplest chat server to the hairiest multi-user game server,
Java can get the job done quite elegantly. Try out my
multiplayer blackjack on www.dagnammit.com for a simple
example. Flash on the client, Java on the server.
Here are some of the pitfalls I encountered while implementing the
protocol on the server with the java.net.Socket class:
(1) This protocol involves frequent transfer of very short
messages. The default Socket object has a built-in delayed-send
feature that is tuned to work great for large file transfers. Turns out, it really sucks
for this short message protocol! Make sure as soon as your
socket is created that you turn this off:
Socket sock = serverSock.accept();
sock.setTcpNoDelay(true);
Otherwise you will see long delays between when you flush your
socket's output stream and when the socket actually decides to send
the bytes.
(2) There is a curious bug in the following code (part of a thread
which stores socket input and reacts to incoming messages):
...
InputStream in = sock.getInputStream();
StringBuffer soFar = new StringBuffer();
byte[] buf = new byte[1024];
boolean isConnected = true;
// loop forever...
while(isConnected) {
// play nice with the other threads and surrender the CPU
Thread.sleep(20);
// collect all the bytes waiting on the input stream
int avail = in.available();
while (avail > 0) {
int amt = avail;
if (amt > buf.length) amt = buf.length;
// THE WRONG WAY see below for the right way
in.read(buf, 0, amt);
int marker = 0;
for (int i=0; i<amt; i++) {
// scan for the zero-byte EOM delimiter
if (buf[i] == (byte)0) {
String tmp = new String(buf, marker, i - marker);
soFar.append(tmp);
handleMessage( soFar.toString() );
soFar.setLength(0);
marker = i + 1;
}
}
if (marker < amt) {
// save all so far, still waiting for the final EOM
soFar.append( new String(buf, marker, amt-marker) );
}
avail = in.available();
}
}
// mop up...
...
When the input stream reports how many bytes are available to read
"without blocking," I expected the call to in.read() to grab exactly
that many bytes. Most of the time it does grab the number of advertised bytes. However, I noticed every once in a while a message would get mangled.
I finally pinpointed the problem and realized that in.read() often
kicks out after reading less (sometimes considerably less) than what .available() reported.
Fortunately, in.read() returns the number of bytes actually grabbed.
The above code is easily amended to work around my incorrect
assumption. Just change this line:
in.read(buf, 0, amt);
to this:
amt = in.read(buf, 0, amt);
One last caveat -- if you plan on having 20+ simultaneous connections
you will need to consider some form of thread pooling. Rather than
creating a new thread for each new socket, I try to assign multiple
sockets to a single event-delegation thread, in some ratio that won't
noticeably affect performance. This thread periodically wakes up and
cycles through all of its sockets, checking for any incoming messages
on each one in turn before going back to sleep.
Each thread consumes considerable system resources, so reducing the
total number of threads in this way can make your server app a lot
more scalable.
(3) You will no doubt manage multiple sockets in your server app.
Much of the server code will deal with broadcasting information to all
sockets or from one client to another. Here is some sample
Java code for sending a string containing an XML-formatted message. Key
elements are sending the EOM (end of message) marker after each
message and flushing the output stream in case it is buffered.
private void sendXml(String xml, Socket sock)
throws java.io.IOException
{
if (xml == null) return;
// make sure other threads aren't already writing to this socket
synchronized (sock) {
OutputStream out = sock.getOutputStream();
// if your app passes very large messages around you may
// want to add buffering here since otherwise this will
// allocate many large byte arrays.
out.write( xml.getBytes() );
out.write( (byte)0 );
out.flush();
}
}
You will want to make sure that this is the only bit of code that
manipulates the OutputStream of your sockets. By forcing all
outgoing communication for a particular socket through this
thread-synchronized point, you can avoid one thread stepping on
another's toes and mangling both messages.
For example: When thread A hits the synchronized block, it finds no
"lock" on the sock object, places its own lock and proceeds. While it
is writing, thread B comes along and wants to write something to the
same socket (for example, a ping thread that sends a <PING />
message every ten seconds to ensure the connection is still good).
Thread B reaches the synchronized block and discovers thread A's lock.
Thread B queues up and must block until thread A exits the
synchronized section and releases the lock.
Mangle prevented.
That's all for now! Happy coding...
-tom
Tom J. McClure
Top Dog
Black Dog Hosting
|