iOS之BLE藍芽SDK開發個人總結(進階篇)

懶得起使用者名稱啊發表於2019-03-05

有關iOS BLE藍芽基礎功能的封裝已經在上篇文章寫完了,本篇文章負責把在SDK封裝過程中遇到的問題知識點進行總結。

封裝SDK實質上是把一些功能給封裝成一個個對應的方法,用SDK的人只需要呼叫相應的方法就能實現對應的功能,而不再需要一個複雜的實現過程。

藍芽功能的實現實質上是通過手機和藍芽互相通訊而建立的,所以通訊的協議是由我們自己進行擬定的。解釋一下協議的擬定,就是手機端和裝置端提前商量好用某些字元代表某種意義,可以理解為手機端和裝置端兩者之間建立了一種特殊的語言。(比如:12345代表讓裝置自爆,那麼裝置收到12345的時候就自爆了?舉個例子)。

好了,現在進入正題

既然有通訊協議,那麼為了安全考慮就一定需要加密傳輸,否則隨便來個人給裝置發個12345。。。。不就完了。

下面就是第一個問題

  1. 有祕密,就肯定需要金鑰進行加解密,但是金鑰又不能直接傳送,否則被擷取了金鑰和沒有金鑰有啥區別。

於是我們採用DH協商金鑰的方法進行計算金鑰。想了解什麼是DH協商金鑰的可以看看這個DH協商金鑰原理DH金鑰計算方法

計算DHKey需要進行超大數的計算,在百度上搜尋了半天終於找到了一個比較好的大數計算的第三方庫?,放到了私人百度雲上JKBigInteger,密碼:ub2s。需要的可進行下載。協商金鑰方面的事從找到這個庫的時候起就沒什麼大問題了。

  1. 有了金鑰,接下來就是加密方法了。目前為止比較主流的加密方式就是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_valtype_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中遇到的一些問題和解決方法。是沒有原始碼的。

  1. SDK要求所有功能都具有一個success和一個failure回撥以及一個判斷超時的timer。呼叫一次功能方法只會執行上面3種結果中的一個。即當success或failure執行時要把這些都給清除保證不會二次執行。同樣timer執行時也是一樣。
  2. 每一次呼叫方法都會得到一個結果,並且不能發生結果錯亂(比如連續呼叫一個方法兩次,第一次的結果不能呼叫第二次的回撥)。

綜上,我需要把每次寫資料對應的方法id,success,failure,timer儲存起來,等收到裝置的回覆時再根據方法id找到儲存的方法對應的success等,根據回覆的資料再判斷要呼叫哪個回撥。並且需要注意的是呼叫完成後要把這一整條資料都清空。

總結下來就3點

  • 保證不會發生回撥覆蓋
  • 保證不會發生回撥錯亂
  • 保證回撥不會多次執行

好了,這些就是我做的Ble藍芽SDK時遇到的比較有意思的問題了。

相關文章