下载 项目源代码
随着每一款新的 MIDP 2.0 设备在高级网络上部署,基于 TCP/ip 的套接字应用程序市场正不断得到扩展。它是对 MIDP 1.0 设备的补充,这些设备要么已经完全支持可选的套接字协议(像 Motorola/Nextel iDEN 电话),或者至少非正式地在半双工中支持它们(像 Nokia Series 60 和其他 Symbian 设备)。结果,随着有企业头脑的开发人员发现该平台不仅可以很好地用于游戏,他们对它也表现出越来越强的兴趣。
在本文中,通过构建一个小的简单终端模拟器,我们将探讨通用连接框架(GCF)中的套接字支持。您现在可以下载项目代码了。
我们的终端模拟器将实现 telnet 协议。telnet 协议是基于文本的和面向命令的协议,它是 Internet 的架构骨干之一,并且广泛用于教育、研究和公司环境中的遗留应用程序和系统管理。在移动形式中具有该功能是一件很好的事情。
我们将首先在一个透明和 GCF 友好的包装器中实现 telnet,然后为显示终端内容而编写一个自定义的 canvas,并最终在 MIDlet 中将所有这些结合在一起。在“让它工作,然后再让它正确”的精神下,我们要使一个最基本的(或者叫“哑”)终端工作,并且这将作为稍后更为复杂终端的基础。同时,我将突出说明 MIDP 程序员在编写普通应用程序以及网络化应用程序时应该知晓的问题和约束。
Telnet:它对什么有好处?
telnet 协议是一个用于在双向网络连接上进行通信的规则集。与来回传送的普通内容相混合的是特殊的 telnet 命令 ,该命令允许连接的两端进行协商并同意将要遵守的规则。这些命令从进入数据中剥离,所以使用该接的应用程序永远无需知道它们。
Telnet 早于现代 Internet 而出现。J. Postel 和 J. Reynolds 于 20 多年前在 RFC 854 (值得一读)中定义了该协议。在那时,“终端”意味着一个屏幕和键盘,使用串行电缆连接到大型计算机。要使用终端,您必须与计算机处于同一座建筑中。Telnet 则允许您将终端连接到网络中,然后在 Internet 上从任何地方进行工作。
Telnet 使您能控制任何具有命令行界面的操作系统。在 UNIX 环境中,您可以获得对机器的完全控制,包括能够开始和停止处理,甚至关机和重新启动。实际上,多数 UNIX 软件都假定用户是在终端上。在 WWW 出现之前,telnet 还提供我们现在称作 Web 服务的信息和服务类型,这些 telnet 服务中的某些仍旧可用,如 Weather Underground。但是,现今 telnet 主要用于对远程计算资源的远程访问。
几乎没有人再使用老式的终端了:在桌面计算机、智能工作站以及越来越多地在移动设备上,我们运行着“假装”是终端的软件程序,称为“虚拟终端”或“终端模拟器”。这正是我们将要构建的。
实现 Telnet
首先从 InputStream 开始。有了 GCF,利用类似于 Connector.open("socket://myhost:23") 这样的代码,您可以从 Connector 处获得一个 SocketConnection。然后从所得到的 Connection 调用 openInputStream() 来获得一个 InputStream ,并开始从套接字读取数据。
要实现 telnet,我们需要观察该命令流并处理它们,将其剥离出来,使应用程序的其他部分永远看不到它们。我们将通过创建我们自己的子类 InputStream 来这样做,该子类将包装从 SocketConnection 处获得的流。我们还将创建我们自己的 OutputStream 子类,用来标记我们的应用程序所发送的看来像是 telnet 命令、但又不应被连接的远程端看作是 telnet 命令的任何数据。
换句话说,我们的应用程序将像通常那样简单地与输入和输出流对话,而在内部,我们的终端模拟器将处理握手和协商,所以应用程序无需担心这些。
我们的 telnet 模拟器必须遵循一个典型性的过程:
我们从输入读取一个字节。如果该字节是除 225 以外的任意数值,我们只需把该字节传送到应用程序并继续读取。
如果字节数值是 255,那么我们就读取下个字节,查看它是否是一条命令。在 telnet 协议中,255 作为 IAC,代表“作为命令解释”(InterPRet As Command)。
如果第二个字节还是 255,那么服务器实际上是想发送数值 255,不是一条命令。在这种情况下,我们只需把该字节传送到应用程序并继续读取。实际上,发送者通过连续发送两个 255 来转义数值 255。
如果第二个字节不是 255,它就是一条命令。对于多数命令,我们只要执行命令内容并继续读取即可。
某些命令 ——如 SB(250)、WILL(251)、WONT(252)、DO(253)和 DONT(254)——是协商命令。每个命令后都跟随着第三个字节-选项,我们读取那个字节,获得选项代码。如果第二个字节是除 SB 以外的任何协商命令,我们执行命令和选项所指定的操作并继续读取。
SB 命令会触发一个子协商。在选项字节后,我们读取其他数据,直到我们遇到一个后面跟有 SE 的 IAC。我们执行选项所指定的操作,使用所提供的额外信息,并继续读取。
因为客户端无需决定它们将要实现哪些已建立的 telnet 选项,所以在客户端和服务器间必须进行协商,以确定两端支持哪些选项。
协商是简单的命令交换。一端使用 WILL 或 D0 命令打开一个协商,使用哪条命令取决于由哪一方执行指定的选项:
WILL 提出第一方能够和原意执行的选项。如果第一方应该开始执行那个选项,则另一方回复 D0,如果另一方无法理解或不支持该选项,则回复 DONT。
DO 告诉另一方开始执行选项。如果另一方开始执行选项,则回复 WILL,如果另一方无法理解或不支持该选项,则回复 WONT。
我们最小的客户端将只处理 4 条命令:WILL、WONT、DO 和 DONT,以及两个选项:TERMINAL_TYPE(24)和 NAWS(“协商窗口大小” 31)。对于所有其他选项,我们将返回 DONT 或 WONT。(我曾考虑支持 RANDOMLY_LOSE_DATA(256)和 SUBLIMINAL_MESSAGE(257)选项,但是我们现在尽量保持其简单。)
Telnet 输入流
要查看以代码表示的协议,可以看看 TelnetInputStream.java 的清单。最令人兴奋的是 read() 方法。在这个摘录中,我去除了某些细节:
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();
}
因为我们忽略了所有的内容而只留下协商命令和多数选项,所以当我们接收到针对 TERMINAL_TYPE 或 NAWS 的 D0 命令时,感兴趣的部分才开始了。
对于 TERMINAL_TYPE,我们设法对我们和另一端都支持哪种传统的终端达成一致意见。某些基于终端的应用程序将利用更高级终端类型的特性,如颜色、粗体和下划线。更为复杂的终端模拟器将模拟多种终端类型,如 ansi、vt100 和 vt102。协商就是设法建立最佳的共同点。最简单的终端类型称为哑终端,这也是我们将支持的一种。
如果我们接收到 IAC DO TERMINAL_TYPE,我们用 IAC WILL TERMINAL_TYPE 响应。依照 RFC 1091 规范,然后远程主机就开始与 IAC SB TERMINAL_TYPE TERMINAL_SEND IAC SE 的子协商,然后我们响应 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;
...
注意,因为 telnet 采取 8 位的 ASCII 字符,而 Java 的字符是 16 位 Unicode,在发送前,我们必须注意将终端类型字符串的每个 Unicode 字符转换成 ASCII 字节。这个转换在几乎所有的 internet 协议中都是必需的,特别是早期协议。我们可以在对 String.getBytes() 的调用中指定 ASCII 编码,但由于这是非常简单的转换,我们可以内部完成它。虽然 MIDP 规范规定了要支持哪种字符编码,但不同的实现程序有时会出错,安全一些总比说抱歉要好。
在“协商窗口大小”(NAWS)情况下,我们只是简单地将屏幕尺寸通知给远程主机。终端应该使用等宽字体,所以屏幕是一个字符网格,具有固定的行数和列数。如果我们知道了屏幕的尺寸以及服务器支持 NAWS 选项,我们就可以在连接时发送屏幕的尺寸,并且如果屏幕尺寸改变了,以后可以再次发送。
正如 RFC 1073 规范所推荐的,当我们接收到 IAC DO NAWS 后,我们响应 IAC WILL NAWS,然后立即发送我们的当前屏幕尺寸。因为有时要求大于 255 的屏幕高度和宽度,所以宽度和高度分别作为两字节的整数发送:先是高位字节,然后是低位字节。这样我们发送 IAC SB NAWS width-high-byte width-low-byte height-high-byte height-low-byte IAC SE。代码如下:
...
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;
}
...
在编写用户界面和知道窗口的宽度和高度之前,我们不能使用该代码。目前,我们只是简单地发送 IAC WONT NAWS。
对于可用性而言,如果我们的类所需要做的所有工作就是从 InputStream 读取数据,那么类会很简洁。但是,我们还需要把数据送回服务器,所以我们需要 OutputStream 用于写入。因为我们支持终端类型和窗口尺寸选项,所以我们还需要所有这些传递给构造函数的信息。为了方便,我们使用了第二个构造函数,它只获取输入和输出流,默认的终端类型是“哑”终端,窗口高度和宽度是 0。
Telnet 输出流
与 telnet 输入流相比,TelnetOutputStream 可是很轻松的事。记住,虽然我们的应用程序可自由写入 255,就像其他任意数值一样,但是远程主机上的 telnet 服务器会尝试将其解释为 IAC,并且接着会查找命令代码。因此输出流的惟一职责是在其写入时注意具有值 255 的字节,如果有则再用一个 255 将其转义。
在这个 TelnetOutputStream.java 的摘录中,您可以看到这个任务与听起来一样简单:
...
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 );
}
}
...
我们分配了一个包含两个字节的静态最终字节数组,目的都是为了避免一次发送一个字节而引起的任何开销,以及避免按需分配数组可能导致的任何开销。对于 MIDP 开发(相对应于 Swing 开发)来说有趣的是,这些细节可能实际上很重要。
Telnet 连接
因为 TelnetOutputStream 需要一个 OutputStream,且 TelnetInputStream 同时需要一个 InputStream 和一个与 TelnetOutputStream 分离的 OutputStream,所以在设置 telnet 会话时要注意很多东西。因为 MIDP 程序员习惯于使用 GCF,我们可以为面向对象的目的而将我们的类包装到更加用户友好的软件包中,从而隐藏了复杂性并与熟悉的使用模式保持一致。TelnetConnection.java 向您展示了如何做。您所需做的就是将您的 StreamConnection 传递到构造器,所以建立一个 telnet 会话就像这样:
...
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 实现 StreamConnection 接口的所有方法,只不过是调用已包装的 StreamConnection 和按需创建我们的自定义流。因为多次关闭 Connections 没有什么损害,我们还实现了 close() 来调用已包装的 StreamConnection 上的 close()。记住,在连接的输入和输出流全部关闭前,连接实际上没有关闭,所以您应该注意跟踪流并显式地关闭它们。您在关闭连接之前或之后关闭这些流没有区别。
请求 Connector 建立一个基于 socket:// 的连接会返回一个 StreamConnection,这也是您应该传递到 TelnetConnection 的内容。使用 telnet 时,好的实践是通过将 READ_WRITE 标志作为第二个可选的参数传递给 Connector.open(),告诉 Connector 您想要对其读写数据。即使您只想从流中读取数据,telnet 协商也将要求您将数据写回到连接。此外,您还应该指定第三个可选的参数,表明如果网络连接超时,也就是在某个时间间隔内没有收到响应时,让框架抛出异常。因为实现 MIDP 的移动设备的种类最多具有间歇的联网,您就需要得体地处理网络故障,获取任何的异常并通知用户连接已经断开。
Telnet Canvas
既然我们的网络基础设施已经就绪,我们需要提供一个用户界面。按照模块化的思想,这个用户界面将不对 telnet 连接做出假定或者根本不管网络连接是否存在。它将简单地接受字节并将其写到屏幕。
虽然我们在输入到来时可以使用 Form 并将 StringItems 或者甚至我们自己的 CustomItems 附加到 Form,但那也与应该使用 Form 的方法完全相反。此外,在多种 MIDP 设备上的 Form 的不同实现,意味着用户体验将有很大的变化,且在多数情况中将不会像我们所预期的那样工作。要对用户体验有完全的控制,包括能够调整我们的输出来适合屏幕的尺寸并指定所显示的字体,我们将创建自己的自定义 Canvas 子类。
使用我们的 TelnetCanvas 很容易:只要创建它、将其放到屏幕上并通过调用 receive() 为其传送 ASCII 字节。
...
TelnetCanvas canvas = new TelnetCanvas();
Display.getDisplay(this).setCurrent( canvas );
canvas.receive( "Hello World!\n" );
...
实现更有意思。让我们从 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;
...
}
除了初始化我们的变量外,我们要在运行时使自己适应于设备,就像所有好的 MIDlets 所应该的那样。终端依照传统都使用等宽字体,所以我们要求最小的字体并要测量高度和宽度,看看屏幕上可以显示多少字符。
我们想避免丢弃所接收的任何输入,所以在最初,我们创建了一个足够大的缓冲区,来保存 4 个屏幕的数据。这个尺寸是随意判断的;我们希望缓冲区足够小以适合内存,但又要足够大,使我们无需为较大的输入而需要经常重新分配。
对于 MIDlets 的可用内存量,不同制造商的不同设备间差别很大,所以您应该始终注意内存占用。因为我们显示 8 位的 ASCII 字符,所以使用 StringBuffer 甚至字符数组来存储内容都没有意义。为任意数值类型分配一个 int 的标准 Java 实践在 MIDP 世界中很多。一个 byte 数组就是所有我们所需的全部,它所占用的空间只是一个 char 数组的一半,是一个 int 数组的四分之一。
然而,不利的一面是我们需要手动地扩大数组和管理内存分配,这可是一件棘手的事情。无论何时我们接收到输入,我们要检查缓冲区是否要满了。如果是,就要尝试扩大缓冲区,如下面摘录所示:
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;
...
}
}
...
}
我们继续按照需要的任意量来扩大缓冲区,如果内存用完,我们可以清空一半现有的缓冲区并重新使用它。在 MIDP 开发中,只要您使用 new 关键字来分配不常见大小的对象的内存时,遵循这个模式是一个好主意:测试 OutOfMemoryErrors 并准备一个备份计划,这样您可以得体地应对故障。
因为您知道行数和列数,所以您可能想要创建一个二维的字节数组来保存屏幕数据。要抵抗住这种诱惑。这样的结构比包含相同数目字节的单个一维数组会消耗更多的内存,因为它实际上是一个数组的数组,每个数组都有开销。性能也很差,因为运行时必须在数组上对每次索引式存取执行范围检查,我们的 paint() 例程将进行很多这样的访问。在较慢的设备上,您可以看出实际的差别。
由于这些原因,您通常应该将多维数据压缩到单个数组中并将偏移量计算在自己的数组中。计算偏移量比听起来要容易,就如在 receive() 方法(将数值写入缓冲区的代码)的第二部分中或者在后面代码中的 paint() 方法中所看到的那样:
...
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();
...
在哑终端中,我们惟一需要注意的格式化代码是退格、换行和回车。要前进一行(一个换行),我们将列的数目添加到插入索引,称为 cursor。要回到一行的开始(回车),我们会回退,直到插入索引落在列的数目的整数倍上。换行的实现也执行一次回车,这经过多年的争论后,现在或多或少是换行的标准方法了。所有其他的内容不是被放到缓冲区中插入点处的可见字符,就是被忽略。
receive() 方法所做的最后事情是调用 repaint()。这个调用告诉用户界面(UI)线程它需要调用 paint() 来更新屏幕。注意我们不知道我们是在 UI 线程上执行还是在其他后台线程上执行,但是利用 repaint(),我们无需关心这些,我们的调用程序也不用关心这些。从套接字读取数据是一种阻塞式操作,可是,我们应该在单独的线程上执行它,以避免锁定 UI。
该 paint() 方法本身总是从 UI 线程中被调用,所以它需要快速执行。所有我们必须做的是算出哪部分缓冲区内容应该在屏幕上并把每个字符在正确的位置描绘出来。与使用等比例字体和计算自己的自动换行相比,使用等宽字体使这个过程成为一个更简单的任务。
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 );
}
}
}
}
}
我们必须要做的第一件事是清空屏幕。每次调用 paint(),Swing 会给您一个“白板”,MIDP 此时呈现出的屏幕与您先前对 paint() 的调用后所保持的屏幕状态相一致。当您预先正好知道已修改的内容时,这是一个极好的功能,但是我们无法严密地跟踪修改。通过每次清除和重绘整个屏幕,我们可以保持代码简单。
我们将背景绘为黑色并为了绘制文本而设置前景色为白色,这不仅是出于传统的原因。对于文本颜色,绿色或琥珀色可能是更好的选择,但至少我们知道黑背景上的白色在所有颜色、灰度和“1 位颜色”(黑和白)屏幕上都是是清晰的。为了方便和清楚起见,我们通过调用 setGrayScale(0) 来设置颜色;setColor( 0, 0, 0 ) 将实现相同的结果。
然后我们循环每个可见的行和列并从缓冲区描绘相应的字符到屏幕。虽然 drawChars() 可能是更快的操作,因为它以单次调用呈现多个字符,但我们还是要使用 drawChar(),因为实际上多数字符位置是空的,我们可以避免每次调用 paint() 时分配字符数组。
注意,我们确实有可滚动的偏移标记存储在 scrollX 和 scrollY 字段中。当我们尝试实现更复杂的终端时,这个功能在以后将变得更加重要,但是在此情况中,垂直滚动条是很有用的。我们让 scrollX 保留为 0。但是每次我们遇到一个换行或自动换行就将 scrollY 递增。这就使终端屏幕自动地滚动,在最新的输出到达屏幕时显示它,正如用户所期待的那样。
因为我们正在记录所有的进入数据,我们应该允许用户向后滚动并查看已经离开屏幕的内容。要实现该功能,我们执行 keyPressed() 和 keyRepeated() 来捕获 UP 和 DOWN 事件,相应地移动滚动偏移量和请求重绘。
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
}
}
您应该记住一个细节:在测试它是设备上的 up 还是 down 按钮之前,您必须将键代码转换为游戏代码。某些设备上映射到 up 或 down 概念的键不只一个,有些有滚轴或其他专用的输入设备;使用 getGameAction() 能使该方法在所有情况下都能正确工作。keyRepeated() 也可做很多相同的工作,但是一次会移动滚动偏移量半个屏幕。
Telnet MIDlet
现在我们有了一个用于后端的前端,我们需要将他们与 MIDlet 结合成一体。Weather Underground 仍旧提供了一个免费的 telnet 服务,使用它编写一个 MIDlet 以检索最新的天气状况是一个有用的练习。而且,这个任务很有代表性,正是您希望用终端模拟 MIDlet 所做的那类事情:连接到远程服务器、登录和提取某类数据显示在屏幕上。
看一看 MIDTerm.java 的清单。它是一个极其标准的 MIDlet,从应用程序描述符中读取配置选项,然后设置显示和命令。在启动时,startApp() 调用 connect(),connect() 则生成调用 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" ) );
}
两个实用程序方法上的代码调用是值得注意的。首先,waitUntil() 读取输入流,可选地将字节写入屏幕,直到它匹配指定的字符串之一,在匹配后它返回匹配的字符串。当您正在为与多种服务器交互而编写脚本时,这类实用程序正是您所需的。其次,toASCII() 是将字符串转换成字节数组的简便方法。
一旦我们连接到服务器,我们会等待直到系统提示我们城市代码(这里我们使用 Washington, D.C.),然后持续发送换行直到我们到达数据的末尾。在较大的屏幕上输出将看起来更好,也非常清晰,而滚动缓冲区可使我们回去查看无法在屏幕上显示的数据。
因为当用户运行您的应用程序时将没有标准的输出或错误,我们要让所有的警告和错误对用户是可见的。通常,您应该使用明显的错误消息来独立地处理每个可能的错误情况,在用户友好性和开发人员有用性之间达到平衡。网络应用程序有很多潜在的故障点,在您开始调试代码之前,您将希望有尽可能多的信息。
最后,如果我们到达输入的末尾或者如果有任何类型的错误,我们要确保将其清除,并关闭线程。
结束语
我们已经建立了一个简单的终端模拟器,它可运行在任何支持可选的 TCP/IP 套接字连接类型的 MIDP 设备上。它由两个单独的可重用组件组成:一个 telnet 协议实现和一个可呈现哑终端输出的 canvas。现在,要使 MIDlet 真正有用,所需的是一点技巧:在终端上更好的格式化以及至少是交互式的用户输入。在后面的文章中我们将看到更多的这些代码行。
(出处:http://www.knowsky.com)