有關iOS BLE藍芽基礎功能的封裝已經在上篇文章寫完了,本篇文章負責把在SDK封裝過程中遇到的問題知識點進行總結。
封裝SDK實質上是把一些功能給封裝成一個個對應的方法,用SDK的人只需要呼叫相應的方法就能實現對應的功能,而不再需要一個複雜的實現過程。
藍芽功能的實現實質上是通過手機和藍芽互相通訊而建立的,所以通訊的協議是由我們自己進行擬定的。解釋一下協議的擬定,就是手機端和裝置端提前商量好用某些字元代表某種意義,可以理解為手機端和裝置端兩者之間建立了一種特殊的語言。(比如:12345代表讓裝置自爆,那麼裝置收到12345的時候就自爆了?舉個例子)。
好了,現在進入正題
既然有通訊協議,那麼為了安全考慮就一定需要加密傳輸,否則隨便來個人給裝置發個12345。。。。不就完了。
下面就是第一個問題
- 有祕密,就肯定需要金鑰進行加解密,但是金鑰又不能直接傳送,否則被擷取了金鑰和沒有金鑰有啥區別。
計算DHKey需要進行超大數的計算,在百度上搜尋了半天終於找到了一個比較好的大數計算的第三方庫?,放到了私人百度雲上JKBigInteger,密碼:ub2s。需要的可進行下載。協商金鑰方面的事從找到這個庫的時候起就沒什麼大問題了。
- 有了金鑰,接下來就是加密方法了。目前為止比較主流的加密方式就是aes,md5,base64等了,這個SDK就是使用aes和md5混合加密的形式進行資料的傳輸。
接下來就是aes加密相關的分享了,-->Aes加密演算法主要還是使用iOS系統自帶的加密演算法,在系統提供的演算法上進行了一層包裝,用起來更方便。
同樣的md5的程式碼不是太多就直接貼出來了
//md5加密字串
+ (NSString *)md5WithString:(NSString *)inputStr
{
//傳入引數,轉化成char
const char *str = [inputStr UTF8String];
//開闢一個16位元組(128位:md5加密出來就是128位/bit)的空間(一個位元組=8字位=8個二進位制數=2個16進位制數)
unsigned char md[CC_MD5_DIGEST_LENGTH];
/*
extern unsigned char * CC_MD5(const void *data, CC_LONG len, unsigned char *md)官方封裝好的加密方法
把str字串轉換成了32位的16進位制數列(這個過程不可逆轉) 儲存到了md這個空間中
*/
CC_MD5(str, (CC_LONG)strlen(str), md);
//建立一個可變字串收集結果
NSMutableString *ret = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
{
/**
X 表示以十六進位制形式輸入/輸出
02 表示不足兩位,前面補0輸出;出過兩位不影響
printf("%02X", 0x123); //列印出:123
printf("%02X", 0x1); //列印出:01
*/
[ret appendFormat:@"%02X",md[i]];
}
//返回一個長度為32的字串
if (!ret || [ret length] == 0)
{
return nil;
}
return ret;
}
複製程式碼
//md5加密data資料
+ (NSString *)md5StringWithData:(NSData *)data
{
//1: 建立一個MD5物件
CC_MD5_CTX md5;
//2: 初始化MD5
CC_MD5_Init(&md5);
//3: 準備MD5加密
CC_MD5_Update(&md5, data.bytes, (CC_LONG)data.length);
//4: 準備一個字串陣列, 儲存MD5加密之後的資料
unsigned char result[CC_MD5_DIGEST_LENGTH];
//5: 結束MD5加密
CC_MD5_Final(result, &md5);
NSMutableString *resultString = [NSMutableString string];
//6:從result陣列中獲取最終結果
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[resultString appendFormat:@"%02X", result[i]];
}
return resultString;
}
複製程式碼
兩個方法,一個是用來加密utf8編碼的字串的,一個是用來加密NSData型別資料的。同樣是使用系統提供的庫,所以使用時需匯入
#import <CommonCrypto/CommonDigest.h>
系統標頭檔案。
至此,有關協議擬定方面的問題就沒什麼問題了。
----------------------這是一條分界線--------------------------
有金鑰,有加密傳輸資料的方法,接下來就是資料來源了。
因為資料的格式有很多種,而在手機和藍芽之間進行傳輸的卻只有一種--NSData,也就是二進位制資料,因此我們就需要設計一套通用性的方法能把各種資料轉換成NSData型別--------也就是俗稱的編碼了。
廢話不多說,直接上程式碼了
+ (NSData *)dataWithByte:(Byte)byte
{
NSData *data = [NSData dataWithBytes:&byte length:sizeof(Byte)];
return data;
}
+ (NSData *)dataWithShort:(short)Short
{
HTONS(Short);
return [NSData dataWithBytes:&Short length:sizeof(short)];
}
+ (NSData *)dataWithInt:(int)Int
{
HTONL(Int);
return [NSData dataWithBytes:&Int length:sizeof(int)];
}
+ (NSData *)dataWithLong:(long)Long
{
HTONLL(Long);
return [NSData dataWithBytes:&Long length:sizeof(long)];
}
+ (NSData *)dataWithString:(NSString *)string
{
return [string dataUsingEncoding:NSUTF8StringEncoding];
}
+ (NSData *)dataWithHexString:(NSString *)str
{
if (!str || [str length] == 0)
{
return nil;
}
NSMutableData *hexData = [[NSMutableData alloc] initWithCapacity:0];
NSRange range;
if ([str length] % 2 == 0)
{
range = NSMakeRange(0, 2);
}
else {
range = NSMakeRange(0, 1);
}
for (NSInteger i = range.location; i < [str length]; i += 2)
{
unsigned int anInt;
//取出range內的子字串
NSString *hexCharStr = [str substringWithRange:range];
//掃描者物件,掃描對應字串
NSScanner *scanner = [[NSScanner alloc] initWithString:hexCharStr];
//掃描16進位制數返回給無符號整型anInt
[scanner scanHexInt:&anInt];
//把這個int型別數轉成1個位元組的NSdata型別
NSData *entity = [[NSData alloc] initWithBytes:&anInt length:1];
[hexData appendData:entity]; //加到可變data型別hexData上
range.location += range.length;
range.length = 2;
}
return hexData;
}
複製程式碼
值得注意的是上面的
HTONS(Short);HTONL(Int);HTONLL(Long);
這3個東西,可能第一次見的時候不明白是什麼意思。解釋一下,HTON指的是Host To Network
即主機位元組順序轉化為網路位元組順序。不懂的可以看看這篇文章scoket程式設計。至於最後一位就好理解了,S表示short型別,L表示int型別,LL表示long型別。
細心可能會發現,不對啊,你這少了浮點數型別,要是我想傳輸浮點數怎麼辦??
額,不得不說OC想要讓float和NSData型別互轉還是挺不好弄的,沒有直接的轉化方法,如果按照上面的那幾種方法類比的話結果是不對的。OC不好弄,沒問題,C語言可以弄。下面是程式碼:
typedef float type_f32;
typedef unsigned char type_u8;
typedef unsigned short type_u16;
typedef union
{
type_f32 f_val;
type_u8 c[4];
} float_u;
type_f32 STREAM_TO_FLOAT32_f(type_u8* p, type_u16 offset)
{
float_u f;
f.c[0] = p[offset + 3];
f.c[1] = p[offset + 2];
f.c[2] = p[offset + 1];
f.c[3] = p[offset];
return f.f_val;
}
type_u8* FLOAT32_TO_STREAM_f(type_u8* p, type_f32 f32)
{
float_u f;
f.f_val = f32;
p[0] = f.c[3];
p[1] = f.c[2];
p[2] = f.c[1];
p[3] = f.c[0];
return p;
}
複製程式碼
上面使用了C語言的聯合體,,等等,結構體我知道,聯合體是個什麼玩意,對於學OC的我們來說可能還真的不清楚C語言的聯合體是什麼?下面簡單解釋一下:
聯合體就是定義了兩種不同型別的變數,如上
type_f32 f_val
和type_u8 c[4]
使得這兩個不同型別的變數共享同一塊地址空間。type_f32
實質上就是float型別,type_u8
實質上是char型別,這個聯合體就是讓一個float型別的數f_val
和一個字元陣列c[4]
共享同一個地址空間,然後提供了兩個從空間種取出不同型別資料的方法。至於上面的陣列的順序是0123,還是3210這個要看硬體端是怎麼寫的了。
接下來就簡單了,只需要把那兩個C語言方法封裝成OC的方法就行了,如下:
+ (NSData *)dataWithFloat:(float)Float
{
Byte byte[4];
FLOAT32_TO_STREAM_f(byte, Float);
return [[NSData alloc] initWithBytes:byte length:sizeof(float)];
}
+ (float)floatWithData:(NSData *)data
{
Byte *byte = (Byte *)[data bytes];
float b = STREAM_TO_FLOAT32_f(byte, 0);
return b;
}
複製程式碼
至於解碼的問題就不多說了,解碼int型別。供參考
//model.rand
int rand;
//意為從receiveData裡面取出第21,22,23,24這4個位元組的資料,賦值給rand
[receiveData getBytes:&rand range:NSMakeRange(20, 4)];
HTONL(rand);
model.rand = rand;
複製程式碼
到現在為止,封裝SDK功能的準備工作已經做完了。
接下來的內容就是記錄一下本人在寫SDK中遇到的一些問題和解決方法。是沒有原始碼的。
- SDK要求所有功能都具有一個success和一個failure回撥以及一個判斷超時的timer。呼叫一次功能方法只會執行上面3種結果中的一個。即當success或failure執行時要把這些都給清除保證不會二次執行。同樣timer執行時也是一樣。
- 每一次呼叫方法都會得到一個結果,並且不能發生結果錯亂(比如連續呼叫一個方法兩次,第一次的結果不能呼叫第二次的回撥)。
綜上,我需要把每次寫資料對應的方法id,success,failure,timer儲存起來,等收到裝置的回覆時再根據方法id找到儲存的方法對應的success等,根據回覆的資料再判斷要呼叫哪個回撥。並且需要注意的是呼叫完成後要把這一整條資料都清空。
總結下來就3點
- 保證不會發生回撥覆蓋
- 保證不會發生回撥錯亂
- 保證回撥不會多次執行
好了,這些就是我做的Ble藍芽SDK時遇到的比較有意思的問題了。