iOS即時通訊詳解

前言

本文會用實例的方式雅倒,將iOS各種IM的方案都簡單的實現(xiàn)一遍很钓。并且提供一些選型袱院、實現(xiàn)細(xì)節(jié)以及優(yōu)化的建議。

注:文中的所有的代碼示例亚享,在github中都有demo:

iOS即時通訊咽块,從入門到“放棄”?(demo)

可以打開項目先預(yù)覽效果欺税,對照著進(jìn)行閱讀侈沪。

言歸正傳,首先我們來總結(jié)一下我們?nèi)崿F(xiàn)IM的方式

第一種方式晚凿,使用第三方IM服務(wù)

對于短平快的公司亭罪,完全可以采用第三方SDK來實現(xiàn)。國內(nèi)IM的第三方服務(wù)商有很多晃虫,類似云信皆撩、環(huán)信、融云哲银、LeanCloud扛吞,當(dāng)然還有其它的很多,這里就不一一舉例了荆责,感興趣的小伙伴可以自行查閱下滥比。

第三方服務(wù)商IM底層協(xié)議基本上都是TCP。他們的IM方案很成熟做院,有了它們盲泛,我們甚至不需要自己去搭建IM后臺濒持,什么都不需要去考慮。

如果你足夠懶寺滚,甚至連UI都不需要自己做柑营,這些第三方有各自一套IM的UI,拿來就可以直接用村视。真可謂3分鐘集成...

但是缺點也很明顯官套,定制化程度太高,很多東西我們不可控蚁孔。當(dāng)然還有一個最最重要的一點奶赔,就是太貴了...作為真正社交為主打的APP,僅此一點杠氢,就足以讓我們望而卻步站刑。當(dāng)然,如果IM對于APP只是一個輔助功能鼻百,那么用第三方服務(wù)也無可厚非绞旅。

另外一種方式,我們自己去實現(xiàn)

我們自己去實現(xiàn)也有很多選擇:

1)首先面臨的就是傳輸協(xié)議的選擇愕宋,TCP還是UDP玻靡?

2)其次是我們需要去選擇使用哪種聊天協(xié)議:

基于Scoket或者WebScoket或者其他的私有協(xié)議、

MQTT

還是廣為人詬病的XMPP?

3)我們是自己去基于OS底層Socket進(jìn)行封裝還是在第三方框架的基礎(chǔ)上進(jìn)行封裝中贝?

4)傳輸數(shù)據(jù)的格式囤捻,我們是用Json、還是XML邻寿、還是谷歌推出的ProtocolBuffer蝎土?

5)我們還有一些細(xì)節(jié)問題需要考慮,例如TCP的長連接如何保持绣否,心跳機(jī)制誊涯,Qos機(jī)制,重連機(jī)制等等...當(dāng)然蒜撮,除此之外暴构,我們還有一些安全問題需要考慮。

一段磨、傳輸協(xié)議的選擇

接下來我們可能需要自己考慮去實現(xiàn)IM取逾,首先從傳輸層協(xié)議來說,我們有兩種選擇:TCPorUDP苹支?

image

這個問題已經(jīng)被討論過無數(shù)次了砾隅,對深層次的細(xì)節(jié)感興趣的朋友可以看看這篇文章:

移動端IM/推送系統(tǒng)的協(xié)議選型:UDP還是TCP?

這里我們直接說結(jié)論吧:對于小公司或者技術(shù)不那么成熟的公司债蜜,IM一定要用TCP來實現(xiàn)晴埂,因為如果你要用UDP的話究反,需要做的事太多。當(dāng)然QQ就是用的UDP協(xié)議儒洛,當(dāng)然不僅僅是UDP精耐,騰訊還用了自己的私有協(xié)議,來保證了傳輸?shù)目煽啃跃穑沤^了UDP下各種數(shù)據(jù)丟包黍氮,亂序等等一系列問題。

總之一句話,如果你覺得團(tuán)隊技術(shù)很成熟,那么你用UDP也行溅话,否則還是用TCP為好远豺。

二、我們來看看各種聊天協(xié)議

首先我們以實現(xiàn)方式來切入淮捆,基本上有以下四種實現(xiàn)方式:

基于Scoket原生:代表框架CocoaAsyncSocket郁油。

基于WebScoket:代表框架SocketRocket。

基于MQTT:代表框架MQTTKit攀痊。

基于XMPP:代表框架XMPPFramework桐腌。

當(dāng)然,以上四種方式我們都可以不使用第三方框架苟径,直接基于OS底層Scoket去實現(xiàn)我們的自定義封裝案站。下面我會給出一個基于Scoket原生而不使用框架的例子,供大家參考一下棘街。

首先需要搞清楚的是蟆盐,其中MQTT和XMPP為聊天協(xié)議,它們是最上層的協(xié)議遭殉,而WebScoket是傳輸通訊協(xié)議石挂,它是基于Socket封裝的一個協(xié)議。而通常我們所說的騰訊IM的私有協(xié)議险污,就是基于WebScoket或者Scoket原生進(jìn)行封裝的一個聊天協(xié)議痹愚。

具體這3種聊天協(xié)議的對比優(yōu)劣如下:

image

所以說到底,iOS要做一個真正的IM產(chǎn)品蛔糯,一般都是基于Scoket或者WebScoket等拯腮,再之上加上一些私有協(xié)議來保證的。

1.我們先不使用任何框架渤闷,直接用OS底層Socket來實現(xiàn)一個簡單的IM疾瓮。

我們客戶端的實現(xiàn)思路也是很簡單,創(chuàng)建Socket飒箭,和服務(wù)器的Socket對接上狼电,然后開始傳輸數(shù)據(jù)就可以了蜒灰。

我們學(xué)過c/c++或者java這些語言,我們就知道肩碟,往往任何教程强窖,最后一章都是講Socket編程,而Socket是什么呢削祈,簡單的來說翅溺,就是我們使用TCP/IP或者UDP/IP協(xié)議的一組編程接口。如下圖所示:

image

我們在應(yīng)用層髓抑,使用socket咙崎,輕易的實現(xiàn)了進(jìn)程之間的通信(跨網(wǎng)絡(luò)的)。想想吨拍,如果沒有socket褪猛,我們要直面TCP/IP協(xié)議,我們需要去寫多少繁瑣而又重復(fù)的代碼羹饰。

如果有對socket概念仍然有所困惑的伊滋,可以看看這篇文章:

從問題看本質(zhì),socket到底是什么队秩?笑旺。

但是這篇文章關(guān)于并發(fā)連接數(shù)的認(rèn)識是錯誤的,正確的認(rèn)識可以看看這篇文章:

單臺服務(wù)器并發(fā)TCP連接數(shù)到底可以有多少

我們接著可以開始著手去實現(xiàn)IM了馍资,首先我們不基于任何框架筒主,直接去調(diào)用OS底層-基于C的BSD Socket去實現(xiàn),它提供了這樣一組接口:

