對於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 inte***ce HttpListener {
void onSetSize(int size);
void onFinish(byte[] data, int size);
void onProgress(int percent);
void onError(int code, String message);
}
實現HttpListener接口的是持續自Form的一個HttpWaitUI屏幕,它顯示一個進度條和一些提示信息,並答利用戶隨時中斷連接:
<p>public class HttpWaitUI extends Form implements CommandListener, </p><p>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) { … } }</p>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()方法中實現如下:
<p>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(); </p><p>// 返回響應大小,或者-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]; </p><p>// 響應大小未知,設定一個固定大小的緩沖區 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) {} } }</p>當下載完畢後,HttpWaitUI就獲得了來自服務器的數據,要傳遞給下一個屏幕處理,HttpWaitUI必需包含對此屏幕的引用並通過一個setData(DataInputStream input)方法讓下一個屏幕能非常方便地讀取數據。因此,定義一個DataHandler接口:
public inte***ce DataHandler {
void setData(DataInputStream input) throws IOException;
}HttpWaitUI響應HttpThread的onFinish事件並調用下一個屏幕的setData方法將數據傳遞給它並顯示下一個屏幕:
<p>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] </p><p>Displayable object cannot handle data."); ControllerMIDlet.replace(displayable); } catch(IOException ioe) { … } }</p>
以下載一則消息為例,一個完整的HTTP GET懇求過程如下:
首先,用戶通過點擊某個屏幕的命令希看浏覽指定的一則消息,在commandAction事件中,我們初始化HttpWaitUI和顯示數據的NewsUI屏幕:
<p>public void commandAction(Command c, Displayable d) { HttpWaitUI wait = new HttpWaitUI("<u><font face="宋體" color="#0000ff"><a href="http://192.168.0.1/news.do?id=1">http://192.168.0.1/news.do?id=1</a></font></u>",</p><p>new NewsUI()); ControllerMIDlet.forward(wait); }</p>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代碼如下:
<p>String[] props = ... // 字段名 String[] values = ... // 字段值 byte[] file = ... // 文件內容 String BOUNDARY = </p><p>"---------------------------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; </p><p>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; </p><p>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 + ";</p><p>boundary=" + BOUNDARY); hc.setRequestProperty("Content-Length", </p><p>String.valueOf(data.length + file.length + end_data.length)); // 輸出: output = hc.openOutputStream(); output.write(data); output.write(file); output.write(end_data); // 讀取服務器響應: // TODO...</p>應用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:
<p>Set-CookIE: ASPSESSIONIDQATSASQB=GNGEEJIDMDFCMOOFLEAKDGGP; </p><p>path=/</p>我們無須關心Session ID的內容,服務器自己會辨認它。我們只需在隨後的懇求中附加上這個Session ID即可:
if(session!=null)
hc.setRequestProperty("CookIE", session);對於URL重寫來保持Session的方法,在PC客戶端可能很有用,但是,由於MIDP程序很難分析出URL中有用的Session信息,因此,不推薦應用這種方法。