閑扯
WebSocket 以前沒用過,之前寫過一篇博客是基于原生socket的(查看)比較復(fù)雜,慎入。今天另外一個(gè)APP需要接websocket了,然后便找到了facebook的 SocketRocket 框架,然后用了一天時(shí)間接上了,完成了掉線自動(dòng)重連,自動(dòng)重登錄,心跳等等功能,用法比原生socket簡單(原生socket基于TCP/UDP協(xié)議)。
為什么用 WebSocket
因?yàn)锳PP里面有個(gè)聊天功能,需要服務(wù)器主動(dòng)推數(shù)據(jù)到APP。HTTP 通信方式只能由客戶端主動(dòng)拉取,服務(wù)器不能主動(dòng)推給客戶端,如果有實(shí)時(shí)的消息,要立刻通知客戶端就麻煩了,要么客戶端每隔幾秒鐘發(fā)一次請(qǐng)求,看看有沒有新數(shù)據(jù),這種方式想想都知道耗流量電量。還一種方式就是走TCP/UDP協(xié)議服務(wù)器主動(dòng)推給你,這種方式省流量。還有就是用websocket,websocket是h5里面的東西,h5我不太會(huì),反正它比原生socket用法簡單。
用法
用 SocketRocket 框架,記住幾個(gè)代理方法就好了,很簡單。
1.創(chuàng)建和設(shè)置代理對(duì)象
SRWebSocket *socket = [[SRWebSocket alloc] initWithURLRequest:
[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://ip地址:端口"]];
socket.delegate = self; // 實(shí)現(xiàn)這個(gè) SRWebSocketDelegate 協(xié)議啊
[socket open]; // open 就是直接連接了
2.連接成功會(huì)調(diào)用這個(gè)代理方法
- (void)webSocketDidOpen:(SRWebSocket *)webSocket {
NSLog(@"連接成功,可以立刻登錄你公司后臺(tái)的服務(wù)器了,還有開啟心跳");
}
3.連接失敗會(huì)調(diào)用這個(gè)方法,看 NSLog 里面的東西
- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error {
NSLog(@"連接失敗,這里可以實(shí)現(xiàn)掉線自動(dòng)重連,要注意以下幾點(diǎn)");
NSLog(@"1.判斷當(dāng)前網(wǎng)絡(luò)環(huán)境,如果斷網(wǎng)了就不要連了,等待網(wǎng)絡(luò)到來,在發(fā)起重連");
NSLog(@"2.判斷調(diào)用層是否需要連接,例如用戶都沒在聊天界面,連接上去浪費(fèi)流量");
NSLog(@"3.連接次數(shù)限制,如果連接失敗了,重試10次左右就可以了,不然就死循環(huán)了。
或者每隔1,2,4,8,10,10秒重連...f(x) = f(x-1) * 2, (x<5) f(x)=10, (x>=5)");
}
4.連接關(guān)閉調(diào)用這個(gè)方法,注意連接關(guān)閉不是連接斷開,關(guān)閉是 [socket close] 客戶端主動(dòng)關(guān)閉,斷開可能是斷網(wǎng)了,被動(dòng)斷開的。
- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean {
NSLog(@"連接斷開,清空socket對(duì)象,清空該清空的東西,還有關(guān)閉心跳!");
}
5.收到服務(wù)器發(fā)來的數(shù)據(jù)會(huì)調(diào)用這個(gè)方法
- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message {
NSLog(@"收到數(shù)據(jù)了,注意 message 是 id 類型的,學(xué)過C語言的都知道,id 是 (void *)
void* 就厲害了,二進(jìn)制數(shù)據(jù)都可以指著,不詳細(xì)解釋 void* 了");
NSLog(@"我這后臺(tái)約定的 message 是 json 格式數(shù)據(jù)
收到數(shù)據(jù),就按格式解析吧,然后把數(shù)據(jù)發(fā)給調(diào)用層");
}
6.向服務(wù)器發(fā)送數(shù)據(jù)
發(fā)送的時(shí)候可能斷網(wǎng),可能socket還在連接,要判斷一些情況,寫在下面了
發(fā)送邏輯是,我有一個(gè) socketQueue 的串行隊(duì)列,發(fā)送請(qǐng)求會(huì)加到這個(gè)隊(duì)列里,然后一個(gè)一個(gè)發(fā)出去,如果掉線了,重連連上后繼續(xù)發(fā)送,對(duì)調(diào)用層透明,調(diào)用層不需要知道網(wǎng)絡(luò)斷開了。
- (void)sendData:(id)data {
WEAKSELF(ws);
dispatch_async(self.socketQueue, ^{
if (ws.socket != nil) {
// 只有 SR_OPEN 開啟狀態(tài)才能調(diào) send 方法啊,不然要崩
if (ws.socket.readyState == SR_OPEN) {
[ws.socket send:data]; // 發(fā)送數(shù)據(jù)
} else if (ws.socket.readyState == SR_CONNECTING) {
NSLog(@"正在連接中,重連后其他方法會(huì)去自動(dòng)同步數(shù)據(jù)");
// 每隔2秒檢測(cè)一次 socket.readyState 狀態(tài),檢測(cè) 10 次左右
// 只要有一次狀態(tài)是 SR_OPEN 的就調(diào)用 [ws.socket send:data] 發(fā)送數(shù)據(jù)
// 如果 10 次都還是沒連上的,那這個(gè)發(fā)送請(qǐng)求就丟失了,這種情況是服務(wù)器的問題了,小概率的
// 代碼有點(diǎn)長,我就寫個(gè)邏輯在這里好了
} else if (ws.socket.readyState == SR_CLOSING || ws.socket.readyState == SR_CLOSED) {
// websocket 斷開了,調(diào)用 reConnect 方法重連
[ws reConnect:^{
NSLog(@"重連成功,繼續(xù)發(fā)送剛剛的數(shù)據(jù)");
[ws.socket send:data];
}];
}
} else {
NSLog(@"沒網(wǎng)絡(luò),發(fā)送失敗,一旦斷網(wǎng) socket 會(huì)被我設(shè)置 nil 的");
NSLog(@"其實(shí)最好是發(fā)送前判斷一下網(wǎng)絡(luò)狀態(tài)比較好,我寫的有點(diǎn)晦澀,socket==nil來表示斷網(wǎng)");
}
});
}
7.心跳機(jī)制
心跳機(jī)制就不難了,開個(gè)定時(shí)器,問下后臺(tái)要每隔多少秒發(fā)送一次心跳請(qǐng)求就好了。然后注意,斷網(wǎng)了或者socket斷開的時(shí)候把心跳關(guān)一下,省資源,不然都斷網(wǎng)了,還在循環(huán)發(fā)心跳,浪費(fèi)CPU和電量。
8.終于接完websocket了,下班回家壓壓驚。我第一次用,其實(shí)不難,就是考慮的情況比較多,整個(gè)邏輯有點(diǎn)多,主要代碼就是上面那些了,其他不重要的代碼我就不復(fù)制粘貼上來了。