//socket 創(chuàng)建并初始化 socket迷帜,返回該 socket 的文件描述符物舒,如果描述符為 -1 表示創(chuàng)建失敗。intsocket(intaddressFamily,inttype,intprotocol)//關(guān)閉socket連接intclose(intsocketFileDescriptor)//將 socket 與特定主機(jī)地址與端口號綁定戏锹,成功綁定返回0冠胯,失敗返回 -1。intbind(intsocketFileDescriptor,sockaddr*addressToBind,intaddressStructLength)//接受客戶端連接請求并將客戶端的網(wǎng)絡(luò)地址信息保存到 clientAddress 中锦针。intaccept(intsocketFileDescriptor,sockaddr*clientAddress,intclientAddressStructLength)//客戶端向特定網(wǎng)絡(luò)地址的服務(wù)器發(fā)送連接請求荠察,連接成功返回0,失敗返回 -1奈搜。intconnect(intsocketFileDescriptor,sockaddr*serverAddress,intserverAddressLength)//使用 DNS 查找特定主機(jī)名字對應(yīng)的 IP 地址悉盆。如果找不到對應(yīng)的 IP 地址則返回 NULL。hostent*gethostbyname(char*hostname)//通過 socket 發(fā)送數(shù)據(jù)馋吗,發(fā)送成功返回成功發(fā)送的字節(jié)數(shù)焕盟,否則返回 -1。intsend(intsocketFileDescriptor,char*buffer,intbufferLength,intflags)//從 socket 中讀取數(shù)據(jù)宏粤,讀取成功返回成功讀取的字節(jié)數(shù)脚翘,否則返回 -1灼卢。intreceive(intsocketFileDescriptor,char*buffer,intbufferLength,intflags)//通過UDP socket 發(fā)送數(shù)據(jù)到特定的網(wǎng)絡(luò)地址,發(fā)送成功返回成功發(fā)送的字節(jié)數(shù)来农,否則返回 -1鞋真。intsendto(intsocketFileDescriptor,char*buffer,intbufferLength,intflags,sockaddr*destinationAddress,intdestinationAddressLength)//從UDP socket 中讀取數(shù)據(jù),并保存發(fā)送者的網(wǎng)絡(luò)地址信息沃于,讀取成功返回成功讀取的字節(jié)數(shù)涩咖,否則返回 -1 。intrecvfrom(intsocketFileDescriptor,char*buffer,intbufferLength,intflags,sockaddr*fromAddress,int*fromAddressLength)

讓我們可以對socket進(jìn)行各種操作繁莹,首先我們來用它寫個客戶端檩互。總結(jié)一下咨演,簡單的IM客戶端需要做如下4件事:

客戶端調(diào)用 socket(...) 創(chuàng)建socket盾似;

客戶端調(diào)用 connect(...) 向服務(wù)器發(fā)起連接請求以建立連接;

客戶端與服務(wù)器建立連接之后雪标,就可以通過send(...)/receive(...)向客戶端發(fā)送或從客戶端接收數(shù)據(jù);

客戶端調(diào)用 close 關(guān)閉 socket溉跃;

根據(jù)上面4條大綱村刨,我們封裝了一個名為TYHSocketManager的單例,來對socket相關(guān)方法進(jìn)行調(diào)用:

TYHSocketManager.h

#import@interfaceTYHSocketManager:NSObject+(instancetype)share;-(void)connect;-(void)disConnect;-(void)sendMsg:(NSString*)msg;@end

TYHSocketManager.m

#import"TYHSocketManager.h"#import#import#import#import@interfaceTYHSocketManager()@property(nonatomic,assign)intclientScoket;@end@implementationTYHSocketManager+(instancetype)share{staticdispatch_once_t onceToken;staticTYHSocketManager*instance=nil;dispatch_once(&onceToken,^{instance=[[selfalloc]init];[instance initScoket];[instance pullMsg];});returninstance;}-(void)initScoket{//每次連接前撰茎,先斷開連接if(_clientScoket!=0){[selfdisConnect];_clientScoket=0;}//創(chuàng)建客戶端socket_clientScoket=CreateClinetSocket();//服務(wù)器Ipconstchar*server_ip="127.0.0.1";//服務(wù)器端口shortserver_port=6969;//等于0說明連接失敗if(ConnectionToServer(_clientScoket,server_ip,server_port)==0){printf("Connect to server error\n");return;}//走到這說明連接成功printf("Connect to server ok\n");}staticintCreateClinetSocket(){intClinetSocket=0;//創(chuàng)建一個socket,返回值為Int嵌牺。(注scoket其實就是Int類型)//第一個參數(shù)addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)。//第二個參數(shù) type 表示 socket 的類型龄糊,通常是流stream(SOCK_STREAM) 或數(shù)據(jù)報文datagram(SOCK_DGRAM)//第三個參數(shù) protocol 參數(shù)通常設(shè)置為0逆粹,以便讓系統(tǒng)自動為選擇我們合適的協(xié)議,對于 stream socket 來說會是 TCP 協(xié)議(IPPROTO_TCP)炫惩,而對于 datagram來說會是 UDP 協(xié)議(IPPROTO_UDP)僻弹。ClinetSocket=socket(AF_INET,SOCK_STREAM,0);returnClinetSocket;}staticintConnectionToServer(intclient_socket,constchar*server_ip,unsignedshortport){//生成一個sockaddr_in類型結(jié)構(gòu)體structsockaddr_in sAddr={0};sAddr.sin_len=sizeof(sAddr);//設(shè)置IPv4sAddr.sin_family=AF_INET;//inet_aton是一個改進(jìn)的方法來將一個字符串IP地址轉(zhuǎn)換為一個32位的網(wǎng)絡(luò)序列IP地址//如果這個函數(shù)成功,函數(shù)的返回值非零他嚷,如果輸入地址不正確則會返回零蹋绽。inet_aton(server_ip,&sAddr.sin_addr);//htons是將整型變量從主機(jī)字節(jié)順序轉(zhuǎn)變成網(wǎng)絡(luò)字節(jié)順序,賦值端口號sAddr.sin_port=htons(port);//用scoket和服務(wù)端地址筋蓖,發(fā)起連接卸耘。//客戶端向特定網(wǎng)絡(luò)地址的服務(wù)器發(fā)送連接請求,連接成功返回0粘咖,失敗返回 -1蚣抗。//注意:該接口調(diào)用會阻塞當(dāng)前線程,直到服務(wù)器返回瓮下。if(connect(client_socket,(structsockaddr*)&sAddr,sizeof(sAddr))==0){returnclient_socket;}return0;}#pragmamark - 新線程來接收消息-(void)pullMsg{NSThread*thread=[[NSThread alloc]initWithTarget:selfselector:@selector(recieveAction)object:nil];[thread start];}#pragmamark - 對外邏輯-(void)connect{[selfinitScoket];}-(void)disConnect{//關(guān)閉連接close(self.clientScoket);}//發(fā)送消息-(void)sendMsg:(NSString*)msg{constchar*send_Message=[msg UTF8String];send(self.clientScoket,send_Message,strlen(send_Message)+1,0);}//收取服務(wù)端發(fā)送的消息-(void)recieveAction{while(1){charrecv_Message[1024]={0};recv(self.clientScoket,recv_Message,sizeof(recv_Message),0);printf("%s\n",recv_Message);}}

如上所示:

我們調(diào)用了initScoket方法翰铡,利用CreateClinetSocket方法了一個scoket钝域,就是就是調(diào)用了socket函數(shù):

ClinetSocket = socket(AF_INET, SOCK_STREAM, 0);

然后調(diào)用了ConnectionToServer函數(shù)與服務(wù)器連接,IP地址為127.0.0.1也就是本機(jī)localhost和端口6969相連两蟀。在該函數(shù)中网梢,我們綁定了一個sockaddr_in類型的結(jié)構(gòu)體,該結(jié)構(gòu)體內(nèi)容如下:

structsockaddr_in{__uint8_t? sin_len;sa_family_t sin_family;in_port_t? sin_port;structin_addrsin_addr;charsin_zero[8];};

里面包含了一些赂毯,我們需要連接的服務(wù)端的scoket的一些基本參數(shù)战虏,具體賦值細(xì)節(jié)可以見注釋。

連接成功之后党涕,我們就可以調(diào)用send函數(shù)和recv函數(shù)進(jìn)行消息收發(fā)了烦感,在這里,我新開辟了一個常駐線程膛堤,在這個線程中一個死循環(huán)里去不停的調(diào)用recv函數(shù)手趣,這樣服務(wù)端有消息發(fā)送過來,第一時間便能被接收到肥荔。

就這樣客戶端便簡單的可以用了绿渣,接著我們來看看服務(wù)端的實現(xiàn)。

一樣燕耿,我們首先對服務(wù)端需要做的工作簡單的總結(jié)下:

服務(wù)器調(diào)用 socket(...) 創(chuàng)建socket中符;

服務(wù)器調(diào)用 listen(...) 設(shè)置緩沖區(qū);

服務(wù)器通過 accept(...)接受客戶端請求建立連接誉帅;

