SSL在IOS中的應用
關於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:
如果最終要得到的資料的長度是個固定長度,用一個字串作為緩衝池,每次收到資料,都用字串拼接對應資料,每當字串的長度和固定長度相同時,便得到一個完整資料,處理完這個資料並清空字串,再進行下一輪的字元拼接。