J2ME編程最佳實踐之聯網開發 – JAVA編程語言程序開發技術文章

由於無線設備所能支持的網絡協議非常有限,僅限於HTTP,Socket,UDP等幾種協議,不同的廠傢可能還支持其他網絡協議,但是,MIDP 1.0規范規定,HTTP協議是必須實現的協議,而其他協議的實現都是可選的。因此,為瞭能在不同類型的手機上移植,我們盡量采用HTTP作為網絡連接的首選協議,這樣還能重用服務器端的代碼。但是,由於HTTP是一個基於文本的效率較低的協議,因此,必須仔細考慮手機和服務器端的通信內容,盡可能地提高效率。

  對於MIDP應用程序,應當盡量做到:

  1.發送請求時,附加一個User-Agent頭,傳入MIDP和自身版本號,以便服務器能識別此請求來自MIDP應用程序,並且根據版本號發送相應的相應。

   2.連接服務器時,顯示一個下載進度條使用戶能看到下載進度,並能隨時中斷連接。

   3.由於無線網絡連接速度還很慢,因此有必要將某些數據緩存起來,可以存儲在內存中,也可以放到RMS中。

  對於服務器端而言,其輸出響應應當盡量做到:

  1. 明確設置Content-Length字段,以便MIDP應用程序能讀取HTTP頭並判斷自身是否有能力處理此長度的數據,如果不能,可以直接關閉連接而不必繼續讀取HTTP正文。

   2. 服務器不應當發送HTML內容,因為MIDP應用程序很難解析HTML,XML雖然能夠解析,但是耗費CPU和內存資源,因此,應當發送緊湊的二進制內容,用DataOutputStream直接寫入並設置Content-Type為application/octet-stream。

   3. 盡量不要重定向URL,這樣會導致MIDP應用程序再次連接服務器,增加瞭用戶的等待時間和網絡流量。

   4. 如果發生異常,例如請求的資源未找到,或者身份驗證失敗,通常,服務器會向瀏覽器發送一個顯示出錯的頁面,可能還包括一個用戶登錄的Form,但是,向MIDP發送錯誤頁面毫無意義,應當直接發送一個404或401錯誤,這樣MIDP應用程序就可以直接讀取HTTP頭的響應碼獲取錯誤信息而不必繼續讀取相應內容。

   5. 由於服務器的計算能力遠遠超過手機客戶端,因此,針對不同客戶端版本發送不同響應的任務應該在服務器端完成。例如,根據客戶端傳送的User-Agent頭確定客戶端版本。這樣,低版本的客戶端不必升級也能繼續使用。

  MIDP的聯網框架定義瞭多種協議的網絡連接,但是每個廠商都必須實現HTTP連接,在MIDP 2.0中還增加瞭必須實現的HTTPS連接。因此,要保證MIDP應用程序能在不同廠商的手機平臺上移植,最好隻使用HTTP連接。雖然HTTP是一個基於文本的效率較低的協議,但是由於使用特別廣泛,大多數服務器應用的前端都是基於HTTP的Web頁面,因此能最大限度地復用服務器端的代碼。隻要控制好緩存,仍然有不錯的速度。

  SUN的MIDP庫提供瞭javax.microediton.io包,能非常容易地實現HTTP連接。但是要註意,由於網絡有很大的延時,必須把聯網操作放入一個單獨的線程中,以避免主線程阻塞導致用戶界面停止響應。事實上,MIDP運行環境根本就不允許在主線程中操作網絡連接。因此,我們必須實現一個靈活的HTTP聯網模塊,能讓用戶非常直觀地看到當前上傳和下載的進度,並且能夠隨時取消連接。

  一個完整的HTTP連接為:用戶通過某個命令發起連接請求,然後系統給出一個等待屏幕提示正在連接,當連接正常結束後,前進到下一個屏幕並處理下載的數據。如果連接過程出現異常,將給用戶提示並返回到前一個屏幕。用戶在等待過程中能夠隨時取消並返回前一個屏幕。

  我們設計一個HttpThread線程類負責在後臺連接服務器,HttpListener接口實現Observer(觀察者)模式,以便HttpThread能提示觀察者下載開始、下載結束、更新進度條等。HttpListener接口如下:

public interface HttpListener {
void onSetSize(int size);
void onFinish(byte[] data, int size);
void onProgress(int percent);
void onError(int code, String message);
}

  實現HttpListener接口的是繼承自Form的一個HttpWaitUI屏幕,它顯示一個進度條和一些提示信息,並允許用戶隨時中斷連接:

