Page 2 of 4
The solution seemed pretty obvious. If you'll recall, in my July column I showed how two applets could communicate using data channels. What I needed was a way for HelloWorld to communicate to a virtual terminal. The answer was to make it out of two applets, then apply the previously workable solution: data channels.
A data channel is an interthread communication object that allows one thread to write a series of objects to another object. I was playing around with some virtual terminal applets on the Web (see the Resources section below) and decided what I really needed was an I/O stream based on the DataChannel class. Note that I could have used PipedInputStream and PipedOutputStream, but these are only one-to-one connections. With a data channel I could connect several applications to the same terminal.
The neat thing about object-oriented languages is that you can define an interface, implement it quickly (but stupidly) to check your ideas, and then optimize the implementation for the desired performance goals. Java I/O streams are conceptually very simple things, and to implement one you need only subclass InputStream or OutputStream and then implement a couple of very simple methods. The first implementation of DataChannelOutputStream was as follows:
package util.comm;
import java.io.OutputStream;
import java.io.IOException;
public class DataChannelOutputStream extends OutputStream {
private DataChannel dc;
public DataChannelOutputStream(String chanID) {
dc = DataChannel.getChannel(chanID);
}
public void write(int c) throws IOException {
dc.putValue(new Integer(c));
}
}
As you can see, 12 lines of code is pretty simple. The constructor takes the name of the underlying DataChannel, and the write() method simply spews integers (actually, they are bytes) into the channel. The other side, DataChannelInputStream, is a bit more complicated and shows why things are not as simple as one might hope. I'll dissect it in sections.
package util.comm;
import java.io.InputStream;
import java.io.IOException;
public class DataChannelInputStream extends InputStream
implements Runnable {
private Thread monitor;
private DataChannel dc;
private byte buffer[] = new byte[1024];
int getIndex = 0;
int putIndex = 0;
public DataChannelInputStream(String chanID) {
monitor = new Thread(this);
dc = DataChannel.getChannel(chanID, monitor, 1024);
monitor.start();
}
In the first part of the class, one thing that immediately stands out is that the input stream has to start a separate thread to monitor the data channel. The basic idea is that this monitor thread will watch the channel, pull off data as it comes across, and store it in the array named buffer. The other interesting bit is that in the previous uses of DataChannels the queue was quite small, but in this class we make it much larger. This was an early attempt at overflow management.
private boolean overflow = false;
private synchronized void add(int b) {
buffer[putIndex] = (byte) b;
putIndex = (putIndex + 1) & 1023;
if (putIndex == getIndex)
overflow = true;
notify();
}
public synchronized int read() throws IOException {
int r;
while (getIndex == putIndex)
try { wait(); } catch (InterruptedException e) { }
r = buffer[getIndex];
getIndex = (getIndex + 1) & 1023;
if (overflow)
throw new IOException("Buffer overflowed - Data Lost!");
return r;
}
In the above section we provide the required read method of all extenders of class OutputStream that are not abstract, and we provide a method add that the monitor thread will use to store data in our holding buffer. Both are synchronized so that the read method can block (using the wait() method) when there is no data to read. The buffer is simply a circular ring of 1024 bytes that gets filled as data comes in, and the add method checks to see if data is overwriting data that hasn't been read. If it is, the flag overflow is set and an IOException is thrown when the buffer overflows so that the client can know that data was lost. More on this a bit later.