iOS之BLE蓝牙SDK开发个人总结(进阶篇)

2,807 阅读8分钟

有关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时遇到的比较有意思的问题了。