Linux下Bluetooth编程,借用了Socket体制。也就是说,BlueZ Kernel部分将Bluetooth协议栈以网络协议的形式添加进网络协议栈,这样极大的方便了用户编程。下面Sam就结合Socket概念将Linux Bluetooth做个研究。
1957年10月4日,星期五,苏联发射了人类历史上第一颗人造地球卫星--Sputnik.这标志着人类外太空时代的开始。这颗卫星篮球大小,在发射98分钟后到达运转轨道,可以通过短波40.002MHz收听到它的声音。这也标志着苏联在航天科技领域超过美国。但当时谁能想到,Sputnik的升空竟然促进了TCP/IP和Internel的出现。(Sam:不知道朝鲜那个轨道高度几百米的卫星会促成什么出现,嘿嘿)。被Sputnik所刺激的美国总统艾森豪威尔五星上将积极推动ARPA。又因为美国政府为了公平起见,每次采购计算机时都从不同设备制造商处购买。大家很快发现,各个计算机无法兼容。1962年,Licklider提出:各个计算机高度自治,但他们也应该能够相互通讯。这就是ARPA网,它成为Internel的前身。
一:理解Socket:
在使用手机与女朋友联系时,必须用手机拨她的号码,然后心情坎坷的等待她的应答。当双方通话时,就建立了一个具有两个端点的通信线路。
Linux中的Socket与电话非常相似。具体问题,稍后再分析。
二:Socket域(domain),类型(type),协议(protoclo)以及Bluetooth中的具体使用:
Berkeley小组在构思BSD Socket时,TCP/IP协议也还处在发展之中,其他一些很有竞争力的协议如X.25等也在发展,其它很多协议还在构思与研究阶段(Bluetooth还没出生)。为了使Socket可以应用于各种不同协议,domain的作用就在于此。
domain指出想要使用的协议族。
不得不佩服Berkeley小组的前瞻力。他们考虑在指定Socket时,可能还需要进一步的细分类目:
1.某个协议族(Domain)中的一个或多个协议。
2.某个协议中的一个或多个地址格式。
这个规则在TCP/IP等协议栈时并不明显,因为某个协议族只有同一种地址格式。但在Bluetooth中则非常有用。
protocol则用来指出在此协议族中的具体某个协议。
虽然在TCP/IP协议栈中,因为协议族中某个type的协议栈只有一种,所以此项为0,但Bluetooth中,这一项则非常有用。
type用来指出此协议族中的具体协议的Socket类型为何种:SOCK_STREAM,SOCK_DGRAM,SOCK_SEQPACKET,SOCK_RAW.
三:Socket地址:
每一种通信协议都对网络地址格式作了明确规定。协议族(Domain)+ 协议(protocol)的作用就是指明使用哪种地址类型。
BSD Socket是在ANSI C 标准被采纳之前开发的,所以没有使用(void*)数据类型来接收结构化的地址。BSD的解决方案是定义了一个通用的地址结构:
struct sockaddr
{
sa_family_t sa_family; //地址族
char sa_data[14]; //地址数据
};
sa_family长度2字节,用来存放地址族。
sa_data长度14字节,用来存放具体的协议的地址数据。
如果是用AF_INET(IPV4),则它的地址类型sockaddr_in如下,刚好与struct sockaddr对应
struct sockaddr_in
{
sa_family_t sin_family; //地址族
uint16_t sip_port; //端口
struct in_addr sin_addr; //Internel 地址
unsigned char sin_zero[8]; //占位字节
};
如果是用Bluetooth协议族(PF_BLUETOOTH)中的协议l2cap(BTPROTO_L2CAP),则地址格式如下:
struct sockaddr_l2
{
sa_family_t l2_family; //地址族
unsigned short l2_psm; //PSM
bdaddr_t l2_bdaddr; //Bluetooth 地址
unsigned short l2_cid;
};
四:Bluetooth Socket的建立和地址绑定:
int socket(int domain, int type, int protocol);
domain:使用 PF_BLUETOOTH。
protocol:使用想要建立的Socket的protocol.如果想建立HCI Socket:BTPROTO_HCI。 L2cap:BTPROTO_L2CAP
type:SOCK_SEQPACKET,以Packet为单位读取。SOCK_SAW:原始Socket。
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
将socket与某个地址绑定。
嘿嘿,接着前面Socket与手机的话题,建立一个Socket。就相当于是一个手机,地址,则相当于手机号码。
一个手机想要别人打进来,就需要让别人知道电话号码。 而一个Bluetooth 设备想要别人能够连接,也需要将Socket与Bluetooth地址绑定。
山寨机让我们知道了双卡双待,Bluetooth也可以实现这一点。建立一个Socket,只是一个手机,它可以与多个bdaddr绑定。这就是hci0,hci1等等。
五:理解网络字序:
对于多字节数据,不同的CPU有不同的组织方式,最基本的字节序位:
小端(little-endian): 将低序字节存储在起始位置。
大端(big-endian):将高序字节存储在其实位置。
Intel CPU使用小端。Motorola等CPU使用大端,网络上传输数据的标准顺序为大端。
他们之间的转化:
htobs(), htonl() 主机到网络
ntohl() , ntohs() 网络到主机。
例一:发送Signaling Packet:
Signaling Command是2个Bluetooth实体之间的L2CAP层命令传输。所以得Signaling Command使用CID 0x0001.
多个Command可以在一个C-frame(control frame)中发送。
如果要直接发送Signaling Command.需要建立SOCK_RAW类型的L2CAP连接Socket。这样才有机会自己填充Command Code,Identifier等。
以下是一个发送signaling Command以及接收Response的简单例子:
int main(int argc, char** argv)
{
int l2_sck = 0;
int iRel = 0;
struct sockaddr_l2 local_l2_addr;
struct sockaddr_l2 remote_l2_addr;
char str[24] ={0};
int len = 0;
int size = 50;
char* send_buf;
char* recv_buf;
int i = 0;
int id = 1; //不要为0
send_buf = malloc(L2CAP_CMD_HDR_SIZE + size);
recv_buf = malloc(L2CAP_CMD_HDR_SIZE + size);
if(argc < 2)
{
printf("/n%s <bdaddr>/n", argv[0]);
exit(0);
}
// create l2cap raw socket
l2_sck = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP); //创建L2CAP protocol的RAW Packet
if(l2_sck < 0)
{
perror("/nsocket:");
return -1;
}
//bind
memset(&local_l2_addr, 0, sizeof(struct sockaddr_l2));
local_l2_addr.l2_family = PF_BLUETOOTH;
bacpy(&local_l2_addr.l2_bdaddr , BDADDR_ANY);
iRel = bind(l2_sck, (struct sockaddr*) &local_l2_addr, sizeof(struct sockaddr_l2));
if(iRel < 0)
{
perror("/nbind()");
exit(0);
}
//connect
memset(&remote_l2_addr, 0 , sizeof(struct sockaddr_l2));
remote_l2_addr.l2_family = PF_BLUETOOTH;
//printf("/nConnect to %s/n", argv[1]);
str2ba(argv[1], &remote_l2_addr.l2_bdaddr);
iRel = connect(l2_sck, (struct sockaddr*)&remote_l2_addr, sizeof(struct sockaddr_l2));
if(iRel < 0)
{
perror("/nconnect()");
exit(0);
}
//get local bdaddr
len = sizeof(struct sockaddr_l2);
memset(&local_l2_addr, 0, sizeof(struct sockaddr_l2));
//注意,getsockname()参数三是一个输入输出参数。输入时,为参数二的总体长度。输出时,
//为实际长度。
iRel = getsockname(l2_sck, (struct sockaddr*) &local_l2_addr, &len);
if(iRel < 0)
{
perror("/ngetsockname()");
exit(0);
}
ba2str(&(local_l2_addr.l2_bdaddr), str);
//printf("/nLocal Socket bdaddr:[%s]/n", str);
printf("l2ping: [%s] from [%s](data size %d) .../n", argv[1], str, size);
for (i = 0; i < size; i++)
send_buf[L2CAP_CMD_HDR_SIZE + i] = 'A';
l2cap_cmd_hdr *send_cmd = (l2cap_cmd_hdr *) send_buf;
l2cap_cmd_hdr *recv_cmd = (l2cap_cmd_hdr *) recv_buf;
send_cmd->ident = id; //如上图所示,这一项为此Command Identifier
send_cmd->len = htobs(size);
send_cmd->code = L2CAP_ECHO_REQ; //如上图所示,此项为Command code.这项定为:
//Echo Request。对端会发送Response回来。code=L2CAP_ECHO_RSP
while(1)
{
send_cmd->ident = id;
if(send(l2_sck, send_buf, size + L2CAP_CMD_HDR_SIZE, 0) <= 0)
{
perror("/nsend():");
}
while(1)
{
if(recv(l2_sck, recv_buf, size + L2CAP_CMD_HDR_SIZE, 0) <= 0)
{
perror("/nrecv()");
}
if (recv_cmd->ident != id)
continue;
if( recv_cmd->code == L2CAP_ECHO_RSP)
{
//printf("/nReceive Response Packet./n");
printf("%d bytes from [%s] id %d/n", recv_cmd->len, argv[1], recv_cmd->ident);
break;
}
}
sleep(1);
id ++;
}
close(l2_sck);
return 0;
}
所以说,如果想要发送接收signaling Command。只需要建立l2cap RAW socket. 并按规则填充command id, command code等。就可以接收发送了。
Command Code: 这个值放在l2cap.h中。
#define L2CAP_COMMAND_REJ 0x01
#define L2CAP_CONN_REQ 0x02
#define L2CAP_CONN_RSP 0x03
#define L2CAP_CONF_REQ 0x04
#define L2CAP_CONF_RSP 0x05
#define L2CAP_DISCONN_REQ 0x06
#define L2CAP_DISCONN_RSP 0x07
#define L2CAP_ECHO_REQ 0x08
#define L2CAP_ECHO_RSP 0x09
#define L2CAP_INFO_REQ 0x0a
#define L2CAP_INFO_RSP 0x0b
例二:任意PSM的L2CAP连接间数据的传输:
此例子中:Server,client其实是使用网络的概念定义的。
server用来监听指定PSM的连接,并监听数据。同时,利用poll来查看peer是否断掉了。
Server:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <poll.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/l2cap.h>
void * Read_thread(void* pSK);
int main(int argc, char** argv)
{
int iRel = 0;
int sk = 0;
struct sockaddr_l2 local_addr;
struct sockaddr_l2 remote_addr;
int len;
int nsk = 0;
pthread_t nth = 0;
struct l2cap_options opts;
int optlen = 0;
int slen = 0;
char str[16] = {0};
if(argc < 2)
{
printf("/nUsage:%s psm/n", argv[0]);
exit(0);
}
// create l2cap socket
sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); //发送数据,使用SOCK_SEQPACKET为好
if(sk < 0)
{
perror("/nsocket():");
exit(0);
}
//bind
local_addr.l2_family = PF_BLUETOOTH;
local_addr.l2_psm = htobs(atoi(argv[argc -1])); //last psm
bacpy(&local_addr.l2_bdaddr, BDADDR_ANY);
iRel = bind(sk, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
if(iRel < 0)
{
perror("/nbind()");
exit(0);
}
//get opts
// in mtu 和 out mtu.每个包的最大值
memset(&opts, 0, sizeof(opts));
optlen = sizeof(opts);
getsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &opts, &optlen);
printf("/nomtu:[%d]. imtu:[%d]. flush_to:[%d]. mode:[%d]/n", opts.omtu, opts.imtu, opts.flush_to, opts.mode);
//set opts. default value
opts.omtu = 0;
opts.imtu = 672;
if (setsockopt(sk, SOL_L2CAP, L2CAP_OPTIONS, &opts, sizeof(opts)) < 0)
{
perror("/nsetsockopt():");
exit(0);
}
//listen
iRel = listen(sk, 10);
if(iRel < 0)
{
perror("/nlisten()");
exit(0);
}
len = sizeof(struct sockaddr_l2);
while(1)
{
memset(&remote_addr, 0, sizeof(struct sockaddr_l2));
nsk = accept(sk, (struct sockaddr*)(&remote_addr), &len);
if(nsk < 0)
{
perror("/naccept():");
continue;
}
ba2str(&(remote_addr.l2_bdaddr), str);
printf("/npeer bdaddr:[%s]./n", str); //得到peer的信息
iRel = pthread_create(&nth, NULL, Read_thread, &nsk);
if(iRel != 0)
{
perror("pthread_create():");
continue;
}
pthread_detach(nth); // 分离之
}
return 0;
}
void * Read_thread(void* pSK)
{
//struct pollfd fds[10];
struct pollfd fds[100];
char buf[1024] = {0};
int iRel = 0;
int exit_val = 0;
//fds[0].fd = *(int*)pSK;
//fds[0].events = POLLIN | POLLHUP;
fds[0].fd = (int)(*(int*)pSK);
fds[0].events = POLLIN | POLLHUP;
while(1)
{
if(poll(fds, 1, -1) < 0)
{
perror("/npoll():");
}
if(fds[0].revents & POLLHUP)
{
//hang up
printf("/n[%d] Hang up/n", *(int*)pSK);
close(*(int*)pSK);
pthread_exit(&exit_val);
break;
}
if(fds[0].revents & POLLIN)
{
memset(buf, 0 , 1024);
//read data
iRel = recv(*(int*)pSK, buf, 572, 0);
//printf("/nHandle[%d] Receive [%d] data:[%s]", *(int*)pSK, iRel, buf);
}
}
return 0;
}
client:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/l2cap.h>
int main(int argc, char** argv)
{
int sk;
int i = 0;
char buf[24] = "Sam is Good Guy!";
struct sockaddr_l2 local_addr;
struct sockaddr_l2 remote_addr;
int iRel = 0;
if(argc < 3)
{
printf("/nUsage:%s <bdaddr> <PSM>/n", argv[0]);
exit(0);
}
sk = socket(PF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
if(sk < 0)
{
perror("/nsocket():");
exit(0);
}
//bind. bluetooth好像不许有无名Socket
local_addr.l2_family = PF_BLUETOOTH;
bacpy(&local_addr.l2_bdaddr, BDADDR_ANY);
iRel = bind(sk, (struct sockaddr *)&local_addr, sizeof(struct sockaddr));
if(iRel < 0)
{
perror("/nbind()");
exit(0);
}
memset(&remote_addr, 0, sizeof(struct sockaddr_l2));
remote_addr.l2_family = PF_BLUETOOTH;
str2ba(argv[1], &remote_addr.l2_bdaddr);
remote_addr.l2_psm = htobs(atoi(argv[argc -1]));
connect(sk, (struct sockaddr*)&remote_addr, sizeof(struct sockaddr_l2));
for(i = 0; i < 60; i++)
{
iRel = send(sk, buf, strlen(buf)+1, 0);
printf("Send [%d] data/n", strlen(buf)+1);
sleep(1);
}
close(sk);
return 0;
}
注意:
1. 在Linux 网络编程中,主动发起连接方,因为不关心地址具体是什么,所以可以作为无名socket,也就是说可以不bind. 但Bluetooth则不可以,一定需要bind.
2. poll可以查出连接断连,但需要注意:断开的revent值为:11001B。也就是说:POLLIN | POLLERR |POLLHUP。
3. 被连接一方,一定要指定PSM。
Service Discovery Protocol(SDP)提供一种能力,让应用程序有方法发现哪种服务可用以及这种服务的特性。
服务发现协议(SDP或Bluetooth SDP)在蓝牙协议栈中对蓝牙环境中的应用程序有特殊的含意,发现哪个服务是可用的和确定这些可用服务的特征。SDP定义了bluetooth client发现可用bluetooth server服务和它们的特征的方法。这个协议定义了客户如何能够寻找基于特定属性的服务而不让客户知道可用服务的任何知识。SDP提供发现新服务的方法,在当客户登录到正在操作的蓝牙服务器的一个区域时是可用的时。
Service discovery机制提供client应用程序侦测server应用程序提供的服务的能力,并且能够得到服务的特性。服务的品质包含服务type或服务class.
SDP也提供SDP server与SDP client之间的通讯。SDP server维护着一个服务条目(service record)列表.每个服务条目描述一个单独的服务属性。 SDP client可以通过发送SDP request来得到服务条目。
如果一个client或者依附于client之上的应用程序决定使用某个service. 它创建一个单独的连接到service提供者。 SDP 只提供侦测Service的机制,但不提供如何利用这些Service的机制。Sam觉得,这里其实是说:SDP只提供侦测Service的办法,但如何用,SDP不管。
每个Bluetooth Device最多只能拥有一个SDP Server。如果一个Bluetooth Device只担任Client,那它不需要SDP Server。但一个Bluetooth Device可以同时担当SDP Server和SDP client.
Service Record(Service 条目):
一个service是一个实体为另一个实体提供信息,执行动作或控制资源。一个service可以由软件,硬件或软硬件结合提供。
所有的Service信息都包含于一个Service Record内。一个Service Record 包含一个Service attribute(Service属性) list.
在一个SDP Server内,每个Service Record拥有一个32-bit的唯一性数据。通常,这个唯一性只是在每个SDP Server内部。 如果SDP Server S1 和SDP Server S2拥有同样的一个Service Record。那他们在不同SDP Sever内的独特数值并不一定相同。
SDP在SDP Server增加或减少Service Record时,并不会通知SDP client.
Service Attribute(Service 属性):
每个Service属性描述servcie的特性.一个Service Attribute由2部分:
Attribute ID + Attribute Value。
Attribute ID:16-bit无符号整数,用于区别一个Service Record内的其它属性。
Attribute Value:Attribute值。
Service Class:
每个Service 都是某个Service Class的实例. Service Class定义了Service Record中包含的Service 属性。属性ID,属性值都被定义好了。
每个Service Class也有一个独特ID。这个Service Class标识符包含在属性值ServiceClassIDList属性中。并描绘为UUID。自从Service Record中的属性格式以及含义依赖于Service Class后,ServiceClassIDList属性变得非常重要。
Searching For Service:
Service Search transaction(事务?)允许client得到Service Record Handle。一旦SDP Client得到Service Record Handle,它就可以请求这个Record内具体属性的值。
如果某个属性值UUID,则可以通过查找UUID查到这个属性。
UUID: universally unique identifier.(唯一性标识符)
SDP协议栈使用request/response模式工作,每个传输过程包括一个request protocol data unit(PDU)和一个response PDU. SDP使用L2CAP连接传输数据。在发送Request PDU但未收到Response PDU之前,不能向同一个server再发送Request PDU。
PDU:protocol Data unit
PDU ID:用来识别PDU。
TransactionID:
用来识别Request PUD以及Response PUD。并用来对比某个Response PUD是否对应着Request PUD。