iOS即時(shí)通訊

前言

本文會(huì)用實(shí)例的方式猿妈,將iOS各種IM的方案都簡(jiǎn)單的實(shí)現(xiàn)一遍芬失。并且提供一些選型、實(shí)現(xiàn)細(xì)節(jié)以及優(yōu)化的建議附迷。

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

iOS即時(shí)通訊,從入門到“放棄”喇伯?(demo)

可以打開項(xiàng)目先預(yù)覽效果喊儡,對(duì)照著進(jìn)行閱讀。

言歸正傳稻据,首先我們來(lái)總結(jié)一下我們?nèi)?shí)現(xiàn)IM的方式

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

對(duì)于短平快的公司,完全可以采用第三方SDK來(lái)實(shí)現(xiàn)攀甚。國(guó)內(nèi)IM的第三方服務(wù)商有很多箩朴,類似云信、環(huán)信秋度、融云炸庞、LeanCloud,當(dāng)然還有其它的很多荚斯,這里就不一一舉例了埠居,感興趣的小伙伴可以自行查閱下。

第三方服務(wù)商IM底層協(xié)議基本上都是TCP事期。他們的IM方案很成熟滥壕,有了它們,我們甚至不需要自己去搭建IM后臺(tái)兽泣,什么都不需要去考慮绎橘。

如果你足夠懶,甚至連UI都不需要自己做,這些第三方有各自一套IM的UI称鳞,拿來(lái)就可以直接用涮较。真可謂3分鐘集成...

但是缺點(diǎn)也很明顯,定制化程度太高冈止,很多東西我們不可控狂票。當(dāng)然還有一個(gè)最最重要的一點(diǎn),就是太貴了...作為真正社交為主打的APP熙暴,僅此一點(diǎn)闺属,就足以讓我們望而卻步。當(dāng)然周霉,如果IM對(duì)于APP只是一個(gè)輔助功能掂器,那么用第三方服務(wù)也無(wú)可厚非。

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

我們自己去實(shí)現(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é)問(wèn)題需要考慮与斤,例如TCP的長(zhǎng)連接如何保持,心跳機(jī)制荚恶,Qos機(jī)制撩穿,重連機(jī)制等等...當(dāng)然,除此之外谒撼,我們還有一些安全問(wèn)題需要考慮食寡。

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

接下來(lái)我們可能需要自己考慮去實(shí)現(xiàn)IM廓潜,首先從傳輸層協(xié)議來(lái)說(shuō)抵皱,我們有兩種選擇:TCP?or?UDP?

這個(gè)問(wèn)題已經(jīng)被討論過(guò)無(wú)數(shù)次了辩蛋,對(duì)深層次的細(xì)節(jié)感興趣的朋友可以看看這篇文章:

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

這里我們直接說(shuō)結(jié)論吧:對(duì)于小公司或者技術(shù)不那么成熟的公司,IM一定要用TCP來(lái)實(shí)現(xiàn)悼院,因?yàn)槿绻阋肬DP的話伤为,需要做的事太多。當(dāng)然QQ就是用的UDP協(xié)議据途,當(dāng)然不僅僅是UDP绞愚,騰訊還用了自己的私有協(xié)議叙甸,來(lái)保證了傳輸?shù)目煽啃裕沤^了UDP下各種數(shù)據(jù)丟包爽醋,亂序等等一系列問(wèn)題蚁署。

總之一句話,如果你覺(jué)得團(tuán)隊(duì)技術(shù)很成熟蚂四,那么你用UDP也行光戈,否則還是用TCP為好。

二遂赠、我們來(lái)看看各種聊天協(xié)議

首先我們以實(shí)現(xiàn)方式來(lái)切入久妆,基本上有以下四種實(shí)現(xiàn)方式:

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

基于WebScoket:代表框架?SocketRocket跷睦。

基于MQTT:代表框架?MQTTKit筷弦。

基于XMPP:代表框架?XMPPFramework。

當(dāng)然抑诸,以上四種方式我們都可以不使用第三方框架烂琴,直接基于OS底層Scoket去實(shí)現(xiàn)我們的自定義封裝。下面我會(huì)給出一個(gè)基于Scoket原生而不使用框架的例子蜕乡,供大家參考一下奸绷。

首先需要搞清楚的是,其中MQTT和XMPP為聊天協(xié)議层玲,它們是最上層的協(xié)議号醉,而WebScoket是傳輸通訊協(xié)議,它是基于Socket封裝的一個(gè)協(xié)議辛块。而通常我們所說(shuō)的騰訊IM的私有協(xié)議畔派,就是基于WebScoket或者Scoket原生進(jìn)行封裝的一個(gè)聊天協(xié)議。

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

協(xié)議優(yōu)劣對(duì)比.png

所以說(shuō)到底润绵,iOS要做一個(gè)真正的IM產(chǎn)品线椰,一般都是基于Scoket或者WebScoket等,再之上加上一些私有協(xié)議來(lái)保證的尘盼。

1.我們先不使用任何框架士嚎,直接用OS底層Socket來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的IM。

我們客戶端的實(shí)現(xiàn)思路也是很簡(jiǎn)單悔叽,創(chuàng)建Socket莱衩,和服務(wù)器的Socket對(duì)接上,然后開始傳輸數(shù)據(jù)就可以了娇澎。

我們學(xué)過(guò)c/c++或者java這些語(yǔ)言笨蚁,我們就知道,往往任何教程,最后一章都是講Socket編程括细,而Socket是什么呢伪很,簡(jiǎn)單的來(lái)說(shuō),就是我們使用TCP/IP?或者UDP/IP協(xié)議的一組編程接口奋单。如下圖所示:

我們?cè)趹?yīng)用層锉试,使用socket,輕易的實(shí)現(xiàn)了進(jìn)程之間的通信(跨網(wǎng)絡(luò)的)览濒。想想呆盖,如果沒(méi)有socket,我們要直面TCP/IP協(xié)議贷笛,我們需要去寫多少繁瑣而又重復(fù)的代碼应又。

如果有對(duì)socket概念仍然有所困惑的,可以看看這篇文章:

從問(wèn)題看本質(zhì)乏苦,socket到底是什么株扛?

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

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

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