服務(wù)器與客戶端建立連接之后淀散,就可以通過 send(...)/receive(...)向客戶端發(fā)送或從客戶端接收數(shù)據(jù);

服務(wù)器調(diào)用 close 關(guān)閉 socket蚜锨;

接著我們就可以具體去實現(xiàn)了

OS底層的函數(shù)是支持我們?nèi)崿F(xiàn)服務(wù)端的档插,但是我們一般不會用iOS去這么做(試問真正的應(yīng)用場景,有誰用iOS做scoket服務(wù)器么...)亚再,如果還是想用這些函數(shù)去實現(xiàn)服務(wù)端郭膛,可以參考下這篇文章:深入淺出Cocoa-iOS網(wǎng)絡(luò)編程之Socket

在這里我用node.js去搭了一個簡單的scoket服務(wù)器针余。源碼如下:

varnet=require('net');varHOST='127.0.0.1';varPORT=6969;// 創(chuàng)建一個TCP服務(wù)器實例饲鄙,調(diào)用listen函數(shù)開始監(jiān)聽指定端口? // 傳入net.createServer()的回調(diào)函數(shù)將作為”connection“事件的處理函數(shù)? // 在每一個“connection”事件中,該回調(diào)函數(shù)接收到的socket對象是唯一的? net.createServer(function(sock){// 我們獲得一個連接 - 該連接自動關(guān)聯(lián)一個socket對象? console.log('CONNECTED: '+sock.remoteAddress+':'+sock.remotePort);sock.write('服務(wù)端發(fā)出:連接成功');// 為這個socket實例添加一個"data"事件處理函數(shù)? sock.on('data',function(data){console.log('DATA '+sock.remoteAddress+': '+data);// 回發(fā)該數(shù)據(jù)圆雁,客戶端將收到來自服務(wù)端的數(shù)據(jù)? sock.write('You said "'+data+'"');});// 為這個socket實例添加一個"close"事件處理函數(shù)? sock.on('close',function(data){console.log('CLOSED: '+sock.remoteAddress+' '+sock.remotePort);});}).listen(PORT,HOST);console.log('Server listening on '+HOST+':'+PORT);

看到這不懂node.js的朋友也不用著急忍级,在這里你可以使用任意語言c/c++/java/oc等等去實現(xiàn)后臺,這里node.js僅僅是樓主的一個選擇伪朽,為了讓我們來驗證之前寫的客戶端scoket的效果轴咱。如果你不懂node.js也沒關(guān)系,你只需要把上述樓主寫的相關(guān)代碼復(fù)制粘貼,如果你本機(jī)有node的解釋器朴肺,那么直接在終端進(jìn)入該源代碼文件目錄中輸入:

node fileName

即可運行該腳本(fileName為保存源代碼的文件名)窖剑。

我們來看看運行效果:

image

服務(wù)器運行起來了,并且監(jiān)聽著6969端口戈稿。

接著我們用之前寫的iOS端的例子西土。客戶端打印顯示連接成功鞍盗,而我們運行的服務(wù)器也打印了連接成功需了。接著我們發(fā)了一條消息,服務(wù)端成功的接收到了消息后般甲,把該消息再發(fā)送回客戶端肋乍,繞了一圈客戶端又收到了這條消息。至此我們用OS底層scoket實現(xiàn)了簡單的IM敷存。

大家看到這是不是覺得太過簡單了墓造?

當(dāng)然簡單,我們僅僅是實現(xiàn)了Scoket的連接锚烦,信息的發(fā)送與接收觅闽,除此之外我們什么都沒有做,現(xiàn)實中涮俄,我們需要做的處理遠(yuǎn)不止于此谱煤,我們先接著往下看。接下來禽拔,我們就一起看看第三方框架是如何實現(xiàn)IM的。

image

2.我們接著來看看基于Socket原生的CocoaAsyncSocket:

這個框架實現(xiàn)了兩種傳輸協(xié)議TCP和UDP室叉,分別對應(yīng)GCDAsyncSocket類和GCDAsyncUdpSocket睹栖,這里我們重點講GCDAsyncSocket。

這里Socket服務(wù)器延續(xù)上一個例子茧痕,因為同樣是基于原生Scoket的框架野来,所以之前的Node.js的服務(wù)端,該例仍然試用踪旷。這里我們就只需要去封裝客戶端的實例曼氛,我們還是創(chuàng)建一個TYHSocketManager單例。

TYHSocketManager.h

#import@interfaceTYHSocketManager:NSObject+(instancetype)share;-(BOOL)connect;-(void)disConnect;-(void)sendMsg:(NSString*)msg;-(void)pullTheMsg;@end

TYHSocketManager.m

#import"TYHSocketManager.h"#import"GCDAsyncSocket.h"http:// for TCPstaticNSString*Khost=@"127.0.0.1";staticconstuint16_t Kport=6969;@interfaceTYHSocketManager()<GCDAsyncSocketDelegate>{GCDAsyncSocket*gcdSocket;}@end@implementationTYHSocketManager+(instancetype)share{staticdispatch_once_t onceToken;staticTYHSocketManager*instance=nil;dispatch_once(&onceToken,^{instance=[[selfalloc]init];[instance initSocket];});returninstance;}-(void)initSocket{gcdSocket=[[GCDAsyncSocket alloc]initWithDelegate:selfdelegateQueue:dispatch_get_main_queue()];}#pragmamark - 對外的一些接口//建立連接-(BOOL)connect{return[gcdSocket connectToHost:Khost onPort:Kport error:nil];}//斷開連接-(void)disConnect{[gcdSocket disconnect];}//發(fā)送消息-(void)sendMsg:(NSString*)msg{NSData*data=[msg dataUsingEncoding:NSUTF8StringEncoding];//第二個參數(shù)令野,請求超時時間[gcdSocket writeData:data withTimeout:-1tag:110];}//監(jiān)聽最新的消息-(void)pullTheMsg{//監(jiān)聽讀數(shù)據(jù)的代理? -1永遠(yuǎn)監(jiān)聽舀患,不超時,但是只收一次消息气破,//所以每次接受到消息還得調(diào)用一次[gcdSocket readDataWithTimeout:-1tag:110];}#pragmamark - GCDAsyncSocketDelegate//連接成功調(diào)用-(void)socket:(GCDAsyncSocket*)sock didConnectToHost:(NSString*)host port:(uint16_t)port{NSLog(@"連接成功,host:%@,port:%d",host,port);[selfpullTheMsg];//心跳寫在這...}//斷開連接的時候調(diào)用-(void)socketDidDisconnect:(GCDAsyncSocket*)sock withError:(nullable NSError*)err{NSLog(@"斷開連接,host:%@,port:%d",sock.localHost,sock.localPort);//斷線重連寫在這...}//寫成功的回調(diào)-(void)socket:(GCDAsyncSocket*)sock didWriteDataWithTag:(long)tag{//? ? NSLog(@"寫的回調(diào),tag:%ld",tag);}//收到消息的回調(diào)-(void)socket:(GCDAsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag{NSString*msg=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];NSLog(@"收到消息:%@",msg);[selfpullTheMsg];}//分段去獲取消息的回調(diào)//- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag//{//? ? //? ? NSLog(@"讀的回調(diào),length:%ld,tag:%ld",partialLength,tag);////}//為上一次設(shè)置的讀取數(shù)據(jù)代理續(xù)時 (如果設(shè)置超時為-1聊浅,則永遠(yuǎn)不會調(diào)用到)//-(NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length//{//? ? NSLog(@"來延時,tag:%ld,elapsed:%f,length:%ld",tag,elapsed,length);//? ? return 10;//}@end

這個框架使用起來也十分簡單,它基于Scoket往上進(jìn)行了一層封裝低匙,提供了OC的接口給我們使用旷痕。至于使用方法,大家看看注釋應(yīng)該就能明白顽冶,這里唯一需要說的一點就是這個方法:

[gcdSocket readDataWithTimeout:-1tag:110];

