http://www.cnjm.net/tech/article2049.html
本文著作權歸原作者,中國JAVA手機網收錄本文的目的是讓更多人閱讀到此文章。轉載請註明出處為中國JAVA手機網<www.cnjm.net>
來自:http://www.cnjm.net/tech/article2049.html
由於無線裝置所能支援的網路通訊協定非常有限,僅限於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://192.168.0.1/news.do?id=1",
new NewsUI());
ControllerMIDlet.forward(wait);
}
NewsUI實現DataHandler介面並負責顯示下載的資料:
public class NewsUI extends Form implements DataHandler {
public void setData(DataInputStream input) throws IOException {
String title = input.readUTF();
Date date = new Date(input.readLong());
String text = input.readUTF();
append(new StringItem("Title", title));
append(new StringItem("Date", date.toString()));
append(text);
}
}
伺服器端只要以String, long, String的順序依次寫入DataOutputStream,MIDP用戶端就可以通過DataInputStream依次取得相應的資料,完全不需要解析XML之類的文本,非常高效而且方便。
需要獲得連網資料的螢幕只需實現DataHandler介面,並向HttpWaitUI傳入一個URL即可複用上述代碼,無須關心如何串連網路以及如何處理使用者中斷串連。
使用POST發送資料
以POST方式發送資料主要是為了向伺服器發送較大量的用戶端的資料,它不受URL的長度限制。POST請求將資料以URL編碼的形式放在HTTP本文中,欄位形式為fieldname=value,用&分隔每個欄位。注意所有的欄位都被作為字串處理。實際上我們要做的就是類比瀏覽器POST一個表單。以下是IE發送一個登陸表單的POST請求:
POST http://127.0.0.1/login.do HTTP/1.0
Accept: image/gif, image/jpeg, image/pjpeg, */*
Accept-Language: en-us,zh-cn;q=0.5
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Content-Length: 28
\r\n
username=admin&password=1234
要在MIDP應用程式中類比瀏覽器發送這個POST請求,首先設定HttpConnection的請求方式為POST:hc.setRequestMethod(HttpConnection.POST);
然後構造出HTTP本文:byte[] data = "username=admin&password=1234".getBytes();
並計算本文長度,填入Content-Type和Content-Length:
hc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
hc.setRequestProperty("Content-Length", String.valueOf(data.length));
然後開啟OutputStream將本文寫入:OutputStream output = hc.openOutputStream();output.write(data);
需要注意的是,資料仍需要以URL編碼格式編碼,由於MIDP庫中沒有J2SE中與之對應的URLEncoder類,因此,需要自己動手編寫這個encode()方法,可以參考java.net.URLEncoder.java的源碼。剩下的便是讀取伺服器響應,代碼與GET一致,這裡就不再詳述。
使用multipart/form-data傳送檔案
如果要在MIDP用戶端向伺服器上傳檔案,我們就必須類比一個POST multipart/form-data類型的請求,Content-Type必須是multipart/form-data。
以multipart/form-data編碼的POST請求格式與application/x-www-form-urlencoded完全不同,multipart/form-data需要首先在HTTP要求標頭設定一個分隔字元,例如ABCD:
hc.setRequestProperty("Content-Type", "multipart/form-data; boundary=ABCD");
然後,將每個欄位用“--分隔字元”分隔,最後一個“--分隔字元--”表示結束。例如,要上傳一個title欄位"Today"和一個檔案C:\1.txt,HTTP本文如下:
--ABCD
Content-Disposition: form-data; name="title"
\r\n
Today
--ABCD
Content-Disposition: form-data; name="1.txt"; filename="C:\1.txt"
Content-Type: text/plain
\r\n
<這裡是1.txt檔案的內容>
--ABCD--
\r\n
請注意,每一行都必須以\r\n結束,包括最後一行。如果用Sniffer程式檢測IE發送的POST請求,可以發現IE的分隔字元類似於——7d4a6d158c9,這是IE產生的一個隨機數,目的是防止上傳檔案中出現分隔字元導致伺服器無法正確識別檔案起始位置。我們可以寫一個固定的分隔字元,只要足夠複雜即可。
傳送檔案的POST代碼如下:
String[] props = ... // 欄位名
String[] values = ... // 欄位值
byte[] file = ... // 檔案內容
String BOUNDARY =
"---------------------------7d4a6d158c9"; // 分隔字元
StringBuffer sb = new StringBuffer();
// 發送每個欄位:
for(int i=0; i sb = sb.append("--");
sb = sb.append(BOUNDARY);
sb = sb.append("\r\n");
sb = sb.append("Content-Disposition: form-data;
name=\""+ props[i] + "\"\r\n\r\n");
sb = sb.append(URLEncoder.encode(values[i]));
sb = sb.append("\r\n");
}
// 傳送檔案:
sb = sb.append("--");
sb = sb.append(BOUNDARY);
sb = sb.append("\r\n");
sb = sb.append("Content-Disposition: form-data;
name=\"1\"; filename=\"1.txt\"\r\n");
sb = sb.append("Content-Type: application/octet-stream\r\n\r\n");
byte[] data = sb.toString().getBytes();
byte[] end_data = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
// 設定HTTP頭:
hc.setRequestProperty("Content-Type", MULTIPART_FORM_DATA + ";
boundary=" + BOUNDARY);
hc.setRequestProperty("Content-Length",
String.valueOf(data.length + file.length + end_data.length));
// 輸出:
output = hc.openOutputStream();
output.write(data);
output.write(file);
output.write(end_data);
// 讀取伺服器響應:
// TODO...
使用Cookie保持Session
通常伺服器使用Session來跟蹤會話。Session的簡單實現就是利用Cookie。當用戶端第一次串連伺服器時,伺服器檢測到用戶端沒有相應的Cookie欄位,就發送一個包含一個識別碼的Set-Cookie欄位。在此後的會話過程中,用戶端發送的請求都包含這個Cookie,因此伺服器能夠識別出用戶端曾經串連過伺服器。
要實現與瀏覽器一樣的效果,MIDP應用程式必須也能識別Cookie,並在每個要求標頭中包含此Cookie。
在處理每次串連的響應中,我們都檢查是否有Set-Cookie這個頭,如果有,則是伺服器第一次發送的Session ID,或者伺服器認為會話逾時,需要重建一個Session ID。如果檢測到Set-Cookie頭,就將其儲存,並在隨後的每次請求中附加它:
String session = null;
String cookie = hc.getHeaderField("Set-Cookie");
if(cookie!=null) {
int n = cookie.indexOf(';');
session = cookie.substring(0, n);
}
使用Sniffer程式可以捕獲到不同的Web伺服器發送的Session。WebLogic Server 7.0返回的Session如下:
Set-Cookie: JSESSIONID=CxP4FMwOJB06XCByBWfwZBQ0IfkroKO2W7FZpkLbmWsnERuN5u2L!-1200402410; path=/
而Resin 2.1返回的Session則是:
Set-Cookie: JSESSIONID= aTMCmwe9F5j9;
path=/
運行ASP.Net的IIS返回的Session:
Set-Cookie: ASPSESSIONIDQATSASQB=GNGEEJIDMDFCMOOFLEAKDGGP;
path=/
我們無須關心Session ID的內容,伺服器自己會識別它。我們只需在隨後的請求中附加上這個Session ID即可:
if(session!=null)
hc.setRequestProperty("Cookie", session);
對於URL重寫來保持Session的方法,在PC用戶端可能很有用,但是,由於MIDP程式很難分析出URL中有用的Session資訊,因此,不推薦使用這種方法。