//socket 創(chuàng)建并初始化 socket奖磁,返回該 socket 的文件描述符,如果描述符為 -1 表示創(chuàng)建失敗繁疤。intsocket(intaddressFamily,inttype,intprotocol)//關(guān)閉socket連接intclose(intsocketFileDescriptor)//將 socket 與特定主機(jī)地址與端口號(hào)綁定,成功綁定返回0秕狰,失敗返回 -1稠腊。intbind(intsocketFileDescriptor,sockaddr *addressToBind,intaddressStructLength)//接受客戶端連接請(qǐng)求并將客戶端的網(wǎng)絡(luò)地址信息保存到 clientAddress 中。intaccept(intsocketFileDescriptor,sockaddr *clientAddress,intclientAddressStructLength)//客戶端向特定網(wǎng)絡(luò)地址的服務(wù)器發(fā)送連接請(qǐng)求鸣哀,連接成功返回0架忌,失敗返回 -1。intconnect(intsocketFileDescriptor,sockaddr *serverAddress,intserverAddressLength)//使用 DNS 查找特定主機(jī)名字對(duì)應(yīng)的 IP 地址我衬。如果找不到對(duì)應(yīng)的 IP 地址則返回 NULL叹放。hostent*gethostbyname(char*hostname)//通過(guò) 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)//通過(guò)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)

讓我們可以對(duì)socket進(jìn)行各種操作,首先我們來(lái)用它寫個(gè)客戶端难礼⊥拊玻總結(jié)一下,簡(jiǎn)單的IM客戶端需要做如下4件事:

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

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

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

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

根據(jù)上面4條大綱,我們封裝了一個(gè)名為TYHSocketManager的單例吧寺,來(lái)對(duì)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_tonceToken;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說(shuō)明連接失敗if(ConnectionToServer(_clientScoket,server_ip, server_port)==0) {? ? ? ? printf("Connect to server error\n");return;? ? }//走到這說(shuō)明連接成功printf("Connect to server ok\n");}staticintCreateClinetSocket(){intClinetSocket =0;//創(chuàng)建一個(gè)socket,返回值為Int。(注scoket其實(shí)就是Int類型)//第一個(gè)參數(shù)addressFamily IPv4(AF_INET) 或 IPv6(AF_INET6)稚机。//第二個(gè)參數(shù) type 表示 socket 的類型幕帆,通常是流stream(SOCK_STREAM) 或數(shù)據(jù)報(bào)文datagram(SOCK_DGRAM)//第三個(gè)參數(shù) protocol 參數(shù)通常設(shè)置為0,以便讓系統(tǒng)自動(dòng)為選擇我們合適的協(xié)議赖条,對(duì)于 stream socket 來(lái)說(shuō)會(huì)是 TCP 協(xié)議(IPPROTO_TCP)失乾,而對(duì)于 datagram來(lái)說(shuō)會(huì)是 UDP 協(xié)議(IPPROTO_UDP)。ClinetSocket = socket(AF_INET, SOCK_STREAM,0);returnClinetSocket;}staticintConnectionToServer(intclient_socket,constchar* server_ip,unsignedshortport){//生成一個(gè)sockaddr_in類型結(jié)構(gòu)體structsockaddr_in sAddr={0};? ? sAddr.sin_len=sizeof(sAddr);//設(shè)置IPv4sAddr.sin_family=AF_INET;//inet_aton是一個(gè)改進(jìn)的方法來(lái)將一個(gè)字符串IP地址轉(zhuǎn)換為一個(gè)32位的網(wǎng)絡(luò)序列IP地址//如果這個(gè)函數(shù)成功纬乍,函數(shù)的返回值非零碱茁,如果輸入地址不正確則會(huì)返回零。inet_aton(server_ip, &sAddr.sin_addr);//htons是將整型變量從主機(jī)字節(jié)順序轉(zhuǎn)變成網(wǎng)絡(luò)字節(jié)順序仿贬,賦值端口號(hào)sAddr.sin_port=htons(port);//用scoket和服務(wù)端地址纽竣,發(fā)起連接。//客戶端向特定網(wǎng)絡(luò)地址的服務(wù)器發(fā)送連接請(qǐng)求茧泪,連接成功返回0蜓氨,失敗返回 -1。//注意:該接口調(diào)用會(huì)阻塞當(dāng)前線程队伟,直到服務(wù)器返回穴吹。if(connect(client_socket, (structsockaddr *)&sAddr,sizeof(sAddr))==0) {returnclient_socket;? ? }return0;}#pragma mark - 新線程來(lái)接收消息- (void)pullMsg{NSThread*thread = [[NSThreadalloc]initWithTarget:selfselector:@selector(recieveAction) object:nil];? ? [thread start];}#pragma mark - 對(duì)外邏輯- (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方法了一個(gè)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ù)中,我們綁定了一個(gè)sockaddr_in類型的結(jié)構(gòu)體缠借,該結(jié)構(gòu)體內(nèi)容如下:

structsockaddr_in{__uint8_tsin_len;sa_family_tsin_family;in_port_tsin_port;structin_addrsin_addr;charsin_zero[8];};

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

連接成功之后硝逢,我們就可以調(diào)用send函數(shù)和recv函數(shù)進(jìn)行消息收發(fā)了,在這里绅喉,我新開辟了一個(gè)常駐線程渠鸽,在這個(gè)線程中一個(gè)死循環(huán)里去不停的調(diào)用recv函數(shù),這樣服務(wù)端有消息發(fā)送過(guò)來(lái)柴罐,第一時(shí)間便能被接收到徽缚。

就這樣客戶端便簡(jiǎn)單的可以用了,接著我們來(lái)看看服務(wù)端的實(shí)現(xiàn)革屠。

一樣凿试,我們首先對(duì)服務(wù)端需要做的工作簡(jiǎn)單的總結(jié)下:

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

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

服務(wù)器通過(guò) accept(...)接受客戶端請(qǐng)求建立連接那婉;

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

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

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

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

在這里我用node.js去搭了一個(gè)簡(jiǎn)單的scoket服務(wù)器隐岛。源碼如下:

