SSL在IOS中的应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ws_752958369/article/details/83719253

关于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:

如果最终要得到的数据的长度是个固定长度,用一个字符串作为缓冲池,每次收到数据,都用字符串拼接对应数据,每当字符串的长度和固定长度相同时,便得到一个完整数据,处理完这个数据并清空字符串,再进行下一轮的字符拼接。

猜你喜欢

转载自blog.csdn.net/ws_752958369/article/details/83719253