public class HttpWaitUI extends Form implements CommandListener, HttpListener {
private Gauge gauge;
private Command cancel;
private HttpThread downloader;
private Displayable displayable;
public HttpWaitUI(String url, Displayable displayable) {
super(“Connecting”);
this.gauge = new Gauge(“Progress”, false, 100, 0);
this.cancel = new Command(“Cancel”, Command.CANCEL, 0);
append(gauge);
addCommand(cancel);
setCommandListener(this);
downloader = new HttpThread(url, this);
downloader.start();
}
public void commandAction(Command c, Displayable d) {
if(c==cancel) {
downloader.cancel();
ControllerMIDlet.goBack();
}
}
public void onFinish(byte[] buffer, int size) { … }
public void onError(int code, String message) { … }
public void onProgress(int percent) { … }
public void onSetSize(int size) { … }
}

  HttpThread是負責處理Http連接的線程類,它接受一個URL和HttpListener:

class HttpThread extends Thread {
private static final int MAX_LENGTH = 20 * 1024; // 20K
private boolean cancel = false;
private String url;
private byte[] buffer = null;
private HttpListener listener;
public HttpThread(String url, HttpListener listener) {
this.url = url;
this.listener = listener;
}
public void cancel() { cancel = true; }

   使用GET獲取內容

  我們先討論最簡單的GET請求。GET請求隻需向服務器發送一個URL,然後取得服務器響應即可。在HttpThread的run()方法中實現如下:

public void run() {
HttpConnection hc = null;
InputStream input = null;
try {
hc = (HttpConnection)Connector.open(url);
hc.setRequestMethod(HttpConnection.GET); // 默認即為GET
hc.setRequestProperty(“User-Agent”, USER_AGENT);
// get response code:
int code = hc.getResponseCode();
if(code!=HttpConnection.HTTP_OK) {
listener.onError(code, hc.getResponseMessage());
return;
}
// get size:
int size = (int)hc.getLength(); // 返回響應大小,或者-1如果大小無法確定
listener.onSetSize(size);
// 開始讀響應:
input = hc.openInputStream();
int percent = 0; // percentage
int tmp_percent = 0;
int index = 0; // buffer index
int reads; // each byte
if(size!=(-1))
buffer = new byte[size]; // 響應大小已知,確定緩沖區大小
else
buffer = new byte[MAX_LENGTH]; // 響應大小未知,設定一個固定大小的緩沖區
while(!cancel) {
int len = buffer.length – index;
len = len>128 ? 128 : len;
reads = input.read(buffer, index, len);
if(reads<=0)
break;
index += reads;
if(size>0) { // 更新進度
tmp_percent = index * 100 / size;
if(tmp_percent!=percent) {
percent = tmp_percent;
listener.onProgress(percent);
}
}
}
if(!cancel && input.available()>0) // 緩沖區已滿,無法繼續讀取
listener.onError(601, “Buffer overflow.”);
if(!cancel) {
if(size!=(-1) && index!=size)
listener.onError(102, “Content-Length does not match.”);
else
listener.onFinish(buffer, index);
}
}
catch(IOException ioe) {
listener.onError(101, “IOException: ” + ioe.getMessage());
}
finally { // 清理資源
if(input!=null)
try { input.close(); } catch(IOException ioe) {}
if(hc!=null)
try { hc.close(); } catch(IOException ioe) {}
}
}

  當下載完畢後,HttpWaitUI就獲得瞭來自服務器的數據,要傳遞給下一個屏幕處理,HttpWaitUI必須包含對此屏幕的引用並通過一個setData(DataInputStream input)方法讓下一個屏幕能非常方便地讀取數據。因此,定義一個DataHandler接口:

public interface DataHandler {
void setData(DataInputStream input) throws IOException;
}

  HttpWaitUI響應HttpThread的onFinish事件並調用下一個屏幕的setData方法將數據傳遞給它並顯示下一個屏幕:

public void onFinish(byte[] buffer, int size) {
byte[] data = buffer;
if(size!=buffer.length) {
data = new byte[size];
System.arraycopy(data, 0, buffer, 0, size);
}
DataInputStream input = null;
try {
input = new DataInputStream(new ByteArrayInputStream(data));
if(displayable instanceof DataHandler)
((DataHandler)displayable).setData(input);
else
System.err.println(“[WARNING] Displayable object cannot handle data.”);
ControllerMIDlet.replace(displayable);
}
catch(IOException ioe) { … }
}

  以下載一則新聞為例,一個完整的HTTP GET請求過程如下:

  首先,用戶通過點擊某個屏幕的命令希望閱讀指定的一則新聞,在commandAction事件中,我們初始化HttpWaitUI和顯示數據的NewsUI屏幕:

public void commandAction(Command c, Displayable d) {
HttpWaitUI wait = new HttpWaitUI(“http://

發佈留言