varnet =require('net');varHOST ='127.0.0.1';varPORT =6969;// 創(chuàng)建一個(gè)TCP服務(wù)器實(shí)例,調(diào)用listen函數(shù)開始監(jiān)聽指定端口? // 傳入net.createServer()的回調(diào)函數(shù)將作為”connection“事件的處理函數(shù)? // 在每一個(gè)“connection”事件中呻引,該回調(diào)函數(shù)接收到的socket對(duì)象是唯一的? net.createServer(function(sock){// 我們獲得一個(gè)連接 - 該連接自動(dòng)關(guān)聯(lián)一個(gè)socket對(duì)象? console.log('CONNECTED: '+? ? ? ? ? sock.remoteAddress +':'+ sock.remotePort);? ? ? ? ? sock.write('服務(wù)端發(fā)出:連接成功');// 為這個(gè)socket實(shí)例添加一個(gè)"data"事件處理函數(shù)? sock.on('data',function(data){console.log('DATA '+ sock.remoteAddress +': '+ data);// 回發(fā)該數(shù)據(jù),客戶端將收到來(lái)自服務(wù)端的數(shù)據(jù)? sock.write('You said "'+ data +'"');? ? ? });// 為這個(gè)socket實(shí)例添加一個(gè)"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的朋友也不用著急吐咳,在這里你可以使用任意語(yǔ)言c/c++/java/oc等等去實(shí)現(xiàn)后臺(tái)逻悠,這里node.js僅僅是樓主的一個(gè)選擇,為了讓我們來(lái)驗(yàn)證之前寫的客戶端scoket的效果韭脊。如果你不懂node.js也沒(méi)關(guān)系童谒,你只需要把上述樓主寫的相關(guān)代碼復(fù)制粘貼,如果你本機(jī)有node的解釋器沪羔,那么直接在終端進(jìn)入該源代碼文件目錄中輸入:

node fileName

即可運(yùn)行該腳本(fileName為保存源代碼的文件名)饥伊。

我們來(lái)看看運(yùn)行效果:

handle2.gif

服務(wù)器運(yùn)行起來(lái)了象浑,并且監(jiān)聽著6969端口。

接著我們用之前寫的iOS端的例子琅豆∮洳颍客戶端打印顯示連接成功,而我們運(yùn)行的服務(wù)器也打印了連接成功茫因。接著我們發(fā)了一條消息蚪拦,服務(wù)端成功的接收到了消息后,把該消息再發(fā)送回客戶端冻押,繞了一圈客戶端又收到了這條消息驰贷。至此我們用OS底層scoket實(shí)現(xiàn)了簡(jiǎn)單的IM。

大家看到這是不是覺(jué)得太過(guò)簡(jiǎn)單了洛巢?

當(dāng)然簡(jiǎn)單括袒,我們僅僅是實(shí)現(xiàn)了Scoket的連接,信息的發(fā)送與接收稿茉,除此之外我們什么都沒(méi)有做锹锰,現(xiàn)實(shí)中,我們需要做的處理遠(yuǎn)不止于此狈邑,我們先接著往下看城须。接下來(lái),我們就一起看看第三方框架是如何實(shí)現(xiàn)IM的米苹。

分割圖.png

2.我們接著來(lái)看看基于Socket原生的CocoaAsyncSocket:

這個(gè)框架實(shí)現(xiàn)了兩種傳輸協(xié)議TCP和UDP糕伐,分別對(duì)應(yīng)GCDAsyncSocket類和GCDAsyncUdpSocket,這里我們重點(diǎn)講GCDAsyncSocket蘸嘶。

這里Socket服務(wù)器延續(xù)上一個(gè)例子良瞧,因?yàn)橥瑯邮腔谠鶶coket的框架,所以之前的Node.js的服務(wù)端训唱,該例仍然試用褥蚯。這里我們就只需要去封裝客戶端的實(shí)例,我們還是創(chuàng)建一個(gè)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(){? ? GCDAsyncSocket *gcdSocket;}@end@implementationTYHSocketManager+ (instancetype)share{staticdispatch_once_tonceToken;staticTYHSocketManager *instance =nil;dispatch_once(&onceToken, ^{? ? ? ? instance = [[selfalloc]init];? ? ? ? [instance initSocket];? ? });returninstance;}- (void)initSocket{? ? gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:selfdelegateQueue:dispatch_get_main_queue()];? ? }#pragma mark - 對(duì)外的一些接口//建立連接- (BOOL)connect{return[gcdSocket connectToHost:Khost onPort:Kport error:nil];}//斷開連接- (void)disConnect{? ? [gcdSocket disconnect];}//發(fā)送消息- (void)sendMsg:(NSString*)msg{NSData*data? = [msg dataUsingEncoding:NSUTF8StringEncoding];//第二個(gè)參數(shù)赞庶,請(qǐng)求超時(shí)時(shí)間[gcdSocket writeData:data withTimeout:-1tag:110];}//監(jiān)聽最新的消息- (void)pullTheMsg{//監(jiān)聽讀數(shù)據(jù)的代理? -1永遠(yuǎn)監(jiān)聽,不超時(shí)澳骤,但是只收一次消息歧强,//所以每次接受到消息還得調(diào)用一次[gcdSocket readDataWithTimeout:-1tag:110];}#pragma mark - GCDAsyncSocketDelegate//連接成功調(diào)用- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString*)host port:(uint16_t)port{NSLog(@"連接成功,host:%@,port:%d",host,port);? ? ? ? [selfpullTheMsg];//心跳寫在這...}//斷開連接的時(shí)候調(diào)用- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullableNSError*)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 = [[NSStringalloc]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í) (如果設(shè)置超時(shí)為-1,則永遠(yuǎn)不會(huì)調(diào)用到)//-(NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length//{//? ? NSLog(@"來(lái)延時(shí)为肮,tag:%ld,elapsed:%f,length:%ld",tag,elapsed,length);//? ? return 10;//}@end

這個(gè)框架使用起來(lái)也十分簡(jiǎn)單摊册,它基于Scoket往上進(jìn)行了一層封裝,提供了OC的接口給我們使用颊艳。至于使用方法茅特,大家看看注釋應(yīng)該就能明白忘分,這里唯一需要說(shuō)的一點(diǎn)就是這個(gè)方法:

[gcdSocket readDataWithTimeout:-1 tag:110];

這個(gè)方法的作用就是去讀取當(dāng)前消息隊(duì)列中的未讀消息。記住白修,這里不調(diào)用這個(gè)方法妒峦,消息回調(diào)的代理是永遠(yuǎn)不會(huì)被觸發(fā)的。而且必須是tag相同熬荆,如果tag不同舟山,這個(gè)收到消息的代理也不會(huì)被處罰。

我們調(diào)用一次這個(gè)方法卤恳,只能觸發(fā)一次讀取消息的代理累盗,如果我們調(diào)用的時(shí)候沒(méi)有未讀消息,它就會(huì)等在那突琳,直到消息來(lái)了被觸發(fā)若债。一旦被觸發(fā)一次代理后,我們必須再次調(diào)用這個(gè)方法拆融,否則蠢琳,之后的消息到了仍舊無(wú)法觸發(fā)我們讀取消息的代理。就像我們?cè)诶又惺褂玫哪菢泳当诿看巫x取到消息之后我們都去調(diào)用:

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

除此之外份蝴,我們還需要說(shuō)的是這個(gè)超時(shí)timeout