這個方法的作用就是去讀取當(dāng)前消息隊列中的未讀消息欺抗。記住,這里不調(diào)用這個方法强重,消息回調(diào)的代理是永遠(yuǎn)不會被觸發(fā)的绞呈。而且必須是tag相同,如果tag不同竿屹,這個收到消息的代理也不會被觸發(fā)报强。

我們調(diào)用一次這個方法,只能觸發(fā)一次讀取消息的代理拱燃,如果我們調(diào)用的時候沒有未讀消息秉溉,它就會等在那,直到消息來了被觸發(fā)碗誉。一旦被觸發(fā)一次代理后召嘶,我們必須再次調(diào)用這個方法,否則哮缺,之后的消息到了仍舊無法觸發(fā)我們讀取消息的代理弄跌。就像我們在例子中使用的那樣,在每次讀取到消息之后我們都去調(diào)用:

//收到消息的回調(diào)-(void)socket:(GCDAsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag{NSString*msg=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];NSLog(@"收到消息:%@",msg);[selfpullTheMsg];}//監(jiān)聽最新的消息-(void)pullTheMsg{//監(jiān)聽讀數(shù)據(jù)的代理尝苇,只能監(jiān)聽10秒铛只,10秒過后調(diào)用代理方法? -1永遠(yuǎn)監(jiān)聽,不超時糠溜,但是只收一次消息淳玩,//所以每次接受到消息還得調(diào)用一次[gcdSocket readDataWithTimeout:-1tag:110];}

除此之外,我們還需要說的是這個超時timeout

這里如果設(shè)置10秒非竿,那么就只能監(jiān)聽10秒蜕着,10秒過后調(diào)用是否續(xù)時的代理方法:

-(NSTimeInterval)socket:(GCDAsyncSocket *)sockshouldTimeoutReadWithTag:(long)tagelapsed:(NSTimeInterval)elapsedbytesDone:(NSUInteger)length

如果我們選擇不續(xù)時,那么10秒到了還沒收到消息红柱,那么Scoket會自動斷開連接承匣。看到這里有些小伙伴要吐槽了锤悄,怎么一個方法設(shè)計的這么麻煩韧骗,當(dāng)然這里這么設(shè)計是有它的應(yīng)用場景的,我們后面再來細(xì)講零聚。

我們同樣來運行看看效果:

image

至此我們也用CocoaAsyncSocket這個框架實現(xiàn)了一個簡單的IM宽闲。

image

3.接著我們繼續(xù)來看看基于webScoket的IM:

這個例子我們會把心跳众眨,斷線重連,以及PingPong機(jī)制進(jìn)行簡單的封裝容诬,所以我們先來談?wù)勥@三個概念:

首先我們來談?wù)勈裁词切奶?/p>

簡單的來說娩梨,心跳就是用來檢測TCP連接的雙方是否可用。那又會有人要問了览徒,TCP不是本身就自帶一個KeepAlive機(jī)制嗎狈定?

這里我們需要說明的是TCP的KeepAlive機(jī)制只能保證連接的存在,但是并不能保證客戶端以及服務(wù)端的可用性.比如會有以下一種情況:

某臺服務(wù)器因為某些原因?qū)е仑?fù)載超高习蓬,CPU 100%纽什,無法響應(yīng)任何業(yè)務(wù)請求,但是使用 TCP 探針則仍舊能夠確定連接狀態(tài)躲叼,這就是典型的連接活著但業(yè)務(wù)提供方已死的狀態(tài)芦缰。

這個時候心跳機(jī)制就起到作用了:

我們客戶端發(fā)起心跳Ping(一般都是客戶端),假如設(shè)置在10秒后如果沒有收到回調(diào)枫慷,那么說明服務(wù)器或者客戶端某一方出現(xiàn)問題让蕾,這時候我們需要主動斷開連接。

服務(wù)端也是一樣或听,會維護(hù)一個socket的心跳間隔探孝,當(dāng)約定時間內(nèi),沒有收到客戶端發(fā)來的心跳誉裆,我們會知道該連接已經(jīng)失效顿颅,然后主動斷開連接。

參考文章:為什么說基于TCP的移動端IM仍然需要心跳弊愣活粱腻?

其實做過IM的小伙伴們都知道,我們真正需要心跳機(jī)制的原因其實主要是在于國內(nèi)運營商NAT超時斩跌。

那么究竟什么是NAT超時呢?

原來這是因為IPV4引起的栖疑,我們上網(wǎng)很可能會處在一個NAT設(shè)備(無線路由器之類)之后。

NAT設(shè)備會在IP封包通過設(shè)備時修改源/目的IP地址. 對于家用路由器來說, 使用的是網(wǎng)絡(luò)地址端口轉(zhuǎn)換(NAPT), 它不僅改IP, 還修改TCP和UDP協(xié)議的端口號, 這樣就能讓內(nèi)網(wǎng)中的設(shè)備共用同一個外網(wǎng)IP. 舉個例子, NAPT維護(hù)一個類似下表的NAT表:

image

NAT設(shè)備會根據(jù)NAT表對出去和進(jìn)來的數(shù)據(jù)做修改, 比如將192.168.0.3:8888發(fā)出去的封包改成120.132.92.21:9202, 外部就認(rèn)為他們是在和120.132.92.21:9202通信. 同時NAT設(shè)備會將120.132.92.21:9202收到的封包的IP和端口改成192.168.0.3:8888, 再發(fā)給內(nèi)網(wǎng)的主機(jī), 這樣內(nèi)部和外部就能雙向通信了, 但如果其中192.168.0.3:8888==120.132.92.21:9202這一映射因為某些原因被NAT設(shè)備淘汰了, 那么外部設(shè)備就無法直接與192.168.0.3:8888通信了滔驶。

我們的設(shè)備經(jīng)常是處在NAT設(shè)備的后面, 比如在大學(xué)里的校園網(wǎng), 查一下自己分配到的IP, 其實是內(nèi)網(wǎng)IP, 表明我們在NAT設(shè)備后面, 如果我們在寢室再接個路由器, 那么我們發(fā)出的數(shù)據(jù)包會多經(jīng)過一次NAT.

國內(nèi)移動無線網(wǎng)絡(luò)運營商在鏈路上一段時間內(nèi)沒有數(shù)據(jù)通訊后, 會淘汰NAT表中的對應(yīng)項, 造成鏈路中斷。

而國內(nèi)的運營商一般NAT超時的時間為5分鐘卿闹,所以通常我們心跳設(shè)置的時間間隔為3-5分鐘揭糕。

接著我們來講講PingPong機(jī)制:

很多小伙伴可能又會感覺到疑惑了,那么我們在這心跳間隔的3-5分鐘如果連接假在線(例如在地鐵電梯這種環(huán)境下)锻霎。那么我們豈不是無法保證消息的即時性么著角?這顯然是我們無法接受的,所以業(yè)內(nèi)的解決方案是采用雙向的PingPong機(jī)制旋恼。

image

當(dāng)服務(wù)端發(fā)出一個Ping吏口,客戶端沒有在約定的時間內(nèi)返回響應(yīng)的ack,則認(rèn)為客戶端已經(jīng)不在線,這時我們Server端會主動斷開Scoket連接产徊,并且改由APNS推送的方式發(fā)送消息昂勒。

同樣的是,當(dāng)客戶端去發(fā)送一個消息舟铜,因為我們遲遲無法收到服務(wù)端的響應(yīng)ack包戈盈,則表明客戶端或者服務(wù)端已不在線,我們也會顯示消息發(fā)送失敗谆刨,并且斷開Scoket連接塘娶。

還記得我們之前CocoaSyncSockt的例子所講的獲取消息超時就斷開嗎?其實它就是一個PingPong機(jī)制的客戶端實現(xiàn)痊夭。我們每次可以在發(fā)送消息成功后刁岸,調(diào)用這個超時讀取的方法,如果一段時間沒收到服務(wù)器的響應(yīng)她我,那么說明連接不可用虹曙,則斷開Scoket連接

最后就是重連機(jī)制:

