由于無(wú)線(xiàn)設備所能支持的網(wǎng)絡(luò )協(xié)議非常有限,僅限于HTTP,Socket,UDP等幾種協(xié)議,不同的廠(chǎng)家可能還支持其他網(wǎng)絡(luò )協(xié)議,但是,MIDP 1.0規范規定,HTTP協(xié)議是必須實(shí)現的協(xié)議,而其他協(xié)議的實(shí)現都是可選的。
對于MIDP應用程序,應當盡量做到:
1.發(fā)送請求時(shí),附加一個(gè)User-Agent頭,傳入MIDP和自身版本號,以便服務(wù)器能識別此請求來(lái)自MIDP應用程序,并且根據版本號發(fā)送相應的相應。
2.連接服務(wù)器時(shí),顯示一個(gè)下載進(jìn)度條使用戶(hù)能看到下載進(jìn)度,并能隨時(shí)中斷連接。
3.由于無(wú)線(xiàn)網(wǎng)絡(luò )連接速度還很慢,因此有必要將某些數據緩存起來(lái),可以存儲在內存中,也可以放到RMS中。
對于服務(wù)器端而言,其輸出響應應當盡量做到:
1. 明確設置Content-Length字段,以便MIDP應用程序能讀取HTTP頭并判斷自身是否有能力處理此長(cháng)度的數據,如果不能,可以直接關(guān)閉連接而不必繼續讀取HTTP正文。
2. 服務(wù)器不應當發(fā)送HTML內容,因為MIDP應用程序很難解析HTML,XML雖然能夠解析,但是耗費CPU和內存資源,因此,應當發(fā)送緊湊的二進(jìn)制內容,用DataOutputStream直接寫(xiě)入并設置Content-Type為application/octet-stream。
3. 盡量不要重定向URL,這樣會(huì )導致MIDP應用程序再次連接服務(wù)器,增加了用戶(hù)的等待時(shí)間和網(wǎng)絡(luò )流量。
4. 如果發(fā)生異常,例如請求的資源未找到,或者身份驗證失敗,通常,服務(wù)器會(huì )向瀏覽器發(fā)送一個(gè)顯示出錯的頁(yè)面,可能還包括一個(gè)用戶(hù)登錄的Form,但是,向MIDP發(fā)送錯誤頁(yè)面毫無(wú)意義,應當直接發(fā)送一個(gè)404或401錯誤,這樣MIDP應用程序就可以直接讀取HTTP頭的響應碼獲取錯誤信息而不必繼續讀取相應內容。
5. 由于服務(wù)器的計算能力遠遠超過(guò)手機客戶(hù)端,因此,針對不同客戶(hù)端版本發(fā)送不同響應的任務(wù)應該在服務(wù)器端完成。例如,根據客戶(hù)端傳送的User-Agent頭確定客戶(hù)端版本。這樣,低版本的客戶(hù)端不必升級也能繼續使用。
MIDP的聯(lián)網(wǎng)框架定義了多種協(xié)議的網(wǎng)絡(luò )連接,但是每個(gè)廠(chǎng)商都必須實(shí)現HTTP連接,在MIDP 2.0中還增加了必須實(shí)現的HTTPS連接。因此,要保證MIDP應用程序能在不同廠(chǎng)商的手機平臺上移植,最好只使用HTTP連接。雖然HTTP是一個(gè)基于文本的效率較低的協(xié)議,但是由于使用特別廣泛,大多數服務(wù)器應用的前端都是基于HTTP的Web頁(yè)面,因此能最大限度地復用服務(wù)器端的代碼。只要控制好緩存,仍然有不錯的速度。
SUN的MIDP庫提供了Javax.microediton.io包,能非常容易地實(shí)現HTTP連接。但是要注意,由于網(wǎng)絡(luò )有很大的延時(shí),必須把聯(lián)網(wǎng)操作放入一個(gè)單獨的線(xiàn)程中,以避免主線(xiàn)程阻塞導致用戶(hù)界面停止響應。事實(shí)上,MIDP運行環(huán)境根本就不允許在主線(xiàn)程中操作網(wǎng)絡(luò )連接。因此,我們必須實(shí)現一個(gè)靈活的HTTP聯(lián)網(wǎng)模塊,能讓用戶(hù)非常直觀(guān)地看到當前上傳和下載的進(jìn)度,并且能夠隨時(shí)取消連接?! ∫粋€(gè)完整的HTTP連接為:用戶(hù)通過(guò)某個(gè)命令發(fā)起連接請求,然后系統給出一個(gè)等待屏幕提示正在連接,當連接正常結束后,前進(jìn)到下一個(gè)屏幕并處理下載的數據。如果連接過(guò)程出現異常,將給用戶(hù)提示并返回到前一個(gè)屏幕。用戶(hù)在等待過(guò)程中能夠隨時(shí)取消并返回前一個(gè)屏幕。
我們設計一個(gè)HttpThread線(xiàn)程類(lèi)負責在后臺連接服務(wù)器,HttpListener接口實(shí)現Observer(觀(guān)察者)模式,以便HttpThread能提示觀(guān)察者下載開(kāi)始、下載結束、更新進(jìn)度條等。HttpListener接口如下:
public interface HttpListener {
void onSetSize(int size);
void onFinish(byte[] data, int size);
void onProgress(int percent);
void onError(int code, String message);
}
實(shí)現HttpListener接口的是繼承自Form的一個(gè)HttpWaitUI屏幕,它顯示一個(gè)進(jìn)度條和一些提示信息,并允許用戶(hù)隨時(shí)中斷連接:
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連接的線(xiàn)程類(lèi),它接受一個(gè)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獲取內容
我們先討論最簡(jiǎn)單的GET請求。GET請求只需向服務(wù)器發(fā)送一個(gè)URL,然后取得服務(wù)器響應即可。在HttpThread的run()方法中實(shí)現如下:
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如果大小無(wú)法確定
listener.onSetSize(size);
// 開(kāi)始讀響應:
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]; // 響應大小未知,設定一個(gè)固定大小的緩沖區
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) { // 更新進(jìn)度
tmp_percent = index * 100 / size;
if(tmp_percent!=percent) {
percent = tmp_percent;
listener.onProgress(percent);
}
}
}
if(!cancel && input.available()>0) // 緩沖區已滿(mǎn),無(wú)法繼續讀取
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就獲得了來(lái)自服務(wù)器的數據,要傳遞給下一個(gè)屏幕處理,HttpWaitUI必須包含對此屏幕的引用并通過(guò)一個(gè)setData(DataInputStream input)方法讓下一個(gè)屏幕能非常方便地讀取數據。因此,定義一個(gè)DataHandler接口:
public interface DataHandler {
void setData(DataInputStream input) throws IOException;
}
HttpWaitUI響應HttpThread的onFinish事件并調用下一個(gè)屏幕的setData方法將數據傳遞給它并顯示下一個(gè)屏幕:
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) { … }
}
以下載一則新聞為例,一個(gè)完整的HTTP GET請求過(guò)程如下:
首先,用戶(hù)通過(guò)點(diǎn)擊某個(gè)屏幕的命令希望閱讀指定的一則新聞,在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實(shí)現DataHandler接口并負責顯示下載的數據:
需要獲得聯(lián)網(wǎng)數據的屏幕只需實(shí)現DataHandler接口,并向HttpWaitUI傳入一個(gè)URL即可復用上述代碼,無(wú)須關(guān)心如何連接網(wǎng)絡(luò )以及如何處理用戶(hù)中斷連接。
使用POST發(fā)送數據
以POST方式發(fā)送數據主要是為了向服務(wù)器發(fā)送較大量的客戶(hù)端的數據,它不受URL的長(cháng)度限制。POST請求將數據以URL編碼的形式放在HTTP正文中,字段形式為fieldname=value,用&分隔每個(gè)字段。注意所有的字段都被作為字符串處理。實(shí)際上我們要做的就是模擬瀏覽器POST一個(gè)表單。以下是IE發(fā)送一個(gè)登陸表單的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應用程序中模擬瀏覽器發(fā)送這個(gè)POST請求,首先設置HttpConnection的請求方式為POST:
hc.setRequestMethod(HttpConnection.POST);
然后構造出HTTP正文:
byte[] data = "username=admin&password=1234".getBytes();
并計算正文長(cháng)度,填入Content-Type和Content-Length:
hc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
hc.setRequestProperty("Content-Length", String.valueOf(data.length));
然后打開(kāi)OutputStream將正文寫(xiě)入:
OutputStream output = hc.openOutputStream();
output.write(data);
需要注意的是,數據仍需要以URL編碼格式編碼,由于MIDP庫中沒(méi)有J2SE中與之對應的URLEncoder類(lèi),因此,需要自己動(dòng)手編寫(xiě)這個(gè)encode()方法,可以參考java.net.URLEncoder.java的源碼。剩下的便是讀取服務(wù)器響應,代碼與GET一致,這里就不再詳述。
使用multipart/form-data發(fā)送文件
hc.setRequestProperty("Content-Type", "multipart/form-data; boundary=ABCD");
然后,將每個(gè)字段用“--分隔符”分隔,最后一個(gè)“--分隔符--”表示結束。例如,要上傳一個(gè)title字段"Today"和一個(gè)文件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發(fā)送的POST請求,可以發(fā)現IE的分隔符類(lèi)似于---------------------------7d4a6d158c9,這是IE產(chǎn)生的一個(gè)隨機數,目的是防止上傳文件中出現分隔符導致服務(wù)器無(wú)法正確識別文件起始位置。我們可以寫(xiě)一個(gè)固定的分隔符,只要足夠復雜即可。
發(fā)送文件的POST代碼如下:
通常服務(wù)器使用Session來(lái)跟蹤會(huì )話(huà)。Session的簡(jiǎn)單實(shí)現就是利用Cookie。當客戶(hù)端第一次連接服務(wù)器時(shí),服務(wù)器檢測到客戶(hù)端沒(méi)有相應的Cookie字段,就發(fā)送一個(gè)包含一個(gè)識別碼的Set-Cookie字段。在此后的會(huì )話(huà)過(guò)程中,客戶(hù)端發(fā)送的請求都包含這個(gè)Cookie,因此服務(wù)器能夠識別出客戶(hù)端曾經(jīng)連接過(guò)服務(wù)器。
要實(shí)現與瀏覽器一樣的效果,MIDP應用程序必須也能識別Cookie,并在每個(gè)請求頭中包含此Cookie。
在處理每次連接的響應中,我們都檢查是否有Set-Cookie這個(gè)頭,如果有,則是服務(wù)器第一次發(fā)送的Session ID,或者服務(wù)器認為會(huì )話(huà)超時(shí),需要重新生成一個(gè)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服務(wù)器發(fā)送的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:
if(session!=null)
hc.setRequestProperty("Cookie", session);
對于URL重寫(xiě)來(lái)保持Session的方法,在PC客戶(hù)端可能很有用,但是,由于MIDP程序很難分析出URL中有用的Session信息,因此,不推薦使用這種方法。
(出處:http://www.hackhome.com/)
聯(lián)系客服