這里如果設(shè)置10秒骂远,那么就只能監(jiān)聽10秒刻恭,10秒過(guò)后調(diào)用是否續(xù)時(shí)的代理方法:

-(NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length

如果我們選擇不續(xù)時(shí),那么10秒到了還沒(méi)收到消息硼一,那么Scoket會(huì)自動(dòng)斷開連接累澡。看到這里有些小伙伴要吐槽了般贼,怎么一個(gè)方法設(shè)計(jì)的這么麻煩愧哟,當(dāng)然這里這么設(shè)計(jì)是有它的應(yīng)用場(chǎng)景的,我們后面再來(lái)細(xì)講哼蛆。

我們同樣來(lái)運(yùn)行看看效果:

handle3.gif

至此我們也用CocoaAsyncSocket這個(gè)框架實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的IM蕊梧。

分割圖.png

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

這個(gè)例子我們會(huì)把心跳,斷線重連人芽,以及PingPong機(jī)制進(jìn)行簡(jiǎn)單的封裝望几,所以我們先來(lái)談?wù)勥@三個(gè)概念:

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

簡(jiǎn)單的來(lái)說(shuō)绩脆,心跳就是用來(lái)檢測(cè)TCP連接的雙方是否可用萤厅。那又會(huì)有人要問(wèn)了橄抹,TCP不是本身就自帶一個(gè)KeepAlive機(jī)制嗎?

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

某臺(tái)服務(wù)器因?yàn)槟承┰驅(qū)е仑?fù)載超高楼誓,CPU 100%,無(wú)法響應(yīng)任何業(yè)務(wù)請(qǐng)求名挥,但是使用 TCP 探針則仍舊能夠確定連接狀態(tài)疟羹,這就是典型的連接活著但業(yè)務(wù)提供方已死的狀態(tài)。

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

我們客戶端發(fā)起心跳Ping(一般都是客戶端)禀倔,假如設(shè)置在10秒后如果沒(méi)有收到回調(diào)榄融,那么說(shuō)明服務(wù)器或者客戶端某一方出現(xiàn)問(wèn)題,這時(shí)候我們需要主動(dòng)斷開連接救湖。

服務(wù)端也是一樣愧杯,會(huì)維護(hù)一個(gè)socket的心跳間隔,當(dāng)約定時(shí)間內(nèi)鞋既,沒(méi)有收到客戶端發(fā)來(lái)的心跳力九,我們會(huì)知道該連接已經(jīng)失效,然后主動(dòng)斷開連接邑闺。

參考文章:為什么說(shuō)基于TCP的移動(dòng)端IM仍然需要心跳钡埃活?

其實(shí)做過(guò)IM的小伙伴們都知道陡舅,我們真正需要心跳機(jī)制的原因其實(shí)主要是在于國(guó)內(nèi)運(yùn)營(yíng)商N(yùn)AT超時(shí)抵乓。

那么究竟什么是NAT超時(shí)呢?

原來(lái)這是因?yàn)镮PV4引起的,我們上網(wǎng)很可能會(huì)處在一個(gè)NAT設(shè)備(無(wú)線路由器之類)之后蹭沛。

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

NAT映射

NAT設(shè)備會(huì)根據(jù)NAT表對(duì)出去和進(jìn)來(lái)的數(shù)據(jù)做修改, 比如將192.168.0.3:8888發(fā)出去的封包改成120.132.92.21:9202, 外部就認(rèn)為他們是在和120.132.92.21:9202通信. 同時(shí)NAT設(shè)備會(huì)將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這一映射因?yàn)槟承┰虮籒AT設(shè)備淘汰了, 那么外部設(shè)備就無(wú)法直接與192.168.0.3:8888通信了臂寝。

我們的設(shè)備經(jīng)常是處在NAT設(shè)備的后面, 比如在大學(xué)里的校園網(wǎng), 查一下自己分配到的IP, 其實(shí)是內(nèi)網(wǎng)IP, 表明我們?cè)贜AT設(shè)備后面, 如果我們?cè)趯嬍以俳觽€(gè)路由器, 那么我們發(fā)出的數(shù)據(jù)包會(huì)多經(jīng)過(guò)一次NAT.

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

而國(guó)內(nèi)的運(yùn)營(yíng)商一般NAT超時(shí)的時(shí)間為5分鐘摊灭,所以通常我們心跳設(shè)置的時(shí)間間隔為3-5分鐘咆贬。

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

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

當(dāng)服務(wù)端發(fā)出一個(gè)Ping眷蜈,客戶端沒(méi)有在約定的時(shí)間內(nèi)返回響應(yīng)的ack,則認(rèn)為客戶端已經(jīng)不在線沈自,這時(shí)我們Server端會(huì)主動(dòng)斷開Scoket連接酌儒,并且改由APNS推送的方式發(fā)送消息。

同樣的是枯途,當(dāng)客戶端去發(fā)送一個(gè)消息忌怎,因?yàn)槲覀冞t遲無(wú)法收到服務(wù)端的響應(yīng)ack包籍滴,則表明客戶端或者服務(wù)端已不在線,我們也會(huì)顯示消息發(fā)送失敗榴啸,并且斷開Scoket連接孽惰。

還記得我們之前CocoaSyncSockt的例子所講的獲取消息超時(shí)就斷開嗎?其實(shí)它就是一個(gè)PingPong機(jī)制的客戶端實(shí)現(xiàn)鸥印。我們每次可以在發(fā)送消息成功后勋功,調(diào)用這個(gè)超時(shí)讀取的方法,如果一段時(shí)間沒(méi)收到服務(wù)器的響應(yīng)库说,那么說(shuō)明連接不可用狂鞋,則斷開Scoket連接

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

理論上,我們自己主動(dòng)去斷開的Scoket連接(例如退出賬號(hào)潜的,APP退出到后臺(tái)等等)要销,不需要重連。其他的連接斷開夏块,我們都需要進(jìn)行斷線重連疏咐。

一般解決方案是嘗試重連幾次,如果仍舊無(wú)法重連成功脐供,那么不再進(jìn)行重連浑塞。

接下來(lái)的WebScoket的例子,我會(huì)封裝一個(gè)重連時(shí)間指數(shù)級(jí)增長(zhǎng)的一個(gè)重連方式政己,可以作為一個(gè)參考酌壕。

言歸正傳,我們看完上述三個(gè)概念之后歇由,我們來(lái)講一個(gè)WebScoket最具代表性的一個(gè)第三方框架SocketRocket卵牍。

我們首先來(lái)看看它對(duì)外封裝的一些方法:

