Character based I/O using Channels
Character Based I/O Using Channels
By Amanda Waite
(May, 2002)
We want to hear from you! Please send us your
FEEDBACK.
The following article may contain actual software programs in source
code form. This source code is made available for developers to use as needed,
pursuant to the terms and conditions of this license.
Overview
The Channels API is part of the new I/O package introduced in Java[tm] 2 Standard Edition
(J2SE[tm]) version 1.4. It gives us a lot of new functions
and new flexibility for performing I/O operations. ByteBuffers are used as the
target of I/O operations using
Channels. We can read into a ByteBuffer or write from a ByteBuffer, but this
seems to suggest that we can only
use 8-bit binary data in our I/O operations, as a ByteBuffer is used for
storing bytes. How then can we use
the Channels API to perform I/O operations on Character based data?
Specifics
The first thing to note is that the ByteBuffer is a lot more flexible than
the Buffers used for the other
primitive types. Because all of the Java[tm] programming language primitives
(excluding boolean) are one or more bytes in length, the byte can
be considered to be the base unit for representing data of any type. For example an
int can be thought of as four bytes,
a char as two bytes and so on. Given this, if we were to put four bytes into a ByteBuffer those bytes could be seen as
representing an int, or possibly two chars. To facilitate this
the ByteBuffer gives us a lot of supporting methods
that allow us to either put or get data of any primitive type, or even allow us to view a
ByteBuffer instance as another type of
Buffer. So the ByteBuffer class has put and get methods such as
putFloat(), getFloat(), putShort() and
getShort() and view methods such as asCharBuffer(),
asLongBuffer() and so on.
I'll refer generally to the put and get methods as putXXXX() and getXXXX() and the view methods as asXXXXBuffer().
Here's an example using the ByteBuffer's putFloat() method:
static final FLOAT_LENGTH = 4;
Float[] fArray = {2.45, 6.45, 7.99, 2.50, 17.32, 66.66};
ByteBuffer bBuf = ByteBuffer.allocate(fArray.length * FLOAT_LENGTH);
for(int i = 0; i < fArray.length; i++)
bBuf.putFloat(fArray[i]);
Note that the putFloat() method can only take a single float as an argument (this is the same the putXXXX()
methods for all of the other primitive types, as well as for all of the getXXXX() methods); there are no 'bulk' putXXXX()
and getXXXX() methods.
Here's another version of this example, this time using a view Buffer:
static final FLOAT_LENGTH = 4;
Float[] fArray = {2.45, 6.45, 7.99, 2.50, 17.32, 66.66};
ByteBuffer bBuf = ByteBuffer.allocate(fArray.length * FLOAT_LENGTH);
bBuf.asFloatBuffer().put(fArray);
As can be seen from both examples, we have created a ByteBuffer that has
a capacity that is four times greater than the
number of floats in the array. We do this because we know that a
float is four bytes in length, so for every float
in the array we need to have four bytes in the ByteBuffer. The
putFloat() method will write the four bytes that represent
each float into the Buffer at the current position. After each
putFloat() call has completed the position will have been
advanced to the end of the newly written data. For his reason we do not need to keep track
of where we are in the Buffer.
The asFloatBuffer() method will return a FloatBuffer that is
just a view of the ByteBuffer. The underlying data store for
the new FloatBuffer is the same as that of the ByteBuffer. Now
that we have a FloatBuffer instance we can call the put()
and get() methods of the FloatBuffer class as normal. The fact
that the underlying store is a ByteBuffer is handled
automatically.
The result of both examples is the same. We now have a ByteBuffer instance (bBuf) that contains a sequence of bytes
that represent six float values. We can now send the contents of bBuf using an I/O operation on a Channel. Here's an
expanded example that shows methods for sending and receiving a float array using a DatagramChannel:
public void sendFloatArray(float[] fArray, DatagramChannel dChannel, InetSocketAddress isa)
throws IOException {
int numBytes = fArray.length * 4;
ByteBuffer bBuf = ByteBuffer.allocate(numBytes);
bBuf.asFloatBuffer().put(fArray);
bBuf.limit(numBytes);
dChannel.send(bBuf, isa);
}
public float[] receiveFloatArray(DatagramChannel dChannel)
throws IOException {
ByteBuffer bBuf = ByteBuffer.allocate(100); // don't know how much data we are going to receive
float[] fArray = null;
dChannel.receive(bBuf);
bBuf.flip();
if(bBuf.hasRemaining()) {
fArray = new float[bBuf.remaining()];
bBuf.get(fArray);
}
return fArray;
}
sendFloatArray() is fairly straightforward. Note that we have to explicitly
set the limit on bBuf. We need to do this
because the position and limit of a view Buffer are independent of the Buffer that it views. receiveFloatArray() is a
little more complicated. We create a ByteBuffer with a capacity
of 100 (which can hold 25 floats) because in this case we assume that we won't
be sent an array that contains
more than 25 elements. We call receive() on the DatagramChannel and the data is read from
the channel into the new ByteBuffer.
The flip() method is a convenience method that we can call on a Buffer after
we have read data into it and will effectively
mark out the beginning and end of the data inside the Buffer. hasRemaining()
tells us if there is data in the Buffer
and remaining() tells us how much data is remaining. We can use this to size the array that will
be used to hold the data.
The above can be applied to all of the primitive types with the exception of
boolean, but there is a complication
when we come to sending characters. Characters and Strings on the Java platform are stored
as 16-bit Unicode. If we were to use the
method described above to transmit and receive Characters and Strings we could easily run
into problems when communicating
with systems that represent characters in a different way. The standard way to transmit
Character based data is by using
a character encoding. Character encodings greatly simplify the process of sending and
receiving Character based data.
Fortunately for us, the Charset API that was introduced with the new I/O package gives us
the ability to encode a CharBuffer
or a String into a ByteBuffer using an encoding of our choice. The following code
illustrates how we do this:
CharBuffer cBuf = CharBuffer.wrap("Hello World!");
Charset charset = Charset.forName("ISO-8859-1");
ByteBuffer bBuf = charset.encode(cBuf);
This will create a new ByteBuffer from the
CharBuffer 'cBuf' using the "ISO-8859-1" encoding.
We can do this for
any of the encodings supported by the Charset class (for more details of the
charsets supported by your implementation
of the Java platform consult the release documentation). To make things simpler we could
have avoid the use of a CharBuffer and encoded the String directly using:
Charset charset = Charset.forName("ISO-8859-1");
ByteBuffer bBuf = charset.encode("Hello World!");
Of course things wouldn't be complete if there was no way to take a
ByteBuffer and decode it back to a CharBuffer. We can see how
this is done in the following more complete example:
public void sendString(String str, DatagramChannel dChannel, InetSocketAddress isa)
throws IOException {
Charset charset = Charset.forName("ISO-8859-1");
dChannel.send(charset.encode(str), isa);
}
public String receiveString(DatagramChannel dChannel)
throws IOException {
ByteBuffer bBuf = ByteBuffer.allocate(100);
dChannel.receive(bBuf);
bBuf.flip();
Charset charset = Charset.forName("ISO-8859-1");
CharBuffer cBuf = charset.decode(bBuf);
return cBuf.toString();
}
Let's look at the sendString() method. The two lines of code that it
contains look fairly complicated so let's break them down into their individual
statements:
Charset charset = Charset.forName("ISO-8859-1");
ByteBuffer bBuf = charset.encode(str);
dChannel.send(bBuf, isa);
This is effectively the same code but it uses reference variables to store intermediate
state (under normal circumstances an unnecessary overhead).
The first thing that we need to do is to get an instance of a Charset that
represents the encoding that we would like to use to encode our Character based data. We
do this using the static forName() of the java.nio.charset.Charset
class. forName() takes an argument of a String that represents
the name of the encoding that we want to use, in this case "ISO-8859-1", and
returns an instance of Charset. We then encode the String 'str'
using the encode() method of the Charset instance. This returns
a ByteBuffer that is effectively an encoded representation of the
String. This ByteBuffer can now be sent over the
DatagramChannel.
At the other end the process is reversed as seen in the receiveString() method. Once
again we don't know how much data will be sent so we have to make some assumptions about
the capacity of the buffer that will be used to hold the incoming data (in this case we
give it a capacity of 100). We receive the data and then flip() the buffer as we did in
the float array example earlier. We then create a Charset instance using the
"ISO-8859-1" encoding and decode the ByteBuffer into a
CharBuffer using the decode() method of the Charset instance.
In order to demonstrate this more fully we now look at the QuoteServer,
QuoteServerThread and QuoteClient classes that are part of an
application used in the Java[tm] Tutorial to demonstrate how to send character based data
from a
server to a client using a DatagramSocket. We will modify
QuoteServerThread and QuoteClient so that they use a
DatagramChannel to send and receive character data.
This is the same as the QuoteServer class used in the Java Tutorial. The main() method simply creates a new QuoteServerThread instance and starts it.
public class QuoteServer {
public static void main(String[] args) throws IOException {
new QuoteServerThread().start();
}
}
When created the QuoteServerThread opens a DatagramChannel on port 4445. This is the DatagramChannel that the Server uses to communicate with its clients.
public QuoteServerThread() throws IOException {
this("QuoteServer");
}
public QuoteServerThread(String name) throws IOException {
super(name);
dChannel = DatagramChannel.open(4445);
try {
in = new BufferedReader(new FileReader("one-liners.txt"));
} catch (FileNotFoundException e) {
System.err.println("Could not open quote file. Serving time instead.");
}
}
The constructor also opens a BufferReader on a file named
one-liners.txt, we use a BufferedReader and not
a FileChannel for simplicity, as it is actually simpler to read lines from a
file using a FileReader than it would be to use a FileChannel.
The rest of the code is very similar to the original QuoteServerThread code except that we use a DatagramChannel instead of a DatagramSocket and we use the method described in this article to encode the quote (represented by dString) using the "ISO-8859-1" encoding as follows:
Charset charset = Charset.ForName("ISO-8859-1");
ByteBuffer buf = charset.encode(dString);
The QuoteClient class is the client side application for the
QuoteServer. It sends a request to the QuoteServer and waits for
a response. It then displays the response to the standard output. Again, the
QuoteClient class differs only from the version used in the Java Tutorial in
that it uses a DatagramChannel to send a request and to receive the response.
It also uses the method described in this article to decode the response using
the "ISO-8859-1" encoding.
DOC ID #1179
|