分类: LINUX
2010-02-04 16:45:21
BTFileSend
3、1、8 Listen for a Connection and Security Manager Registeration. 11
在层次结构的蓝牙协议栈中,对象交换协议OBEX(Object Exchange)是一种面向应用的会话层协议(图1),它运行于蓝牙协议栈的顶部,支持文件传输(File Transfer),对象“推”操作(Object Push Profile),同步(Synchronization)等多种应用,提供了设备间简单易行的对象交换手段。可交换的对象可是文件、图像,也可是应用支持的任何数据单位。对象交换采用了基于查询—应答方式的Client/Server 模式,任意两台蓝牙设备间都可组成主从关系,主动发起方是主设备(Client),被找到者是从设备(Server)。
图1
与OBEX相关的过程包括:连接建立(Connect)、连接断开(Disconnect)、发文件(Put)、接收文件(Get)、操作放弃(Abort)、设置路径(Set Path)。对象交换包括文件和目录的交换。Client 端能初始化各种请求,如发送和接收文件,浏览Server 端对象,因此具备解释OBEX 文件夹及其中文件的格式的能力;Server 端与之相对应,作为一种远程目标设备,提供了对象交换的服务以及使用OBEX 文件夹格式的浏览功能,Server 端的只读文件夹以及只读文件,可以限制对象的发出及文件/目录的删除与建立。
OBEX 协议为不同原因产生不同事件,如:何时建立连接,何时接受OBEX 命令,何时完成API 请求。这些事件由底层协议栈产生的其它事件触发,该事件在其线程执行中无时间占用,所以该事件将被尽快执行。
由于初始化事件不同,Client 和Server 端的执行过程是不相同的。Server 端的应用经常是空闲的,等待着连接的建立和Client 端发出的操作请求。在两次发出请求之间,Client 端将等待Server 的应答。
下图简单阐述了Client 端的执行流程:
图2Client执行流程
1、通过用户界面发出PUT / GET 请求;
2、应用程序通过调用 OBEX 层API 请求一个传输层连接,OBEX 协议接口层执行设备恢复功能,该设备与其它设备建立Transport 层连接,当连接完成后,OBEX 层将执行的状态(Success/Failure)通过事件的句柄(Callback 回调函数)通知给应用程序;
3、当传输层连接建立后,应用程序又通知OBEX 层API 建立OBEX 层的连接,操作完成后,事件句柄又将返回结果通知给应用程序;
4、应用程序通过调用 OBEX 层API 来执行(GET/PUT/SETPATH)命令来交换文件。OBEX命令解释器依次呼叫包分析器,将数据交给栈接口层打包,再交给蓝牙协议栈下层传输;
5、贯穿整个交换过程,对象库负责存储或取出对象数据,而该数据通过命令解释器传递,然后,栈接口层通过包分析器来查询命令解释器,看还有没有数据要发送(通过读对象库),直到没有数据发送为止(对象被全部传完);
6、对象交换完成后,执行结果被通知给应用程序;
7、应用程序能够通过OBEX 层API 交换更多的对象,交换完成后,通知OBEX 断开连接。
Server 端执行流程:
1、Server 端通过栈接口层获得从底层协议栈(embedded stack)中得到的连接信息,栈接口层通过事件回调函数将连接信息告知应用;
2、当OBEX 包开始到达栈接口层,便被送到包分析器,包分析器将OBEX 包和其它有效信息一并加入到命令解释器中,命令解释器通过事件回调通知应用执行相应的操作,应用再通知OBEX 层API 接收和丢弃该操作。同样,通过OBEX 层API 可以获得信息包头的信息,帮助决定操作将被接收或丢弃;
3、如果是接收操作,命令解释器就通知对象库执行读或写操作,命令解释器再呼叫包分析器进行打包,通过下层传递到对方设备。Server 端应用并不负责对象数据的交换;
4、有更多的数据包到达时,执行的流程同前。当数据包通过蓝牙协议栈到达OBEX 栈接口层时,OBEX 栈接口层通过包分析器到命令解释器获取更多数据。命令解释器依次访问对象库读这些数据,该过程知道对象传完后才结束;
5、完成后,命令解释器通知应用事件句柄交换的状态,同时,Server 端返回空闲状态,等待更多的OBEX 请求或Client 端的断开请求。
OBEX 文件传输应用的实现过程主要包括初始化、建立连接、文件操作三个阶段。系统首先执行初始化,初始化过程包括Client、Server、对象库的初始化。用户在界面上输入PUT/GET 对象操作请求,系统查询连接是否建立,如未建立,首先进行传输层的连接,成功后再进行OBEX 层连接,直到系统返回连接成功标志后便可进行文件的操作。
建立连接的过程如图3。蓝牙设备通过寻呼消息来建立连接,查询(Inquiry)到指定范围内设备的地址码列表信息,选定某个设备进行连接,客户端查询SDP,在获得了与OBEX Server 应用的对应点后,即建立了传输层的连接。通知应用建立OBEX 层的连接。客户端的文件操作包括两部分,第一步是初始化对象库入口(Object Store Entry),因为OBEX 对交换对象的管理正是通过该入口实现的,因此首先通过初始化入口可以获得目标对象的句柄。 第二步是建立OBEX 信息包的头部(OBEX header),信息包头是用来传递交换的对象属性,如交换对象的数目等,OBEX 定义了多种类型的信息头,不同的操作(如PUT, Abort)建立不同的信息头,向下层发出的连接请求以及回应也是通过读写信息包头的指定域实现的,最后,OBEX 在回调函数中,根据接收到的事件,调用相应的函数完成其功能。
]
IDE Carbide C++ 1.2
S60_3RD_MR SDK
NCF 1.2
S60_SDK_BT_Driver
Nokia N73 环境测试通过
引擎主要实现上述基于OBEX的蓝牙文件传输过程,设计为与程序主逻辑独立的模块方便以后扩展。
Device Discovery----->SDP QUERY--àConnection--àSend and Receive Data--àClose Connection.
BT 的客户端需要先查询到目标设备,通常在Symbian c++ 下有两种办法使用RNotifier和使用RHostResolver。RNotifier 虽然简单但不支持更多的高级操作例如过虑不同服务类型的蓝牙设备,在这里使用后者。
使用RHostResolver 联接的过程如下:
1、使用RSocketServ 对象。
在引擎类中定义:
RSocketServ iSocketServ;
并联接Socket服务,加载指定的协议类型KBTLinkManager:
// get socket server session
User::LeaveIfError(iSocketServ.Connect());
CleanupClosePushL(iScoketServ);
// load protocol for discovery
TProtocolDesc pdesc;
User::LeaveIfError(iSocketServ.FindProtocol(KBTLinkMgr(), pdesc));
使用RHostResolver 对象
// initialize host resolver
User::LeaveIfError(iResolver.Open(iSocketServ, pdesc.iAddrFamily, pdesc.iProtocol));
开始寻找可查询设备
TinquirySockAddr iAddr 用来标识远程设备的地址。
TNameEntry iEntry 用来标识远程设备的名字。
并设置查询方式KGIAC 为普通查询 LIAC 为过虑查询,后者有更好的优先响应。
设置好调用 setAction 可开始查询。
// start device discovery by invoking remote address lookup
iAddr.SetIAC(KGIAC);
iAddr.SetAction(KHostResInquiry|KHostResName|KHostResIgnoreCache);
iResolver.GetByAddress(iAddr, iEntry, iStatus);
SetActive();
程序设计中将该查询的过程设计为活动对象,相对应的Run()会处理查询结果,一直到收到KErrHostResNoMoreResults 结束,否则调用RHostResolver::Next()方法去查询下一个设备,从而获得设备列表。
Run()
{
if ( iStatus!=KErrNone)
{
// all devices (if any) done, notify
iObserver.HandleDeviceDiscoveryCompleteL();
}
else
{
// new device data entry
TDeviceData *devData = new (ELeave) TDeviceData();
devData->iDeviceName = iEntry().iName;
devData->iDeviceAddr = static_cast
devData->iDeviceServicePort = 0;
// add device data entry
iDevDataList->Append(devData);
iDiscoveredDeviceCount++;
// get next discovered device
iResolver.Next(iEntry, iStatus);
// wait for resolver to complete
SetActive();
}
}
上述过程可参考程序中的DeviceDiscoverer.cpp与BTEngine.cpp文件。
使用agent API 对远程服务进行查询,使用DeviceDiscoverer传给的结果对每个设备上的服务做一个搜索:
CSdpAgent
---SetRecordFilterL() 用于搜索满足条件的服务记录ID。
---NextRecordRequest() 处于提交搜索需求。
引擎代码中实现如下:
// init new service discovery agent
iAgent = CSdpAgent::NewL( *this, iDevData->iDeviceAddr );
// set search properties for agent
iSpat = CSdpSearchPattern::NewL();
// use our service id to filter the services discovered
// -> will return only the services with matching service id(s)
TUUID serviceUUID(KBTFileSend_serviceID);
iSpat->AddL(serviceUUID);
iAgent->SetRecordFilterL(*iSpat);
// initiate search
// this will result in call to NextRecordRequestComplete()
iAgent->NextRecordRequestL();
同时覆盖NextRecordRequestComplete()这个方法,用于更新服务的列表与服务的属性。
if ( aError==KErrNone && aTotalRecordsCount>0 )
{
// we got records, retrieve attributes for record
// request protocol descriptor from remote device records,
// we need this to retrieve remote port to connect to later on..
iAgent->AttributeRequestL(aHandle, KSdpAttrIdProtocolDescriptorList);
}
else
{
// done with this device, store data if changed
if ( iDevDataChanged )
{
iDevData->iDeviceServicePort=iPort;
(*iDevDataList)[iDeviceIdx]=iDevData;
iDiscoveredServiceCount++;
}
// discover services on next device, if any left
iDeviceIdx++;
if ( iDeviceIdx
{
// more devices to probe, proceed
DiscoverServicesOnDeviceL((*iDevDataList)[iDeviceIdx]);
}
else
{
FinishDiscovery();
// all devices done, notify
iObserver.HandleServiceDiscoveryCompleteL();
}
}
同时还需要覆盖AttributeRequestComplete()这个方法用于处理每个服务记录属性查询完成后程序的处理逻辑。
if ( aError==KErrNone )
{
// done with attributes for this record, request next
// service record
iAgent->NextRecordRequestL();
}
else
{
// error, should terminate discoverer?
}
对于查询到服务记录的结果我们可以使用覆盖MSdpAttributeValueVisitor的VisitAttributeValueL方法来分析他。
switch (aType)
{
case ETypeUUID:
{
TPtrC8 uuid(aValue.UUID().ShortestForm());
iLastUUID.SetL(uuid);
break;
}
case ETypeUint:
{
if ( iLastUUID==KRFCOMM )
{
// previous call to this method with rfcomm UUID, therefore
// this one will be the value, rfcomm service channel (port)
iPort=aValue.Uint();
// mark device data changed, so the device data record in
// device data list will be updated.
iDevDataChanged=ETrue;
}
break;
}
default:
// rest don't really matter..
break;
需要对每个传送过来的CSdpAttrValue值进行处理。
通过对上述程序的设计我们掌握了Client 端查询服务所需要的流程。进行完上述操作后就可以进行 Connection 操作了。
通过上述两个过程我们获得了远程设备的地址和端口仅仅联接就可以了。
TObexBluetoothProtocolInfo protocolInfo;
protocolInfo.iTransport.Copy(KStrRFCOMM);
protocolInfo.iAddr.SetBTAddr(aRemote);
protocolInfo.iAddr.SetPort(aPort);
_LIT(KDevAddrAndPort, "DevAddr: %S, DevPort: %d");
TBuf<75> devAddrAndPort;
TBuf<12> remoteDevAddr;
aRemote.GetReadable(remoteDevAddr);
devAddrAndPort.Format(KDevAddrAndPort, &remoteDevAddr, aPort);
LOG(ELevel1, devAddrAndPort);
if (iClient)
{
delete iClient;
iClient = NULL;
}
iClient = CObexClient::NewL(protocolInfo);
iClient->Connect(iStatus);
SetActive();
就象在第一部分析的那样当传输层的联接建立后这时候会通知应用程序建立OBEX联接,这里需要使用CObexClient iClient对象。
当联接建立后Client 就可以要求PUT对象
if (iState != EWaitingToSend)
{
User::Leave(KErrDisconnected);
}
else if (IsActive())
{
User::Leave(KErrInUse);
}
iClient->Put(*iCurrObject, iStatus);
SetActive();
if (iState == EWaitingToGetDevice)
{
return;
}
if (iState == EWaitingToSend)
{
// Disconnecting...
iState = EDisconnecting;
iClient->Disconnect(iStatus);
iCurrObject->Reset();
SetActive();
}
else
{
User::Leave(KErrInUse);
}
Listen for a Connection and Security Manager Registeration--à启动OBEX 服务--àSDP database Registration--à --àAccept a connection--à Receive Data--àClose Connection
设备在服务注册之前必须监听连接,用于决定RFCOMM的端口。
代码如下:
// Local variable to channel to listen to.
TInt channel;
RSocketServ socketServer;
// Connect to SocetServer
User::LeaveIfError( socketServer.Connect() );
CleanupClosePushL( socketServer );
RSocket socket;
// Open the Socket connection
User::LeaveIfError( socket.Open(socketServer, KStrRFCOMM) );
CleanupClosePushL( socket );
// Retrieve to one channel that is available.
User::LeaveIfError( socket.GetOpt( KRFCOMMGetAvailableServerChannel,KSolBtRFCOMM, channel ) );
// Set the Socket's Port.
TBTSockAddr sockaddr;
sockaddr.SetPort( channel );
所有的蓝牙服务端都需要进行安全注册,从而可协商和远程主机进行通信时使用的授权能力。安全注册的过程需要使用RBTMan 对象来获得会话,同时使用RBTSecuritySettings 来进行注册和注销设置。另一个办法就是在获得RFCOMM端口时就指定安全策略在设置监听端口的同时指定。在这里使用后者,没有比较过两者的优越性,只是后者更方便简洁。代码如下:
// Set the security according to.
TBTServiceSecurity serviceSecurity;
serviceSecurity.SetUid ( KUidBTFileSend );
serviceSecurity.SetAuthentication ( aAuthentication );
serviceSecurity.SetEncryption ( aEncryption );
serviceSecurity.SetAuthorisation ( aAuthorisation );
serviceSecurity.SetDenied( aDenied );
// Attach the security settings.
sockaddr.SetSecurity(serviceSecurity);
// Bind and start listening the port with security stetted,
User::LeaveIfError(socket.Bind(sockaddr));
User::LeaveIfError(socket.Listen(KSimultainousSocketsOpen));
// now close the socket and the socket server
CleanupStack::PopAndDestroy(); // socket
CleanupStack::PopAndDestroy(); // socketServer
//LOG(ELevel1, _L("CBTObexServer::SetSecurityWithChannelL()"));
return channel;
// start the OBEX server
TObexBluetoothProtocolInfo obexProtocolInfo;
obexProtocolInfo.iTransport.Copy(KStrRFCOMM);
obexProtocolInfo.iAddr.SetPort( channel );
iObexServer = CObexServer::NewL( obexProtocolInfo );
iObexServer->Start( this );
、1、10 SDP database Registeration
为了在服务端注册服务需要使用 RSdp 对象取得一个服务查询数据库的会话,使用RSdpDatabase::Open()来获得会话的子会话对象,可使用获得的子会话对象增加、修改和删除远程可查询的服务记录和记录的属性。
RSdp iSdp; // service discovery protocol session
RSdpDatabase iSdpDB; // service discovery database (sdp)
TSdpServRecordHandle iRecord; // service record
相关的处理代码如下:
// open sdp session
User::LeaveIfError(iSdp.Connect());
// open sdp database session
User::LeaveIfError(iSdpDB.Open(iSdp));
// create a record of the correct service class
TUUID serviceUUID(KBTFileSend_serviceID);
iSdpDB.CreateServiceRecordL(serviceUUID, iRecord);
接下来的工作使用子会话来增加记录和记录的属性到数据库中相关的处理如下:
// add a protocol to the record
CSdpAttrValueDES* protocolDescriptorList = CSdpAttrValueDES::NewDESL(NULL);
CleanupStack::PushL(protocolDescriptorList);
TBuf8<1> channel;
channel.Append((TChar)aChannel);
// create protocol list for our service
protocolDescriptorList
->StartListL() // list of protocols required for this method
->BuildDESL()
->StartListL()
->BuildUUIDL(KL2CAP)
->EndListL()
->BuildDESL()
->StartListL()
->BuildUUIDL(KRFCOMM)
->BuildUintL(channel)
->EndListL()
->BuildDESL()
->StartListL()
->BuildUUIDL(KBtProtocolIdOBEX)
->EndListL()
->EndListL();
// set protocol list to the record
iSdpDB.UpdateAttributeL(iRecord, KSdpAttrIdProtocolDescriptorList,
*protocolDescriptorList);
CleanupStack::PopAndDestroy(protocolDescriptorList);
// add a name to the record
iSdpDB.UpdateAttributeL(iRecord,
KSdpAttrIdBasePrimaryLanguage +
KSdpAttrIdOffsetServiceName,
KBTServiceName);
// add a description to the record
iSdpDB.UpdateAttributeL(iRecord,
KSdpAttrIdBasePrimaryLanguage +
KSdpAttrIdOffsetServiceDescription,
KBTServiceDesc);
// set service available
UpdateAvailabilityL(ETrue);
第一个被加入记录的属性KSdpAttrIdProtocolDescriptorList安包括了一个用于访问服务的协议栈顺序列表。每个协议都有一个UUID值,并且可指定相关的可选参数。这个列表使用 CSdpAttrValueDES 对象来处理。
在上述列表中第一个加入L2CAP 协议,第二个是RFCOMM同时指定相关的端口,第三个才是我们要使用的OBEX 协议。我们又一步了解了OBEX协议是基于RFCOMM以上的高层协议。
同时将服务的名字与相对应的服务的详细描述写入该服务数据库记录。
最后设置服务记录可被查询。
当服务不可用时可使用如下代码删除服务记录库的记录:
// delete out record from service discovery database
iSdpDB.DeleteRecordL(iRecord);
// close sdp and sdp db sessions
iSdpDB.Close();
iSdp.Close();
iRecord=0;
使用CObexBufObject 对象可以方便的写入文件操作。如下:
iObexBufObject->WriteToFile(iFileName);
先停止OBEX服务再来关闭再删除服务记录关闭会话。
// Stopping OBEX Server...
if (iObexServer)
{
if(iObexServer->IsStarted())
{
iObexServer->Stop();
}
delete iObexServer;
iObexServer = NULL;
LOG(ELevel1, _L("CBTObexServer::DisconnectL()"));
}
if (iAdvertiser->IsAdvertising())
{
iAdvertiser->StopAdvertiserL();
}
StopAdvertiserL():
// delete out record from service discovery database
iSdpDB.DeleteRecordL(iRecord);
// close sdp and sdp db sessions
iSdpDB.Close();
iSdp.Close();
iRecord=0;
该开发涉及到 “能力权限”蓝牙需要LocalServices 的一个能力,因此需要用户进行授权,需要在MMP 文件中指出CAPABILITY LocalServices。