@interfaceSRWebSocket:NSObject@property(nonatomic,weak)id delegate;@property(nonatomic,readonly) SRReadyState readyState;@property(nonatomic,readonly,retain)NSURL*url;@property(nonatomic,readonly)CFHTTPMessageRefreceivedHTTPHeaders;// 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#pragma mark - SRWebSocketDelegate@protocolSRWebSocketDelegate// 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

方法也很簡(jiǎn)單,分為兩個(gè)部分:

一部分為SRWebSocket的初始化沦泌,以及連接糊昙,關(guān)閉連接,發(fā)送消息等方法谢谦。

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

收到消息的回調(diào),連接失敗的回調(diào)回挽,關(guān)閉連接的回調(diào)没咙,收到pong的回調(diào),是否需要把data消息轉(zhuǎn)換成string的代理方法千劈。

接著我們還是舉個(gè)例子來(lái)實(shí)現(xiàn)以下祭刚,首先來(lái)封裝一個(gè)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"#define dispatch_main_async_safe(block)\if([NSThreadisMainThread]) {\? ? ? ? block();\? ? }else{\dispatch_async(dispatch_get_main_queue(), block);\? ? }staticNSString* Khost =@"127.0.0.1";staticconstuint16_t Kport =6969;@interfaceTYHSocketManager(){? ? SRWebSocket *webSocket;NSTimer*heartBeat;NSTimeIntervalreConnectTime;? ? }@end@implementationTYHSocketManager+ (instancetype)share{staticdispatch_once_tonceToken;staticTYHSocketManager *instance =nil;dispatch_once(&onceToken, ^{? ? ? ? instance = [[selfalloc]init];? ? ? ? [instance initSocket];? ? });returninstance;}//初始化連接- (void)initSocket{if(webSocket) {return;? ? }? ? ? ? ? ? webSocket = [[SRWebSocket alloc]initWithURL:[NSURLURLWithString:[NSStringstringWithFormat:@"ws://%@:%d", Khost, Kport]]];? ? ? ? webSocket.delegate =self;//設(shè)置代理線程queueNSOperationQueue*queue = [[NSOperationQueuealloc]init];? ? queue.maxConcurrentOperationCount =1;? ? ? ? [webSocket setDelegateOperationQueue:queue];//連接[webSocket open];? ? ? ? }//初始化心跳- (void)initHeartBeat{? ? ? ? dispatch_main_async_safe(^{? ? ? ? ? ? ? ? [selfdestoryHeartBeat];? ? ? ? ? ? ? ? __weaktypeof(self) weakSelf =self;//心跳設(shè)置為3分鐘,NAT超時(shí)一般為5分鐘heartBeat = [NSTimerscheduledTimerWithTimeInterval:3*60repeats:YESblock:^(NSTimer* _Nonnull timer) {NSLog(@"heart");//和服務(wù)端約定好發(fā)送什么作為心跳標(biāo)識(shí),盡可能的減小心跳包大小[weakSelf sendMsg:@"heart"];? ? ? ? }];? ? ? ? [[NSRunLoopcurrentRunLoop]addTimer:heartBeat forMode:NSRunLoopCommonModes];? ? })? ? }//取消心跳- (void)destoryHeartBeat{? ? dispatch_main_async_safe(^{if(heartBeat) {? ? ? ? ? ? [heartBeat invalidate];? ? ? ? ? ? heartBeat =nil;? ? ? ? }? ? })? }#pragma mark - 對(duì)外的一些接口//建立連接- (void)connect{? ? [selfinitSocket];//每次正常連接的時(shí)候清零重連時(shí)間reConnectTime =0;}//斷開連接- (void)disConnect{if(webSocket) {? ? ? ? [webSocket close];? ? ? ? webSocket =nil;? ? }}//發(fā)送消息- (void)sendMsg:(NSString*)msg{? ? [webSocket send:msg];? ? }//重連機(jī)制- (void)reConnect{? ? [selfdisConnect];//超過(guò)一分鐘就不再重連 所以只會(huì)重連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];? ? });//重連時(shí)間2的指數(shù)級(jí)增長(zhǎng)if(reConnectTime ==0) {? ? ? ? reConnectTime =2;? ? }else{? ? ? ? reConnectTime *=2;? ? }}//pingPong- (void)ping{? ? ? ? [webSocket sendPing:nil];}#pragma mark - SRWebSocketDelegate- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message{NSLog(@"服務(wù)器返回收到消息:%@",message);}- (void)webSocketDidOpen:(SRWebSocket *)webSocket{NSLog(@"連接成功");//連接成功了開始發(fā)送心跳[selfinitHeartBeat];}//open失敗的時(shí)候調(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];? ? }//斷開連接時(shí)銷毀心跳[selfdestoryHeartBeat];}//sendPing的時(shí)候,如果網(wǎng)絡(luò)通的話遮怜,則會(huì)收到回調(diào),但是必須保證ScoketOpen鸿市,否則會(huì)crash- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData*)pongPayload{NSLog(@"收到pong回調(diào)");}//將收到的消息锯梁,是否需要把data轉(zhuǎn)換為NSString,每次收到消息都會(huì)被調(diào)用焰情,默認(rèn)YES//- (BOOL)webSocketShouldConvertTextFrameToString:(SRWebSocket *)webSocket//{//? ? NSLog(@"webSocketShouldConvertTextFrameToString");////? ? return NO;//}

.m文件有點(diǎn)長(zhǎng)陌凳,大家可以參照github中的demo進(jìn)行閱讀,這回我們添加了一些細(xì)節(jié)的東西了内舟,包括一個(gè)簡(jiǎn)單的心跳合敦,重連機(jī)制,還有webScoket封裝好的一個(gè)pingpong機(jī)制验游。

代碼非常簡(jiǎn)單充岛,大家可以配合著注釋讀一讀,應(yīng)該很容易理解耕蝉。

需要說(shuō)一下的是這個(gè)心跳機(jī)制是一個(gè)定時(shí)的間隔崔梗,往往我們可能會(huì)有更復(fù)雜實(shí)現(xiàn),比如我們正在發(fā)送消息的時(shí)候垒在,可能就不需要心跳蒜魄。當(dāng)不在發(fā)送的時(shí)候在開啟心跳之類的。微信有一種更高端的實(shí)現(xiàn)方式场躯,有興趣的小伙伴可以看看:

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

還有一點(diǎn)需要說(shuō)的就是這個(gè)重連機(jī)制谈为,demo中我采用的是2的指數(shù)級(jí)別增長(zhǎng),第一次立刻重連踢关,第二次2秒伞鲫,第三次4秒,第四次8秒...直到大于64秒就不再重連签舞。而任意的一次成功的連接榔昔,都會(huì)重置這個(gè)重連時(shí)間。

