摘要
在这个MIDP系列的最后一部分,我将介绍MIDlet和外部系统之间的通信方法。使用Java2 Micro Edition的MIDP中包含的API,开发者可以与外部的系统交互。这篇文章将以一个详细的例子介绍这些API,它演示了J2ME设备和一个基于servlet的Web系统可以进行的交互。
在前面的两个部分,我介绍的MIDP API的特性和功能都是和设备自身的运作和信息存储相关的。在这个最后的部分,我将集中介绍网络设备和大型网络的交互。
网络设备可以使用数不清的协议来进行互相通信。在这篇文章中,我将集中介绍HttpConnection接口,你可以通过它来访问存储在一个Web服务器上的信息。在介绍本文的例子之前,我将讨论javax.microedition.io接口和类的交互,而例子则集中介绍一个MIDP设备和一个基于JSP系统之间的交互。
Connection层次
javax.microedition.io包中的所有接口都是基于Connection接口的。其它的连接接口继承Connection中的方法,并且定义了用来访问相关变量和该Connection类型动作的方法。我将在本文中谈论最常用的接口,其它的接口留给读者研究。
HTTPConnection说明
HTTPConnection接口建立在Connection接口上,它还提供了一些其它的方法来进行HTTP交互。以下就是其中一些有用的方法列表:
String getHeaderField(int index)
String getHeaderField(String name)
long getHeaderFieldDate(String name, long def)
int getHeaderFieldInt(String name, int def)
String getHeaderFieldKey(int n)
String getHost()
long getLastModified()
int getPort()
String getProtocol()
String getQuery()
String getRef()
String getRequestMethod()
String getRequestProperty(String key)
int getResponseCode()
String getResponseMessage()
String getURL()
void setRequestMethod(String method)
void setRequestProperty(String key, String value)
这些方法可让你如基于servlet的系统一样访问HTTP字段。
其它的接口在API规范文档中有明确的定义。这些接口带有一些方法,可以使用不同的协议与设备收发数据包或者流数据。在这里我将不会很详细地讨论这些,因为在形式上是一样的。
Connector对象
MIDP API如何知道要创建哪个接口并返回给调用的类呢?答案是返回的Connector是根据传送给连接字符串的值。
以下的连接字符串是提醒Connector对象系统正在查找一个HttpConnection:
HttpConnection httpConn = Connector.open("http://www.itpath.com");
MIDP Connector对象分析连接的字符串,知道这是一个用作访问网页的URL,于是就会返回一个HttpConnection接口的实现给调用的类。
其它的连接协议需要不同的连接字符串。下表就是它们的一个列表:
协议 连接字符串
Http http://www.yahoo.com
Stream-based Socket Socket://localhost:6160
Datagram-based Socket - listening datagram://:6160
Datagram-based Socket - sending datagram://121.232.121.232:6160
Serial Port comm.:0;baudrate=5000
File file://helloWorld.txt
例子
以下的例子将本文讨论的东西都结合在一起。在这个例子中,MIDlet访问存储在一个远程系统上的信息。该信息以一个XML的形式返回给MIDlet。通过分析该XML,MIDlet就会根据这些数据构造一个用户界面。
用户界面由一个问题构成。用户提交后将会请求服务器将数据加进去。然后返回更新的数据给用户。
通过这个详细的例子,你可以对J2ME Connection API的基本用法和语法有更进一步的了解。
VoterMidlet
VoterMidlet是本例子中唯一的MIDlet。在下载时,它创建VoteResults对象的一个实例:
public class VoterMidlet extends MIDlet implements ScreenCallback
{;
private Display _display;
// midlet has three screens
private VoteResults voteResults = new VoteResults(
(ScreenCallback) this);
public VoterMidlet()
{;
_display = Display.getDisplay(this);
_display.setCurrent(voteResults);
};
public void exit()
{;
try
{;
this.destroyApp(true);
}; catch (MIDletStateChangeException e)
{;};
};
...
};
ScreenCallback
如上所示,VoterMidlet实现了ScreenCallback接口。该接口将UI类和一些事件隐藏起来,否则UI类可能需要一个到MIDlet的引用。ScreenCallback接口包含有一个单一的方法public void exit(),UI屏幕使用这个方法来提醒MIDlet,用户已经按下“Exit”按钮。
通过为ScreenCallback写代码,其它的MIDlet方法就与UI屏幕的开发者无关。这是很重要的,因为如果使用不当,一些MIDlet方法会给程序带来破坏性的后果。
VoteResults
VoteResults是一个用户接口类,用来显示投票的结果给用户。为了让例子简化,该接口实现了两个Model-View-Controller类:View和Controller。
该构造器由MIDlet中接收唯一一个参数--ScreenCallback接口。如上所述,该接口可让Screen能够回调MIDlet的一些方法。
构造器初始化该对象并且创建例子的用户界面,以下是其中令人感兴趣的部分:
VoteSummary voteSummary = ResourceUtility.getVoteSummary();
initialize(voteSummary);
以上的代码是负责初始化MIDlet和JSP页面的通信,它模拟的是一个真实的系统。ResourceUtility通过HTTP参数访问一个URL,并且由该JSP中得到信息。它使用该信息来创建一个VoteSummary对象。在后面的例子中我们还将进一步讨论这个接口。
然后就会调用initialize()方法来创建UI显示,它包括有两个StringItems显示前面的投票结果,还有一个ChoiceGroup包含有所有可能的投票和相应的投票值:
public void initialize(VoteSummary voteSummary)
{;
append( getNumVotesString( voteSummary.getNumVotes() )) ;
append( getAvgVoteString( voteSummary.getAvgVote() ));
append( showVoteResults( voteSummary.getVotes() ));
};
当用户输入时,就会调用commandAction()方法。该方法接收设备的输入。如果输入的命令是“Vote”,当前的选项就会由ChoiceGroup中得到。这时就会构造一个Vote对象,并且传送给ResourceUtility.addEntry()方法。该方法就会将信息传送给JSP,JSP就会将新投票加入到记录中,并且返回一个更新的VoteSummary对象。然后它就会调用update()方法:
public void commandAction(Command c, Displayable s)
{;
String command = c.getLabel();
if ( command.equals("Exit") )
{;
_screenCallback.exit();
};
else if ( command.equals("Vote") )
{;
// get the selected item
int selectedIndex = _voteResults.getSelectedIndex();
Vote newVote = new Vote(""+ selectedIndex, null, null);
VoteSummary voteSummary = ResourceUtility.addEntry( newVote);
update( voteSummary );
};
};
VoteSummary
VoteSummary对象包含有当前投票的状态信息。它跟踪全部的投票数字,平均的投票,以及维护投票的一个Vector(可得到每个选项的投票信息):
public VoteSummary(String numVotes, String avgVote, Vector votes)
{;
_numVotes = numVotes;
_avgVote = avgVote;
_votes = votes;
};
根据由JSP返回到MIDlet的XML,就会创建VoteSummary对象。
ResourceUtility
ResourceUtility类由远程服务器上得到XML信息。它使用HttpConnection由JSP中得到XML,然后就会通过XMLUtil 将这些XML转换为一个VoteSummary 对象:
public static VoteSummary getVoteSummary()
{;
String xml = loadVoteResults();
return convertXMLToVoteSummary( xml);
};
上面的getVoteSummary()方法访问这个对象。该方法会依次调用loadVoteResults()和convertXMLToVoteSummary()方法。loadVoteResults()方法如下所示,它调用了backendComms()方法,该方法用来为后台系统增加投票:
public static String loadVoteResults()
{;
return backendComms(LOAD_URL, "");
};
private static String backendComms(String requestURL, String requeststring)
{;
HttpConnection httpConnection = null;
DataInputStream dataInputStream = null;
StringBuffer messagebuffer = new StringBuffer();
String requestString = requestURL + requeststring;
Try
{;
// Open an HTTP connection with the Web server
httpConnection = (HttpConnection)
Connector.open(requestString, Connector.READ_WRITE);
// Set the request method to GET
httpConnection.setRequestMethod(HttpConnection.GET);
// Retrieve the response back from the servlet
dataInputStream =
new DataInputStream(httpConnection.openInputStream());
int inputChar;
// Check the Content-Length first
long contentLength = httpConnection.getLength();
if(contentLength!=-1)
{;
for(int i = 0;I {;
if((inputChar = dataInputStream.read())!= -1)
{;
messagebuffer.append((char)inputChar);
};
};
}; else {;
// if the content-length is not available
while ((inputChar = dataInputStream.read()) != -1)
{;
messagebuffer.append((char) inputChar);
};
};
dataInputStream.close();
}; catch (IOException ioe) {;
messagebuffer = new StringBuffer("ERROR!");
}; catch (Exception e){;
e.printStackTrace();
}; finally {;
try {;
if (httpConnection != null){
httpConnection.close();
}; catch (IOException ignored) {;};
try {;
if (dataInputStream != null)
dataInputStream.close();
}; catch (IOException ignored) {;};
};
return messagebuffer.toString();
};
以下我们讨论一下backendComms()方法。首先由try-catch块开始。第一步是打开一个到服务器的读/写HttpConnection。上面我已经说过,该方法得到XML并且对服务器发出写请求,因此我们需要使用一个READ_WRITE连接:
try
{;
// Open an HTTP connection with the Web server
httpConnection = (HttpConnection)
Connector.open(requestString, Connector.READ_WRITE);
// Set the request method to GET
httpConnection.setRequestMethod(HttpConnection.GET);
// Retrieve the response back from the JSP
dataInputStream =
new DataInputStream(httpConnection.openInputStream());
要得到一个HttpConnection实现,我们调用Connector.open()方法。该类包含有静态的方法,可根据连接字符串产生用作通信的相应接口。在这个例子中,我们要得到一个HttpConnection,因此我们将Connector.open()的响应放入HttpConnection接口。
一旦我们发出请求,我们就可以使用HttpConnection对象的openInputStream()方法,通过它可由JSP中得到结果的InputStream:
if(contentLength!=-1)
{;
for(int i = 0;I {;
if((inputChar = dataInputStream.read())!= -1)
{;
messagebuffer.append((char)inputChar);
};
};
}; else {;
// if the content-length is not available
while ((inputChar = dataInputStream.read()) != -1)
{;
messagebuffer.append((char) inputChar);
};
};
然后就可以读取该响应内容的长度,如果没有找到,你也可以一直读取它直到得到一个EOF或者出现一个错误的字符。每个由DataInputStream读取的字符都被加入到StringBuffer中。这个StringBuffer包含有一个XML表示的投票结果,它将被传送回调用的方法,然后再传送到convertXMLToVoteSummary()方法,如下所示:
private static VoteSummary convertXMLToVoteSummary(String xml)
{;
InputStreamReader insr = new InputStreamReader(
new ByteArrayInputStream(xml.getBytes() ) );
VoteSummary voteSummary = null;
Try
{;
voteSummary = XMLUtil.getVoteResults(insr);
}; catch (IOException ioe)
{;};
return voteSummary;
};
该方法将XML字符串转换为一个Java对象,这样我们就可以使用方法来得到数据,而不是分析数据。我们使用XMLUtil.getVoteResults()方法来做这个转换。在接触该方法前,我们先完成ResourceUtility对象。最后的一个方法是addEntry(),将新的投票传送到JSP。如上所述,为了效率,它再次使用backendComms()方法:
public static VoteSummary addEntry(Vote vote)
{;
StringBuffer requestString = new StringBuffer();
requestString.append(QUESTION + vote.toRequestString() );
String xml = backendComms(LOAD_URL, requestString.toString() );
return convertXMLToVoteSummary(xml);
};
XMLUtil
XMLUtil对象的作用是将由JSP接收的XML转换为一个VoteSummary对象,以便应用使用。要完成转换,它必须使用一个XML分析器来解析数据。为了减少MIDlet的整体大小,所以要使用一个轻量级的分析器以令MIDlet的整体大小最小化。在这个例子中,我使用的是Enhydra的kXML分析器。
以下就是这个对象中的getVoteResults()方法,我们来看看转换过程是如何进行的:
public static VoteSummary getVoteResults(InputStreamReader insr)
throws IOException
{;
XmlParser parser = new XmlParser (insr);
Document document = new Document();
document.parse( parser );
file://uncomment to see the document written to your console.
file://document.write( new XmlWriter( new OutputStreamWriter
( System.out) ) );
前几行是设置XmlParser和Document对象的。在创建这些对象后,就会调用Document.parse()方法,并且传送XmlParser作为参数。结果的文档包含有一个由JSP传送来的XML信息。如果你对验证信息的格式感兴趣,你可以将ocument.write调用前面的注释去掉,以看请文档的结构:
Element voteExampleElement = document.getElement("vote-example");
Element voteElement = voteExampleElement.getElement("votes");
String numVotes = getTextFromElement(voteElement, "number");
String avgVote = getTextFromElement(voteElement, "average");
在创建Document后,我们现在可使用DOM方法来得到Element对象并且获取其中的值。头两行是用来在XML文档中移动,以到达含有数据的节点。在得到voteElement节点后,我们就可以使用getTextFromElement()方法,这个方法执行多个命令从文档中得到文本节点,并且返回它的值:
Element choicesElement = voteElement.getElement("choices");
int childCount = choicesElement.getChildCount();
上面的两行代码用来取得在选择区域下的节点数目。使用childCount,你可以构造节点,这样就无需使用当前的投票数目。这样做是很重要的,因为以后你可能需要加入更多的投票。使用这些调用,你可以无需修改代码就做到这一点:
for (int i = 0; i < childCount; i++)
{;
if ( choicesElement.getType(i) == Xml.ELEMENT)
{;
choiceElement = choicesElement.getElement(i);
choicevalue = getTextFromElement(choiceElement,"value");
choiceName = getTextFromElement(choiceElement,"name");
choiceVotes = getTextFromElement(choiceElement,"number");
vote = new Vote(choicevalue, choiceName, choiceVotes);
vEntries.addElement(vote);
};
};
return new VoteSummary( numVotes, avgVote, vEntries);
这样我们就可以得到子元素的数目,然后可以遍历它们并且取得其中的投票信息。其中一些子节点是Element,而其它的是没有数据的Text节点。在这个例子中,我们仅关心Elements,因此我们要在循环中设置相应的条件。该循环将由选项Element中得到每个子Elements,并且构造一个Vote对象,该对象包含有值(用作计算)、名字和投票的数目。这些vote对象都被依次加入到Vector中。最后,这个方法初始化一个新的VoteSummary对象,该对象包含有以上三个信息。
private static String getTextFromElement(Element elementRoot,
String elementName)
{;
String returnText = elementRoot.getElement(elementName).getText();
return returnText;
};
我们使用getTextFromElement()方法来令代码的可读性更强。它的作用是深入树形结构一层,并且得到其中的Text节点信息。
voter.jsp
这个系统的最后一个元素是模拟后端系统的JSP。在这个例子中,我创建了一个简单的JSP,它使用类变量来跟踪投票计算。
具体的JSP见源代码。它只是跟踪投票并且返回以XML表示的数据。
最后的要点
这个例子讲解了MIDP和一个服务器端的组件如果通过HttpConnection来进行交互。通过发出一个请求,MIDP可以通过普通的HTTP GET/POST调用来与JSP组件通信。在这个例子中,调用返回XML信息。
使用kXML分析器,XML可以被有效地转换为一个对象,图形类就可以由该对象中获取数据。你也要注意到,在应用中的ChoiceGroup中并没有定义选项的数目,而是通过服务器端的组件动态设置。
与任何多层开发任务一样,要考虑如何有效地实现Model-View-Controller模型而避免创建多余的代码,这个想法在这个例子中也有体现,这个模型就是JSP。view就是显示全部数据信息的VoteResults对象。在这里的controller也是VoteResults对象,由于它实现了CommandListener接口。
通信
在这篇文章中,我们主要讨论了一个可产生HTTP请求的MIDP通信系统。这些请求可以是由使用标准协议的已知端口到任何使用私有协议的应用级协议。其中的API都可以很灵活地以一个很类似的方式处理这些请求。
在与远程机器交互的时候,如果你处于一个覆盖以外的区域(例如进入一个隧道时),这时会怎样呢?与WAP不同,在处于覆盖以外的区域时,具有MIDP功能的电话仍然可以继续运行它的应用。这个技术可以做到让用户使用离线的功能,并且在回到覆盖区域时,具有与在线应用同步的能力。例如一个基于MIDP的email应用:用户可以继续读取和回应已经下载到本地RMS datastore的email。在回到覆盖区域时,回复的信息可以发送到服务器,从而分发到不同的接收者。
当前发布的J2ME Wireless Toolkit版本并不支持HTTPS协议。不过,早些发布的开发者版本确实支持HTTPS。通过这些API的支持,你可以实现在Java电话上交易股票或者进行买卖。