理論上,我們自己主動去斷開的Scoket連接(例如退出賬號鸦难,APP退出到后臺等等)根吁,不需要重連。其他的連接斷開合蔽,我們都需要進(jìn)行斷線重連击敌。

一般解決方案是嘗試重連幾次,如果仍舊無法重連成功拴事,那么不再進(jìn)行重連沃斤。

接下來的WebScoket的例子,我會封裝一個重連時間指數(shù)級增長的一個重連方式刃宵,可以作為一個參考衡瓶。

言歸正傳,我們看完上述三個概念之后牲证,我們來講一個WebScoket最具代表性的一個第三方框架SocketRocket哮针。

我們首先來看看它對外封裝的一些方法:

@interfaceSRWebSocket:NSObject<NSStreamDelegate>@property(nonatomic,weak)id<SRWebSocketDelegate>delegate;@property(nonatomic,readonly)SRReadyState readyState;@property(nonatomic,readonly,retain)NSURL*url;@property(nonatomic,readonly)CFHTTPMessageRef receivedHTTPHeaders;// Optional array of cookies (NSHTTPCookie objects) to apply to the connections@property(nonatomic,readwrite)NSArray*requestCookies;// This returns the negotiated protocol.// It will be nil until after the handshake completes.@property(nonatomic,readonly,copy)NSString*protocol;// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol.-(id)initWithURLRequest:(NSURLRequest*)request protocols:(NSArray*)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;-(id)initWithURLRequest:(NSURLRequest*)request protocols:(NSArray*)protocols;-(id)initWithURLRequest:(NSURLRequest*)request;// Some helper constructors.-(id)initWithURL:(NSURL*)url protocols:(NSArray*)protocols allowsUntrustedSSLCertificates:(BOOL)allowsUntrustedSSLCertificates;-(id)initWithURL:(NSURL*)url protocols:(NSArray*)protocols;-(id)initWithURL:(NSURL*)url;// Delegate queue will be dispatch_main_queue by default.// You cannot set both OperationQueue and dispatch_queue.-(void)setDelegateOperationQueue:(NSOperationQueue*)queue;-(void)setDelegateDispatchQueue:(dispatch_queue_t)queue;// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes.-(void)scheduleInRunLoop:(NSRunLoop*)aRunLoop forMode:(NSString*)mode;-(void)unscheduleFromRunLoop:(NSRunLoop*)aRunLoop forMode:(NSString*)mode;// SRWebSockets are intended for one-time-use only.? Open should be called once and only once.-(void)open;-(void)close;-(void)closeWithCode:(NSInteger)code reason:(NSString*)reason;// Send a UTF8 String or Data.-(void)send:(id)data;// Send Data (can be nil) in a ping message.-(void)sendPing:(NSData*)data;@end#pragmamark - SRWebSocketDelegate@protocolSRWebSocketDelegate<NSObject>// message will either be an NSString if the server is using text// or NSData if the server is using binary.-(void)webSocket:(SRWebSocket*)webSocket didReceiveMessage:(id)message;@optional-(void)webSocketDidOpen:(SRWebSocket*)webSocket;-(void)webSocket:(SRWebSocket*)webSocket didFailWithError:(NSError*)error;-(void)webSocket:(SRWebSocket*)webSocket didCloseWithCode:(NSInteger)code reason:(NSString*)reason wasClean:(BOOL)wasClean;-(void)webSocket:(SRWebSocket*)webSocket didReceivePong:(NSData*)pongPayload;// Return YES to convert messages sent as Text to an NSString. Return NO to skip NSData -> NSString conversion for Text messages. Defaults to YES.-(BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket*)webSocket;@end

方法也很簡單,分為兩個部分:

一部分為SRWebSocket的初始化坦袍,以及連接十厢,關(guān)閉連接,發(fā)送消息等方法捂齐。

另一部分為SRWebSocketDelegate蛮放,其中包括一些回調(diào):

收到消息的回調(diào),連接失敗的回調(diào)奠宜,關(guān)閉連接的回調(diào)包颁,收到pong的回調(diào)瞻想,是否需要把data消息轉(zhuǎn)換成string的代理方法。

接著我們還是舉個例子來實現(xiàn)以下娩嚼,首先來封裝一個TYHSocketManager單例:

TYHSocketManager.h

#importtypedefenum:NSUInteger{disConnectByUser,disConnectByServer,}DisConnectType;@interfaceTYHSocketManager:NSObject+(instancetype)share;-(void)connect;-(void)disConnect;-(void)sendMsg:(NSString*)msg;-(void)ping;@end

TYHSocketManager.m

#import"TYHSocketManager.h"#import"SocketRocket.h"#definedispatch_main_async_safe(block)\? ? if ([NSThread isMainThread]) {\? ? ? ? block();\? ? } else {\? ? ? ? dispatch_async(dispatch_get_main_queue(), block);\? ? }staticNSString*Khost=@"127.0.0.1";staticconstuint16_t Kport=6969;@interfaceTYHSocketManager()<SRWebSocketDelegate>{SRWebSocket*webSocket;NSTimer*heartBeat;NSTimeInterval reConnectTime;}@end@implementationTYHSocketManager+(instancetype)share{staticdispatch_once_t onceToken;staticTYHSocketManager*instance=nil;dispatch_once(&onceToken,^{instance=[[selfalloc]init];[instance initSocket];});returninstance;}//初始化連接-(void)initSocket{if(webSocket){return;}webSocket=[[SRWebSocket alloc]initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"ws://%@:%d",Khost,Kport]]];webSocket.delegate=self;//設(shè)置代理線程queueNSOperationQueue*queue=[[NSOperationQueue alloc]init];queue.maxConcurrentOperationCount=1;[webSocket setDelegateOperationQueue:queue];//連接[webSocket open];}//初始化心跳-(void)initHeartBeat{dispatch_main_async_safe(^{[selfdestoryHeartBeat];__weaktypeof(self)weakSelf=self;//心跳設(shè)置為3分鐘蘑险,NAT超時一般為5分鐘heartBeat=[NSTimer scheduledTimerWithTimeInterval:3*60repeats:YES block:^(NSTimer*_Nonnull timer){NSLog(@"heart");//和服務(wù)端約定好發(fā)送什么作為心跳標(biāo)識,盡可能的減小心跳包大小[weakSelf sendMsg:@"heart"];}];[[NSRunLoop currentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];})}//取消心跳-(void)destoryHeartBeat{dispatch_main_async_safe(^{if(heartBeat){[heartBeat invalidate];heartBeat=nil;}})}#pragmamark - 對外的一些接口//建立連接-(void)connect{[selfinitSocket];//每次正常連接的時候清零重連時間reConnectTime=0;}//斷開連接-(void)disConnect{if(webSocket){[webSocket close];webSocket=nil;}}//發(fā)送消息-(void)sendMsg:(NSString*)msg{[webSocket send:msg];}//重連機(jī)制-(void)reConnect{[selfdisConnect];//超過一分鐘就不再重連 所以只會重連5次 2^5 = 64if(reConnectTime>64){return;}dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(reConnectTime*NSEC_PER_SEC)),dispatch_get_main_queue(),^{webSocket=nil;[selfinitSocket];});//重連時間2的指數(shù)級增長if(reConnectTime==0){reConnectTime=2;}else{reConnectTime*=2;}}//pingPong-(void)ping{[webSocket sendPing:nil];}#pragmamark - SRWebSocketDelegate-(void)webSocket:(SRWebSocket*)webSocket didReceiveMessage:(id)message{NSLog(@"服務(wù)器返回收到消息:%@",message);}-(void)webSocketDidOpen:(SRWebSocket*)webSocket{NSLog(@"連接成功");//連接成功了開始發(fā)送心跳[selfinitHeartBeat];}//open失敗的時候調(diào)用-(void)webSocket:(SRWebSocket*)webSocket didFailWithError:(NSError*)error{NSLog(@"連接失敗.....\n%@",error);//失敗了就去重連[selfreConnect];}//網(wǎng)絡(luò)連接中斷被調(diào)用-(void)webSocket:(SRWebSocket*)webSocket didCloseWithCode:(NSInteger)code reason:(NSString*)reason wasClean:(BOOL)wasClean{NSLog(@"被關(guān)閉連接待锈,code:%ld,reason:%@,wasClean:%d",code,reason,wasClean);//如果是被用戶自己中斷的那么直接斷開連接漠其,否則開始重連if(code==disConnectByUser){[selfdisConnect];}else{[selfreConnect];}//斷開連接時銷毀心跳[selfdestoryHeartBeat];}//sendPing的時候,如果網(wǎng)絡(luò)通的話竿音,則會收到回調(diào)和屎,但是必須保證ScoketOpen,否則會crash-(void)webSocket:(SRWebSocket*)webSocket didReceivePong:(NSData*)pongPayload{NSLog(@"收到pong回調(diào)");}//將收到的消息春瞬,是否需要把data轉(zhuǎn)換為NSString柴信,每次收到消息都會被調(diào)用,默認(rèn)YES//- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket//{//? ? NSLog(@"webSocketShouldConvertTextFrameToString");////? ? return NO;//}

