下載 項目源代碼
隨著每一款新的 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 早于現代 Internet 而出現。J. Postel 和 J. Reynolds 于 20 多年前在 RFC 854 (值得一讀)中定義了該協議。在那時,“終端”意味著一個屏幕和鍵盤,使用串行電纜連接到大型計算機。要使用終端,您必須與計算機處于同一座建筑中。Telnet 則允許您將終端連接到網絡中,然后在 Internet 上從任何地方進行工作。
Telnet 使您能控制任何具有命令行界面的操作系統。在 UNIX 環境中,您可以獲得對機器的完全控制,包括能夠開始和停止處理,甚至關機和重新啟動。實際上,多數 UNIX 軟件都假定用戶是在終端上。在 WWW 出現之前,telnet 還提供我們現在稱作 Web 服務的信息和服務類型,這些 telnet 服務中的某些仍舊可用,如 Weather Underground。但是,現今 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)選項,但是我們現在盡量保持其簡單。)
要查看以代碼表示的協議,可以看看 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 輸入流相比,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 開發)來說有趣的是,這些細節可能實際上很重要。
因為 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
和按需創建我們的自定義流。因為多次關閉 Connection
s 沒有什么損害,我們還實現了 close()
來調用已包裝的 StreamConnection
上的 close()。
記住,在連接的輸入和輸出流全部關閉前,連接實際上沒有關閉,所以您應該注意跟蹤流并顯式地關閉它們。您在關閉連接之前或之后關閉這些流沒有區別。
請求 Connector
建立一個基于 socket://
的連接會返回一個 StreamConnection
,這也是您應該傳遞到 TelnetConnection
的內容。使用 telnet 時,好的實踐是通過將 READ_WRITE
標志作為第二個可選的參數傳遞給 Connector.open(),
告訴 Connector
您想要對其讀寫數據。即使您只想從流中讀取數據,telnet 協商也將要求您將數據寫回到連接。此外,您還應該指定第三個可選的參數,表明如果網絡連接超時,也就是在某個時間間隔內沒有收到響應時,讓框架拋出異常。因為實現 MIDP 的移動設備的種類最多具有間歇的聯網,您就需要得體地處理網絡故障,獲取任何的異常并通知用戶連接已經斷開。
既然我們的網絡基礎設施已經就緒,我們需要提供一個用戶界面。按照模塊化的思想,這個用戶界面將不對 telnet 連接做出假定或者根本不管網絡連接是否存在。它將簡單地接受字節并將其寫到屏幕。
雖然我們在輸入到來時可以使用 Form
并將 StringItem
s 或者甚至我們自己的 CustomItem
s 附加到 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
關鍵字來分配不常見大小的對象的內存時,遵循這個模式是一個好主意:測試 OutOfMemoryError
s 并準備一個備份計劃,這樣您可以得體地應對故障。
因為您知道行數和列數,所以您可能想要創建一個二維的字節數組來保存屏幕數據。要抵抗住這種誘惑。這樣的結構比包含相同數目字節的單個一維數組會消耗更多的內存,因為它實際上是一個數組的數組,每個數組都有開銷。性能也很差,因為運行時必須在數組上對每次索引式存取執行范圍檢查,我們的 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()
也可做很多相同的工作,但是一次會移動滾動偏移量半個屏幕。
現在我們有了一個用于后端的前端,我們需要將他們與 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.companysz.com)
新聞熱點
疑難解答