|
by Michael Powers
January, 2004
Download the project code
The market for TCP/IP-based socket applications expands with every
new MIDP 2.0 device that is deployed on an advanced network. This is in
addition to the MIDP 1.0 devices that already either fully support the
optional socket protocols, like the Motorola/Nextel iDEN phones, or at
least unofficially support them in half-duplex, like the Nokia Series
60 and other Symbian devices. As a result, enterprise-minded developers
are showing increasing interest in the platform as they discover it's
good for more than just games.
In this article you and I will explore the socket support in the
Generic Connection Framework (GCF) by building a small and simple
terminal emulator. Right now is a good time for you to download the project code.
Our terminal emulator will implement the telnet protocol.
Text-based and command-oriented, telnet is one of the Internet's
architectural backbones and is used widely in education, research, and
corporate environments for legacy applications and system
administration. That kind of functionality is very nice to have in a
mobile form.
We will first implement telnet in a transparent and GCF-friendly
wrapper, then write a custom canvas for displaying terminal content,
and finally tie it all together in a MIDlet. In the spirit of "get it
working, then get it right," we're going to get a very basic (or
"dumb") terminal working, and that will serve as the foundation for a
more sophisticated terminal later on. Along the way, I'll highlight the
issues and constraints of which MIDP programmers should be aware when
writing applications in general and networked applications in
particular.
Telnet: What Is It Good For?
The telnet protocol is a set of rules for communicating over a
two-way network connection. Mixed with the normal content going back
and forth are special telnet commands that allow the two ends
of the connection to negotiate and agree on which rules will be
followed. These commands are stripped from incoming data so the
applications using the connection never have to know about them.
Telnet predates the modern Internet. J. Postel and J. Reynolds defined the protocol more than twenty years ago in RFC 854,
which is worth a read. Up to that time, "terminal" meant a screen and
keyboard that you connected to a massive computer with a serial cable.
You had to be in the same building as the computer to work with it.
Telnet allowed you to plug your screen and keyboard into the network,
and do your work from anywhere on the Internet.
Telnet enables you to control any operating system that has a
command-line interface. In UNIX environments you get complete control
over the machine, including the ability to start and stop processes and
even shut down and restart. In fact, most UNIX software assumes the
user is at a terminal. Before the advent of the World Wide Web, telnet
also provided the kinds of information and services that we now call
web services, and some of these telnet services are still available,
like the Weather Underground. Today, though, telnet is used primarily for remote access to distant computing resources.
Almost nobody uses an old-fashioned terminal anymore: on desktop
computers, on intelligent workstations, and increasingly on mobile
devices we run software programs that pretend to be terminals, called
"virtual terminals" or "terminal emulators." That's what we're going to
build.
Implementing Telnet
The place to start is with the InputStream. With the GCF, you obtain a SocketConnection from the Connector with code like Connector.open("socket://myhost:23"). Then from the resulting Connection you call openInputStream() to get an InputStream and start reading from the socket.
To implement telnet, we need to watch this stream for commands and
process them, stripping them out so that the rest of the application
never sees them. We'll do so by creating our own subclass of InputStream that will wrap the stream we obtain from the SocketConnection. We'll also create our own subclass of OutputStream
to mark any data sent by our application that looks like a telnet
command but should not be treated as such by the remote end of the
connection.
In other words, our application will simply talk to the input and
output streams as usual, and behind the scenes our terminal emulator
will handle the handshaking and negotiation so the application doesn't
have to worry about it.
Our telnet emulator must follow a characteristic procedure:
We read a byte from the input. If it's any value except 255, we just pass the byte through to the application and read on.
If the byte's value is 255, then we read the next byte to see if it's a command. In the telnet protocol 255 is known as IAC, for "Interpret As Command."
If the second byte is also 255, then the server actually intended to
send the value 255, not a command. In this case, we just pass the 255
through to the application and read on. In effect, the sender "escapes"
the value 255 by sending two in a row.
If the second byte is not 255, it is a command. For most commands we just do what we're told and read on.
Certain commands – SB (250), WILL (251), WONT (252), DO (253), and DONT (254) – are negotiation commands. Each is followed by a third byte, the option, and we read that byte to get the option code. If the second byte is any negotiation command but SB, we take the action indicated by the command and the option, and read on.
An SB command triggers a sub-negotiation. After the option byte we read additional data until we encounter an IAC followed by an SE.
What's in those bytes depends on the option being negotiated. We take
the action specified by the option, using the extra information
supplied, and read on.
Because clients are free to decide which of the established telnet
options they will implement, a negotiation must take place between
client and server to determine which options both sides support.
Negotiation is a simple exchange of commands. One side opens a negotiation with either a WILL or a DO command, depending on which party is to perform the specified option:
WILL suggests an option that the first side is able and willing to perform. The other side replies DO if the first side should start performing that option, or DONT if the option is not understood or not supported.
DO tells the other side to start performing an option. The other side replies WILL if it started performing the option or WONT if it doesn't understand or support the option.
Our minimal client will handle only four commands, WILL, WONT, DO, and DONT, and two options, TERMINAL_TYPE (24) and NAWS ("Negotiate About Window Size," 31). For all other options we will return DONT or WONT. (I gave strong consideration to supporting the RANDOMLY_LOSE_DATA (256) and SUBLIMINAL_MESSAGE (257) options, but we're trying to keep it simple.)
The Telnet Input Stream
To see the protocol expressed in code, take a look at the listing for TelnetInputStream.java. All of the excitement is in the read() method. In this excerpt, I eliminate some of the detail:
public int read() throws IOException
{
byte b;
b = (byte) input.read();
if ( b != IAC ) return b; // not an IAC, skip.
b = (byte) input.read();
if ( b == IAC ) return b; // two IACs isn't.
if ( b != SB ) // handle command
{
switch ( b )
{
// basic commands
case GA:
case NOP:
case DAT:
case BRK:
case IP:
case AO:
case AYT:
case EC:
case EL:
// not implemented: ignore for now
System.err.println(
"Ignored command: "
+ b + " : " + reply[2] );
return read();
// option prefixes
case DO:
case DONT:
case WILL:
case WONT:
// read next byte to determine option
reply[2] = (byte) input.read();
switch ( reply[2] )
{
case TERMINAL_TYPE:
...
case WINDOW_SIZE:
...
default:
// unsupported option: break
...
}
break;
default:
// unsupported option: suppress and exit
System.err.println(
"Unsupported command: " + b );
}
}
else // handle begin-sub
{
b = (byte) input.read();
reply[2] = b;
switch ( b )
{
case TERMINAL_TYPE:
...
default:
reply[1] = WONT;
write( reply );
}
}
return read();
}
|
Because we ignore all but negotiation commands and most options, the interesting part begins when we receive a DO command for TERMINAL_TYPE or NAWS.
In the case of TERMINAL_TYPE, we're trying to agree on
what kind of classic terminal we and the other side both support. Some
terminal-based applications will take advantage of the more advanced
terminal types' features, like colors, bolding, and underlining. More
sophisticated terminal emulators will emulate several kinds of
terminal, like ansi, vt100, and vt102. The negotiation tries to establish the best common denominator. The simplest kind of terminal is known affectionately as a dumb terminal, and that's the one we'll support.
If we receive IAC DO TERMINAL_TYPE, we respond IAC WILL TERMINAL_TYPE. According to the RFC 1091 specification, the remote host then starts a sub-negotiation with IAC SB TERMINAL_TYPE TERMINAL_SEND IAC SE, and we then respond IAC SB TERMINAL_TYPE TERMINAL_IS d u m b IAC SE.
...
case TERMINAL_TYPE:
...
reply[1] = SB;
write( reply );
char[] c = terminal.toCharArray();
byte[] bytes = new byte[c.length+3];
int i = 0;
bytes[i++] = TERMINAL_IS;
for ( ; i < c.length+1; i++ )
{
bytes[i] = (byte) c[i-1];
}
bytes[i++] = IAC;
bytes[i++] = SE;
write( bytes );
break;
...
|
Note that because telnet assumes 8-bit ASCII characters and Java's
characters are 16-bit Unicode, we must take care to convert each
Unicode character of our terminal-type string into an ASCII byte before
sending. This conversion is necessary in almost all internet protocols,
especially the early ones. We could have specified an ASCII encoding in
the call to String.getBytes(), but it's such a simple
transformation we're doing it inline. While the MIDP specification
mandates what character encodings are supported, the various
implementors sometimes get it wrong, and it's better to be safe than
sorry.
In the case of Negotiate About Window Size (NAWS),
we're simply notifying the remote host of the dimensions of the screen.
Terminals are expected to use monospaced fonts, so the screen is a grid
of characters with a fixed number of rows and columns. If we know our
screen dimensions and the server supports the NAWS option, we can send the size of our screen when we connect, and again later if the screen size changes.
As the RFC 1073 specification recommends, when we receive IAC DO NAWS, we respond IAC WILL NAWS
and then we immediately send our current screen dimensions. Because
screen heights and widths of more than 255 are sometimes desired, the
width and height are each sent as two-byte integers: first the
high-order byte, then the low-order byte. Thus we send IAC SB NAWS width-high-byte width-low-byte height-high-byte height-low-byte IAC SE. The code will look like this:
...
case WINDOW_SIZE:
// do allow and reply with window size
if ( b == DO && width > 0 && height > 0 )
{
reply[1] = WILL;
write( reply );
reply[1] = SB;
write( reply );
byte[] bytes = new byte[6];
bytes[0] = (byte) (width >> 8);
bytes[1] = (byte) (width & 0xff);
bytes[2] = (byte) (height >> 8);
bytes[3] = (byte) (height & 0xff);
bytes[4] = IAC;
bytes[5] = SE;
write( bytes );
break;
}
...
|
We can't use this code until we write our user interface, and know the width and height of the window. For now, we simply send IAC WONT NAWS.
Now a word on usability: Our class would be neat and clean if all it needed to do was read from an InputStream. We need to send data back to the server, however, so we require an OutputStream
for writing. Because we support the terminal-type and window-size
options, we also need all of this information passed to our
constructor. For convenience, there is a second constructor that takes
only the input and output streams, defaulting the terminal type to
"dumb" and the window height and width to zero.
The Telnet Output Stream
By comparison with our telnet input stream, TelnetOutputStream
is a piece of cake. Remember that, while our application is free to
write 255 just as it would any other value, the telnet server on the
remote host would try to interpret it as an IAC and then
look for a command code. Our output stream's sole responsibility then
is to watch for bytes with value 255 as they are written, and escape
them with an additional 255.
In this excerpt from TelnetOutputStream.java you can see that this task is just as easy as it sounds:
...
private OutputStream output;
private final static byte[] ESCAPED =
{ (byte) 255, (byte) 255 };
public TelnetOutputStream( OutputStream inOutput )
{
output = inOutput;
}
public void write( int b ) throws IOException
{
if ( b == 255 )
{
output.write( ESCAPED );
}
else
{
output.write( b );
}
}
...
|
We allocate a static final byte array containing the two bytes, both
to avoid any overhead with sending bytes one at a time and to avoid any
overhead that might result from allocating the array on demand. The fun
thing about MIDP development (as opposed to, say, Swing development) is
that these details can actually matter.
Telnet Connection
Because our TelnetOutputStream requires an OutputStream, and our TelnetInputStream requires both an InputStream and an OutputStream that's separate from TelnetOutputStream,
there's a lot to remember when setting up a telnet session. Because
MIDP programmers are used to working with the GCF, it makes good
object-oriented sense to wrap up our classes in a more user-friendly
package, abstracting the complexity and conforming to a familiar usage
pattern. TelnetConnection.java shows you how. All you have to do is pass your StreamConnection to the constructor, so establishing a telnet session looks like this:
...
StreamConnection connection;
connection = (StreamConnection)
Connector.open("socket://wunderground.com:3000" ),
Connector.READ_WRITE, true );
connection = new TelnetConnection( connection );
InputStream input = connection.openInputStream();
OutputStream output = connection.openOutputStream();
...
|
TelnetConnection implements all the methods of the StreamConnection interface, simply calling to the wrapped StreamConnection and creating our custom streams on demand. Because there's no harm in closing Connections more than once, we also implement close() to call close() on the wrapped StreamConnection.
Remember that connections don't actually close until their input and
output streams have both been closed, so you should take care to track
your streams and close them explicitly. Whether you close them before
or after you close the connection makes no difference.
Asking the Connector for a socket://-based connection returns a StreamConnection and that's what you should pass to the TelnetConnection. When using telnet, it's good practice to tell the Connector you intend to read and write to it, by passing the READ_WRITE flag as the second optional argument to Connector.open().
Even if you intend only to read from the stream, the telnet negotiation
will require you to write data back to the connection. Furthermore, you
should also specify the third optional parameter, to indicate you want
the framework to throw exceptions if the network connection times out;
that is, it fails to receive a response within a certain interval.
Because the kinds of mobile devices that implement MIDP have
intermittent networking at best, you'll want to handle network failures
gracefully, catching any exceptions and notifying the user that the
connection has been lost.
Telnet Canvas
Now that we have our network infrastructure in place, we need to
provide a user interface. In the spirit of modularity, this user
interface will make no assumptions about the telnet connection or even
the existence of a network connection at all. It will simply accept
bytes and write them to the screen.
While we could use a Form and append StringItems or even our own CustomItems to the Form as input arrives, that would be clearly contrary to the way Forms are intended to be used. Moreover, the variety of implementations of Form
across the many MIDP devices means that the user experience would vary
widely and in most cases would not work as we expect. To have full
control over the user experience, including the ability to tailor our
output to the dimensions of the screen and specify the displayed fonts,
we will create our own custom subclass of Canvas.
Using our TelnetCanvas is easy: just create it, put it on screen, and feed it ASCII bytes by calling receive().
...
TelnetCanvas canvas = new TelnetCanvas();
Display.getDisplay(this).setCurrent( canvas );
canvas.receive( "Hello World!\n" );
...
|
The implementation is more interesting. Let's start with the constructor in TelnetCanvas.java:
public TelnetCanvas()
{
int width = getWidth();
int height = getHeight();
// get font and metrics
font = Font.getFont( Font.FACE_MONOSPACE,
Font.STYLE_PLAIN, Font.SIZE_SMALL );
fontHeight = (short) font.getHeight();
fontWidth = (short) font.stringWidth( "w" );
// calculate how many rows and columns we display
columns = (short) ( width / fontWidth );
rows = (short) ( height / fontHeight );
// divide extra space evenly around edges of screen
insetX = (short) ( ( width - columns*fontWidth ) / 2 );
insetY = (short) ( ( height - rows*fontHeight ) / 2 );
// initialize state: start with 4 screens of buffer
buffer = new byte[rows*columns*4];
cursor = 0;
...
}
|
In addition to initializing our variables, we tailor ourselves to
the device at runtime, as all good MIDlets should. Terminals by
tradition use a monospaced font, so we request the smallest one and
measure the height and width to see how many characters we can fit onto
the screen.
We want to avoid discarding any input that we receive, so initially
we create a buffer big enough to hold four screens of data. This size
is an arbitrary judgment call; we want the buffer to be small enough to
fit into memory, yet large enough that we don't have to reallocate it
constantly for larger inputs.
The amount of memory available to MIDlets varies greatly across
different devices from different manufacturers, so you should always
keep an eye on memory consumption. Because we're displaying 8-bit ASCII
characters, it doesn't make sense to use a StringBuffer or even a character array to store our content. The standard Java practice of allocating an int for any kind of numeric value can be overkill in the MIDP world. A byte array is all we need, and it takes up only half as much space as a char array, a quarter as much as an int array.
The downside, however, is that we need to grow our array and manage
memory allocation manually, which can be tricky business. Whenever we
receive input, we check to see whether the buffer is nearing full. If
it is, we attempt to grow our buffer as in this snippet:
public void receive( byte b )
{
...
// grow buffer as needed
if ( cursor + columns > buffer.length )
{
try
{
// expand by sixteen screenfuls at a time
byte[] tmp =
new byte[ buffer.length + rows*columns*16 ];
System.arraycopy(
buffer, 0, tmp, 0, buffer.length );
buffer = tmp;
}
catch ( OutOfMemoryError e )
{
// no more memory to grow:
// just clear half and reuse the existing buffer
System.err.println(
"Could not allocate buffer larger than: "
+ buffer.length );
int i, half = buffer.length / 2;
for ( i = 0; i < half; i++ )
buffer[i] = buffer[i+half];
for ( i = half; i < buffer.length; i++ )
buffer[i] = 0;
...
}
}
...
}
|
We keep growing the buffer by an arbitrary amount as needed, and if
we run out of memory, we clear half the existing buffer and reuse it.
In MIDP development, it's a good idea to follow this pattern whenever
you use the new keyword to allocate an object of non-trivial size: test for OutOfMemoryErrors and have a backup plan so you can fail gracefully.
Because you know the number of rows and columns, you might be
tempted to create a two-dimensional array of bytes to hold the screen
data. Resist that temptation. Such a structure consumes more memory
that a single one-dimensional array containing the same number of
bytes, because it's actually an array of arrays, and each array has
some overhead. Performance is also worse because the runtime must
perform a bounds check for each indexed access on an array, and our paint() routine would make many such accesses. On slower devices you can actually see the difference.
For these reasons, you should generally collapse multi-dimensional
data into a single array and calculate the offsets into the array
yourself. Calculating offsets is easier than it sounds, as you can see
in the second half of the receive() method (the code that writes the values into the buffer) and in the paint() method later in the code:
...
switch ( b )
{
case 8: // backspace
cursor--;
break;
case 10: // line feed
cursor = cursor + columns - ( cursor % columns );
break;
case 13: // carriage return
cursor = cursor - ( cursor % columns );
break;
default:
if ( b > 31 )
{
// only show visible characters
buffer[cursor++] = b;
}
// ignore all others
}
...
repaint();
...
|
In a dumb terminal the only formatting codes we need to respect are
backspaces, line feeds, and carriage returns. To advance a single row
(a line feed), we add the number of columns to our insertion index,
which we call a cursor. To back up to the beginning of a row (a
carriage return), we back up until our insertion index falls on a
multiple of the number of columns. Our implementation of line feed also
performs a carriage return, which after years of debate
is now the more or less standard way of doing things. Everything else
is either a visible character to be placed in the buffer at the
insertion point, or ignored.
The last thing the receive() method does is to call repaint(). This call tells the user interface (UI) thread that it needs to call paint()
to update the screen. Note that we don't know whether we're executing
on the UI thread or some other background thread, but with repaint()
we don't need to care and neither does our caller. Reading from a
socket is a blocking operation, however, and we should do it on a
separate thread, to avoid locking up the UI.
The paint() method itself is always called from the UI
thread, so it needs to be fast. All we have to do is figure out which
part of the buffer should be on screen and paint each of the individual
characters in the right place. Using a monospaced font makes this a
much simpler task than if we were working with a proportional font and
calculating our own line-wrapping.
public void paint( Graphics g )
{
// clear screen
g.setGrayScale( 0 ); // black
g.fillRect( 0, 0, getWidth(), getHeight() );
// draw content from buffer
g.setGrayScale( 255 ); // white
g.setFont( font );
int i;
byte b;
for ( int y = 0; y < rows; y++ )
{
for ( int x = 0; x < columns; x++ )
{
i = (y+scrollY)*columns+(x+scrollX);
if ( i < buffer.length )
{
b = buffer[i];
if ( b != 0 )
{
g.drawChar( (char) b,
insetX + x*fontWidth,
insetY + y*fontHeight,
g.TOP | g.LEFT );
}
}
}
}
}
|
The first thing we must do is clear the screen. Where Swing gives you a blank slate each time paint() is called, MIDP gives you the screen the same way you left it after the previous call to paint().
This is a nice feature when you know in advance exactly what has
changed, but we're not tracking changes that closely. We keep the code
simple by erasing and repainting the entire screen each time.
We paint the background black and set the foreground color to white
for drawing the text – and not only for the sake of tradition. Green or
amber might be better choices for text color, but at least we know that
white-on-black will be legible on all color, grayscale, and "1-bit
color" (black and white) screens. We set the color by calling setGrayScale(0) for convenience and clarity; setColor( 0, 0, 0 ) would achieve the same result.
Then we loop through each visible row and column and paint the
corresponding character from the buffer on the screen. Even though drawChars() is probably a faster operation because it will render several characters with a single call, we use drawChar() because in practice most of the character positions are empty, and we can avoid allocating a character array with each call to paint().
Note that we do have the notion of a scrollable offset stored in the scrollX and scrollY
fields. This feature will become more important later, when we try to
implement a more sophisticated terminal, but even at this point
vertical scrollback is very useful. We leave scrollX at zero, but we increment scrollY
each time we encounter a line feed or line wrap. This lets our terminal
scroll automatically to show the latest output as it arrives, just as
our users expect.
Because we're recording all the incoming data, we should allow the
user to scroll backwards and see the content that got pushed off the
screen. To this end, we implement keyPressed() and keyRepeated() to capture UP and DOWN events, moving the scroll offset accordingly and requesting a repaint.
public void keyPressed( int keyCode )
{
int gameAction = getGameAction( keyCode );
switch ( gameAction )
{
case DOWN:
// scroll down one row
scrollY++;
if ( scrollY > calcLastVisibleScreen() )
{
scrollY = calcLastVisibleScreen();
}
repaint();
break;
case UP:
// scroll up one row
scrollY--;
if ( scrollY < 0 ) scrollY = 0;
repaint();
break;
default:
// ignore
}
}
|
One detail you should remember: You must convert the key code to a
game code before testing whether it was the up or down button on the
device. Some devices have more than one key that maps to the concept of
up or down, and some have scrollwheels or other specialized inputs;
using getGameAction() enables the method to do the right thing in all cases. keyRepeated() does much the same thing, but moves the scroll offset by half a screen at a time.
A Telnet MIDlet
Now that we have a front end for our back end, we need to tie them together in a MIDlet. The Weather Underground
still provides a free telnet service, and writing a MIDlet that uses it
to retrieve the latest weather conditions is a useful exercise
Furthermore, this task is very representative of the kinds of things
you would want to do with a terminal-emulating MIDlet: connecting to a
remote server, logging in, and extracting some kind of data for display
onscreen.
Take a look at the listing for MIDTerm.java.
It's a pretty standard MIDlet, reading configuration options from the
application descriptor and then setting up the display and commands. On
startup, startApp() calls connect(), which spawns a new Thread that calls run():
public void run()
{
String connectString = "socket://" + host + ':' + port;
try
{
canvas.receive( toASCII( "Connecting...\n" ) );
connection = new TelnetConnection(
(StreamConnection) Connector.open(
connectString, Connector.READ_WRITE, true ) );
input = connection.openInputStream();
output = connection.openOutputStream();
// server interaction script
try
{
// suppress content until first "continue:"
waitUntil(
input, new String[] { "ontinue:" }, false );
output.write( toASCII( "\n" ) );
output.flush();
// show content until city code prompt
waitUntil(
input, new String[] { "code--" }, true );
output.write( toASCII( city + '\n' ) );
canvas.receive( toASCII( city + '\n' ) );
output.flush();
// keep advancing pages until "Selection:" prompt
while ( !"Selection:".equals(
waitUntil( input, new String[] {
"X to exit:", "Selection:" }, true ) ) )
{
output.write( toASCII( "\n" ) );
output.flush();
canvas.receive( toASCII( "\n" ) );
}
// exit will cause disconnect
output.write( toASCII( "X\n" ) );
output.flush();
canvas.receive( toASCII( "X\n" ) );
// keep reading until "Done" or disconnected
waitUntil( input, new String[] { "Done" }, true );
}
catch ( IOException ioe )
{
System.err.println(
"Error while communicating: "
+ ioe.toString() );
canvas.receive( toASCII( "\nLost connection." ) );
}
catch ( Throwable t )
{
System.err.println(
"Unexpected error while communicating: "
+ t.toString() );
canvas.receive( toASCII(
"\nUnexpected error: " + t.toString() ) );
}
}
catch ( IllegalArgumentException iae )
{
System.err.println( "Invalid host: " + host );
canvas.receive( toASCII( "Invalid host: " + host ) );
}
catch ( ConnectionNotFoundException cnfe )
{
System.err.println(
"Connection not found: " + connectString );
canvas.receive( toASCII(
"Connection not found: " + connectString ) );
}
catch ( IOException ioe )
{
System.err.println(
"Error on connect: " + ioe.toString() );
canvas.receive( toASCII(
"Error on connect: " + ioe.toString() ) );
}
catch ( Throwable t )
{
System.err.println(
"Unexpected error on connect: " + t.toString() );
canvas.receive( toASCII(
"Unexpected error on connect: " + t.toString() ) );
}
// clean up
disconnect();
canvas.receive( toASCII( "\nDisconnected.\n" ) );
}
|
The code calls on two utility methods that are worthy of note. First, waitUntil()
reads the input stream, optionally writing the bytes to the screen,
until it matches one of the specified strings, at which point it
returns the matched string. This kind of utility is exactly what you
need when you're scripting interaction with any kind of server. Second,
toASCII() is a convenience method that converts Strings to byte arrays.
Once we connect to the server, we skip down until we're prompted for
a city code (here we use Washington, D.C.), and then keep sending line
feeds until we reach the end of the data. While the output would look
better on a larger screen, it is clearly legible, and the scrollback
buffer lets us go back to see the data that doesn't fit on the screen.
Because users will have no standard output or error when they run
your application, we make all of the warnings and errors visible to the
user. Generally, you should take care to handle independently each of
the possible error conditions with a distinct error message, striking a
balance between user-friendly and developer-useful. There are many
potential points of failure for a network application and you'll want
as much information as possible before you set out to debug your code.
Finally, if we reach the end of the input or if there's any kind of error, we make sure to clean up, and the thread exits.
Summary
We've built a simple terminal emulator that can run on any MIDP
device that supports the optional TCP/IP socket connection type. It
consists of two independently reusable components: an implementation of
the telnet protocol and a canvas that can render dumb-terminal output.
What's needed now to make a truly useful MIDlet is a little more
sophistication: better formatting in the terminal and, not least,
interactive user input. We'll see more along those lines in a future
article.
For More Information
The links in this article are repeated here:
About the Author: Michael Powers
is Principal of mpowers LLC, a software consultancy for desktop and
wireless platforms, and he has been working with Java technology in its
various incarnations since its inception. His award-winning Piranha
Pricecheck MIDlet is taking the world by storm, and is a free download
from mpowers.net.
Back To Top
|