之前詳細談過不少關於HTTP協議的知識點,TCP/IP也通過tcpdump做過簡單的介紹,但網絡協議的本質其實是連接,設備或者端之間連接的方式有多種,常見的http或者基於tcp的socket只是森林一葉,還有些不那麼常見的協議比如藍牙。適當腦洞,也能玩出不少新花樣來。
談到藍牙,很容易讓人聯想到藍牙穿戴設備,好像聽起來更靠近硬件層一些。蘋果其實對iOS和OSX上的藍牙已做了一層很好的封裝,看過CoreBluetooth Framework的大致API之後,基本上就將其流程明白個大概。難點在於理解其工作模式和理清一些關鍵概念,比如Peripehral, Central, Service, characteristics等等,不要被這些陌生的單詞嚇到,網絡協議的應用大多脫不了CS的架構模型,這裡和大家一起對照傳統的Client/Server架構來梳理下iOS和OSX上CoreBluetooth的重要知識點。我畫了一張圖,方便大家一目了然的明白CoreBluetooth的工作原理。
我們只需要把Peripehral, Central, Service, characteristics幾個概念理清,再各自對應到我們之前關於CS的知識體系之中就可以輕松的做一層自己的封裝了。
初次查看CoreBluetooth文檔的時候,很容易把Central理解成Server,其實剛好相反,Peripheral才是我們的Server。正如上圖所示,Peripheral和Central之間建立的是一對多的關系。每個Peripheral會以廣播的模式告訴外界自己能提供哪些Service,這裡Service的概念和我們傳統CS架構當中的Service基本是一致的,每個PeriPheral可以提供多個Service,而每個Service呢,會包含多個characteristic,characteristic是個陌生但十分關鍵的概念,可以把characteristic理解成一個Service模塊具體提供哪些服務,比如一個心率監測Service同時包含心率測量characteristic和地理位置定位characteristic。
Peripheral作為Server,Central作為Client,Peripheral廣播自己的Service和characteristic,Central訂閱某一個具體的characteristic,Peripheral就和Central之間通過characteristic建立了一個雙向的數據通道,整個模型非常簡潔而且符合我們CS的架構體系。接下來具體看下CoreBluetooth的相關API。
首先值得開心一把的是iOS和OSX使用的是同一套API封裝,都是基於CoreBluetooth Framework,只在極細小的地方有些差異,完全可以做一層library的封裝在兩個平台上無縫銜接使用。
在具體搭建基於CoreBluetooth應用之前,要先確立到底哪一方作為Peripheral,哪一方又是Central。Macbook,iPhone,iPad都能成為Peripheral或者Central。我們通過代碼的方式再看一遍上面的架構流程。
創建Peripheral,也就是我們的Server:
_peripheral = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
生成Service以備添加到Peripheral當中:
CBMutableService *transferService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID] primary:YES];
生成characteristics以備添加到Service當中:
self.transferCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID] properties:CBCharacteristicPropertyNotify|CBCharacteristicPropertyWrite value:nil permissions:CBAttributePermissionsReadable|CBAttributePermissionsWriteable];
建立Peripheral,Server,characteristics三者之間的關系並開始廣播服務:
//建立關系 transferService.characteristics = @[self.transferCharacteristic]; [self.peripheral addService:transferService]; //開始廣播 [self.peripheral startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : @[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] }];
創建我們的Central,也就是client:
_central = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
掃描可用的Peripheral:
[self.central scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
掃描到Peripheral之後連接:
[self.central connectPeripheral:targetPeripheral options:nil];
連接成功之後查找可用的Service:
[peripheral discoverServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]]];
找到Service之後,進一步查找可用的Characteristics並訂閱:
//查找Characteristics [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]] forService:service];
查找到Characteristics訂閱:
//訂閱 [peripheral setNotifyValue:YES forCharacteristic:characteristic];
訂閱之後Central和Peripheral之間就建立了一個雙向的數據通道,後續二者之間的數據傳輸就可以通過characteristic來完成了。
有了數據通道,接下來就是如何傳輸數據了。說到數據傳輸就免不了要確定應用層的協議,類似平時我們使用socket實現游戲的網絡模塊時,需要自定義應用層協議才能實現業務數據的交換,協議的設計這裡就不展開說了,之前有過相關經驗的童鞋完全可以把協議層遷移過來。
再看下Peripheral是如何向Central發送數據的,首先Peripheral會向自己的characteristic寫數據:
[self.peripheral updateValue:chunk forCharacteristic:self.transferCharacteristic onSubscribedCentrals:@[self.central]];
Central那一端會通過如下回調收到來自Peripheral的數據流:
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error;
這裡值得注意的是二者數據的發送與獲取,是以二進制流的方式發送的,是NSData的形式封裝的,Peripheral可以持續不停的發送二進制流,所以Central端收到的時候需要自己做協議的解析,根據自定義協議將整個流拆成一個個的業務Packet包。
而Central發送的時候確是封裝成了一個個的Request,比如Central端調用如下API發送數據:
[self.discoveredPeripheral writeValue:data forCharacteristic:self.discoveredCharacterstic type:CBCharacteristicWriteWithoutResponse];
Peripheral端會收到如下回調:
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray*)requests
數據被封裝成了單獨的CBATTRequest,直接去Request當中取value就可以獲取到Central所發送過來的數據。
我之前測試協議的時候發現一個不大不小的坑,多個Central(比如A和B)端同時一個Peripheral發送數據的時候,Peripheral會收到多個CBATTRequest,奇怪的是每個CBATTRequest當中的Central都會指向最先建立連接的A,結果導致Peripheral端無法判斷write請求的數據來自哪一個Central。
藍牙不僅僅能應用於穿戴式設備,還能做一些好玩的小眾應用或者游戲,其本質是一個小型封閉的局域網,不用經過第三方的Server或者Cloud,很安全。
比如兩台iPhone設備之間通過基於藍牙的IM App進行聊天(距離這麼近,為什麼不當面聊,黑人問號?)。
比如一些基於藍牙對戰的小游戲。
比如通過藍牙在iPhone和Macbook之間做數據同步。