.m文件有點長宽气,大家可以參照github中的demo進(jìn)行閱讀随常,這回我們添加了一些細(xì)節(jié)的東西了,包括一個簡單的心跳萄涯,重連機(jī)制绪氛,還有webScoket封裝好的一個pingpong機(jī)制。

代碼非常簡單涝影,大家可以配合著注釋讀一讀枣察,應(yīng)該很容易理解。

需要說一下的是這個心跳機(jī)制是一個定時的間隔燃逻,往往我們可能會有更復(fù)雜實現(xiàn)序目,比如我們正在發(fā)送消息的時候,可能就不需要心跳伯襟。當(dāng)不在發(fā)送的時候在開啟心跳之類的猿涨。微信有一種更高端的實現(xiàn)方式,有興趣的小伙伴可以看看:

微信的智能心跳實現(xiàn)方式

還有一點需要說的就是這個重連機(jī)制姆怪,demo中我采用的是2的指數(shù)級別增長叛赚,第一次立刻重連,第二次2秒稽揭,第三次4秒俺附,第四次8秒...直到大于64秒就不再重連。而任意的一次成功的連接淀衣,都會重置這個重連時間。

最后一點需要說的是召调,這個框架給我們封裝的webscoket在調(diào)用它的sendPing方法之前膨桥,一定要判斷當(dāng)前scoket是否連接蛮浑,如果不是連接狀態(tài),程序則會crash只嚣。

客戶端的實現(xiàn)就大致如此沮稚,接著同樣我們需要實現(xiàn)一個服務(wù)端,來看看實際通訊效果册舞。

webScoket服務(wù)端實現(xiàn)

在這里我們無法沿用之前的node.js例子了蕴掏,因為這并不是一個原生的scoket,這是webScoket调鲸,所以我們服務(wù)端同樣需要遵守webScoket協(xié)議盛杰,兩者才能實現(xiàn)通信。

其實這里實現(xiàn)也很簡單藐石,我采用了node.js的ws模塊即供,只需要用npm去安裝ws即可。

什么是npm呢于微?舉個例子逗嫡,npm之于Node.js相當(dāng)于cocospod至于iOS,它就是一個拓展模塊的一個管理工具株依。如果不知道怎么用的可以看看這篇文章:npm的使用

我們進(jìn)入當(dāng)前腳本目錄驱证,輸入終端命令,即可安裝ws模塊:

$ npm install ws

大家如果懶得去看npm的小伙伴也沒關(guān)系,直接下載github中的WSServer.js這個文件運行即可。

該源文件代碼如下:

varWebSocketServer=require('ws').Server,wss=newWebSocketServer({port:6969});wss.on('connection',function(ws){console.log('client connected');ws.send('你是第'+wss.clients.length+'位');//收到消息回調(diào)ws.on('message',function(message){console.log(message);ws.send('收到:'+message);});// 退出聊天? ws.on('close',function(close){console.log('退出連接了');});});console.log('開始監(jiān)聽6969端口');

代碼沒幾行践险,理解起來很簡單啡省。

就是監(jiān)聽了本機(jī)6969端口,如果客戶端連接了挠铲,打印lient connected,并且向客戶端發(fā)送:你是第幾位。

如果收到客戶端消息后车份,打印消息,并且向客戶端發(fā)送這條收到的消息牡彻。

接著我們同樣來運行一下看看效果:

image

運行我們可以看到扫沼,主動去斷開的連接,沒有去重連庄吼,而server端斷開的缎除,我們開啟了重連。感興趣的朋友可以下載demo實際運行一下总寻。

image

4.我們接著來看看MQTT:

MQTT是一個聊天協(xié)議器罐,它比webScoket更上層,屬于應(yīng)用層渐行。

它的基本模式是簡單的發(fā)布訂閱轰坊,也就是說當(dāng)一條消息發(fā)出去的時候铸董,誰訂閱了誰就會受到。其實它并不適合IM的場景肴沫,例如用來實現(xiàn)有些簡單IM場景粟害,卻需要很大量的、復(fù)雜的處理颤芬。

比較適合它的場景為訂閱發(fā)布這種模式的悲幅,例如微信的實時共享位置,滴滴的地圖上小車的移動站蝠、客戶端推送等功能汰具。

首先我們來看看基于MQTT協(xié)議的框架-MQTTKit:

這個框架是c來寫的,把一些方法公開在MQTTKit類中沉衣,對外用OC來調(diào)用郁副,我們來看看這個類:

@interfaceMQTTClient:NSObject{structmosquitto*mosq;}@property(readwrite,copy)NSString*clientID;@property(readwrite,copy)NSString*host;@property(readwrite,assign)unsignedshortport;@property(readwrite,copy)NSString*username;@property(readwrite,copy)NSString*password;@property(readwrite,assign)unsignedshortkeepAlive;@property(readwrite,assign)BOOL cleanSession;@property(nonatomic,copy)MQTTMessageHandler messageHandler;+(void)initialize;+(NSString*)version;-(MQTTClient*)initWithClientId:(NSString*)clientId;-(void)setMessageRetry:(NSUInteger)seconds;#pragmamark - Connection-(void)connectWithCompletionHandler:(void(^)(MQTTConnectionReturnCode code))completionHandler;-(void)connectToHost:(NSString*)host? ? completionHandler:(void(^)(MQTTConnectionReturnCode code))completionHandler;-(void)disconnectWithCompletionHandler:(void(^)(NSUInteger code))completionHandler;-(void)reconnect;-(void)setWillData:(NSData*)payload? ? ? ? ? ? toTopic:(NSString*)willTopic? ? ? ? ? ? withQos:(MQTTQualityOfService)willQos? ? ? ? ? ? retain:(BOOL)retain;-(void)setWill:(NSString*)payload? ? ? ? toTopic:(NSString*)willTopic? ? ? ? withQos:(MQTTQualityOfService)willQos? ? ? ? retain:(BOOL)retain;-(void)clearWill;#pragmamark - Publish-(void)publishData:(NSData*)payload? ? ? ? ? ? toTopic:(NSString*)topic? ? ? ? ? ? withQos:(MQTTQualityOfService)qos? ? ? ? ? ? retain:(BOOL)retain? completionHandler:(void(^)(intmid))completionHandler;-(void)publishString:(NSString*)payload? ? ? ? ? ? ? toTopic:(NSString*)topic? ? ? ? ? ? ? withQos:(MQTTQualityOfService)qos? ? ? ? ? ? ? retain:(BOOL)retain? ? completionHandler:(void(^)(intmid))completionHandler;#pragmamark - Subscribe-(void)subscribe:(NSString*)topicwithCompletionHandler:(MQTTSubscriptionCompletionHandler)completionHandler;-(void)subscribe:(NSString*)topic? ? ? ? ? withQos:(MQTTQualityOfService)qoscompletionHandler:(MQTTSubscriptionCompletionHandler)completionHandler;-(void)unsubscribe:(NSString*)topicwithCompletionHandler:(void(^)(void))completionHandler;

這個類一共分為4個部分:初始化、連接豌习、發(fā)布存谎、訂閱,具體方法的作用可以先看看方法名理解下肥隆,我們接著來用這個框架封裝一個實例既荚。

同樣,我們封裝了一個單例MQTTManager栋艳。

MQTTManager.h

#import@interfaceMQTTManager:NSObject+(instancetype)share;-(void)connect;-(void)disConnect;-(void)sendMsg:(NSString*)msg;@end

MQTTManager.m

#import"MQTTManager.h"#import"MQTTKit.h"staticNSString*Khost=@"127.0.0.1";staticconstuint16_t Kport=6969;staticNSString*KClientID=@"tuyaohui";@interfaceMQTTManager(){MQTTClient*client;}@end@implementationMQTTManager+(instancetype)share{staticdispatch_once_t onceToken;staticMQTTManager*instance=nil;dispatch_once(&onceToken,^{instance=[[selfalloc]init];});returninstance;}//初始化連接-(void)initSocket{if(client){[selfdisConnect];}client=[[MQTTClient alloc]initWithClientId:KClientID];client.port=Kport;[client setMessageHandler:^(MQTTMessage*message){//收到消息的回調(diào)恰聘,前提是得先訂閱NSString*msg=[[NSString alloc]initWithData:message.payload encoding:NSUTF8StringEncoding];NSLog(@"收到服務(wù)端消息:%@",msg);}];[client connectToHost:Khost completionHandler:^(MQTTConnectionReturnCode code){switch(code){caseConnectionAccepted:NSLog(@"MQTT連接成功");//訂閱自己ID的消息,這樣收到消息就能回調(diào)[client subscribe:client.clientID withCompletionHandler:^(NSArray*grantedQos){NSLog(@"訂閱tuyaohui成功");}];break;caseConnectionRefusedBadUserNameOrPassword:NSLog(@"錯誤的用戶名密碼");//....default:NSLog(@"MQTT連接失敗");break;}}];}#pragmamark - 對外的一些接口//建立連接-(void)connect{[selfinitSocket];}//斷開連接-(void)disConnect{if(client){//取消訂閱[client unsubscribe:client.clientID withCompletionHandler:^{NSLog(@"取消訂閱tuyaohui成功");}];//斷開連接[client disconnectWithCompletionHandler:^(NSUInteger code){NSLog(@"斷開MQTT成功");}];client=nil;}}//發(fā)送消息-(void)sendMsg:(NSString*)msg{//發(fā)送一條消息吸占,發(fā)送給自己訂閱的主題[client publishString:msg toTopic:KClientID withQos:ExactlyOnce retain:YES completionHandler:^(intmid){}];}@end

實現(xiàn)代碼很簡單晴叨,需要說一下的是:

1)當(dāng)我們連接成功了,我們需要去訂閱自己clientID的消息矾屯,這樣才能收到發(fā)給自己的消息兼蕊。

2)其次是這個框架為我們實現(xiàn)了一個QOS機(jī)制,那么什么是QOS呢件蚕?

