Now that you know about the connection types provided by CLDC, MIDP 1.0, and 2.0, you are ready to implement two example applications: a terminal program and a chat application. These example applications will be CLDC-based.
Prior to the PC era, it was quite common to have a single centralized computer and a set of simple hardware terminals providing text-based interfaces to the main computer. A terminal program is an application that simulates this kind of interface by providing a local text interface to a remote process. Before the World Wide Web became popular, terminal programs were widely used to access so-called mailboxes or bulletin board systems (BBS) over a serial modem connection. Today, terminal programs implementing the telnet protocol are still used for command line access; for example, for server configuration.
For the sample terminal application, we will take advantage of the fact that the GCF is indeed very generic. You will enter a URI, and your terminal program will connect to the given address and display what the other end of the connection sends. For example, if you connect to an HTTP address, the program will display the HTML code of the requested page. You may also use the example to view the raw positioning data read from a GPS receiver using a serial connection. An input line allows you to send data over the connection.
Here, we will only describe the network related parts of the application. The complete sources of the MIDP and PDAP versions are shown in Listings 6.2 and 6.3, respectively. The MidpTerminal and PdapTerminal application user interfaces consist of a widget in which you can enter a URL that is used to open a particular connection. The connection is established by activating the Connect command or by pressing the Connect button, depending on the platform in which the application is running.
The core of the Terminal implementation is contained in an inner class of the main application called Handler. This class “handles” establishing the connection and receiving new data in the background. For that purpose, the Handler stores the connection, as well as the corresponding input and output streams in member variables. It also contains a variable leave that determines whether the handler should leave the receive loop and terminate itself:
class Handler extends Thread { StreamConnection connection; InputStream in; OutputStream out; boolean leave;
The constructor of the Handler takes a URL as input and establishes a corresponding stream connection. It is called from the main application when the user enters an address and requests a corresponding connection. The Handler is not able to handle datagram or serversocket protocols. Trying to do so would cause a class cast exception.
When the connection is established, the out and in variables are set. Then, the establishment of the connection is reported using the show() method of the main class:
public Handler (String url) throws IOException { connection = (StreamConnection) Connector.open (url, Connector.READ_WRITE, true); out = connection.openOutputStream (); in = connection.openInputStream (); show ("opened: "+ url + " "); }
The main work is performed in the run() method of the Handler thread. The run() method is invoked automatically when the main application calls the start() method, which runs the handler as a separate thread in the background. In the run() method, incoming data is collected in a string buffer until the leave flag is set, no more data is available, or the buffered data reaches a limit of 1024 bytes. In that case, the collected data is shown to the user by handing it over to the show() method. Then, the buffer is cleared and a new iteration of the read loop is entered. If read encounters a -1, that means that the stream is closed remotely. In that case, disconnect() is called in order to terminate the Handler and to release the connection:
public void run () { StringBuffer buf = new StringBuffer (); try { while (!leave) { do { int i = read (); if (i == -1) disconnect (); // ignore control characters except from cr else if (i == ' ' || i >= ' ') buf.append ((char) i); } while (!leave && in.available () > 0 && buf.length () < 1024); show (buf.toString ()); buf.setLength (0); // clear buffer } } catch (Exception e) { if (!leave) show (e.toString () + " "); disconnect (); } } }
You might have noticed that the method Handler.read() is called for reading data from the stream instead of just calling in.read(). The only difference is that Handler.read() implements telnet parameter negotiation, which allows you to use the Terminal sample application as a telnet client by connecting to port 23 of a corresponding host. The Telnet protocol is widely used to connect computer systems remotely with a command-line interface. For details of the Telnet protocol please refer RFC854.
The main application classes, MidpTermial and PdapTerminal, mainly handle the user interface. The only method that is independent from the user interface is disconnect(), which closes the connection if a connection exists and notifies the corresponding Handler to terminate.
An important difference of the PDAP implementation when compared to the MIDP implementation is that the show() method does not manipulate the user interface directly. Because the PDAP AWT is not thread safe, it is necessary to manipulate the user interface indirectly by calling invokeAndWait() with an instance implementing the Runnable interface. Here, you use the class Appender for that purpose. The Appender instance encapsulates the string to be appended to the list of incoming data. When the AWT calls the run() method of the Appender, AWT has made sure that it is currently safe to manipulate the user interface. Now, the Appender adds its payload to the list showing the data sent from the remote end of the connection.
Figure 6.4 shows an example session of the terminal program. Note that the terminal is only a minimal implementation for demonstration purposes. It does not handle any control sequences such as cursor control, except from carriage return characters ( ). Feel free to extend the sample as you like for your purposes. For example, for applications relying on binary data transfer, it might be better to use a hex format for sending and receiving data. Listing 6.2 contains the MIDP version of the terminal program, and Listing 6.3 shows the PDAP version.
import java.io.*; import javax.microedition.io.*; import javax.microedition.midlet.*; import java.awt.*; import java.awt.event.*; public class PdapTerminal extends MIDlet implements ActionListener { /** The Handler class cares about establishing the connection and receiving and displaying data in the background. */ class Handler extends Thread { StreamConnection connection; InputStream in; OutputStream out; boolean leave; /** Establishes a connection to the given URI */ public Handler (String uri) throws IOException { connection = (StreamConnection) Connector.open (uri, Connector.READ_WRITE, true); out = connection.openOutputStream (); in = connection.openInputStream (); show ("opened: "+uri + " "); } /** Like in.read (), but additional performs telnet parameter negotiations. */ public int read () throws IOException { while (true) { int i = in.read (); if (i != 0x0ff) return i; int cmd = in.read (); if (cmd == 0x0ff) return 0x0ff; int opt = in.read (); if (cmd == 0xfd || cmd == 0x0fb) { out.write (0x0ff); out.write (cmd == 0xfd ? 252 : 254); out.write (opt); out.flush (); } } } /** Collects incoming data in the background and shows it if the buffer size reaches 1 k or no more data is available at the moment. */ public void run () { StringBuffer buf = new StringBuffer (); try { while (!leave) { do { int i = in.read (); if (i == -1) disconnect (); else if (i == ' ' || i >= ' ') buf.append ((char) i); } while (!leave && in.available () > 0 && buf.length () < 1024); show (buf.toString ()); buf.setLength (0); } } catch (Exception e) { if (!leave) show (e.toString () + " "); disconnect (); } } } /** Class for thread safe appending of information to the list of incoming data */ class Appender implements Runnable { String data; Appender (String data) { this.data = data; } public void run () { int i0 = data.indexOf (' '), //System.out.println ("Adder: cr index is: "+i0); if (i0 == -1) i0 = data.length (); incoming.replaceItem (incoming.getItem (incoming.getItemCount () - 1) + data.substring (0, i0), incoming.getItemCount ()-1); i0++; while (i0 <= data.length ()) { int i = data.indexOf (' ', i0); if (i == -1) i = data.length (); incoming.add (data.substring (i0, i)); i0 = i+1; } } } Frame frame = new Frame (); TextField urlField = new TextField (""); List incoming = new List (); TextField sendField = new TextField (); Button connectButton = new Button ("connect"); Button sendButton = new Button ("send"); Handler handler; /** Initializes GUI */ public PdapTerminal () { frame = new Frame ("GcfTerminal"); frame.addWindowListener (new WindowAdapter () { public void windowClosing (WindowEvent e) { destroyApp (true); notifyDestroyed (); } }); connectButton.addActionListener (this); Panel topPanel = new Panel (new BorderLayout ()); //topPanel.add ("West", protocolChoice); topPanel.add ("Center", urlField); topPanel.add ("East", connectButton); sendButton.addActionListener (this); Panel bottomPanel = new Panel (new BorderLayout ()); bottomPanel.add ("Center", sendField); bottomPanel.add ("East", sendButton); frame.add ("North", topPanel); frame.add ("Center", incoming); frame.add ("South", bottomPanel); frame.pack (); } /** Shows the given string thread safe by handing a new Appender to invokeLater */ public void show (String s) { try { Toolkit.getDefaultToolkit ().getSystemEventQueue () .invokeAndWait (new Appender (s)); } catch (Exception e) { throw new RuntimeException (e.toString ()); } } /** Shows the main frame on the device screen */ public void startApp () { frame.show (); } /** Handles the buttons by opening a connection or sending text */ public void actionPerformed (ActionEvent event) { try { if (event.getSource () == sendButton && handler != null) { handler.out.write (sendField.getText ().getBytes ()); handler.out.write (' '), handler.out.write (' '), handler.out.flush (); sendField.setText (""); } else if (event.getSource () == connectButton) { disconnect (); handler = new Handler (urlField.getText ()); handler.start (); } } catch (Exception e) { incoming.add (e.toString ()); incoming.add (""); disconnect (); } } /** Closes the connection if any */ public void disconnect () { if (handler != null) { handler.leave = true; show ("disconnected! "); try { handler.connection.close (); handler.in.close (); handler.out.close (); } catch (IOException e) { } handler = null; } } public void pauseApp() { } public void destroyApp (boolean unconditional) { disconnect (); frame.setVisible (false); } |
In addition to the telnet client, we would like to show you how to build a “real” client-server application, where the server runs on a desktop computer, and the CLDC device takes over the role of the client. To keep things simple, we have chosen a chat application as an example. The idea is that you can connect to a server, see the messages from other people connected to the same server, and write your own messages that become visible to the other users. Because HTTP is the only protocol available for all devices, we will use HTTP as the communication protocol for our application. However, using HTTP includes a significant drawback: HTTP does not provide server initiated transmissions, so the clients need to connect to the server and to “poll” for new data from time to time. It might be possible to work around this limitation by keeping an HTTP connection open for each client and to forward data to all connected clients automatically. However, it might be possible that the gateway used by the device for HTTP access does not support this, so we will stick to polling here.
In order to allow the clients to receive only their new messages, all messages have an unique number, which is managed by a simple server-sided counter. Thus, the client can submit the number of the newest message it has already received, and the server will send only newer messages that have higher numbers assigned. If no number is given, the server will just send the 10 most recent messages.
Thus, we can define the following behaviors for reading:
Client—Sends a request of type GET.
Server—First it sends a line containing the number that will be assigned to the next message. Then, if the URL is of the form /?start=N, a list of all messages, starting with message number N, is transmitted. For all other URLs, the last 10 messages are submitted. All items are separated by a pair of carriage return and linefeed control characters ( ).
For submitting text, you will use the HTTP POST command with an identical URL, but you will also send the nickname and the text in the body of the request. In return, the server sends the same content as for the GET command:
Client—Sends a request of type POST.
Server—Sends the same reply as for the GET command. The text just sent by the client is included in the list of messages. Thus, at least one message is sent.
Now that we have defined the communication protocol, we can start with implementing a corresponding server.
The server depends on the java.io and java.net packages. It stores the number of the current message, a buffer for text, and a J2SE server socket in member variables:
import java.io.*; import java.net.*; public class ChatServer { int current = 0; String [] lines = new String [256]; ServerSocket serverSocket;
The constructor of the server gets a port number as input and creates a corresponding server socket:
public ChatServer (int port) throws IOException { serverSocket = new ServerSocket (port); System.out.println ("Serving port: "+port); }
The run() method of the server contains a loop that waits for incoming connections. When a request is accepted, a buffered reader and a writer corresponding to input and output streams associated with the connection are handed over to the handleRequest() method. Usually, the actual handling of the request would be performed in a separate thread, enabling the server to handle new requests immediately. However, in order to keep the server as simple as possible, you handle the request in the current thread, blocking new requests for the corresponding amount of time:
public void run () { while (true) { try { Socket socket = serverSocket.accept (); handleRequest (new BufferedReader (new InputStreamReader (socket.getInputStream ())), new OutputStreamWriter (socket.getOutputStream ())); socket.close (); } catch (Exception e) { e.printStackTrace (System.err); } } }
The main functionality of the chat server is performed in the handleRequest() method. It gets the socket reader and writer as input from the run() method. At first, it reads the HTTP request line from the client and prints it to system.out:
public void handleRequest (BufferedReader reader, Writer writer) throws IOException { String request = reader.readLine (); System.out.println ("handling: "+request);
The next step is to analyze the request line. For that purpose, it is divided into the method, the requested address, and the version part, which are separated by space characters:
int s0 = request.indexOf (' '), int s1 = request.indexOf (' ', s0+1); String method = request.substring (0, s0); String url = request.substring (s0+1, s1);
Now, the first line to be submitted is determined by analyzing the request URL:
int start = -1; // default; int cut = url.indexOf ("?start="); if (cut != -1) start = Integer.parseInt (url.substring (cut+7)); if (start < 0) start = count – 10;
Additional header lines are skipped by reading from the stream until an empty line or the end of the stream is reached. An empty line marks the end of the HTTP headers and the beginning of the content of the request:
while (true) { String s = reader.readLine (); System.out.println ("header: "+s); if (s == null || s.length () == 0) break; }
Now, if the HTTP-Request method is POST, the nickname and the sender are read from the HTTP content. A corresponding string is appended to the message ring buffer of the server:
if (method.equalsIgnoreCase ("post")) { String nick = reader.readLine ().substring (5); String text = reader.readLine ().substring (5); System.out.println ("nick="+nick); System.out.println ("text="+text); lines [(current++) % lines.length] = nick + ": "+ text; // skip possible additional crlf from bad http implementations if (reader.ready ()) reader.readLine (); }
Finally, an HTTP OK status report is sent back to the client, together with the number that will be assigned to the next incoming messages, and the list of messages that was requested by the client:
writer.write ("HTTP/1.0 200 OK "); writer.write ("Content-Type: text/plain "); writer.write ("Connection: close "); // Header is separated from content by a blank line. writer.write (" "); writer.write (""+current+" "); if (start < current - lines.length) start = current - lines.length; if (start < 0) start = 0; for (int i = start; i < current; i++) { writer.write (lines [i % lines.length]); writer.write (" "); } writer.close (); }
The main method of the chat server sets up the server listening to the port given as the command-line parameter. If no port number was given, it defaults to port 8080:
public static void main (String [] argv) throws IOException { if (argv.length == 0) new ChatServer (8080).run (); else if (argv.length == 1) new ChatServer (Integer.parseInt (argv[0])).run (); else System.out.println ("Usage: java ChatServer [port]"); } }
Note
The Java Servlet API provides better support for implementing HTTP-based server applications than using raw sockets. However, we did not want to introduce an additional dependency for this example application.
Now, you have developed a simple HTTP based chat server. You can test it using a simple Web browser. By starting the server on the local machine and pointing a Web browser to the address http://localhost:8080, you can get a list of the 10 most recent messages. This list will probably be empty, but you can use a very simple HTML page to send messages to the server:
<html><head><title>Chat Server Test</title></head> <body> <form target="main" method="post" action="http://localhost:8080/" enctype="text/plain"> <table> <tr><td>Nickname:</td><td><input name="nick" /></td></tr> <tr><td>Text:</td><td><input size="80" name="text" /></td></tr> </table> <input value="Submit Text" type="submit" /> </form> </body> </html>
However, the main idea is to use J2ME devices as clients for the chat server. Listings 6.4 and 6.5 contain the MIDP and PDAP versions of the chat client. Again, the main task is performed in the transmit method in both cases, which sends a new string to the server and updates the display of the client with the messages received from the server.
The transfer method is called from two points in the program:
From the event handler when the user requests to send some text. In that case, the string to be submitted to the server is given as a parameter.
Periodically from a refresh task every four seconds with null as a parameter, in order to keep the message display of the client updated.
The transfer method takes the string to be sent to the server as parameter, or null if the local list of messages should only be updated from the server without sending a new message. Depending on this parameter, a HTTP connection is opened in READ or READ_WRITE mode. The URI is constructed from the hostname that was queried by the user interface and stored in the host variable and the start parameter, denoting from which message number the server should start sending. The count variable is initialized with the value -1, so for the first request, the server will send back the 10 most recent messages. The count variable is updated later in this method from the response of the server. Because IO exceptions might be thrown during the connection, you include the whole method in a try-catch block. The Boolean return value indicates whether the transfer was performed successfully:
boolean transfer (String submit) { // if null, just read try { HttpConnection connection = (HttpConnection) Connector.open (host + "/?start="+count, submit != null ? Connector.READ_WRITE : Connector.READ);
If submit is not null, a corresponding writer is obtained from the HTTP connection, and the method is set to POST. Then, the message stored in the submit variable is submitted as content of the request:
Writer writer = null; if (submit != null) { connection.setRequestMethod (HttpConnection.POST); writer = new OutputStreamWriter (((StreamConnection) connection).openOutputStream ()); writer.write ("nick="+nick + " "); writer.write ("text="+submit+" "); writer.close (); }
Now you open a reader in order to read the new messages submitted with the reply from the server:
Reader reader = new InputStreamReader (((StreamConnection) connection).openInputStream ());
First, you read the new count value, denoting the number that will be assigned to the next message arriving at the server. This value is important in order to know where to start the next request. readLine() is a static method of this class that reads a line from the given reader:
count = Integer.parseInt (readLine (reader));
Now you read the messages submitted by the server until you reach the end of the stream:
while (true) { String s = readLine (reader); if (s == null || s.length () == 0) break; addLine (s); }
Next, you close the readers and the corresponding connection. Also, you return true in order to indicate that the transfer was performed successfully:
reader.close (); connection.close (); return true; }
Finally, if an exception occurred in the connection, you add the corresponding error string to the display, and then call the disconnect() method which stops the timer that ensures the display is updated periodically. A false value is returned in order to indicate that an exception has occurred:
catch (Exception e) { addLine (e.toString ()); disconnect (); return false; } }
The only other part of the application relevant for communications is RefreshTask:
class RefreshTask extends TimerTask { public void run () { transfer (null); } }
When a connection is established, it is launched with parameters to request an update of the messages from the server every four seconds:
timer = new Timer (); timer.schedule (new RefreshTask (), 0, 4000);
Figure 6.5 shows emulated MIDP and PDAP clients connected to a local chat server. Note that the resulting chat application is minimalistic. For example, it does not check if two different users are using the same nickname. The application is intended to show the basic HTTP functionality only. Feel free to extend or change the application as you like for your own purposes.