`
lixinye0123
  • 浏览: 332325 次
  • 性别: Icon_minigender_1
  • 来自: 温州
社区版块
存档分类
最新评论

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://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

信息,因此,不推荐使用这种方法。

分享到:
评论

相关推荐

    J2ME编程简介(内含游戏案例)

    通过这本书的学习,读者不仅可以掌握J2ME编程的基础,还能了解到游戏开发的一些通用原则和技术。此外,通过实践案例,读者将有机会将理论知识转化为实际操作,增强编程和问题解决的能力。总的来说,这本书是J2ME...

    J2ME游戏编程

    **J2ME游戏编程**是移动设备上的游戏开发领域,主要针对Java Micro Edition(J2ME)平台。J2ME是一种轻量级的Java平台,主要用于嵌入式系统,包括手机、PDA和其他小型设备。它由两部分组成:KVM(K Virtual Machine...

    J2ME编程源代码(书中代码)

    **J2ME编程源代码详解** Java Micro Edition (J2ME) 是Java平台的一个子集,主要用于嵌入式设备和移动设备的开发,如手机、智能手表和家用电器等。J2ME为开发者提供了一个标准化的环境来创建跨平台的应用程序。在...

    j2ME无线编程开发基础与入门

    **J2ME无线编程开发基础与入门** J2ME(Java Micro Edition)是Java技术的一个重要分支,由美国Sun Microsystems(后被Oracle收购)在20世纪90年代末提出,旨在为小型设备和嵌入式系统提供Java编程能力。J2ME的主要...

    J2ME游戏开发(中文译本)

    7. **网络编程**:J2ME支持联网功能,书中有章节讲述如何实现在线对战、数据同步和更新等网络特性。 8. **性能优化**:由于移动设备硬件限制,性能优化是J2ME游戏开发的关键。书中会给出如何减少内存消耗、提高代码...

    JAVA基于J2ME的手机游戏开发(论文+源代码)

    Java基于J2ME的手机游戏开发是一个广泛的领域,涉及到许多技术细节和步骤。...这个主题涵盖了从基本的编程概念到高级的游戏设计技巧,对于希望进入移动游戏开发领域的程序员来说,是一个有价值的参考资料。

    J2me开发大全随书源码大全.rar

    同时,由于J2ME与Android在某些底层概念上相似,如Java语言基础和面向对象编程,因此,这些J2ME的实践经验也有助于开发者过渡到Android游戏开发。 在实际学习过程中,建议按照以下步骤进行: 1. **阅读源码**:...

    J2ME使用教程,手机游戏开发

    在“16314_教案”这个压缩包文件中,可能包含了J2ME的教程材料,包括概念解释、代码示例、开发步骤和可能的项目实践。通过深入学习这些内容,开发者可以逐步掌握J2ME的开发技能,从而创作出属于自己的手机游戏或其他...

    j2me手机编程必看教程.avi

    3. **必看教程.avi**:这表明该视频教程对于那些希望深入j2ME编程的人来说是不可或缺的学习资源。.avi文件是一种常见的视频格式,可以在多种媒体播放器上观看。 【知识点详解】: 1. **j2ME架构**:j2ME由配置...

    J2ME开发大全,开发技巧,实例

    **J2ME开发大全, 开发技巧与实例详解** J2ME,全称为Java Micro Edition,是Java平台的一个子集,...《J2ME开发大全》这本书可能涵盖了以上所有知识点,并提供了丰富的示例代码和实践指导,是J2ME学习者的宝贵资源。

    J2ME游戏开发中的WTK游戏框架

    "J2ME" 是Java 2 Micro Edition的缩写,它是Java平台的一个版本,用于小型设备和嵌入式系统的编程。 "WTK" 是Wireless Toolkit的缩写,它是Java ME开发环境的一部分,主要用于创建 MIDP(Mobile Information ...

    精通J2ME无线编程.pdf

    通过阅读《精通J2ME无线编程》,读者不仅可以掌握J2ME编程的基本技能,还能深入理解移动开发中的挑战和解决方案,为开发出适应各种设备特性的高质量应用打下坚实基础。无论你是初学者还是经验丰富的开发者,这本书都...

    J2ME手机游戏案例精编随书电子教程+实例+开发教程

    在《J2ME开发教程.pdf》中,你会找到关于J2ME编程环境的设置,例如NetBeans或Eclipse IDE的配置,以及如何使用Java Wireless Toolkit (J2ME SDK)进行开发和模拟测试。教程可能还会讲解如何创建MIDlet(J2ME应用程序...

    J2ME开发实例7.rar_j2me

    通过这些源代码,我们可以深入理解J2ME的编程机制和最佳实践。 1. **J2ME架构与配置** J2ME由配置(Configurations)和配置文件(Profiles)组成。配置定义了基础的Java虚拟机(JVM)和核心库,而配置文件则添加了...

    J2ME学习资料打包下载

    本压缩包包含了丰富的J2ME学习资源,包括游戏开发和中文教程,对于想要入门或提升J2ME编程技能的开发者来说极具价值。 "j2me游戏开发.rar":这个文件很可能是一个关于J2ME游戏开发的教程或实例集合。J2ME的游戏开发...

    j2me手机高级编程

    这本书的源代码包含多个章节的实例,涵盖了从基础到高级的J2ME编程技巧。 1. **J2ME基础知识**:J2ME由配置(Configurations)和 profiles(Profiles)组成,其中配置定义了硬件和系统资源的基本需求,而profiles则...

    J2ME 开发入门课件

    本课件是一套完整的J2ME开发入门教程,包含了从基础知识到实践应用的全方位讲解,对于想要学习Java在小型设备上开发的初学者来说,具有极高的学习价值。 **1. J2ME架构与平台** J2ME由配置(Configurations)和 ...

    j2me经典游戏源码

    10. **错误处理和调试**:源码将展示如何在J2ME环境中处理异常和进行调试,这对于任何开发者都是宝贵的实践经验。 通过研究这个"j2me经典游戏源码",你可以深入了解J2ME的编程模型,掌握在有限资源设备上构建可扩展...

    J2ME移动应用开发讲义及开发源码非常实用

    4. **网络通信**:讨论如何利用J2ME的套接字和流进行网络通信,实现数据的发送和接收,这对于开发联网应用至关重要。 5. **数据存储**:涵盖本地数据存储的方法,如Record Management System (RMS) 和 Persistent ...

    j2me开发指南第五讲

    总结来说,J2ME开发涵盖从基础的MIDP编程到复杂的图形和AI设计,开发者需要熟练掌握这些概念和技术,才能创造出吸引人的移动游戏。通过实践和不断学习,J2ME开发者能够在这个领域取得显著成就。

Global site tag (gtag.js) - Google Analytics