基於WebSocket協議的iOS端即時聊天
好好好久沒有在cnblogs上寫博客,不過在這裏寫的最早的一篇博客的時間戳,真是時間久遠啊,那時候還沒畢業。不在cnblogs的期間,在github pages、簡書上寫過博客,github pages的markdown還是不錯的,不過百度不能檢索到文章,也就是通過百度,永遠無法導流搜索到我的文章(感動moving),簡書感覺更適合抒情雞湯,可能我不太能融入那個用戶群體。不過現在我回來了,那些在github pages上的文章,我暫時也不遷移了,畢竟人生本來就不完美,提醒自己不能有強迫癥(內心默念三次)。續上上一篇博客的時間(2016-04-29)繼續回到這裏,中間的間隔兩年多,甚至更長。這段空白時間的大概情況介紹完了~
---------------------臟兮兮的分割線---------------------
言歸正傳,最近因為公司產品的需要,計劃在移動端開發即時聊天的通訊功能。即時聊天的第三方SDK供應商也是非常多的,因為項目高度的自由定制性,數據隱私等方面的考慮,最終Server-Client端都由自己來實現,服務端采用worker man的PHP socket服務器架構。
在長連接雙向通信上,選擇的是WebSocket協議。開發主要負責iOS Client端的開發,按照開發第三方SDK的標準,將關鍵的部分封裝起來,只留出必要的API供外部調用,將相關代碼模塊化,方便後期向公司其他項目中移植聊天模塊。(不能自己坑自己,遇到移植的需求的可能性是非常大的,所以與其散漫的寫代碼,不如按照SDK的標準去做開發。)
WebSocket
WebSocket 協議在2008年誕生,2011年成為國際標準。WebSocket 協議本質上是一個基於 TCP 的協議。是建立在 TCP 協議之上的全雙工通訊協議,與 HTTP 協議有著良好的兼容性。默認端口也是80和443,並且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器,所以服務器端的實現比較容易。協議標識符是ws,請求地址格式:ws://example.com:80/path
握手過程:
為了建立一個 WebSocket 連接,客戶端首先要向服務器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息。如下所示:
客戶端請求Header:
1 --- request header --- 2 GET /chat HTTP/1.1 3 Upgrade: websocket 4 Connection: Upgrade 5 Host: 127.0.0.1:8001 6 Origin: http://127.0.0.1:8001 7 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== 8 Sec-WebSocket-Version: 13
其中附加頭信息"Upgrade: WebSocket"表明這是一個申請協議升級的 HTTP 請求,服務器端解析這些附加的頭信息,根據Sec-WebSocket-Key的字符串,通過sha1算法處理,將response信息(sec-Websocket-Accept字符串)返回給客戶端,客戶端能成功解碼字符串,就和服務器端的 WebSocket連接就建立起來了。
服務器的Response:
1 HTTP/1.1 101 Switching Protocols 2 Content-Length: 0 3 Upgrade: websocket 4 Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= 5 Server: TornadoServer/4.5.1 6 Connection: Upgrade 7 Date: Wed, 21 Jun 2017 03:29:14 GMT
雙方就可以通過這個連接通道自由的傳遞信息,並且這個連接會持續存在直到客戶端或者服務器端的某一方主動的關閉連接。
使用封裝Websocket的SocketRocket(Objective-C)
上面是WebSocket握手連接通信,而站在巨人的肩膀上,這裏使用的是Github上facebook的SocketRocket項目,這是關於WebSocket的Objective-C的封裝,提供簡單的API,讓開發者不用去跟底層協議打交道,而是關註於鏈路上的數據處理,邏輯層。關於SocketRocket的Features使用等,在Github上有詳細介紹,使用起來也非常簡單。需要註意SRWebSocketDelegate協議的相關方法:
//當收到服務器的Message時調用,這裏的message是id類型,可以是NSString,也可以是NSData。
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message;
//當與服務器建立連接時調用
- (void)webSocketDidOpen:(SRWebSocket *)webSocket;
//當發生未知錯誤的時調用,可能是網絡原因等
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error;
//當關閉WebSocket時調用
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
//接收到服務器的Pong時調用
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
//返回YES表示對messages進行轉換,以NSString的形式發送,返回NO,表示跳過NSData->NSString的轉換,直接以NSData來傳遞。默認YES
- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket;
使用方式與之前學Java時使用的socket通訊類似,大概流程如下所示:init websocket -> open -> connected -> sendMsg -> handle server response -> close
具體的代碼也是非常容易在網上找到的,就不大段的貼代碼了。
上述的通道握手建立並且能與服務器簡單通信後,就要考慮各種情況的處理,包括斷網,信號差等,就需要考慮斷線重連,發送心跳包確定是否與服務器保持著連接的狀態。
這裏心跳包的發送是定時執行的,使用NSTimer的方式。
1 dispatch_main_async_safe(^{ 2 3 [self destoryHeartBeat]; 4 5 __weak typeof(self) weakSelf = self; 6 //心跳設置為3分鐘,NAT超時一般為5分鐘 7 _heartBeat = [NSTimer scheduledTimerWithTimeInterval:3 * 60 repeats:YES block:^(NSTimer * _Nonnull timer) { 8 NSLog(@"heart"); 9 //和服務端約定好發送什麽作為心跳標識,盡可能的減小心跳包大小 10 [weakSelf sendHeartBeatMessage]; 11 }]; 12 [[NSRunLoop currentRunLoop]addTimer:_heartBeat forMode:NSRunLoopCommonModes]; 13 })
錯誤斷網等重連的實現:
1 - (void)reConnect { 2 3 [self stopSocket]; 4 5 if (_connectInterval < 2) { 6 _connectInterval = 2; 7 }else{ 8 _connectInterval = _connectInterval + 2; 9 } 10 11 // 斷開連接後每過n+2秒後重新建立一次連接 12 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_connectInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 13 [self startSocket]; 14 }); 15 }
一個好的與服務器連接的Websocket模塊需要細細的打磨,這裏展示的都是很粗糙的模塊,需要根據以後的需求,出現的問題進行不斷的修正,才能有一個好用的Websocket模塊。想到了一句話:細節決定成敗。所以打磨好生活工作學習中的每一個細節~
基於WebSocket協議的iOS端即時聊天