最后一點(diǎn)需要說(shuō)的是瘪菌,這個(gè)框架給我們封裝的webscoket在調(diào)用它的sendPing方法之前撒会,一定要判斷當(dāng)前scoket是否連接,如果不是連接狀態(tài)师妙,程序則會(huì)crash诵肛。

客戶端的實(shí)現(xiàn)就大致如此,接著同樣我們需要實(shí)現(xiàn)一個(gè)服務(wù)端,來(lái)看看實(shí)際通訊效果怔檩。

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

在這里我們無(wú)法沿用之前的node.js例子了褪秀,因?yàn)檫@并不是一個(gè)原生的scoket,這是webScoket薛训,所以我們服務(wù)端同樣需要遵守webScoket協(xié)議媒吗,兩者才能實(shí)現(xiàn)通信。

其實(shí)這里實(shí)現(xiàn)也很簡(jiǎn)單乙埃,我采用了node.js的ws模塊闸英,只需要用npm去安裝ws即可。

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

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

$ npm install ws

大家如果懶得去看npm的小伙伴也沒(méi)關(guān)系鸠珠,直接下載github中的??WSServer.js這個(gè)文件運(yùn)行即可巍耗。

該源文件代碼如下:

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端口');

代碼沒(méi)幾行,理解起來(lái)很簡(jiǎn)單渐排。

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

如果收到客戶端消息后,打印消息吓歇,并且向客戶端發(fā)送這條收到的消息孽水。

接著我們同樣來(lái)運(yùn)行一下看看效果:

運(yùn)行我們可以看到,主動(dòng)去斷開的連接城看,沒(méi)有去重連女气,而server端斷開的,我們開啟了重連测柠。感興趣的朋友可以下載demo實(shí)際運(yùn)行一下炼鞠。

分割圖.png

4.我們接著來(lái)看看MQTT:

MQTT是一個(gè)聊天協(xié)議,它比webScoket更上層轰胁,屬于應(yīng)用層谒主。

它的基本模式是簡(jiǎn)單的發(fā)布訂閱,也就是說(shuō)當(dāng)一條消息發(fā)出去的時(shí)候赃阀,誰(shuí)訂閱了誰(shuí)就會(huì)受到霎肯。其實(shí)它并不適合IM的場(chǎng)景,例如用來(lái)實(shí)現(xiàn)有些簡(jiǎn)單IM場(chǎng)景,卻需要很大量的观游、復(fù)雜的處理搂捧。

比較適合它的場(chǎng)景為訂閱發(fā)布這種模式的,例如微信的實(shí)時(shí)共享位置懂缕,滴滴的地圖上小車的移動(dòng)允跑、客戶端推送等功能。

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

這個(gè)框架是c來(lái)寫的搪柑,把一些方法公開在MQTTKit類中聋丝,對(duì)外用OC來(lái)調(diào)用,我們來(lái)看看這個(gè)類:

@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)BOOLcleanSession;@property(nonatomic,copy) MQTTMessageHandler messageHandler;+ (void) initialize;+ (NSString*) version;- (MQTTClient*) initWithClientId: (NSString*)clientId;- (void) setMessageRetry: (NSUInteger)seconds;#pragma mark - Connection- (void) connectWithCompletionHandler:(void(^)(MQTTConnectionReturnCode code))completionHandler;- (void) connectToHost: (NSString*)host? ? completionHandler:(void(^)(MQTTConnectionReturnCode code))completionHandler;- (void) disconnectWithCompletionHandler:(void(^)(NSUIntegercode))completionHandler;- (void) reconnect;- (void)setWillData:(NSData*)payload? ? ? ? ? ? toTopic:(NSString*)willTopic? ? ? ? ? ? withQos:(MQTTQualityOfService)willQosretain:(BOOL)retain;- (void)setWill:(NSString*)payload? ? ? ? toTopic:(NSString*)willTopic? ? ? ? withQos:(MQTTQualityOfService)willQosretain:(BOOL)retain;- (void)clearWill;#pragma mark - Publish- (void)publishData:(NSData*)payload? ? ? ? ? ? toTopic:(NSString*)topic? ? ? ? ? ? withQos:(MQTTQualityOfService)qosretain:(BOOL)retaincompletionHandler:(void(^)(intmid))completionHandler;- (void)publishString:(NSString*)payload? ? ? ? ? ? ? toTopic:(NSString*)topic? ? ? ? ? ? ? withQos:(MQTTQualityOfService)qosretain:(BOOL)retaincompletionHandler:(void(^)(intmid))completionHandler;#pragma mark - Subscribe- (void)subscribe:(NSString*)topicwithCompletionHandler:(MQTTSubscriptionCompletionHandler)completionHandler;- (void)subscribe:(NSString*)topic? ? ? ? ? withQos:(MQTTQualityOfService)qoscompletionHandler:(MQTTSubscriptionCompletionHandler)completionHandler;- (void)unsubscribe: (NSString*)topicwithCompletionHandler:(void(^)(void))completionHandler;

這個(gè)類一共分為4個(gè)部分:初始化拌屏、連接、發(fā)布术荤、訂閱倚喂,具體方法的作用可以先看看方法名理解下,我們接著來(lái)用這個(gè)框架封裝一個(gè)實(shí)例瓣戚。

同樣端圈,我們封裝了一個(gè)單例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_tonceToken;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 = [[NSStringalloc]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(@"錯(cuò)誤的用戶名密碼");//....default:NSLog(@"MQTT連接失敗");break;? ? ? ? }? ? ? ? ? ? }];}#pragma mark - 對(duì)外的一些接口//建立連接- (void)connect{? ? [selfinitSocket];}//斷開連接- (void)disConnect{if(client) {//取消訂閱[client unsubscribe:client.clientID withCompletionHandler:^{NSLog(@"取消訂閱tuyaohui成功");? ? ? ? }];//斷開連接[client disconnectWithCompletionHandler:^(NSUIntegercode) {NSLog(@"斷開MQTT成功");? ? ? ? }];? ? ? ? ? ? ? ? client =nil;? ? }}//發(fā)送消息- (void)sendMsg:(NSString*)msg{//發(fā)送一條消息,發(fā)送給自己訂閱的主題[client publishString:msg toTopic:KClientID withQos:ExactlyOnceretain:YEScompletionHandler:^(intmid) {? ? ? ? ? ? }];}@end

實(shí)現(xiàn)代碼很簡(jiǎn)單仑嗅,需要說(shuō)一下的是:

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

2)其次是這個(gè)框架為我們實(shí)現(xiàn)了一個(gè)QOS機(jī)制鸵贬,那么什么是QOS呢?

