在使用SocketRocket之前先了解關於網絡層的幾個概念
關於Socket
我們都知道socket是套接字,描述ip地址和端口,它本身並不是協議,而是一個調用接口,爲了大家直接使用更底層的協議(TCP或UDP),是對TCP/IP 或 UDP/IP的封裝。socket處於網絡層中的第五層,是一個抽象層。
關於WebSocket
websocket是一個協議,是基於http協議的,是建立在TCP連接之上的,是應用層上的一個應用層協議,和socket不是一個概念。
WebSocket的特點
websocket可以傳輸文本和二進制。
websocket的協議頭是ws開頭的,並不是http。
WebSocket和HTTP協議
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。
HTTP 協議是一種無狀態的、無連接的、單向的應使用層協議。它採使用了請求/響應模型。通信請求只能由用戶端發起,服務端對請求做出應答解決。這種通信模型有一個弊端:HTTP 協議無法實現服務器主動向用戶端發起消息。這種單向請求的特點,註定了假如服務器有連續的狀態變化,用戶端要獲知就非常麻煩。大多數 Web 應使用程序將通過頻繁的異步JavaScript和XML(AJAX)請求實現長輪詢。輪詢的效率低,非常白費資源(由於必需不停連接,或者者 HTTP 連接始終打開)。
WebSocket 連接允許用戶端和服務器之間進行全雙工通信,以便任一方都可以通過建立的連接將數據推送到另一端。WebSocket 只要要建立一次連接,即可以一直保持連接狀態。這相比於輪詢方式的不停建立連接顯然效率要大大提高。
WebSocket與Socket的關係
Socket其實並不是一個協議,而是爲了方便用TCP或者UDP而籠統出來的一層,是位於應使用層和傳輸控制層之間的一組接口。是應使用層與TCP/IP協議族通信的中間軟件籠統層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對使用戶來說,一組簡單的接口就是一律,讓Socket去組織數據,以符合指定的協議。當兩臺主機通信時,必需通過Socket連接,Socket則利使用TCP/IP協議建立TCP連接。TCP連接則更依靠於底層的IP協議,IP協議的連接則依賴於鏈路層等更低層次。
WebSocket則是一個典型的應使用層協議。
區別是Socket是傳輸控制層協議,WebSocket是應使用層協議。
框架
在iOS 平臺上,我們知道socket的開源框架有 CocoaAsyncSocket, 而websocket的框架有Facebook的 SocketRocket, 以及socket.io-client-swift。
⤵️下面介紹我們今天的主角
SocketRocket
SocketRocket是一個WebSocket客戶端(WebSocket是適用於Web應用的下一代全雙工通訊協議,被成爲“Web的TCP”,它實現了瀏覽器與服務器的雙向通信),採用Object-C編寫。SocketRocket遵循最新的WebSocket規範RFC 6455。
特性:
支持TLS (wss)。
使用NSStream/CFNetworking。
使用ARC。
採用並行架構。大部分的工作由後端的工作隊列(worker queues)完成。
基於委託編程。
1. 集成
- 使用cocoapods
只需要在podfile文件中加入pod 'SocketRocket'
,然後執行pod install
就可以了 - 不使用cocoapods
-
添加文件
把下面的三個文件拖入項目中 -
添加依賴庫
在Build Phases -> Link Binary With Libraries里加入如下frameworks:- libicucore.dylib
- CFNetwork.framework
- Security.framework
- Foundation.framework
-
2. 使用
2.1 添加引用
#import "SocketRocket.h"
2.2 寫代理方法
@interface ViewController ()<SRWebSocketDelegate>
2.3 寫成屬性
@property (strong, nonatomic) SRWebSocket *socket;
2.4 初始化
這裏的server_ip
爲宏定義static NSString *const server_ip = @"ws://";
存放後臺提供的ws地址, 調用open
方法即開啓長連接
//初始化 WebSocket
- (void)initWebSocket{
if (_socket) {
return;
}
//Url
NSURL *url = [NSURL URLWithString:server_ip];
//請求
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
//初始化請求`
_socket = [[SRWebSocket alloc] initWithURLRequest:request];
//代理協議`
_socket.delegate = self;
// 實現這個 SRWebSocketDelegate 協議啊`
//直接連接`
[_socket open]; // open 就是直接連接了
}
2.5 代理方法的實現
這裏需要注意
①如果沒有連接成功就先調用send
方法會崩潰進入斷言, 一定要等webSocketDidOpen
回調完成在發送文本幀/數據包
②和後臺協商好發包的格式, 如果沒有統一會被關閉連接, 一般爲JSON格式的二進制流, 音頻是PCM數據流
#pragma mark -- SRWebSocketDelegate
//收到服務器消息是回調
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message{
NSLog(@"收到服務器返回消息:%@",message);
}
//連接成功
- (void)webSocketDidOpen:(SRWebSocket *)webSocket{
NSLog(@"連接成功,可以立刻登錄你公司後臺的服務器了,還有開啓心跳");
[self initHeart]; //開啓心跳
if (self.socket != nil) {
// 只有 SR_OPEN 開啓狀態才能調 send 方法啊,不然要崩
if (_socket.readyState == SR_OPEN) {
NSString *jsonString = @"{\"sid\": \"13b313a3-fea9-4e28-9e56-352458f7007f\"}";
[_socket send:jsonString]; //發送數據包
} else if (_socket.readyState == SR_CONNECTING) {
NSLog(@"正在連接中,重連後其他方法會去自動同步數據");
// 每隔2秒檢測一次 socket.readyState 狀態,檢測 10 次左右
// 只要有一次狀態是 SR_OPEN 的就調用 [ws.socket send:data] 發送數據
// 如果 10 次都還是沒連上的,那這個發送請求就丟失了,這種情況是服務器的問題了,小概率的
// 代碼有點長,我就寫個邏輯在這裏好了
} else if (_socket.readyState == SR_CLOSING || _socket.readyState == SR_CLOSED) {
// websocket 斷開了,調用 reConnect 方法重連
}
} else {
NSLog(@"沒網絡,發送失敗,一旦斷網 socket 會被我設置 nil 的");
NSLog(@"其實最好是發送前判斷一下網絡狀態比較好,我寫的有點晦澀,socket==nil來表示斷網");
}
}
//連接失敗的回調
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error{
NSLog(@"連接失敗,這裏可以實現掉線自動重連,要注意以下幾點");
NSLog(@"1.判斷當前網絡環境,如果斷網了就不要連了,等待網絡到來,在發起重連");
NSLog(@"2.判斷調用層是否需要連接,例如用戶都沒在聊天界面,連接上去浪費流量");
//關閉心跳包
[webSocket close];
[self reConnect];
}
//連接斷開的回調
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean;
{
NSLog(@"Close");
}
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload;
{
NSLog(@"Pong");
}
2.6 心跳包(定時器實現)
//保活機制 探測包
- (void)initHeart{
__weak typeof(self) weakSelf = self;
_heatBeat = [NSTimer scheduledTimerWithTimeInterval:3*60 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf.socket send:@"heart"];
NSLog(@"已發送");
}];
[[NSRunLoop currentRunLoop] addTimer:_heatBeat forMode:NSRunLoopCommonModes];
}
如果開啓了心跳記得在合適的地方銷燬定時器, 避免內存泄漏
//斷開連接時銷燬心跳
- (void)destoryHeart{
}
2.7 重連機制
- (void)reConnect{
//每隔一段時間重連一次
//規定64不在重連,2的指數級
if (_reConnectTime > 60) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.socket = nil;
[self initWebSocket];
});
if (_reConnectTime == 0) {
_reConnectTime = 2;
}else{
_reConnectTime *= 2;
}
}
參考文獻:
SocketRocket的簡單使用
socketRocket 封裝,添加重連機制,block回調
SocketRocket源碼分析