iOS之BLE蓝牙SDK开发个人总结(进阶篇)
有关iOS BLE蓝牙基础功能的封装已经在上篇文章写完了,本篇文章负责把在SDK封装过程中遇到的问题知识点进行总结。
封装SDK实质上是把一些功能给封装成一个个对应的方法,用SDK的人只需要调用相应的方法就能实现对应的功能,而不再需要一个复杂的实现过程。蓝牙功能的实现实质上是通过手机和蓝牙互相通信而建立的,所以通信的协议是由我们自己进行拟定的。解释一下协议的拟定,就是手机端和设备端提前商量好用某些字符代表某种意义,可以理解为手机端和设备端两者之间建立了一种特殊的语言。(比如:12345代表让设备自爆,那么设备收到12345的时候就自爆了?举个例子)。
好了,现在进入正题 既然有通信协议,那么为了安全考虑就一定需要加密传输,否则随便来个人给设备发个12345。。。。不就完了。
下面就是第一个问题
- 有秘密,就肯定需要密钥进行加解密,但是密钥又不能直接发送,否则被截取了密钥和没有密钥有啥区别。
于是我们采用DH协商密钥的方法进行计算密钥。想了解什么是DH协商密钥的可以看看这个DH协商密钥原理和DH密钥计算方法。计算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
系统头文件。
----------------------这是一条分界线--------------------------
有密钥,有加密传输数据的方法,接下来就是数据来源了。 因为数据的格式有很多种,而在手机和蓝牙之间进行传输的却只有一种--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语言的联合体是什么?下面简单解释一下:
联合体就是定义了两种不同类型的变量,如上接下来就简单了,只需要把那两个C语言方法封装成OC的方法就行了,如下:type_f32 f_val
和type_u8 c[4]
使得这两个不同类型的变量共享同一块地址空间。type_f32
实质上就是float类型,type_u8
实质上是char类型,这个联合体就是让一个float类型的数f_val
和一个字符数组c[4]
共享同一个地址空间,然后提供了两个从空间种取出不同类型数据的方法。至于上面的数组的顺序是0123,还是3210这个要看硬件端是怎么写的了。
+ (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执行时也是一样。
- 每一次调用方法都会得到一个结果,并且不能发生结果错乱(比如连续调用一个方法两次,第一次的结果不能调用第二次的回调)。
总结下来就3点
- 保证不会发生回调覆盖
- 保证不会发生回调错乱
- 保证回调不会多次执行
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天
- 2020-04-07vue中Axios的封装和API接口的管理