QoS(Quality of Service孙技,服務(wù)質(zhì)量)指一個網(wǎng)絡(luò)能夠利用各種基礎(chǔ)技術(shù),為指定的網(wǎng)絡(luò)通信提供更好的服務(wù)能力, 是網(wǎng)絡(luò)的一種安全機(jī)制排作, 是用來解決網(wǎng)絡(luò)延遲和阻塞等問題的一種技術(shù)牵啦。

在這里,它提供了三個選項:

typedefenumMQTTQualityOfService:NSUInteger{AtMostOnce,AtLeastOnce,ExactlyOnce}MQTTQualityOfService;

分別對應(yīng)最多發(fā)送一次妄痪,至少發(fā)送一次哈雏,精確只發(fā)送一次。

QOS(0),最多發(fā)送一次:如果消息沒有發(fā)送過去,那么就直接丟失裳瘪。

QOS(1),至少發(fā)送一次:保證消息一定發(fā)送過去履因,但是發(fā)幾次不確定。

QOS(2),精確只發(fā)送一次:它內(nèi)部會有一個很復(fù)雜的發(fā)送機(jī)制盹愚,確保消息送到,而且只發(fā)送一次站故。

更詳細(xì)的關(guān)于該機(jī)制可以看看這篇文章:MQTT協(xié)議筆記之消息流QOS皆怕。

同樣的我們需要一個用MQTT協(xié)議實現(xiàn)的服務(wù)端,我們還是node.js來實現(xiàn)西篓,這次我們還是需要用npm來新增一個模塊mosca愈腾。

我們來看看服務(wù)端代碼:

MQTTServer.js

varmosca=require('mosca');varMqttServer=newmosca.Server({port:6969});MqttServer.on('clientConnected',function(client){console.log('收到客戶端連接,連接ID:',client.id);});/**

* 監(jiān)聽MQTT主題消息

**/MqttServer.on('published',function(packet,client){vartopic=packet.topic;console.log('有消息來了','topic為:'+topic+',message為:'+packet.payload.toString());});MqttServer.on('ready',function(){console.log('mqtt服務(wù)器開啟岂津,監(jiān)聽6969端口');});

服務(wù)端代碼沒幾行虱黄,開啟了一個服務(wù),并且監(jiān)聽本機(jī)6969端口吮成。并且監(jiān)聽了客戶端連接橱乱、發(fā)布消息等狀態(tài)。

接著我們同樣來運行一下看看效果:

image

至此粱甫,我們實現(xiàn)了一個簡單的MQTT封裝泳叠。

5.XMPP:XMPPFramework框架

結(jié)果就是并沒有XMPP...因為個人感覺XMPP對于IM來說實在是不堪重用。僅僅只能作為一個玩具demo茶宵,給大家練練手危纫。網(wǎng)上有太多XMPP的內(nèi)容了,相當(dāng)一部分用openfire來做服務(wù)端乌庶,這一套東西實在是太老了种蝶。還記得多年前,樓主初識IM就是用的這一套東西...

如果大家仍然感興趣的可以看看這篇文章:iOS 的 XMPPFramework 簡介瞒大。這里就不舉例贅述了螃征。

三、關(guān)于IM傳輸格式的選擇:

引用陳宜龍大神文章(iOS程序犭袁)中一段:

使用 ProtocolBuffer 減少 Payload

滴滴打車40%糠赦;

攜程之前分享過会傲,說是采用新的Protocol Buffer數(shù)據(jù)格式+Gzip壓縮后的Payload大小降低了15%-45%。數(shù)據(jù)序列化耗時下降了80%-90%拙泽。

采用高效安全的私有協(xié)議淌山,支持長連接的復(fù)用,穩(wěn)定省電省流量

【高效】提高網(wǎng)絡(luò)請求成功率顾瞻,消息體越大泼疑,失敗幾率隨之增加。

【省流量】流量消耗極少荷荤,省流量退渗。一條消息數(shù)據(jù)用Protobuf序列化后的大小是 JSON 的1/10移稳、XML格式的1/20、是二進(jìn)制序列化的1/10会油。同 XML 相比个粱, Protobuf 性能優(yōu)勢明顯。它以高效的二進(jìn)制方式存儲翻翩,比 XML 小 3 到 10 倍都许,快 20 到 100 倍。

【省電】省電

【高效心跳包】同時心跳包協(xié)議對IM的電量和流量影響很大嫂冻,對心跳包協(xié)議上進(jìn)行了極簡設(shè)計:僅 1 Byte 胶征。

【易于使用】開發(fā)人員通過按照一定的語法定義結(jié)構(gòu)化的消息格式,然后送給命令行工具桨仿,工具將自動生成相關(guān)的類睛低,可以支持java、c++服傍、python钱雷、Objective-C等語言環(huán)境。通過將這些類包含在項目中吹零,可以很輕松的調(diào)用相關(guān)方法來完成業(yè)務(wù)消息的序列化與反序列化工作急波。語言支持:原生支持c++、java瘪校、python澄暮、Objective-C等多達(dá)10余種語言。 2015-08-27 Protocol Buffers v3.0.0-beta-1中發(fā)布了Objective-C(Alpha)版本阱扬, 2016-07-28 3.0 Protocol Buffers v3.0.0正式版發(fā)布泣懊,正式支持 Objective-C。

