Orange SPV是第一款进入市场的运行Smartphone 2002的移动电话,现在我们可以开始开发Smartphone应用程序了。本文我们建立了一个可以使用ASP.NET Web服务的Smartphone客户端来提供地理信息。本文假定读者有Web服务、ASP.NET和Win32编程的基本知识。
.NET框架组件使我们有了一组类帮助使用Web服务,从分析WSDL文档到自动建立代理类再到程序依赖。这使大多数依靠Web服务的编程比较琐碎,可是对于Smartphone来说简单框架组件(Compact Framework)还不能使用,因此我们需要作其它的选择。
典型的COM能够使用Soap工具包,对Smartphone来说它也不能使用,但是我们将使用它来帮助跟踪调用。
我们可以依靠Web服务编程仍然不使用这些技术,为了实现它,我们需要查看Web服务的格式和传送。在.NET 1.0 Web服务中使用两种标准来通讯:SOAP和WSDL。
Web服务基础
如果没有基于SOAP的API帮助使用Web服务,我们就需要直接处理SOAP消息和WSDL文档指定的格式。请注意使用WSDL使用的好处之一是类型元素中的概要信息。这些信息指定了SOAP正文的格式,启动WSDL察觉客户端(例如.NET)来定义SOAP包可以进行串并转换的类。
对于Smartphone来说,我们必须构造自己的SOAP消息以确保它们可以被Web服务接受。这些消息通过HTTP POST发送到服务器,我们的客户端等待SOAP响应。这需要按次序分析、检查soap错误,返回提取的值。看起来这需要大量的工作,但是在实际中它与Soap 工具包提供的低层API相近。
手动地从纯WSDL构造SOAP消息是复杂的事务(如map.wsdl显示)。这需要定位正确的操作元素,找到输入消息,映射正确类型。如果Web服务用的是ASP.NET,简单的途径是查看产生的asmx页,选择Webmethod,并查看SOAP请求示例(图1)。
图1 从ASP.NET产生的Web服务文档
如果你不能访问这些信息,有另一个机制用于支持WSDL的客户端调用Web服务,并跟踪SOAP包。SOAP工具包提供了一个工具Trace Utility(MsSoapT3.exe),它对调试Web服务很有用。
图2 Trace Utility,来自Soap工具包3.0
Smartphone Web服务
在上面的请求中很容易看出怎样构造SOAP消息。在这个例子中,我建立了一个SoapWriter类提供写SOAP消息的低层功能。
#include "Soap.h"
SoapWriter *pSoap = new SoapWriter();
pSoap->StartEnvelope();
pSoap->StartBody();
pSoap->StartElement(L"GetLatLong", L"http://mapmobile");
pSoap->WriteElementString(L"addressLine", L"new bond street");
pSoap->WriteElementString(L"city", L"bath");
pSoap->StartElement(L"postCode");
pSoap->EndElement(L"postCode");
pSoap->WriteElementString(L"country", L"UK");
pSoap->EndElement(L"GetLatLong");
pSoap->EndBody();
pSoap->EndEnvelope();
pSoap->FinalizeSoap();
为了向Web服务发送SOAP请求,我将使用WinInet API。WinInet一般用于HTTP和FTP通讯。它为Internet访问提供了高层次API而没有采用WinSock 编程。幸运的是Web服务使用SOAP调用,在我们的例子中使用HTTP调用。
为了使发送SOAP请求更加容易,我建立了SoapConnector类,它允许我们发送SOAP请求和载入响应信息。一旦连接到了Web服务,客户端就可能作多个调用,使用存取程序检索SOAP响应信息。
#include "Soap.h"
SoapWriter *pSoap = new SoapWriter();
//在此处建立SOAP 请求
SoapConnector *pCon = new SoapConnector();
pCon->Init();
//连接到服务器和Web服务
pCon->Connect(L"http://chungw02:8080/mapmobile/map.asmx");
//同SOAP 消息和SoapAction 一起调用
pCon->Invoke(pSoap, L"http://mapmobile/GetLatLong");
//提取响应信息
int iLen = 0;
pCon->GetSoapLength(&iLen);
TCHAR *pResponse = new TCHAR[iLen+1];
pCon->GetSoap(&pResponse);
客户端接收到的XML文件接着被载入DOM分析器并返回提取的值。本例中,我建立了一个将msxml中的IXMLDOMDocument接口暴露的包装类,这需要COM存在。
#include "Soap.h"
//读取SOAP 响应信息
pSoapReader = new SoapReader();
pSoapReader->Init();
//通过传递SoapConnector或者Xml字符串载入SOAP响应信息
pSoapReader->LoadXml(pCon);
一旦它被载入了,可以通过m_pDom成员访问DOM,选择节点值或运行XPath查询。
MSXML::IXMLDOMNode *pNode = NULL;
MSXML::IXMLDOMNodeList *pNodeList = NULL;
MSXML::IXMLDOMNode *pTextNode = NULL;
TCHAR *lpNodeValue = NULL;
TCHAR *XPath = new TCHAR[50];
_tcscpy(XPath, L"/soap:Envelope/soap:Body/Node");
VARIANT vNodeVal;
HRESULT hr;
//使用前面建立的pSoapReader
try {
//选择节点
hr = pSoapReader ->m_pDom->selectSingleNode(XPath, &pNode);
if (FAILED(hr))
__leave;
if (pNode == NULL)
__leave;
//获取子节点
hr = pNode->get_childNodes(&pNodeList);
if (FAILED(hr))
__leave;
//获取下一个节点
hr = pNodeList->get_item(0, &pTextNode);
if (FAILED(hr))
__leave;
//获取文本节点值
VariantInit(&vNodeVal);
hr = pTextNode->get_nodeValue(&vNodeVal);
if (FAILED(hr)) {
VariantClear(&vNodeVal);
__leave;
}
//将值指定给lpNodeValue
lpNodeValue = TCHAR[SysStringLen(vNodeVal.bstrVal) + 1];
_tcscpy(lpNodeValue, vNodeVal.bstrVal);
VariantClear(&vNodeVal);
//使用lpNodeValue处理事务
}
__finally {
if (pNode != NULL)
pNode->Release();
if (pNodeList != NULL)
pNodeList->Release();
if (pTextNode != NULL)
pTextNode->Release();
if (lpNodeValue !=NULL)
delete[] lpNodeValue;
}
作为选择,我提供了一个辅助方法SelectSingleTextNode来提取文本节点。它分配了TCHAR大小空间来适应文本节点的值。
//注意这些有辅助方法SelectSingleTextNode分配
TCHAR *latitude = NULL;
TCHAR *longitude = NULL;
//使用前面建立的pSoapReader
pSoapReader->SelectSingleTextNode(L"/soap:Envelope/soap:Body/GetLatLongResponse
/latitude", &latitude);
pSoapReader->SelectSingleTextNode(L"/soap:Envelope/soap:Body/GetLatLongResponse
/longitude", &longitude);
//使用返回值处理事务
//清除
if (latitude != NULL)
delete[] latitude;
if (longitude != NULL)
delete[] longitude;
即将调用的Web服务返回包含文本节点数据的SOAP消息,因此SelectSingleTextNode方法提供了从Web服务调用中提取数据的一个有用的机制。如果你经常需要提取数据的不同方式,例如属性数据和评估节点集合,SoapReader类可以扩充包含其它的辅助方法。
到此为止SoapWriter、SoapConnector和SoapReader提供了构造、调用和从Web服务读取响应信息的功能。为了用这些类实现客户端,我们需要了解SOAP消息的格式。下面将看到建立Web服务和Smartphone客户端。
MapMobile和MapPoint .NET Web服务
例程包含一个能从Web服务检索地图信息并在Smartphone上的显示的客户端。地理搜索和地图数据由MapPoint .NET提供,它是一个商用Web服务。尽管Smartphone终端用户并没有MapPoint .NET帐号,我建立了一个叫MapMobile的包装Web服务来依靠MapPoint .NET进行调用和认证。对于Smartphone 2002来说没有摘要(digest)认证提供者,因此如果我要直接与MapPoint .NET对话,需要手工实现认证头(header)。包装的其它原因是包含了改变地理数据提供者的能力,而不需要影响终端用户,例如从MapPoint .NET 2.0升级到3.0。
图3. MapMobile结构
我们的包装服务暴露了两个简单的Web服务方法,使客户端能够定位和显示地图。第一个调用返回地址的经度和纬度,第二个调用返回包含匹配的GIF图象的字节数组。
[WebMethod]
[SoapHeader("_phoneNumber", Direction=SoapHeaderDirection.In)]
public void GetLatLong(string addressLine, string city, string postCode, string
country, out double latitude, out double longitude)
[WebMethod]
[SoapHeader("_phoneNumber", Direction=SoapHeaderDirection.In)]
public byte[] GetMap(double latitude, double longitude, double zoom, int width,
int height, string tag)
为了防止MapMobile Web服务的无限制调用,所有的调用都包含一个SOAP头,它包含了Smartphone的电话号码。在服务器端,检测头信息是否为有效的数字。如果失败了,SOAP故障将返回客户端。这种认证机制只供例子使用,不禁止有效号码的欺骗。由于提供了很多安全选项,它们依靠你的Web服务,你可以选择不认证,使用数字署名,或使用新全局XML Web服务结构(Global XML Web Services Architecture,GXA)。
SmartMap客户端
客户端应用程序提供了一个输入屏幕用于输入地址。接着调用MapMobile Web服务并检索存储在文件系统中的地图。这使用户可以查看下载的最新地图。
图4. SmartMap用户界面
我们的客户端应用程序包含一个启动屏幕(主窗体类)和三个对话框(输入屏幕、Web服务调用状态和地图显示)。起初启动屏幕显示,跟着显示Find Location对话框。设备电话号码也被提取和保存,用于soap头中。在仿真程序中这些调用不会工作,因此在我们的例子中,如果调用失败就将数字设为1234567890以供测试。
TCHAR *g_PhoneNumber = new TCHAR[40];
SMS_ADDRESS pAddr;
//获取电话号码
SmsGetPhoneNumber(&pAddr);
_tcscpy(g_PhoneNumber, pAddr.ptsAddress);
当用户选择Search菜单选项,应用程序为MapMobile Web服务构造正确的SOAP调用。这包括包含设备电话号码的SOAP头。
SoapWriter *pSoap = new SoapWriter();
sw->StartEnvelope();
//构造头
sw->StartHeader();
sw->StartHeaderElement(L"MapMobileHeader", L"http://mapmobile");
sw->WriteHeaderElementString(L"PhoneNumber", g_PhoneNumber);
sw->EndHeaderElement(L"MapMobileHeader");
sw->EndHeader();
//Soap正文
sw->StartBody();
//此处构造SOAP正文
sw->EndBody();
sw->EndEnvelope();
sw->FinalizeSoap();
这完成后SoapConnector将被载入SoapReader中用于提取64位编码数据。
TCHAR *map64;
//分配内存到目标变量
hrSearch =
pSoapReader->SelectSingleTextNode(L"/soap:Envelope/soap:Body/GetMapResponse/GetMap
Result", &map64);
我们需要不断检查HRESULT,如果SoapReader失败了,就检查响应信息是否包含Soap故障。
TCHAR &soapfault;
hrSearch =
pSoapReader->SelectSingleTextNode(L"/soap:Envelope/soap:Body/soap:Fault
/faultstring", &soapfault);
if (SUCCEEDED(hrSearch))
MessageBox(hDlg, soapfault, L"Soap Error", MB_OK | MB_ICONWARNING);
else
MessageBox(hDlg, L"Call failed", L"Error", MB_OK | MB_ICONWARNING);
图5. Soap故障传回客户端
如果所有调用成功完成,64位字符串将解码成为一个字符数组并作为SmartMap.gif保存。我们希望该文件在电话关闭也保存在My Documents文件夹中。但是固化存储器的文件结构可以被OEM的改变,因此调用SHGetSpecialFolderPath API来检索这些信息。
CHAR szMapFile[MAX_PATH];
SHGetSpecialFolderPath(hwnd, szMapFile, CSIDL_PERSONAL , FALSE);
_tcscat(szMapFile, L"\\SmartMap.gif");
//在Orange SPV上 szMapFile包含在 \IPSM\My Document\SmartMap.gif
//在模拟器上为: \My Document\SmartMap.gif
最后建立map对话框,它包含一个图象控件(IDC_IMAGE)。
图6.选择其它数据
当该对话框载入后,保存的地图图象将从文件系统中读入并且STM_SETIMAGE传递到对话框句柄。该图象使用IMGDECMP.dll图象库载入,它时CE 3.0平台的一部分。
HBITMAP g_hbm = NULL;
ReadPictureFile(hDlg, szMapFile, & g_hbm);
SendDlgItemMessage(hDlg, IDC_IMAGE, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM) g_hbm);
图7.选择其它数据
这时SmartMap客户端几乎已经完成。如果可能的话,用户需要能查看最新地图。为了实现功能,使用了一个弹出菜单,在Find Location对话框的Option按钮中。如果文件存在就会捕获一个WM_INITMENUPOPUP消息,由此弹出菜单被激活或者禁止。
//菜单弹出前调用
case WM_INITMENUPOPUP:
{
//查看是否文件存在和是否激活最新地图菜单
DWORD dwFile = GetFileAttributes(szMapFile);
BOOL bEnabled = (dwFile == 0xFFFFFFFF ? FALSE : TRUE);
EnableMenuItem((HMENU)wParam, ID_EXIT_LASTMAP, MF_BYCOMMAND |
(bEnabled?MF_ENABLED:MF_GRAYED));
break;
}
如果用户可以选择该菜单,接着map对话框将再次被建立,它从文件系统自动载入图象。
总结
我曾经查阅过Web服务、工具和技术所包含的内容来帮助查看SOAP包。使用提供的例程,我们看到了在Smartphone 2002上,在例子MapPoint .NET中使用Web服务的一条途径。通过建立Web服务察觉(service-aware)客户端,我们获得了工作于混合连接环境,连接和断开连接,合计和储存数据的移动设备上的新一代应用程序的好处,并提供了新的经验。