針對無線裝置資源限制 (CPU、記憶體、頻寬、延遲),本文整理出 J2ME 網路開發的最佳實踐方案,涵蓋協定選擇、客戶端/伺服器端優化、多執行緒架構、進度回饋、斷點續傳、Cookie 會話管理等完整體系。
HTTP/HTTPS 是唯一強制支援的協定 (MIDP 1.0/2.0)。Socket/UDP 屬選配,僅作為效能極致需求的補充。為追求最大相容性與程式碼複用,**首選 HTTP**。
| # | 準則 | 實作要點 |
|---|---|---|
| 1 | 設定 User-Agent | conn.setRequestProperty("User-Agent", "MyApp/2.1 MIDP/2.0"); 讓伺服器識別版本 |
| 2 | 顯示下載進度 | Gauge 進度條 + 取消命令,callSerially 更新 UI |
| 3 | 資料快取策略 | 記憶體快取熱點資料,RMS 持久化離線資料,設定 TTL 淘汰 |
| 4 | 非同步網路執行緒 | Observer 模式 (HttpListener) 解耦網路層與 UI |
| 5 | 優雅錯誤處理 | 區分網路錯誤/HTTP 錯誤/業務錯誤,友善提示並可重試 |
| # | 準則 | 實作要點 |
|---|---|---|
| 1 | 輸出 Content-Length | 讓客戶端預先判斷記憶體需求,避免 OOM |
| 2 | 拒絕 HTML 回應 | 回傳緊湊二進位 (application/octet-stream) 或 JSON |
| 3 | 避免重導向 | 301/302 會導致雙倍連線成本與延遲 |
| 4 | 標準 HTTP 狀態碼 | 404/401/500 等,客戶端只讀 Header 即知錯誤 |
| 5 | 版本適配邏輯前置 | 依 User-Agent 回傳相容格式,舊版客戶端免更新 |
// 觀察者介面
public interface HttpListener {
void onSetSize(int totalBytes); // 開始下載,已知總大小
void onProgress(int percent); // 進度更新 0-100
void onFinish(byte[] data, int size); // 完成,傳回資料
void onError(int code, String msg); // 錯誤代碼 + 訊息
}
// 背景下載執行緒
class HttpThread extends Thread {
private static final int MAX_BUFFER = 20 * 1024; // 20KB 上限
private final String url;
private final HttpListener listener;
private volatile boolean cancelled = false;
private byte[] buffer;
public HttpThread(String url, HttpListener listener) {
this.url = url; this.listener = listener;
}
public void cancel() { cancelled = true; }
public void run() {
HttpConnection conn = null;
InputStream in = null;
try {
conn = (HttpConnection) Connector.open(url);
conn.setRequestMethod(HttpConnection.GET);
conn.setRequestProperty("User-Agent", "MyApp/1.0");
int rc = conn.getResponseCode();
if (rc != HttpConnection.HTTP_OK) {
listener.onError(rc, conn.getResponseMessage());
return;
}
int total = (int) conn.getLength();
listener.onSetSize(total);
in = conn.openInputStream();
buffer = (total > 0) ? new byte[total] : new byte[MAX_BUFFER];
int idx = 0, lastPercent = 0;
while (!cancelled) {
int space = buffer.length - idx;
int readLen = Math.min(space, 128);
int read = in.read(buffer, idx, readLen);
if (read <= 0) break;
idx += read;
if (total > 0) {
int pct = idx * 100 / total;
if (pct != lastPercent) {
lastPercent = pct;
listener.onProgress(pct);
}
}
}
if (cancelled) return;
if (in.available() > 0) { listener.onError(601, "Buffer overflow"); return; }
if (total > 0 && idx != total) { listener.onError(102, "Length mismatch"); return; }
listener.onFinish(buffer, idx);
} catch (IOException e) {
listener.onError(101, "IOException: " + e.getMessage());
} finally {
closeQuietly(in);
closeQuietly(conn);
}
}
}
public class HttpWaitScreen extends Form implements CommandListener, HttpListener {
private final Gauge gauge = new Gauge("進度", false, 100, 0);
private final Command cancel = new Command("取消", Command.CANCEL, 0);
private final HttpThread downloader;
private final Displayable nextScreen;
public HttpWaitScreen(String url, Displayable nextScreen) {
super("連線中...");
this.nextScreen = nextScreen;
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();
Navigator.goBack(); // 返回上一畫面
}
}
public void onSetSize(int size) { /* 可選:顯示總大小 */ }
public void onProgress(int pct) { gauge.setValue(pct); }
public void onFinish(byte[] data, int size) {
byte[] payload = (size == data.length) ? data : Arrays.copyOf(data, size);
try (DataInputStream dis = new DataInputStream(
new ByteArrayInputStream(payload))) {
if (nextScreen instanceof DataHandler) {
((DataHandler) nextScreen).setData(dis);
}
Navigator.forward(nextScreen);
} catch (IOException e) {
onError(103, "Data parse error");
}
}
public void onError(int code, String msg) {
Alert alert = new Alert("錯誤", msg, null, AlertType.ERROR);
alert.setTimeout(3000);
Display.getDisplay(MIDletContext.get()).setCurrent(alert, Navigator.getPrevious());
}
}
// 下一畫面實作此介面接收資料
public interface DataHandler {
void setData(DataInputStream in) throws IOException;
}
// 使用者點選新聞標題
public void commandAction(Command c, Displayable d) {
if (c == readCmd) {
String url = "https://api.example.com/news.do?id=" + newsId;
HttpWaitScreen wait = new HttpWaitScreen(url, new NewsDetailScreen());
Navigator.forward(wait);
}
}
// 詳細畫面解析資料
public class NewsDetailScreen extends Form implements DataHandler {
public void setData(DataInputStream in) throws IOException {
String title = in.readUTF();
long time = in.readLong();
String body = in.readUTF();
append(new StringItem("標題", title));
append(new StringItem("時間", new Date(time).toString()));
append(new StringItem(null, body));
}
}
byte[] body = "user=admin&pass=1234".getBytes();
conn.setRequestMethod(HttpConnection.POST);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Content-Length", String.valueOf(body.length));
OutputStream os = conn.openOutputStream();
os.write(body); os.flush();
String BOUNDARY = "----MobileFormBoundary" + System.currentTimeMillis();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
// 文字欄位
writeField(dos, "title", "My Photo");
// 檔案欄位
writeFile(dos, "photo", "image.jpg", "image/jpeg", imageBytes);
byte[] header = baos.toByteArray();
byte[] footer = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
conn.setRequestProperty("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY);
conn.setRequestProperty("Content-Length",
String.valueOf(header.length + footer.length));
OutputStream os = conn.openOutputStream();
os.write(header); os.write(footer); os.flush();
String sessionId = null;
// 每次請求前附加 Cookie
if (sessionId != null) {
conn.setRequestProperty("Cookie", sessionId);
}
// 每次回應檢查 Set-Cookie
String cookie = conn.getHeaderField("Set-Cookie");
if (cookie != null) {
int semi = cookie.indexOf(';');
sessionId = (semi > 0) ? cookie.substring(0, semi) : cookie;
// 可選:持久化至 RMS 供下次啟動使用
}
常見 Session ID 格式:JSESSIONID=xxx; path=/ (Tomcat/Jetty)、ASPSESSIONID=xxx (IIS)。
遵循這套方案,您將能開發出在 2G/3G/早期智慧型手機上都能流暢、穩定、省電運作的 J2ME 網路應用程式。