QoS(Quality of Service脖捻,服務(wù)質(zhì)量)指一個(gè)網(wǎng)絡(luò)能夠利用各種基礎(chǔ)技術(shù)阔逼,為指定的網(wǎng)絡(luò)通信提供更好的服務(wù)能力, 是網(wǎng)絡(luò)的一種安全機(jī)制, 是用來(lái)解決網(wǎng)絡(luò)延遲和阻塞等問(wèn)題的一種技術(shù)地沮。

在這里嗜浮,它提供了三個(gè)選項(xiàng):

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

分別對(duì)應(yīng)最多發(fā)送一次,至少發(fā)送一次摩疑,精確只發(fā)送一次危融。

QOS(0),最多發(fā)送一次:如果消息沒(méi)有發(fā)送過(guò)去,那么就直接丟失雷袋。

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

QOS(2),精確只發(fā)送一次:它內(nèi)部會(huì)有一個(gè)很復(fù)雜的發(fā)送機(jī)制,確保消息送到寨腔,而且只發(fā)送一次速侈。

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

同樣的我們需要一個(gè)用MQTT協(xié)議實(shí)現(xiàn)的服務(wù)端迫卢,我們還是node.js來(lái)實(shí)現(xiàn)倚搬,這次我們還是需要用npm來(lái)新增一個(gè)模塊mosca。

我們來(lái)看看服務(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('有消息來(lái)了','topic為:'+topic+',message為:'+ packet.payload.toString());? ? });? MqttServer.on('ready',function(){console.log('mqtt服務(wù)器開啟每界,監(jiān)聽6969端口');? });

服務(wù)端代碼沒(méi)幾行,開啟了一個(gè)服務(wù)家卖,并且監(jiān)聽本機(jī)6969端口眨层。并且監(jiān)聽了客戶端連接、發(fā)布消息等狀態(tài)上荡。

接著我們同樣來(lái)運(yùn)行一下看看效果:

至此趴樱,我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的MQTT封裝。

5.XMPP:XMPPFramework框架

結(jié)果就是并沒(méi)有XMPP...因?yàn)閭€(gè)人感覺(jué)XMPP對(duì)于IM來(lái)說(shuō)實(shí)在是不堪重用酪捡。僅僅只能作為一個(gè)玩具demo叁征,給大家練練手。網(wǎng)上有太多XMPP的內(nèi)容了逛薇,相當(dāng)一部分用openfire來(lái)做服務(wù)端捺疼,這一套東西實(shí)在是太老了。還記得多年前永罚,樓主初識(shí)IM就是用的這一套東西...

如果大家仍然感興趣的可以看看這篇文章:iOS 的 XMPPFramework 簡(jiǎn)介啤呼。這里就不舉例贅述了。

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

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

使用 ProtocolBuffer 減少 Payload

滴滴打車40%媳友;

攜程之前分享過(guò),說(shuō)是采用新的Protocol Buffer數(shù)據(jù)格式+Gzip壓縮后的Payload大小降低了15%-45%产捞。數(shù)據(jù)序列化耗時(shí)下降了80%-90%醇锚。

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

【高效】提高網(wǎng)絡(luò)請(qǐng)求成功率焊唬,消息體越大,失敗幾率隨之增加看靠。

【省流量】流量消耗極少赶促,省流量。一條消息數(shù)據(jù)用Protobuf序列化后的大小是 JSON 的1/10挟炬、XML格式的1/20鸥滨、是二進(jìn)制序列化的1/10嗦哆。同 XML 相比, Protobuf 性能優(yōu)勢(shì)明顯婿滓。它以高效的二進(jìn)制方式存儲(chǔ)老速,比 XML 小 3 到 10 倍,快 20 到 100 倍凸主。

【省電】省電

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

【易于使用】開發(fā)人員通過(guò)按照一定的語(yǔ)法定義結(jié)構(gòu)化的消息格式卿吐,然后送給命令行工具旁舰,工具將自動(dòng)生成相關(guān)的類,可以支持java嗡官、c++箭窜、python、Objective-C等語(yǔ)言環(huán)境衍腥。通過(guò)將這些類包含在項(xiàng)目中磺樱,可以很輕松的調(diào)用相關(guān)方法來(lái)完成業(yè)務(wù)消息的序列化與反序列化工作。語(yǔ)言支持:原生支持c++紧阔、java坊罢、python续担、Objective-C等多達(dá)10余種語(yǔ)言擅耽。 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)用也早已在使用它(采用的是改造過(guò)的Protobuf協(xié)議)

如何測(cè)試驗(yàn)證 Protobuf 的高性能?

對(duì)數(shù)據(jù)分別操作100次询兴,1000次乃沙,10000次和100000次進(jìn)行了測(cè)試,

縱坐標(biāo)是完成時(shí)間诗舰,單位是毫秒警儒,

反序列化

序列化

字節(jié)長(zhǎng)度

數(shù)據(jù)來(lái)源

數(shù)據(jù)來(lái)自:項(xiàng)目?thrift-protobuf-compare眶根,測(cè)試項(xiàng)為 Total Time蜀铲,也就是 指一個(gè)對(duì)象操作的整個(gè)時(shí)間,包括創(chuàng)建對(duì)象属百,將對(duì)象序列化為內(nèi)存中的字節(jié)序列记劝,然后再反序列化的整個(gè)過(guò)程。從測(cè)試結(jié)果可以看到 Protobuf 的成績(jī)很好.

缺點(diǎn):

可能會(huì)造成 APP 的包體積增大族扰,通過(guò) Google 提供的腳本生成的 Model厌丑,會(huì)非扯ㄅ罚“龐大”,Model 一多怒竿,包體積也就會(huì)跟著變大砍鸠。

如果 Model 過(guò)多,可能導(dǎo)致 APP 打包后的體積驟增愧口,但 IM 服務(wù)所使用的 Model 非常少睦番,比如在 ChatKit-OC 中只用到了一個(gè) Protobuf 的 Model:Message對(duì)象,對(duì)包體積的影響微乎其微耍属。

在使用過(guò)程中要合理地權(quán)衡包體積以及傳輸效率的問(wèn)題托嚣,據(jù)說(shuō)去哪兒網(wǎng),就曾經(jīng)為了減少包體積厚骗,進(jìn)而減少了 Protobuf 的使用示启。

