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

有关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 系统头文件。
至此,有关协议拟定方面的问题就没什么问题了。
----------------------这是一条分界线--------------------------
有密钥,有加密传输数据的方法,接下来就是数据来源了。 因为数据的格式有很多种,而在手机和蓝牙之间进行传输的却只有一种--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点
  • 保证不会发生回调覆盖
  • 保证不会发生回调错乱
  • 保证回调不会多次执行
【iOS之BLE蓝牙SDK开发个人总结(进阶篇)】好了,这些就是我做的Ble蓝牙SDK时遇到的比较有意思的问题了。

    推荐阅读