【可靠】微信和手機(jī) QQ 這樣的主流 IM 應(yīng)用也早已在使用它(采用的是改造過的Protobuf協(xié)議)

image

如何測試驗證 Protobuf 的高性能麻惶?

對數(shù)據(jù)分別操作100次馍刮,1000次,10000次和100000次進(jìn)行了測試窃蹋,

縱坐標(biāo)是完成時間卡啰,單位是毫秒,

反序列化

序列化

字節(jié)長度

image

image

image

數(shù)據(jù)來源警没。

image

數(shù)據(jù)來自:項目thrift-protobuf-compare匈辱,測試項為 Total Time,也就是 指一個對象操作的整個時間杀迹,包括創(chuàng)建對象亡脸,將對象序列化為內(nèi)存中的字節(jié)序列,然后再反序列化的整個過程。從測試結(jié)果可以看到 Protobuf 的成績很好.

缺點:

可能會造成 APP 的包體積增大浅碾,通過 Google 提供的腳本生成的 Model大州,會非常“龐大”垂谢,Model 一多厦画,包體積也就會跟著變大。

如果 Model 過多滥朱,可能導(dǎo)致 APP 打包后的體積驟增苛白,但 IM 服務(wù)所使用的 Model 非常少,比如在 ChatKit-OC 中只用到了一個 Protobuf 的 Model:Message對象焚虱,對包體積的影響微乎其微。

在使用過程中要合理地權(quán)衡包體積以及傳輸效率的問題懂版,據(jù)說去哪兒網(wǎng)鹃栽,就曾經(jīng)為了減少包體積,進(jìn)而減少了 Protobuf 的使用躯畴。

綜上所述民鼓,我們選擇傳輸格式的時候:ProtocolBuffer>Json>XML

如果大家對ProtocolBuffer用法感興趣可以參考下這兩篇文章:

ProtocolBuffer for Objective-C 運行環(huán)境配置及使用

iOS之ProtocolBuffer搭建和示例demo

三、IM一些其它問題

1.IM的可靠性:

我們之前穿插在例子中提到過:

心跳機(jī)制蓬抄、PingPong機(jī)制丰嘉、斷線重連機(jī)制、還有我們后面所說的QOS機(jī)制嚷缭。這些被用來保證連接的可用饮亏,消息的即時與準(zhǔn)確的送達(dá)等等。

上述內(nèi)容保證了我們IM服務(wù)時的可靠性阅爽,其實我們能做的還有很多:比如我們在大文件傳輸?shù)臅r候使用分片上傳路幸、斷點續(xù)傳、秒傳技術(shù)等來保證文件的傳輸付翁。

2.安全性:

我們通常還需要一些安全機(jī)制來保證我們IM通信安全简肴。

例如:防止 DNS 污染、帳號安全百侧、第三方服務(wù)器鑒權(quán)砰识、單點登錄等等

3.一些其他的優(yōu)化:

類似微信,服務(wù)器不做聊天記錄的存儲佣渴,只在本機(jī)進(jìn)行緩存辫狼,這樣可以減少對服務(wù)端數(shù)據(jù)的請求,一方面減輕了服務(wù)器的壓力辛润,另一方面減少客戶端流量的消耗予借。

我們進(jìn)行http連接的時候盡量采用上層API,類似NSUrlSession。而網(wǎng)絡(luò)框架盡量使用AFNetWorking3灵迫。因為這些上層網(wǎng)絡(luò)請求都用的是HTTP/2 秦叛,我們請求的時候可以復(fù)用這些連接。

更多優(yōu)化相關(guān)內(nèi)容可以參考參考這篇文章:

IM 即時通訊技術(shù)在多應(yīng)用場景下的技術(shù)實現(xiàn)瀑粥,以及性能調(diào)優(yōu)

四挣跋、音視頻通話

IM應(yīng)用中的實時音視頻技術(shù),幾乎是IM開發(fā)中的最后一道高墻狞换。原因在于:實時音視頻技術(shù) = 音視頻處理技術(shù) + 網(wǎng)絡(luò)傳輸技術(shù) 的橫向技術(shù)應(yīng)用集合體避咆,而公共互聯(lián)網(wǎng)不是為了實時通信設(shè)計的。

實時音視頻技術(shù)上的實現(xiàn)內(nèi)容主要包括:音視頻的采集修噪、編碼查库、網(wǎng)絡(luò)傳輸、解碼黄琼、播放等環(huán)節(jié)樊销。這么多項并不簡單的技術(shù)應(yīng)用,如果把握不當(dāng)脏款,將會在在實際開發(fā)過程中遇到一個又一個的坑围苫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市撤师,隨后出現(xiàn)的幾起案子剂府,更是在濱河造成了極大的恐慌,老刑警劉巖剃盾,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腺占,死亡現(xiàn)場離奇詭異,居然都是意外死亡痒谴,警方通過查閱死者的電腦和手機(jī)湾笛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闰歪,“玉大人嚎研,你說我怎么就攤上這事】馓龋” “怎么了临扮?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長教翩。 經(jīng)常有香客問我杆勇,道長,這世上最難降的妖魔是什么饱亿? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任蚜退,我火速辦了婚禮闰靴,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钻注。我一直安慰自己蚂且,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布幅恋。 她就那樣靜靜地躺著杏死,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捆交。 梳的紋絲不亂的頭發(fā)上淑翼,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音品追,去河邊找鬼玄括。 笑死,一個胖子當(dāng)著我的面吹牛肉瓦,可吹牛的內(nèi)容都是我干的遭京。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼风宁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蛹疯?” 一聲冷哼從身側(cè)響起戒财,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捺弦,沒想到半個月后饮寞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡列吼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年幽崩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寞钥。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡慌申,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出理郑,到底是詐尸還是另有隱情蹄溉,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布您炉,位于F島的核電站柒爵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赚爵。R本人自食惡果不足惜棉胀,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一法瑟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唁奢,春花似錦霎挟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至论笔,卻和暖如春采郎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狂魔。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工蒜埋, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人最楷。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓整份,卻偏偏與公主長得像,于是被迫代替她去往敵國和親籽孙。 傳聞我的和親對象是個殘疾皇子烈评,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 前言 本文會用實例的方式,將iOS各種IM的方案都簡單的實現(xiàn)一遍犯建。并且提供一些選型讲冠、實現(xiàn)細(xì)節(jié)以及優(yōu)化的建議。 注:...
    Sky109閱讀 16,912評論 4 117
  • 前言 本文會用實例的方式适瓦,將iOS各種IM的方案都簡單的實現(xiàn)一遍竿开。并且提供一些選型、實現(xiàn)細(xì)節(jié)以及優(yōu)化的建議玻熙。 注:...
    biyuhuaping閱讀 1,747評論 1 2
  • 前言 本文會用實例的方式否彩,將iOS各種IM的方案都簡單的實現(xiàn)一遍。并且提供一些選型嗦随、實現(xiàn)細(xì)節(jié)以及優(yōu)化的建議列荔。 注:...
    maTianHong閱讀 2,356評論 4 12
  • 前言: 自己去實現(xiàn)即時通訊有很多選擇: 1)首先面臨的就是傳輸協(xié)議的選擇,TCP還是UDP枚尼? 2)其次是我們需要去...
    飛哥漂流記閱讀 572評論 1 2
  • 前言 本文會用實例的方式,將iOS各種IM的方案都簡單的實現(xiàn)一遍锭汛。并且提供一些選型笨奠、實現(xiàn)細(xì)節(jié)以及優(yōu)化的建議袭蝗。 注:...
    涂耀輝閱讀 94,179評論 232 1,748