关于SSL的一些介绍,在上篇文章中《关于SSL的初步理解》有介绍过。下面主要介绍SSL在IOS下的应用.
首先,由于SSL提供了一套数据加密通信的安全协议,其实现过程偏底层,且过程极其复杂。好在Github上为我们提供了一套开源的Socket框架CocoaAsyncSocket,基于TCP、UDP的功能封装也是相当的完整。
一.目录结构
实现方式也是基于GCD完成,CocoaAsyncSocket中主要包含两个类:
1.GCDAsyncSocket
用GCD搭建的基于TCP/IP协议的socket网络库
GCDAsyncSocket is a TCP/IP socket networking library built atop Grand Central Dispatch. -- 引自CocoaAsyncSocket.
2.GCDAsyncUdpSocket
用GCD搭建的基于UDP/IP协议的socket网络库.
GCDAsyncUdpSocket is a UDP/IP socket networking library built atop Grand Central Dispatch..-- 引自CocoaAsyncSocket.
二.客户端的创建
1.继承GCDAsyncSocketDelegate协议
2.声明属性
@property (nonatomic,strong)GCDAsyncSocket *clientSocket;//客户端socket
3.创建socket并制定代理对象为self
dispatch_queue_t delegateQueue = dispatch_queue_create("dispatch_queue_concrate", DISPATCH_QUEUE_CONCURRENT);
//dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
self.clientSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:delegateQueue];
4.连接指定主机对应端口,连接的主机为IP地址,并非DNS名称.
NSError *error;
BOOL isConnect = [self.clientSocket connectToHost:@"127.0.0.1" onPort:5036 error:&error];
if (!isConnect) {
NSLog(@"连接失败,:%@",error);
}else{
NSLog(@"连接成功");
}
5.实现GCDAsyncSocketDelegate代理方法
1.连接成功代理回调
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
NSLog(@"socket连接成功:%@,port:%hu",host,port);
[self addSecurtyTransport];
//开始读取来自server端的数据
[sock readDataWithTimeout:-1 tag:0];
}
2.开始手动签名验证回调,需要实现startTLS方法才会被执行。
-(void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler
{
NSLog(@"thread:%@",[NSThread currentThread]);
/*
* This is only called if startTLS is invoked with options that include:
* - GCDAsyncSocketManuallyEvaluateTrust == YES
// 服务器自签名证书:
//openssl req -new -x509 -nodes -days 365 -newkey rsa:1024 -out kohler_local_communicate.crt -keyout kohler_local_communicate.key
//Mac平台API(SecCertificateCreateWithData函数)需要der格式证书,分发到终端后需要转换一下
//openssl x509 -outform der -in kohler_local_communicate.crt -out kohler_local_communicate.der
*/
//1.获取根证书p12文件 2.导入钥匙串 3.从钥匙串导出根证书cer文件即可 root-decode64
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"root-decode64" ofType:@"cer"];
NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
OSStatus status = -1;
SecTrustResultType result = kSecTrustResultDeny;
if (cerData) {
SecCertificateRef cert1;
// 将DER encoded X.509转换成 SecCertificateRef
cert1 = SecCertificateCreateWithData(NULL, (__bridge CFDataRef) cerData);
NSArray *caArray = [NSArray arrayWithObjects:(__bridge id)(cert1), nil];
// 设置证书用于验证
SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)caArray);
// 同步验证服务器证书和本地证书是否匹配,会一直阻塞验证
status = SecTrustEvaluate(trust, &result);
CFRelease(cert1);
}else{
NSLog(@"local certificate could't be loaded!");
completionHandler(NO);
}
if ((status == noErr &&
(result == kSecTrustResultProceed || result == kSecTrustResultUnspecified))) {
// 成功通过验证,证书可信
NSLog(@"成功通过验证,证书可信");
completionHandler(YES);
}else{
CFArrayRef arrayRefTrust = SecTrustCopyProperties(trust);
NSLog(@"error in connection occured\n %@", arrayRefTrust);
completionHandler(NO);
}
}
3.SSL握手成功,建立安全通信链接(也是需要实现startTLS才有回调)
-(void)socketDidSecure:(GCDAsyncSocket *)sock
{
NSLog(@"----SSL握手成功,建立安全通信链接----");
}
4.读取服务端数据
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSLog(@"didReadData:%@,---tag:%li",data,tag);
NSString *receivedStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"收到了一条消息:%@,---tag:%li",receivedStr,tag);
//继续读取来自server端的数据
[sock readDataWithTimeout:-1 tag:0];
}
5.发送数据到服务端回调,使用writeData方法触发。
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
NSLog(@"发送了一条消息:%li",tag);
}
6.socket断开连接回调
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
self.clientSocket = nil;
self.clientSocket.delegate = nil;
NSLog(@"socket连接中断:%@,with error:%@",sock,err);
}
7.建立心跳连接。
// 计时器
@property (nonatomic, strong) NSTimer *connectTimer;
// 添加定时器
- (void)addTimer
{
// 长连接定时器
self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
// 把定时器添加到当前运行循环,并且调为通用模式
[[NSRunLoop currentRunLoop] addTimer:self.connectTimer forMode:NSRunLoopCommonModes];
}
// 心跳连接
- (void)longConnectToSocket
{
// 发送固定格式的数据,指令@"longConnect"
float version = [[UIDevice currentDevice] systemVersion].floatValue;
NSString *longConnect = [NSString stringWithFormat:@"123%f",version];
NSData *data = [longConnect dataUsingEncoding:NSUTF8StringEncoding];
[self.clientSocket writeData:data withTimeout:- 1 tag:0];
}
注意:心跳连接中发送给服务端的数据只是作为测试代码,根据你们公司需求,或者和后台商定好心跳包的数据以及发送心跳的时间间隔.因为这个项目的服务端socket也是我写的,所以,我自定义心跳包协议.客户端发送心跳包,服务端也需要有对应的心跳检测,以此检测客户端是否在线.
8.客户端开始SSL/TLS传输
- (void)addSecurtyTransport
{
NSMutableDictionary *settings = [[NSMutableDictionary alloc] init];
//开始手动SSL证书验证,必定要设置此key
[settings setObject:[NSNumber numberWithBool:YES]
forKey:GCDAsyncSocketManuallyEvaluateTrust];
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"];
NSData *p12Data = [NSData dataWithContentsOfFile:cerPath];
if (p12Data) {
//解密p12文件
CFDataRef inPKCS12Data = (CFDataRef)CFBridgingRetain(p12Data);
CFStringRef password = CFSTR("123456");
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import(inPKCS12Data, options, &items);
CFRelease(options);
CFRelease(password);
if (securityError == errSecSuccess) {
NSLog(@"Success opening p12 certificate.");
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
SecIdentityRef myIdent = (SecIdentityRef)CFDictionaryGetValue(identityDict,
kSecImportItemIdentity);
SecIdentityRef certArray[1] = { myIdent };
CFArrayRef myCerts = CFArrayCreate(NULL, (void *)certArray, 1, NULL);
[settings setObject:(id)CFBridgingRelease(myCerts) forKey:(NSString *)kCFStreamSSLCertificates];
}else{
NSLog(@"fail opening p12 certificate.");
}
}
[self.clientSocket startTLS:settings];
}
三.服务端的创建
1.继承GCDAsyncSocketDelegate
2.创建服务端,并指定代理为self.
dispatch_queue_t delegateQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:delegateQueue];
3.开启端口监听,并启动服务器。
NSError *err;
BOOL isOpen = [_serverSocket acceptOnPort:5036 error:&err];
if (!isOpen) {
NSLog(@"服务端开启失败:%@,",err);
}else{
NSLog(@"服务端开启成功,端口号:5036");
}
4.实现代理方法
1.服务端接收到来自客户端连接
-(void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
NSLog(@"收到一条新链接--->服务端的socket %@ ,客户端的socket %@",sock,newSocket);
//这里需要保存一下新建立的socket连接,不然server端会马上断开连接。
[self->_clientSockets addObject:newSocket];
//开始读取来自客户端的数据流
[newSocket readDataWithTimeout:-1 tag:0];
}
2.接收客户端数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
[self showMessageWithStr:text];
// 第一次读取到的数据直接添加
if (self.clientPhoneTimeDicts.count == 0)
{
[self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text];
}
else
{
// 键相同,直接覆盖,值改变
[self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[self.clientPhoneTimeDicts setObject:[self getCurrentTime] forKey:text];
}];
}
[sock readDataWithTimeout:- 1 tag:0];
}
3.发送回复数据到客户端,由writeData方法触发
-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
NSLog(@"%@,发送了一条消息:%li,",sock,tag);
}
4.建立检测心跳连接
// 检测心跳计时器
@property (nonatomic, strong) NSTimer *checkTimer;
// 添加计时器
- (void)addTimer
{
// 长连接定时器
self.checkTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(checkLongConnect) userInfo:nil repeats:YES];
// 把定时器添加到当前运行循环,并且调为通用模式
[[NSRunLoop currentRunLoop] addTimer:self.checkTimer forMode:NSRunLoopCommonModes];
}
// 检测心跳
- (void)checkLongConnect
{
[self.clientPhoneTimeDicts enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 获取当前时间
NSString *currentTimeStr = [self getCurrentTime];
// 延迟超过10秒判断断开
if (([currentTimeStr doubleValue] - [obj doubleValue]) > 10.0)
{
[self showMessageWithStr:[NSString stringWithFormat:@"%@已经断开,连接时差%f",key,[currentTimeStr doubleValue] - [obj doubleValue]]];
[self showMessageWithStr:[NSString stringWithFormat:@"移除%@",key]];
[self.clientPhoneTimeDicts removeObjectForKey:key];
}
else
{
[self showMessageWithStr:[NSString stringWithFormat:@"%@处于连接状态,连接时差%f",key,[currentTimeStr doubleValue] - [obj doubleValue]]];
}
}];
}
5.socket断开连接
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err
{
NSLog(@"socketDidDisconnect:%@,with error:%@",sock,err);
}
四.数据粘包处理
1.粘包现象
例如:包数据为:abcd
2.粘包解决思路
方法1:
发送方将数据包加上包头和包尾,包头、包体以及包尾用字典形式包装成json字符串,接收方,通过解析获取json字符串中的包体,便可进行进一步处理.
方法2:
添加前缀.和包内容拼接成同一个字符串,使用componentsSeparatedByString:方法,以ab为分隔符,将每个包内容存入数组中,再取对应数组中的数据操作即可.
方法3:
如果最终要得到的数据的长度是个固定长度,用一个字符串作为缓冲池,每次收到数据,都用字符串拼接对应数据,每当字符串的长度和固定长度相同时,便得到一个完整数据,处理完这个数据并清空字符串,再进行下一轮的字符拼接。