前言
在新公司入職的兩個月時間里學習到了不少新的知識。其中聊天室就是近期在研究公司代碼時學習碰到的一個技術點。其實在上一家公司也接觸過做即時通訊的需求,但之前由于工期比較緊所以就選擇了用環信(一個比較主流的第三方即時通訊sdk)來實現。但其中實現的原理沒有深究,學到的東西并不多。因此借著這個機會拜讀一下同事的代碼,寫下這篇學習筆記補充一下即時通訊這一塊漏掉的知識。
實現方式
iOS 不用第三方sdk實現即時通訊的主流方法主要有4種:1、基于Scoket原生:代表框架 CocoaAsyncSocket。2、基于WebScoket:代表框架 SocketRocket。3、基于MQTT:代表框架 MQTTKit。4、基于XMPP:代表框架 XMPPFramework。這四種方式都各有利弊。我們公司選擇的實現方式是基于WebSocket實現的,因此我先從這個方式入手,其他3種方式待以后探究。
什么是WebSocket?
我們在客戶端開發的過程中,相信我們遇到最多的網絡協議是HTTP協議。WebSocket 和HTTP 一樣是網絡協議的一種。那么我們已經有強大的HTTP協議了為什么還需要另外一種網絡協議呢?那是因為HTTP協議有一個很大的弊端--通訊只能有客戶端發起。客戶端發起的request 和服務器下發的respond 是一一對應的。在HTTP協議下如果客戶端有連續的狀態變化,客戶端想要獲取就比較麻煩。我們只能依靠輪詢機制(每隔一段時間向服務器請求一次,了解服務器最新的數據)來獲取。但是輪詢不但耗費性能,而且也并非真正意義上的實現即時性。這就導致HTTP 協議并不適合用于即時通訊。而WebSocket就是解決這一問題而發明的。WebSocket借用了HTTP的協議來完成一次握手,在建立連接后,WebSocket 服務器和 Browser/Client Agent 都能主動的向對方發送或接收數據了。
如何在iOS項目中使用WebSocket
在上面介紹即時通訊的實現方式時提到,WebSocket的代表框架是SocketRocket。團隊的項目也是用SocketRocket來實現聊天室功能的。因此我也來順帶了解一下SocketRocket這個框架。SocketRocket是Facebook開源的一個用于 iOS, macOS and tvOS客戶端的websocket框架。SocketRocket是對WebSocket的封裝。
1、集成
集成SocketRocket方法非常簡單,用cocoapods 在podfile中加入 pod 'SocketRocket',執行pod install指令就可以完成集成。
2、實現聊天室功能需要做哪些事情?
那我們在一次建立一個即時聊天的過程中,我們需要做哪些事情呢?①、首先我們需要建立連接 ②、遵守并指定代理 ③、打開連接加載請求 ④、關閉連接 ⑤、發送消息 ⑥、通過代理方法來獲取接收到的消息
-(void)SRWebSocketOpen{
? ? ? ? ? ? //如果是同一個url return
? ? if (self.socket) {
? ? ? ? ? ? ? ? return;
? ? ? ? }
? ? self.socket = [[SRWebSocket alloc] initWithURLRequest: ? ? ? ? ? ? ? ? ? [NSURLRequest requestWithURL:[NSURL URLWithString:@"ws:xxxxxxxxxxx"]]];//這里填寫你服務器的地址
? ? self.socket.delegate = self;? //SRWebSocketDelegate 協議
? ? ? ? ? ? [self.socket open];? ? //開始連接
}
①、建立連接 ②、遵守并指定代理 ③、打開連接加載請求
在SRWebSocketOpen方法里,我們先創建一個SRWebSocket對象,并設置了SocketRocket的回調代理。在完成以上兩個操作后,調用[_socket Open];開始建立連接。
-(void)SRWebSocketClose{
? ? ? ? if (self.socket){
? ? ? ? ? ? ? ? [self.socket close];
? ? ? ? ? ? ? ? self.socket = nil;
? ? ? ? ? ? ? ? //斷開連接時銷毀心跳
? ? ? ? ? ? ? ? [self destoryHeartBeat];
? ? ? ? }
}
④、關閉連接:
在SRWebSocketClose方法里,通過調用[_socket close]; 關閉連接并銷毀心跳。等一下,什么是心跳?這部分內容在下一個章節說明,暫時先可以理解為檢測連接是否正常的一個機制。
- (void)sendData:(id)data {
? ? ? ? [self.socket send:data] ;
}
⑤、發送消息
- (void)sendData:(id)data 方法中通過調用[_socket send:data];方法發送消息。這個data可以是一個UTF8的字符串或者NSData對象。
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message? {
? ? if (webSocket == self.socket) {
? ? ? ? ? ? ? ? NSLog(@"接收到后臺下發的信息,在這解析。");
? ? ? ? }
}
⑥、通過代理方法來獲取接收到的消息
當接收到信息時,會通過- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message代理方法回調。
按照常規,實現以上方法就已經實現了即時聊天的基本功能。但是,在現實的使用過程中,往往會出現一些特殊的情況需要處理。例如:1、在建立了鏈接后,如何確保客戶端和服務端的之間的鏈接有效可用?2、雖然WebSocket通過握手建立了鏈接,但是在鏈接過程中可能遇到因為網絡不好等的原因導致的連接中斷情況,鏈接斷了如何處理?3、在即時聊天項目中通常還要實現APSN即時推送功能,那服務器如何判斷什么時候通過WebSocket發送消息,什么時候走APNs離線推送呢?
解決以上三個問題,就涉及到了三個webSocket的機制
1、心跳機制
用NSTimer每隔固定時間向服務器發一個心跳包,以此來告訴服務器,這個客戶端還活著。事實上這是為了保持長連接,至于這個包的內容,是沒有什么特別規定的,不過一般都是很小的包,或者只包含包頭的一個空包。
//初始化心跳
- (void)initHeartBeat {
? ? ? dispatch_main_async_safe(^{
? ? ? ? ? ? ? ? ? ? [self destoryHeartBeat]; ? ? ? ? //心跳設置為3分鐘,NAT超時一般為5分鐘
? ? ? ? ? ? ? ? ? ? heartBeat = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(sentheart) userInfo:nil repeats:YES]; ? ? ? ? //和服務端約定好發送什么作為心跳標識,盡可能的減小心跳包大小
? ? ? ? ? ? ? ? ? ? [[NSRunLoop currentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];
? ? ? ? })
}
//取消心跳
- (void)destoryHeartBeat {
? ? dispatch_main_async_safe(^{
? ? ? ? ? ? ? ? if (heartBeat) {
? ? ? ? ? ? ? ? ? ? ? ? if ([heartBeat respondsToSelector:@selector(isValid)]){
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? if ([heartBeat isValid]){
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [heartBeat invalidate];
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? heartBeat = nil;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? }
? ? })
}
-(void)sentheart{
? ? ? ? ? ? //發送心跳 和后臺可以約定發送什么內容? 一般可以調用ping? 這里根據后臺的要求 發送了data給他
? ? ? ? ? ? [self sendData:@"heart"];
}
2、重連機制
重連機制比較好理解,值得注意的是當重連到一定次數仍然失敗后要提示用戶網絡存在問題,就沒必要再繼續重連了。
- (void)reConnect {
? ? ? ? ? ? [self SRWebSocketClose];
? ? ? ? ? ? ? ? if (reConnectTime > 50) {
? ? ? ? ? ? ? ? // 重連50次都失敗
? ? ? ? ? ? ? ? reConnectTime = 0;
? ? ? ? ? ? ? ? // 在這里彈出提示告知用戶網絡不好
? ? ? ? ? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }
? ? ? ? ? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
? ? ? ? ? ? ? ? ? ? self.socket = nil;
? ? ? ? ? ? ? ? ? ? [self SRWebSocketOpen];
? ? ? ? ? ? NSLog(@"重連");
? ? ? ? ? ? });
? ? ? ? ? ? ? ? ? ? reConnectTime ++;
? ? }
3、pingpong機制
當服務端發出一個Ping,客戶端沒有在約定的時間內返回響應的ack,則認為客戶端已經不在線,這時我們Server端會主動斷開Scoket連接,并且改由APNS推送的方式發送消息。
//pingPong
- (void)ping{
? ? if (self.socket.readyState == SR_OPEN) {
? ? ? ? ? ? ? ? [self.socket sendPing:nil];
? ? ? ? }
}
//sendPing的時候,如果網絡通的話,則會收到回調,但是必須保證ScoketOpen,否則會crash
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload {
? ? ? ? NSLog(@"收到pong回調");
}
至此,用SocketRocket建立一個聊天室所需的基本方法大概都列了一下。當然,中間仍有許多代碼邏輯需要處理。這些內容會在仔細研讀后繼續整理出來。
寫在最后
這篇文章只是我自己的學習筆記。第一次寫簡書,有點語無倫次,不好意思。文中的代碼內容大多都是參考公司項目及網上的一些簡書作者的文章。非常感謝這些博主及公司同事的無私分享。最后附上這些博主的原文。
iOS--SocketRocket框架的使用及測試服務器的搭建