WebSocket
- WebSocket 是 HTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議呻粹。
- HTTP 協(xié)議是一種無狀態(tài)的奢方、無連接的侣监、單向的應(yīng)用層協(xié)議。它采用了請求/響應(yīng)模型齿诉。通信請求只能由客戶端發(fā)起,服務(wù)端對請求做出應(yīng)答處理晌姚。這種通信模型有一個(gè)弊端:HTTP 協(xié)議無法實(shí)現(xiàn)服務(wù)器主動向客戶端發(fā)起消息粤剧。這種單向請求的特點(diǎn),注定了如果服務(wù)器有連續(xù)的狀態(tài)變化挥唠,客戶端要獲知就非常麻煩抵恋。大多數(shù) Web 應(yīng)用程序?qū)⑼ㄟ^頻繁的異步JavaScript和XML(AJAX)請求實(shí)現(xiàn)長輪詢。輪詢的效率低猛遍,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接馋记,或者 HTTP 連接始終打開)。
- WebSocket 連接允許客戶端和服務(wù)器之間進(jìn)行全雙工通信懊烤,以便任一方都可以通過建立的連接將數(shù)據(jù)推送到另一端梯醒。WebSocket 只需要建立一次連接,就可以一直保持連接狀態(tài)腌紧。這相比于輪詢方式的不停建立連接顯然效率要大大提高茸习。
WebSocket與Socket的關(guān)系
Socket其實(shí)并不是一個(gè)協(xié)議,而是為了方便使用TCP或UDP而抽象出來的一層壁肋,是位于應(yīng)用層和傳輸控制層之間的一組接口号胚。是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口浸遗。在設(shè)計(jì)模式中猫胁,Socket其實(shí)就是一個(gè)門面模式,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面跛锌,對用戶來說弃秆,一組簡單的接口就是全部,讓Socket去組織數(shù)據(jù)髓帽,以符合指定的協(xié)議菠赚。當(dāng)兩臺主機(jī)通信時(shí),必須通過Socket連接郑藏,Socket則利用TCP/IP協(xié)議建立TCP連接衡查。TCP連接則更依靠于底層的IP協(xié)議,IP協(xié)議的連接則依賴于鏈路層等更低層次必盖。
WebSocket則是一個(gè)典型的應(yīng)用層協(xié)議拌牲。
區(qū)別是Socket是傳輸控制層協(xié)議俱饿,WebSocket是應(yīng)用層協(xié)議。
WebSocket的框架SocketRocket
- 工具類封裝
#import <Foundation/Foundation.h>
#import <SocketRocket.h>
extern NSString * const GQNotification_SocketRocketDidOpen;
extern NSString * const GQNotification_SocketRocketDidClose;
extern NSString * const GQNotification_SocketRocketDidReceive;
@interface GQSocketRocketManager : NSObject
+ (instancetype)gq_shareInstance;
/** 獲取連接狀態(tài) */
@property (nonatomic, assign, readonly) SRReadyState socketReadyState;
/** 開啟連接 */
- (void)gq_openWithURLString:(NSString *)urlString;
/** 關(guān)閉連接 */
- (void)gq_close;
/** 發(fā)送數(shù)據(jù) */
- (void)gq_sendData:(id)data;
@end
#import "GQSocketRocketManager.h"
#import "GQKit.h"
NSString * const GQNotification_SocketRocketDidOpen = @"GQNotification_SocketRocketDidOpen";
NSString * const GQNotification_SocketRocketDidClose = @"GQNotification_SocketRocketDidClose";
NSString * const GQNotification_SocketRocketDidReceive = @"GQNotification_SocketRocketDidReceive";
@interface GQSocketRocketManager()<SRWebSocketDelegate>
@property (nonatomic,strong) SRWebSocket *socket;
@property (nonatomic,strong) NSTimer *heartBeat;
@property (nonatomic,assign) NSTimeInterval reConnectTime;
@property (nonatomic,copy) NSString *urlString;
@end
@implementation GQSocketRocketManager
+ (instancetype)gq_shareInstance {
static GQSocketRocketManager *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[GQSocketRocketManager alloc] init];
});
return instance;
}
- (void)gq_openWithURLString:(NSString *)urlString {
if (self.socket) {
return;
}
if (!urlString) {
return;
}
self.urlString = urlString;
self.socket = [[SRWebSocket alloc] initWithURLRequest:
[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];
self.socket.delegate = self;
[self.socket open];
}
- (void)gq_close {
if (self.socket){
[self.socket close];
self.socket = nil;
[self destoryHeartBeat];
}
}
- (void)gq_sendData:(id)data {
__weak __typeof(self) weakSelf = self;
dispatch_queue_t queue = dispatch_queue_create("zy", NULL);
dispatch_async(queue, ^{
if (weakSelf.socket != nil) {
// 只有 SR_OPEN 開啟狀態(tài)才能調(diào) send 方法啊塌忽,不然要崩
if (weakSelf.socket.readyState == SR_OPEN) {
[weakSelf.socket send:data]; // 發(fā)送數(shù)據(jù)
} else if (weakSelf.socket.readyState == SR_CONNECTING) {
GQLog(@"正在連接中");
[self reConnect];
} else if (weakSelf.socket.readyState == SR_CLOSING || weakSelf.socket.readyState == SR_CLOSED) {
[self reConnect];
}
} else {
}
});
}
//重連機(jī)制
- (void)reConnect {
[self gq_close];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.reConnectTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.socket = nil;
[self gq_openWithURLString:self.urlString];
});
if (self.reConnectTime == 0) {
self.reConnectTime = 2;
}
}
//取消心跳
- (void)destoryHeartBeat {
GQ_dispatch_main_async_safe(^{
if (self.heartBeat) {
if ([self.heartBeat respondsToSelector:@selector(isValid)]){
if ([self.heartBeat isValid]){
[self.heartBeat invalidate];
self.heartBeat = nil;
}
}
}
})
}
//初始化心跳
- (void)initHeartBeat {
GQ_dispatch_main_async_safe(^{
[self destoryHeartBeat];
self.heartBeat = [NSTimer timerWithTimeInterval:3 target:self selector:@selector(sentheart) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.heartBeat forMode:NSRunLoopCommonModes];
})
}
-(void)sentheart {
//發(fā)送心跳
[self gq_sendData:[@"1" dataUsingEncoding:NSUTF8StringEncoding]];
}
#pragma mark - delegate
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
//每次正常連接的時(shí)候清零重連時(shí)間
self.reConnectTime = 0;
//開啟心跳
[self initHeartBeat];
if (webSocket == self.socket) {
GQLog(@"socket連接成功");
[[NSNotificationCenter defaultCenter] postNotificationName:GQNotification_SocketRocketDidOpen object:nil];
}
}
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
if (webSocket == self.socket) {
GQLog(@"socket連接失敗");
_socket = nil;
[self reConnect];
}
}
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
if (webSocket == self.socket) {
GQLog(@"socket連接斷開 被關(guān)閉連接稍途,code:%ld,reason:%@,wasClean:%d",(long)code,reason,wasClean);
[self reConnect];
}
}
/*該函數(shù)是接收服務(wù)器發(fā)送的pong消息,其中最后一個(gè)是接受pong消息的砚婆,
在這里就要提一下心跳包械拍,一般情況下建立長連接都會建立一個(gè)心跳包,
用于每隔一段時(shí)間通知一次服務(wù)端装盯,客戶端還是在線坷虑,這個(gè)心跳包其實(shí)就是一個(gè)ping消息,
*/
- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload {
// NSString *reply = [[NSString alloc] initWithData:pongPayload encoding:NSUTF8StringEncoding];
}
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
if (webSocket == self.socket) {
GQLog(@"socket收到數(shù)據(jù)message:%@",message);
[[NSNotificationCenter defaultCenter] postNotificationName:GQNotification_SocketRocketDidReceive object:message];
}
}
- (SRReadyState)socketReadyState {
return self.socket.readyState;
}
@end
封裝的步驟邏輯
- 1.開啟連接
- (void)gq_openWithURLString:(NSString *)urlString;
- 2.開啟心跳
像心跳一樣每隔固定時(shí)間發(fā)一次埂奈,以此來告訴服務(wù)器迄损,這個(gè)客戶端還活著。事實(shí)上這是為了保持長連接账磺,至于這個(gè)包的內(nèi)容芹敌,是沒有什么特別規(guī)定的,不過一般都是很小的包垮抗,或者只包含包頭的一個(gè)空包氏捞。所以心跳就是一個(gè)定時(shí)器
heartBeat
,每3秒發(fā)送一次冒版。
心跳的每一次發(fā)送其實(shí)就是一個(gè)ping消息液茎,其服務(wù)器返回的是pong消息,-(void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload
辞嗡,接收不到pong那就是心跳斷開了捆等。
- 3.重連機(jī)制
當(dāng)自己網(wǎng)絡(luò)斷開時(shí)候,需要重連续室;
當(dāng)后臺主動斷開栋烤,正常斷開,會調(diào)用- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
挺狰,處理可以是關(guān)閉連接SRWebSocketClose
明郭,也可以是重連,一般是后者她渴;
當(dāng)后臺是網(wǎng)絡(luò)不好达址,或者崩潰蔑祟,調(diào)用- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error
趁耗,會重新連接;