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 );
}
}
}
}
}
|