什么是蓝牙service和characteristic?如何理解蓝牙profile? ATT和GATT两者如何区分?什么是attribute? attribute和characteristic的区别是什么?蓝牙的互联互通为什么能够做的这么成功?
本文除了阐述上述问题 ,并重点阐述蓝牙协议栈中的ATT层和GATT层。
低功耗蓝牙协议栈构架结构:
如图所示,ATT和GATT是蓝牙协议栈最重要的两层,也是蓝牙应用开发者打交道最多的两层,用户开发应用程序或者说service/profile的时候,调用的都是GATT API,而GATT又调用ATT API。在讲解ATT 和 GATT之前 。我们先看一下蓝牙核心规范中一个重要概念:Client/Server (客户端/服务端)架构。
BLE client/server(c/s)架构
BLE采用了client/server (c/s)架构来进行数据交互,C/S架构是一种非常常见的架构,在我们身边随处可见,比如我们经常使用的浏览器和服务器也是一种C/S架构 ,这其中浏览器都是客户端client ,服务器是服务端server,server比如淘宝服务器,提供商品信息,广告,社交等服务,而浏览器就是客户端,比如微软的IE ,就可以用来请求这些服务,并使用server提供的服务。BLE与此相似,一般而言,设备提供服务,因此设备时server,手机使用设备提供的服务,因此手机就是client。比如蓝牙体温计,它可以提供 "体温" 数据服务,因此是一个server,而手机则可以请求 "体温" 数据以显示在手机上,因此手机是一个 client.
服务是以数据为载体的,所以说server提供服务其实就是提供各种有价值的数据。
C/S架构
客户端要访问一个数据,就发送一个request/请求(其实就是一条命令或者PDU),服务端再把数据返回 给 客户端(一条response/响应命令或者PDU),这就是 C/S架构。
ATT
讲解之前我们先讲解attribute,那么什么是attribute? 其实就是一条条的数据。前面说过,每个蓝牙设备就是来提供服务的,而服务就是众多数据的集合,这个集合可以称为数据库,数据库里面每一个条目都是一个attribute。所以我们把attribute翻译为数据条目。大家可以把蓝牙设备 想象称为一个表格,表格里面每一行就是一个attribute。attribute可以用下图来表示:
- Attribute handle ,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个 句柄来访问,也就是说ATT PUD一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按照顺序的 为相关attribute生成句柄。
- Attribute type,Attribute类型 ,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128bit的,所以我们有足够的UUID来表达万事万物 。其中有一个UUID非常特殊,他被蓝牙联盟官方UUID,这个UUID如下所示:
由于这个UUID众所周知,蓝牙 联盟将自己定义的attribute或者数据只用16bit UUID来表示,比如0x1234,其实他也是128bit,完整表示为:
Attribute type一般是由service和characteristic规格来定义,站在蓝牙协议栈角度来看,ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层就是定义serveice和characteristic,GATT层用来赋予每个数据一个具体的含义,让数据变得有及结构和意义。换句话说,没有GATT层 ,低功耗蓝牙也可以通信起来,但是会产生兼容性问题以及通信效率低。
- Attribute value,就是数据真实的值,0到512字节长。
- Attribute permissions,Attribute的权限属性,权限属性不会 直接 在 空口包中体现,在隐含 在ATT命令的操作结果中。假设一个attribute read属性设为open(即读操作不需要任何权限),那么client去读这个attribute时server将直接返回attribute的值;如果这个attribute read属性设为 authentication(即 需要配对才能访问),如果client没有与server配对而直接去访问这个attribute,那么server会返回一个错误码:告诉client你的权限不够,此时client会对server发起配对请求,以满足这个attribute的读属性要求,从而在第二次读操作时 server将把相应的数据返回给client。目前主要有如下四种权限属性:
- Open ,直接可以读或者写
- No Access ,禁止读或者写
- Authentication ,需要 配对才能读或者写,由于配对有很多中类型,因此authentication又衍生多种子类型,比如带不带MITM,有没有LESC
- Authorization,跟open一样,不过server返回attribute的 值之前需要应用先授权,也就是说应用可以在回调函数里面去修改读或者写的原始值。
- Signed,签名后才可以读或者写,这个用的比较少。
一个应用所有的 attribute组成一个database,也称为attribute table,一个attribute table示例如下 所示:
上图:原始attribute数据库(这个表格不能算是 原始attribute,因为它已经把bin数据 转成字符了,大家可以 把相关字符都看成bin数据,就看成原始attribute表格)。
设备支持的服务不同,attribute table就不同。这里说明一下,当你添加,修改或者删除服务时,那么attribute table就会变,attribute table变了,它占用的RAM空间就会变。
ATT,全称attribute protocol(数据交互协议)。说到底,ATT是由 一群ATT命令组成,就是上文 所述的request(请求)和response(响应)命令。ATT也是蓝牙空口包中的最上层,也就是说,ATT就是大家对蓝牙数据包进行分析的最多的地方。
ATT命令,正式称谓ATT PDU (Protocol Data Unit,协议数据交互单元)包括4类:读,写,notify(通知)和 indicate(指示)。这些命令又可以分成两种:如果它需要response,那么会在相应命令后面加上request。相反,如果它只是需要ACK而不需要response,那么它 的后面就不会带request。这里要特别强调 一点,ATT所有命令都是 "必达" 的。也就是说每个命令发出去以后,就会立马等ACK信息,如果收到ACK包,发送方认为命令完成;否则发送方一直重发该命令到导致BLE连接断开。换句话说,只要你的BLE连接没有断开,那么 你之前发送的数据包,不管他是用什么ATT PDU来发送的,它肯定被对方收到了。
有时候经常会出现一种情况,大家有时候会碰见 " 丢包 ",其实不是在空中丢包或者被干扰了 ,而是我们 的 发送代码写的有问题,导致包没有被安全的送达到协议栈射频 FIFO中,从而 出现所谓的 " 丢包 "。
既然每个ATT命令都必达对方,那么还需要request类型的命令做什么?
如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。换句话说,不带request的命令,虽然协议栈底层确保了该命令必达对方,但应用层其实并不知道(私有实现方法除外),当你需要实现一个通信序列的时候,这种命令就显得不足了。而采用request/response方式的命令时,request命令发出去之后,必须等到相应的response命令回复才能进行下一步操作,比如发送下一个request命令,这样应用层可以严格按照规定逻辑执行一系列的操作,这个在很多应用场合是非常有用的。Request/response命令对还有一个副作用:大大降低通信的有效速率(吞吐率),因为request/response命令必须在不同的连接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就导致一个数据包的发送需要跨两个连接间隔甚至更多。而不带request后缀的ATT命令就没有这个限制,ACK可以在同一个连接间隔中回复,这样一个连接间隔中可以同时发出多个数据包,这样将大大提高通信速率。大家可以参考下图来理解request和非request命令的区别:
注:第1个连接间隔中的蓝色包为request命令,旁边的灰色包是该request的ACK;第2个连接间隔的绿色包是response包,而它的ACK是第3个连接间隔中的蓝色包
注:图中的绿色包就是非request命令,而紧随其后的灰色包就是它的ACK
不带request的命令只有2个:write command和notification,其余的命令都是带request:所有 read命令,所有write 命令,find命令以及indicate命令,完整的ATT命令(ATT PDU)列表如下所示:
GATT,Service(服务)和Characteristic(特征数据)
在讲解GATT之前,我们先看一下什么是profile?
Profile是一个大家经常见到的英文单词,但是总感觉领会不到这个词的内涵。Profile,英文本意就是 脸的侧面轮廓,这里大家注意一定要注意,脸的轮廓 不等于脸本身,但是profile本身是对脸的一种抽象,描述和定义,蓝牙规范其实也是使用profile这个引申意义而已,换句话说 ,蓝牙的profile跟英文字典中的profile是同一个意思。要定义蓝牙,必须要有一个规范,这就是蓝牙核心规范v4.2/v5.0/v5.1……蓝牙规范非常复杂和庞大 ,大部分蓝牙设备只实现了蓝牙规范中很少一部分,那么没有实现的这些规范对这个蓝牙设备来说 能不能称为 规范?当然不能!所谓规范或者规格 ,就是强制的,就必须实现。针对这种情况,profile可以很好的应对。我们把蓝牙某部分规范称为profile,这个profile如果设备要实现它,那么它就是强制的,如果设备不用它,也没有关系,这就是profile。基于此,我们可以把profile翻译成子规范或者条件规范或者剖面规范。“蓝牙规范包含很多子规范”,这句话用中文说问题不是很大,但是你把它翻译成英文,那就很难了!这就是 英文需要用profile的原因(而不是spec), 以及为什么profile在规范中出现的如此频繁。
GATT,全称generic attribute profile,对数据进行一般化/抽象化的子规范,说白了就是对数据进行逻辑化表达的规定。前面说过了,attribute就是一条一条的数据,那么这条数据表示什么?如何对其进行分类?这就是GATT要做的事情,GATT将数据赋予含义,并呈现一定的逻辑结构。
Service和characteristic就是GATT层定义的,前面说过,server端提供 服务,服务就是数据,而数据就是一条一条的attribute,而service和characteristic就是数据的逻辑呈现,后者说用户能看到的数据最终都转化为 service和characteristic。比如,一个数据 “37”。有可能是说体温 “37度”,也可能说心率 “37次”或者湿度 “37%”,因此必须对数据进行分类和定义。
在蓝牙规格中,每一个具体的蓝牙应用是由多个service组成,而每一个service又是多个characteristic组成,这样我们可以把上面的图一转化为图3.
图三:service和characteristic
那么service/characteristic和attribute之间到底是一个怎样的关系?如前所述,service/charateristic是attribute的逻辑表现形式,而attribute是service/characteristic具体实现方式。尤其要注意的是,一条characteristic不是对应一条attribute具体实现方式。尤其注意的是,一条chararcteristic不是对应一条attribute,而是由多条attribute组成。虽然一个数据最有价值的部分是它的值(value )。=,但是 仅有value是不够的,比如27,到底是表示27°温度还是27%湿度;如果表示的是温度,那么它的单位是摄氏度还是华氏度。同时每个数据还有相应的读写属性以及权限属性,因此一个characteristic包含三种类型的数据条目(attribute):characteristic声明体条目(declaration attribute),characteristic 值条目(value characteristic)以及characteristic描述符条目(descriptor attribute) (一个characteristic可以有多个描述符条目),如下所示:
由于一个service可以包含多个characteristic,characteristic declaration就是每个characteristic的分界符,解析时一旦遇到characteristic declaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value attribute的读写属性等。Charateristic value就是数据的值了,他也是一个单独的attribute,这个比较好理解就不再说了。Charactristic descriptor就是数据的额外信息,比如 温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。Descriptor属于可选条目,也就是说,一个characteristic可以不包含任何一条descriptor。这里着重提一种特殊的descriptor:CCCD。一般而言,都是client来访问server的characteristic,即通过ATT读或者写PDU访问相关数据。如果server想直接把自己的characteristic的值告诉client,就需要通过notiy或者indicate PDU,跟其他PDU相比,这两个PUD是由server自己决定什么时候开始传送,而不是被动接收client的命令请求。但client毕竟是客户,他得有自主权,所以引入了一个CCCD来帮助client控制server的行为。client可以通过禁止CCCD以允许notify或者 indicate命令,client可以通过禁止CCCD以允许notify或者indicate命令。重新总结一下。当CCCD使能的情况下,server可以随时notify或者indicate数据 给client;当禁止的时候,哪怕server有数据,它也不能notify或者indicate给client。这里强调一下。当characteristic具有notify或者indicate操作功能时,蓝牙规范要求必须为其添加CCCD attribute.
重复强调,不管是characteristic declaration,characteristic value还是characteristic descriptor,实现的时候,都是一条数据条目,即attribute.
引入了GATT,我们就可以把图2的 attribute table进行GATT化,得到下面有内涵,有层次,有定义的数据表格:
所谓开发蓝牙应用程序,其实就是开发service和characteristic。通过API,添加自己需要的characteristic和service,你自己的蓝牙设备就诞生了。只要characteristic和service是符合GATT规范的,你可以随意添加任何characteristic和service,并将他们组合成一个专门的蓝牙设备。由于这个蓝牙设备是按照规范来定义的,所以它可以与任何其他蓝牙设备,比如手机,互联互通,并完成所要求的的交互动作。这里的蓝牙设备,我们还可以进一步细分为蓝牙profile设备和非profile蓝牙设备。前面也提过,profile就是一个子规范,蓝牙profile设备包含的所有实际service和characteristic都是按照profile规格来添加和定义的,比如说心率计profile,就是一个蓝牙联盟定义的蓝牙设备,蓝牙联盟有一份专门的spec来定义心率计profile,在这份spec中规定了心率计profile除了包含心率service,还包含电池service,设备信息service等。从这可以看出,心率profile和心率service是包含关系,前者包含后者。