綜上所述,我們選擇傳輸格式的時(shí)候:ProtocolBuffer?>?Json?>?XML

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

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

iOS之ProtocolBuffer搭建和示例demo

三领舰、IM一些其它問(wèn)題

1.IM的可靠性:

我們之前穿插在例子中提到過(guò):

心跳機(jī)制夫嗓、PingPong機(jī)制、斷線重連機(jī)制冲秽、還有我們后面所說(shuō)的QOS機(jī)制舍咖。這些被用來(lái)保證連接的可用,消息的即時(shí)與準(zhǔn)確的送達(dá)等等锉桑。

上述內(nèi)容保證了我們IM服務(wù)時(shí)的可靠性排霉,其實(shí)我們能做的還有很多:比如我們?cè)诖笪募鬏數(shù)臅r(shí)候使用分片上傳、斷點(diǎn)續(xù)傳民轴、秒傳技術(shù)等來(lái)保證文件的傳輸攻柠。

2.安全性:

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

例如:防止 DNS 污染后裸、帳號(hào)安全瑰钮、第三方服務(wù)器鑒權(quán)、單點(diǎn)登錄等等

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

類似微信微驶,服務(wù)器不做聊天記錄的存儲(chǔ)浪谴,只在本機(jī)進(jìn)行緩存,這樣可以減少對(duì)服務(wù)端數(shù)據(jù)的請(qǐng)求因苹,一方面減輕了服務(wù)器的壓力苟耻,另一方面減少客戶端流量的消耗。

我們進(jìn)行http連接的時(shí)候盡量采用上層API容燕,類似NSUrlSession梁呈。而網(wǎng)絡(luò)框架盡量使用AFNetWorking3。因?yàn)檫@些上層網(wǎng)絡(luò)請(qǐng)求都用的是HTTP/2 蘸秘,我們請(qǐng)求的時(shí)候可以復(fù)用這些連接官卡。

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

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

四、音視頻通話

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

實(shí)時(shí)音視頻技術(shù)上的實(shí)現(xiàn)內(nèi)容主要包括:音視頻的采集饭寺、編碼、網(wǎng)絡(luò)傳輸叫挟、解碼艰匙、播放等環(huán)節(jié)。這么多項(xiàng)并不簡(jiǎn)單的技術(shù)應(yīng)用抹恳,如果把握不當(dāng)员凝,將會(huì)在在實(shí)際開發(fā)過(guò)程中遇到一個(gè)又一個(gè)的坑。

因?yàn)闃侵髯约簩?duì)這塊的技術(shù)理解很淺奋献,所以引用了一個(gè)系列的文章來(lái)給大家一個(gè)參考健霹,感興趣的朋友可以看看:

即時(shí)通訊音視頻開發(fā)(一):視頻編解碼之理論概述

即時(shí)通訊音視頻開發(fā)(二):視頻編解碼之?dāng)?shù)字視頻介紹

即時(shí)通訊音視頻開發(fā)(三):視頻編解碼之編碼基礎(chǔ)

即時(shí)通訊音視頻開發(fā)(四):視頻編解碼之預(yù)測(cè)技術(shù)介紹

即時(shí)通訊音視頻開發(fā)(五):認(rèn)識(shí)主流視頻編碼技術(shù)H.264

即時(shí)通訊音視頻開發(fā)(六):如何開始音頻編解碼技術(shù)的學(xué)習(xí)

即時(shí)通訊音視頻開發(fā)(七):音頻基礎(chǔ)及編碼原理入門

即時(shí)通訊音視頻開發(fā)(八):常見的實(shí)時(shí)語(yǔ)音通訊編碼標(biāo)準(zhǔn)

即時(shí)通訊音視頻開發(fā)(九):實(shí)時(shí)語(yǔ)音通訊的回音及回音消除?概述

即時(shí)通訊音視頻開發(fā)(十):實(shí)時(shí)語(yǔ)音通訊的回音消除?技術(shù)詳解

即時(shí)通訊音視頻開發(fā)(十一):實(shí)時(shí)語(yǔ)音通訊丟包補(bǔ)償技術(shù)詳解

即時(shí)通訊音視頻開發(fā)(十二):多人實(shí)時(shí)音視頻聊天架構(gòu)探討

即時(shí)通訊音視頻開發(fā)(十三):實(shí)時(shí)視頻編碼H.264的特點(diǎn)與優(yōu)勢(shì)

即時(shí)通訊音視頻開發(fā)(十四):實(shí)時(shí)音視頻數(shù)據(jù)傳輸協(xié)議介紹

即時(shí)通訊音視頻開發(fā)(十五):聊聊P2P與實(shí)時(shí)音視頻的應(yīng)用情況

即時(shí)通訊音視頻開發(fā)(十六):移動(dòng)端實(shí)時(shí)音視頻開發(fā)的幾個(gè)建議

即時(shí)通訊音視頻開發(fā)(十七):視頻編碼H.264、V8的前世今生

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓶蚂,一起剝皮案震驚了整個(gè)濱河市糖埋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窃这,老刑警劉巖瞳别,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異钦听,居然都是意外死亡洒试,警方通過(guò)查閱死者的電腦和手機(jī)倍奢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門朴上,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人卒煞,你說(shuō)我怎么就攤上這事痪宰。” “怎么了畔裕?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵衣撬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我扮饶,道長(zhǎng)具练,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任甜无,我火速辦了婚禮扛点,結(jié)果婚禮上哥遮,老公的妹妹穿的比我還像新娘。我一直安慰自己陵究,他們只是感情好眠饮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著铜邮,像睡著了一般仪召。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上松蒜,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天扔茅,我揣著相機(jī)與錄音,去河邊找鬼秸苗。 笑死咖摹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的难述。 我是一名探鬼主播萤晴,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼胁后!你這毒婦竟也來(lái)了店读?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤攀芯,失蹤者是張志新(化名)和其女友劉穎屯断,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侣诺,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡殖演,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了年鸳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趴久。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖搔确,靈堂內(nèi)的尸體忽然破棺而出彼棍,到底是詐尸還是另有隱情,我是刑警寧澤膳算,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布座硕,位于F島的核電站,受9級(jí)特大地震影響涕蜂,放射性物質(zhì)發(fā)生泄漏华匾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一机隙、第九天 我趴在偏房一處隱蔽的房頂上張望蜘拉。 院中可真熱鬧刊头,春花似錦、人聲如沸诸尽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)您机。三九已至穿肄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間际看,已是汗流浹背咸产。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仲闽,地道東北人脑溢。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像赖欣,于是被迫代替她去往敵國(guó)和親屑彻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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