開(kāi)源一個(gè)自用的Android IM庫(kù)秀撇,基于Netty+TCP+Protobuf實(shí)現(xiàn)

歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處:http://www.reibang.com/p/00ba0ac2fc96

寫(xiě)在前面

一直想寫(xiě)一篇關(guān)于im即時(shí)通訊分享的文章浩考,無(wú)奈工作太忙,很難抽出時(shí)間被盈。今天終于從公司離職了,打算好好休息幾天再重新找工作搭伤,趁時(shí)間空閑只怎,決定靜下心來(lái)寫(xiě)一篇文章,畢竟從前輩那里學(xué)到了很多東西怜俐。工作了五年半身堡,這三四年來(lái)一直在做社交相關(guān)的項(xiàng)目,有
直播拍鲤、
即時(shí)通訊贴谎、
短視頻分享
社區(qū)論壇
等產(chǎn)品季稳,深知即時(shí)通訊技術(shù)在一個(gè)項(xiàng)目中的重要性擅这,本著開(kāi)源分享的精神,也趁這機(jī)會(huì)總結(jié)一下景鼠,所以寫(xiě)下這篇文章仲翎,文中有不對(duì)之處歡迎批評(píng)與指正。

本文將介紹:

  • Protobuf序列化
  • TCP拆包與粘包
  • 長(zhǎng)連接握手認(rèn)證
  • 心跳機(jī)制
  • 重連機(jī)制
  • 消息重發(fā)機(jī)制
  • 讀寫(xiě)超時(shí)機(jī)制
  • 離線消息
  • 線程池
  • AIDL跨進(jìn)程通信

本想花一部分時(shí)間介紹一下利用AIDL實(shí)現(xiàn)多進(jìn)程通信铛漓,提升應(yīng)用彼菹悖活率,無(wú)奈這種方法在目前大部分Android新版本上已失效浓恶,而且也比較復(fù)雜玫坛,所以考慮再三,把AIDL這一部分去掉包晰,需要了解的童鞋可以私信我湿镀。

先來(lái)看看效果:


最終運(yùn)行效果

不想看文章的同學(xué)可以直接移步到Github fork源碼:github地址

接下來(lái)炕吸,讓我們進(jìn)入正題。


為什么使用TCP肠骆?

這里需要簡(jiǎn)單解釋一下算途,TCP/UDP/WebSocket的區(qū)別。
這里就很好地解釋了TCP/UDP的優(yōu)缺點(diǎn)和區(qū)別蚀腿,以及適用場(chǎng)景嘴瓤,簡(jiǎn)單地總結(jié)一下:

  • 優(yōu)點(diǎn):

    • TCP的優(yōu)點(diǎn)體現(xiàn)在穩(wěn)定可靠上莉钙,在傳輸數(shù)據(jù)之前廓脆,會(huì)有三次握手來(lái)建立連接,而且在數(shù)據(jù)傳遞時(shí)磁玉,有確認(rèn)停忿、窗口、重傳蚊伞、擁塞控制機(jī)制席赂,在數(shù)據(jù)傳完之后,還會(huì)斷開(kāi)連接用來(lái)節(jié)約系統(tǒng)資源时迫。
    • UDP的優(yōu)點(diǎn)體現(xiàn)在颅停,比TCP稍安全,UDP沒(méi)有TCP擁有的各種機(jī)制掠拳,是一個(gè)無(wú)狀態(tài)的傳輸協(xié)議癞揉,所以傳遞數(shù)據(jù)非常快溺欧,沒(méi)有TCP的這些機(jī)制喊熟,被攻擊利用的機(jī)制就少一些,但是也無(wú)法避免被攻擊姐刁。
  • 缺點(diǎn):

    • TCP缺點(diǎn)就是芥牌,效率低占用系統(tǒng)資源高聂使,易被攻擊胳泉,TCP在傳遞數(shù)據(jù)之前要先建立連接,這會(huì)消耗時(shí)間岩遗,而且在數(shù)據(jù)傳遞時(shí)扇商,確認(rèn)機(jī)制、重傳機(jī)制宿礁、擁塞機(jī)制等都會(huì)消耗大量時(shí)間案铺,而且要在每臺(tái)設(shè)備上維護(hù)所有的傳輸連接。
    • UDP缺點(diǎn)就是不可靠梆靖,不穩(wěn)定控汉,因?yàn)闆](méi)有TCP的那些機(jī)制笔诵,UDP在傳輸數(shù)據(jù)時(shí),如果網(wǎng)絡(luò)質(zhì)量不好姑子,就會(huì)很容易丟包乎婿,造成數(shù)據(jù)的缺失。
  • 適用場(chǎng)景:

    • TCP:當(dāng)對(duì)網(wǎng)絡(luò)通訊質(zhì)量有要求時(shí)街佑,比如HTTP谢翎、HTTPS、FTP等傳輸文件的協(xié)議沐旨, POP森逮、SMTP等郵件傳輸?shù)膮f(xié)議。
    • UDP:對(duì)網(wǎng)絡(luò)通訊質(zhì)量要求不高時(shí)磁携,要求網(wǎng)絡(luò)通訊速度要快的場(chǎng)景褒侧。

至于WebSocket,后續(xù)可能會(huì)專門寫(xiě)一篇文章來(lái)介紹谊迄。
綜上所述闷供,決定采用TCP協(xié)議。


為什么使用Protobuf统诺?

對(duì)于App網(wǎng)絡(luò)傳輸協(xié)議这吻,我們比較常見(jiàn)的、可選的篙议,有三種,分別是json/xml/protobuf怠硼,老規(guī)矩鬼贱,我們先分別來(lái)看看這三種格式的優(yōu)缺點(diǎn):

  • 優(yōu)點(diǎn):

    • json優(yōu)點(diǎn)就是較XML格式更加小巧,傳輸效率較xml提高了很多香璃,可讀性還不錯(cuò)这难。
    • xml優(yōu)點(diǎn)就是可讀性強(qiáng),解析方便葡秒。
    • protobuf優(yōu)點(diǎn)就是傳輸效率快(據(jù)說(shuō)在數(shù)據(jù)量大的時(shí)候姻乓,傳輸效率比xml和json快10-20倍),序列化后體積相比Json和XML很小眯牧,支持跨平臺(tái)多語(yǔ)言蹋岩,消息格式升級(jí)和兼容性還不錯(cuò),序列化反序列化速度很快学少。
  • 缺點(diǎn):

    • json缺點(diǎn)就是傳輸效率也不是特別高(比xml快剪个,但比protobuf要慢很多)。
    • xml缺點(diǎn)就是效率不高版确,資源消耗過(guò)大扣囊。
    • protobuf缺點(diǎn)就是使用不太方便乎折。

在一個(gè)需要大量的數(shù)據(jù)傳輸?shù)膱?chǎng)景中,如果數(shù)據(jù)量很大侵歇,那么選擇protobuf可以明顯的減少數(shù)據(jù)量骂澄,減少網(wǎng)絡(luò)IO,從而減少網(wǎng)絡(luò)傳輸所消耗的時(shí)間惕虑》爻澹考慮到作為一個(gè)主打社交的產(chǎn)品,消息數(shù)據(jù)量會(huì)非常大枷遂,同時(shí)為了節(jié)約流量樱衷,所以采用protobuf是一個(gè)不錯(cuò)的選擇。


為什么使用Netty酒唉?

首先矩桂,我們來(lái)了解一下,Netty到底是個(gè)什么東西痪伦。網(wǎng)絡(luò)上找到的介紹:Netty是由JBOSS提供的基于Java NIO的開(kāi)源框架侄榴,Netty提供異步非阻塞、事件驅(qū)動(dòng)网沾、高性能癞蚕、高可靠、高可定制性的網(wǎng)絡(luò)應(yīng)用程序和工具辉哥,可用于開(kāi)發(fā)服務(wù)端和客戶端桦山。

  • 為什么不用Java BIO?

    • 一連接一線程醋旦,由于線程數(shù)是有限的恒水,所以這樣非常消耗資源,最終也導(dǎo)致它不能承受高并發(fā)連接的需求饲齐。
    • 性能低钉凌,因?yàn)轭l繁的進(jìn)行上下文切換,導(dǎo)致CUP利用率低捂人。
    • 可靠性差御雕,由于所有的IO操作都是同步的,即使是業(yè)務(wù)線程也如此滥搭,所以業(yè)務(wù)線程的IO操作也有可能被阻塞酸纲,這將導(dǎo)致系統(tǒng)過(guò)分依賴網(wǎng)絡(luò)的實(shí)時(shí)情況和外部組件的處理能力,可靠性大大降低瑟匆。
  • 為什么不用Java NIO福青?

    • NIO的類庫(kù)和API相當(dāng)復(fù)雜,使用它來(lái)開(kāi)發(fā),需要非常熟練地掌握Selector无午、ByteBuffer媒役、ServerSocketChannel、SocketChannel等宪迟。
    • 需要很多額外的編程技能來(lái)輔助使用NIO,例如酣衷,因?yàn)镹IO涉及了Reactor線程模型,所以必須必須對(duì)多線程和網(wǎng)絡(luò)編程非常熟悉才能寫(xiě)出高質(zhì)量的NIO程序次泽。
    • 想要有高可靠性穿仪,工作量和難度都非常的大,因?yàn)榉?wù)端需要面臨客戶端頻繁的接入和斷開(kāi)意荤、網(wǎng)絡(luò)閃斷啊片、半包讀寫(xiě)、失敗緩存玖像、網(wǎng)絡(luò)阻塞的問(wèn)題紫谷,這些將嚴(yán)重影響我們的可靠性,而使用原生NIO解決它們的難度相當(dāng)大捐寥。
    • JDK NIO中著名的BUG--epoll空輪詢笤昨,當(dāng)select返回0時(shí),會(huì)導(dǎo)致Selector空輪詢而導(dǎo)致CUP100%握恳,官方表示JDK1.6之后修復(fù)了這個(gè)問(wèn)題瞒窒,其實(shí)只是發(fā)生的概率降低了,沒(méi)有根本上解決乡洼。
  • 為什么用Netty崇裁?

    • API使用簡(jiǎn)單,更容易上手束昵,開(kāi)發(fā)門檻低
    • 功能強(qiáng)大拔稳,預(yù)置了多種編解碼功能,支持多種主流協(xié)議
    • 定制能力高妻怎,可以通過(guò)ChannelHandler對(duì)通信框架進(jìn)行靈活地拓展
    • 高性能,與目前多種NIO主流框架相比泞歉,Netty綜合性能最高
    • 高穩(wěn)定性逼侦,解決了JDK NIO的BUG
    • 經(jīng)歷了大規(guī)模的商業(yè)應(yīng)用考驗(yàn),質(zhì)量和可靠性都有很好的驗(yàn)證腰耙。

以上摘自:為什么要用Netty開(kāi)發(fā)

  • 為什么不用第三方SDK榛丢,如:融云、環(huán)信挺庞、騰訊TIM晰赞?
    這個(gè)就見(jiàn)仁見(jiàn)智了,有的時(shí)候,是因?yàn)楣镜募夹g(shù)選型問(wèn)題掖鱼,因?yàn)橛玫谌降腟DK然走,意味著消息數(shù)據(jù)需要存儲(chǔ)到第三方的服務(wù)器上,再者戏挡,可擴(kuò)展性芍瑞、靈活性肯定沒(méi)有自己開(kāi)發(fā)的要好,還有一個(gè)小問(wèn)題褐墅,就是收費(fèi)拆檬。比如,融云免費(fèi)版只支持100個(gè)注冊(cè)用戶妥凳,超過(guò)100就要收費(fèi)竟贯,群聊支持人數(shù)有限制等等...
    融云收費(fèi)

Mina其實(shí)跟Netty很像,大部分API都相同逝钥,因?yàn)槭峭粋€(gè)作者開(kāi)發(fā)的屑那。但感覺(jué)Mina沒(méi)有Netty成熟,在使用Netty的過(guò)程中晌缘,出了問(wèn)題很輕易地可以找到解決方案齐莲,所以,Netty是一個(gè)不錯(cuò)的選擇磷箕。

好了选酗,廢話不多說(shuō),直接開(kāi)始吧岳枷。


準(zhǔn)備工作

  • 首先芒填,我們新建一個(gè)Project,在Project里面再新建一個(gè)Android Library空繁,Module名稱暫且叫做im_lib殿衰,如圖所示:


    新建項(xiàng)目
  • 然后,分析一下我們的消息結(jié)構(gòu)盛泡,每條消息應(yīng)該會(huì)有一個(gè)消息唯一id闷祥,發(fā)送者id,接收者id傲诵,消息類型凯砍,發(fā)送時(shí)間等,經(jīng)過(guò)分析拴竹,整理出一個(gè)通用的消息類型悟衩,如下:

    • msgId 消息id
    • fromId 發(fā)送者id
    • toId 接收者id
    • msgType 消息類型
    • msgContentType 消息內(nèi)容類型
    • timestamp 消息時(shí)間戳
    • statusReport 狀態(tài)報(bào)告
    • extend 擴(kuò)展字段

    根據(jù)上述所示,我整理了一個(gè)思維導(dǎo)圖栓拜,方便大家參考:

    消息結(jié)構(gòu)

    這是基礎(chǔ)部分座泳,當(dāng)然惠昔,大家也可以根據(jù)自己需要自定義比較適合自己的消息結(jié)構(gòu)。

    我們根據(jù)自定義的消息類型來(lái)編寫(xiě)proto文件挑势。


    編寫(xiě)proto文件

    然后執(zhí)行命令(我用的mac镇防,windows命令應(yīng)該也差不多):


    執(zhí)行protoc命令

    然后就會(huì)看到,在和proto文件同級(jí)目錄下薛耻,會(huì)生成一個(gè)java類营罢,這個(gè)就是我們需要用到的東東:
    生成的protobuf java類文件

    我們打開(kāi)瞄一眼:


    打開(kāi)的protobuf java類文件

    東西比較多,不用去管饼齿,這是google為我們生成的protobuf類饲漾,直接用就行,怎么用呢缕溉?直接用這個(gè)類文件考传,拷到我們開(kāi)始指定的項(xiàng)目包路徑下就可以啦:
    導(dǎo)入protobuf java類文件到項(xiàng)目中

    添加依賴后,可以看到证鸥,MessageProtobuf類文件已經(jīng)沒(méi)有報(bào)錯(cuò)了僚楞,順便把netty的jar包也導(dǎo)進(jìn)來(lái)一下,還有fastjson的:
    導(dǎo)入protobuf以及netty的依賴

    建議用netty-all-x.x.xx.Final的jar包枉层,后續(xù)熟悉了泉褐,可以用精簡(jiǎn)的jar包。

    至此鸟蜡,準(zhǔn)備工作已結(jié)束膜赃,下面,我們來(lái)編寫(xiě)java代碼揉忘,實(shí)現(xiàn)即時(shí)通訊的功能跳座。


封裝

為什么需要封裝呢?說(shuō)白了泣矛,就是為了解耦疲眷,為了方便日后切換到不同框架實(shí)現(xiàn),而無(wú)需到處修改調(diào)用的地方您朽。舉個(gè)栗子狂丝,比如Android早期比較流行的圖片加載框架是Universal ImageLoader,后期因?yàn)槟承┰蚧┳埽髡咄V沽司S護(hù)該項(xiàng)目几颜,目前比較流行的圖片加載框架是Picasso或Glide,因?yàn)閳D片加載功能可能調(diào)用的地方非常多魂奥,如果不作一些封裝仆潮,早期使用了Universal ImageLoader的話已卸,現(xiàn)在需要切換到Glide遣蚀,那改動(dòng)量將非常非常大,而且還很有可能會(huì)有遺漏中燥,風(fēng)險(xiǎn)度非常高。

那么圃郊,有什么解決方案呢薪寓?

很簡(jiǎn)單,我們可以用工廠設(shè)計(jì)模式進(jìn)行一些封裝炮赦,工廠模式有三種:簡(jiǎn)單工廠模式怜跑、抽象工廠模式、工廠方法模式吠勘。在這里性芬,我采用工廠方法模式進(jìn)行封裝,具體區(qū)別剧防,可以參見(jiàn):通俗講講我對(duì)簡(jiǎn)單工廠植锉、工廠方法、抽象工廠三種設(shè)計(jì)模式的理解

我們分析一下峭拘,ims(IM Service俊庇,下文簡(jiǎn)稱ims)應(yīng)該是有初始化建立連接鸡挠、重連辉饱、關(guān)閉連接釋放資源拣展、判斷長(zhǎng)連接是否關(guān)閉彭沼、發(fā)送消息等功能,基于上述分析瞎惫,我們可以進(jìn)行一個(gè)接口抽象:

抽象的ims接口1

抽象的ims接口2

OnEventListener是與應(yīng)用層交互的listener:
OnEventListener

IMConnectStatusCallback是ims連接狀態(tài)回調(diào)監(jiān)聽(tīng)器:
IMConnectStatusCallback

然后寫(xiě)一個(gè)Netty tcp實(shí)現(xiàn)類:


Netty tcp ims1

Netty tcp ims2

接下來(lái)溜腐,寫(xiě)一個(gè)工廠方法:


ims實(shí)例工廠方法

封裝部分到此結(jié)束,接下來(lái)瓜喇,就是實(shí)現(xiàn)了挺益。


初始化

我們先實(shí)現(xiàn)init(Vector<String> serverUrlList, OnEventListener listener, IMSConnectStatusCallback callback)方法,初始化一些參數(shù)乘寒,以及進(jìn)行第一次連接等:

初始化參數(shù)

其中望众,MsgDispatcher是消息轉(zhuǎn)發(fā)器,負(fù)責(zé)將接收到的消息轉(zhuǎn)發(fā)到應(yīng)用層:

MsgDispatcher

ExecutorServiceFactory是線程池工廠伞辛,負(fù)責(zé)調(diào)度重連及心跳線程:

ExecutorServiceFactory1

ExecutorServiceFactory2

ExecutorServiceFactory3


連接及重連

resetConnect()方法作為連接的起點(diǎn)烂翰,首次連接以及重連邏輯,都是在resetConnect()方法進(jìn)行邏輯處理蚤氏,我們來(lái)瞄一眼:

resetConnect

可以看到甘耿,非首次進(jìn)行連接,也就是連接一個(gè)周期失敗后竿滨,進(jìn)行重連時(shí)佳恬,會(huì)先讓線程休眠一段時(shí)間捏境,因?yàn)檫@個(gè)時(shí)候也許網(wǎng)絡(luò)狀況不太好,接著毁葱,判斷ims是否已關(guān)閉或者是否正在進(jìn)行重連操作垫言,由于重連操作是在子線程執(zhí)行,為了避免重復(fù)重連倾剿,需要進(jìn)行一些并發(fā)處理筷频。開(kāi)始重連任務(wù)后,分四個(gè)步驟執(zhí)行:

  • 改變重連狀態(tài)標(biāo)識(shí)
  • 回調(diào)連接狀態(tài)到應(yīng)用層
  • 關(guān)閉之前打開(kāi)的連接channel
  • 利用線程池執(zhí)行一個(gè)新的重連任務(wù)

ResetConnectRunnable是重連任務(wù)前痘,核心的重連邏輯都放到這里執(zhí)行:

ResetConnectRunnable1

ResetConnectRunnable2

ResetConnectRunnable3

toServer()是真正連接服務(wù)器的地方:

toServer

initBootstrap()是初始化Netty Bootstrap:

initBootstrap

注:NioEventLoopGroup線程數(shù)設(shè)置為4凛捏,可以滿足QPS是一百多萬(wàn)的情況了,至于應(yīng)用如果需要承受上千萬(wàn)上億流量的芹缔,需要另外調(diào)整線程數(shù)葵袭。參考自:netty實(shí)戰(zhàn)之百萬(wàn)級(jí)流量NioEventLoopGroup線程數(shù)配置

接著,我們來(lái)看看TCPChannelInitializerHanlder

TCPChannelInitializerHandler

其中乖菱,ProtobufEncoderProtobufDecoder是添加對(duì)protobuf的支持坡锡,LoginAuthRespHandler是接收到服務(wù)端握手認(rèn)證消息響應(yīng)的處理handler,HeartbeatRespHandler是接收到服務(wù)端心跳消息響應(yīng)的處理handler窒所,TCPReadHandler是接收到服務(wù)端其它消息后的處理handler鹉勒,先不去管,我們重點(diǎn)來(lái)分析下LengthFieldPrependerLengthFieldBasedFrameDecoder吵取,這就需要引申到TCP的拆包與粘包啦禽额。


TCP的拆包與粘包

  • 什么是TCP拆包?為什么會(huì)出現(xiàn)TCP拆包皮官?

    簡(jiǎn)單地說(shuō)脯倒,我們都知道TCP是以“流”的形式進(jìn)行數(shù)據(jù)傳輸?shù)模襎CP為提高性能捺氢,發(fā)送端會(huì)將需要發(fā)送的數(shù)據(jù)刷入緩沖區(qū)藻丢,等待緩沖區(qū)滿了之后,再將緩沖區(qū)中的數(shù)據(jù)發(fā)送給接收方摄乒,同理悠反,接收方也會(huì)有緩沖區(qū)這樣的機(jī)制,來(lái)接收數(shù)據(jù)馍佑。
    拆包就是在socket讀取時(shí)斋否,沒(méi)有完整地讀取一個(gè)數(shù)據(jù)包,只讀取一部分拭荤。

  • 什么是TCP粘包茵臭?為什么會(huì)出現(xiàn)TCP粘包?

    同上舅世。
    粘包就是在socket讀取時(shí)旦委,讀到了實(shí)際意義上的兩個(gè)或多個(gè)數(shù)據(jù)包的內(nèi)容踊沸,同時(shí)將其作為一個(gè)數(shù)據(jù)包進(jìn)行處理。

引用網(wǎng)上一張圖片來(lái)解釋一下在TCP出現(xiàn)拆包社证、粘包以及正常狀態(tài)下的三種情況,如侵請(qǐng)聯(lián)系我刪除:


TCP拆包评凝、粘包追葡、正常狀態(tài)

了解了TCP出現(xiàn)拆包/粘包的原因,那么奕短,如何解決呢宜肉?通常來(lái)說(shuō),有以下四種解決方式:

  • 消息定長(zhǎng)
  • 用回車換行符作為消息結(jié)束標(biāo)志
  • 用特殊分隔符作為消息結(jié)束標(biāo)志翎碑,如\t谬返、\n等,回車換行符其實(shí)就是特殊分隔符的一種日杈。
  • 將消息分為消息頭和消息體遣铝,在消息頭中用字段標(biāo)識(shí)消息總長(zhǎng)度。

netty針對(duì)以上四種場(chǎng)景莉擒,給我們封裝了以下四種對(duì)應(yīng)的解碼器:

  • FixedLengthFrameDecoder酿炸,定長(zhǎng)消息解碼器
  • LineBasedFrameDecoder,回車換行符消息解碼器
  • DelimiterBasedFrameDecoder涨冀,特殊分隔符消息解碼器
  • LengthFieldBasedFrameDecoder填硕,自定義長(zhǎng)度消息解碼器。

我們用到的就是LengthFieldBasedFrameDecoder自定義長(zhǎng)度消息解碼器鹿鳖,同時(shí)配合LengthFieldPrepender編碼器使用扁眯,關(guān)于參數(shù)配置,建議參考netty--最通用TCP黏包解決方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender這篇文章翅帜,講解得比較細(xì)致姻檀。我們配置的是消息頭長(zhǎng)度為2個(gè)字節(jié),所以消息包的最大長(zhǎng)度需要小于65536個(gè)字節(jié)涝滴,netty會(huì)把消息內(nèi)容長(zhǎng)度存放消息頭的字段里施敢,接收方可以根據(jù)消息頭的字段拿到此條消息總長(zhǎng)度,當(dāng)然狭莱,netty提供的LengthFieldBasedFrameDecoder已經(jīng)封裝好了處理邏輯僵娃,我們只需要配置lengthFieldOffset、lengthFieldLength腋妙、lengthAdjustment默怨、initialBytesToStrip即可,這樣就可以解決TCP的拆包與粘包骤素,這也就是netty相較于原生nio的便捷性匙睹,原生nio需要自己處理拆包/粘包等問(wèn)題愚屁。


長(zhǎng)連接握手認(rèn)證

接著,我們來(lái)看看LoginAuthHandlerHeartbeatRespHandler

  • LoginAuthRespHandler是當(dāng)客戶端與服務(wù)端長(zhǎng)連接建立成功后痕檬,客戶端主動(dòng)向服務(wù)端發(fā)送一條登錄認(rèn)證消息霎槐,帶入與當(dāng)前用戶相關(guān)的參數(shù),比如token梦谜,服務(wù)端收到此消息后丘跌,到數(shù)據(jù)庫(kù)查詢?cè)撚脩粜畔ⅲ绻呛戏ㄓ行У挠脩粞渥瑒t返回一條登錄成功消息給該客戶端闭树,反之,返回一條登錄失敗消息給該客戶端荒澡,這里报辱,就是在接收到服務(wù)端返回的登錄狀態(tài)后的處理handler,比如:

    LoginAuthRespHandler

    可以看到单山,當(dāng)接收到服務(wù)端握手消息響應(yīng)后碍现,會(huì)從擴(kuò)展字段取出status,如果status=1米奸,則代表握手成功鸵赫,這個(gè)時(shí)候就先主動(dòng)向服務(wù)端發(fā)送一條心跳消息,然后利用Netty的IdleStateHandler讀寫(xiě)超時(shí)機(jī)制躏升,定期向服務(wù)端發(fā)送心跳消息辩棒,維持長(zhǎng)連接,以及檢測(cè)長(zhǎng)連接是否還存在等膨疏。

  • HeartbeatRespHandler是當(dāng)客戶端接收到服務(wù)端登錄成功的消息后一睁,主動(dòng)向服務(wù)端發(fā)送一條心跳消息,心跳消息可以是一個(gè)空包佃却,消息包體越小越好者吁,服務(wù)端收到客戶端的心跳包后,原樣返回給客戶端饲帅,這里复凳,就是收到服務(wù)端返回的心跳消息響應(yīng)的處理handler,比如:

    HeartbeatRespHandler

    這個(gè)就比較簡(jiǎn)單灶泵,收到心跳消息響應(yīng)育八,無(wú)需任務(wù)處理,直接打印一下方便我們分析即可赦邻。


心跳機(jī)制及讀寫(xiě)超時(shí)機(jī)制

心跳包是定期發(fā)送髓棋,也可以自己定義一個(gè)周期,比如Android微信智能心跳方案,為了簡(jiǎn)單按声,此處規(guī)定應(yīng)用在前臺(tái)時(shí)膳犹,8秒發(fā)送一個(gè)心跳包,切換到后臺(tái)時(shí)签则,30秒發(fā)送一次须床,根據(jù)自己的實(shí)際情況修改一下即可。心跳包用于維持長(zhǎng)連接以及檢測(cè)長(zhǎng)連接是否斷開(kāi)等渐裂。

接著豺旬,我們利用Netty的讀寫(xiě)超時(shí)機(jī)制,來(lái)實(shí)現(xiàn)一個(gè)心跳消息管理handler:

HeartbeatHandler

可以看到芯义,利用userEventTriggered()方法回調(diào),通過(guò)IdleState類型妻柒,可以判斷讀超時(shí)/寫(xiě)超時(shí)/讀寫(xiě)超時(shí)扛拨,這個(gè)在添加IdleStateHandler時(shí)可以配置,下面會(huì)貼上代碼举塔。首先我們可以在READER_IDLE事件里绑警,檢測(cè)是否在規(guī)定時(shí)間內(nèi)沒(méi)有收到服務(wù)端心跳包響應(yīng),如果是央渣,那就觸發(fā)重連操作计盒。在WRITER_IDEL事件可以檢測(cè)客戶端是否在規(guī)定時(shí)間內(nèi)沒(méi)有向服務(wù)端發(fā)送心跳包,如果是芽丹,那就主動(dòng)發(fā)送一個(gè)心跳包北启。發(fā)送心跳包是在子線程中執(zhí)行,我們可以利用之前寫(xiě)的work線程池進(jìn)行線程管理拔第。
addHeartbeatHandler()代碼如下:
addHeartbeatHandler

從圖上可看到咕村,在IdleStateHandler里,配置的讀超時(shí)為心跳間隔時(shí)長(zhǎng)的3倍蚊俺,也就是3次心跳沒(méi)有響應(yīng)時(shí)懈涛,則認(rèn)為長(zhǎng)連接已斷開(kāi),觸發(fā)重連操作泳猬。寫(xiě)超時(shí)則為心跳間隔時(shí)長(zhǎng)批钠,意味著每隔heartbeatInterval會(huì)發(fā)送一個(gè)心跳包。讀寫(xiě)超時(shí)沒(méi)用到得封,所以配置為0埋心。

onConnectStatusCallback(int connectStatus)為連接狀態(tài)回調(diào),以及一些公共邏輯處理:


onConnectStatusCallback

連接成功后忙上,立即發(fā)送一條握手消息踩窖,再次梳理一下整體流程:

  • 客戶端根據(jù)服務(wù)端返回的host及port,進(jìn)行第一次連接晨横。
  • 連接成功后洋腮,客戶端向服務(wù)端發(fā)送一條握手認(rèn)證消息(1001)
  • 服務(wù)端在收到客戶端的握手認(rèn)證消息后箫柳,從擴(kuò)展字段里取出用戶token,到本地?cái)?shù)據(jù)庫(kù)校驗(yàn)合法性啥供。
  • 校驗(yàn)完成后悯恍,服務(wù)端把校驗(yàn)結(jié)果通過(guò)1001消息返回給客戶端,也就是握手消息響應(yīng)伙狐。
  • 客戶端收到服務(wù)端的握手消息響應(yīng)后涮毫,從擴(kuò)展字段取出校驗(yàn)結(jié)果。若校驗(yàn)成功贷屎,客戶端向服務(wù)端發(fā)送一條心跳消息(1002)罢防,然后進(jìn)入心跳發(fā)送周期,定期間隔向服務(wù)端發(fā)送心跳消息唉侄,維持長(zhǎng)連接以及實(shí)時(shí)檢測(cè)鏈路可用性咒吐,若發(fā)現(xiàn)鏈路不可用,等待一段時(shí)間觸發(fā)重連操作属划,重連成功后恬叹,重新開(kāi)始握手/心跳的邏輯。

看看TCPReadHandler收到消息是怎么處理的:

TCPReadHandler1

TCPReadHandler2

可以看到同眯,在channelInactive()及exceptionCaught()方法都觸發(fā)了重連绽昼,channelInactive()方法在當(dāng)鏈路斷開(kāi)時(shí)會(huì)調(diào)用,exceptionCaught()方法在當(dāng)出現(xiàn)異常時(shí)會(huì)觸發(fā)须蜗,另外硅确,還有諸如channelUnregistered()、channelReadComplete()等方法可以重寫(xiě)明肮,在這里就不貼了疏魏,相信聰明的你一眼就能看出方法的作用。
我們仔細(xì)看一下channelRead()方法的邏輯晤愧,在if判斷里大莫,先判斷消息類型,如果是服務(wù)端返回的消息發(fā)送狀態(tài)報(bào)告類型官份,則判斷消息是否發(fā)送成功只厘,如果發(fā)送成功,從超時(shí)管理器中移除舅巷,這個(gè)超時(shí)管理器是干嘛的呢羔味?下面講到消息重發(fā)機(jī)制的時(shí)候會(huì)詳細(xì)地講。在else里钠右,收到其他消息后赋元,會(huì)立馬給服務(wù)端返回一個(gè)消息接收狀態(tài)報(bào)告,告訴服務(wù)端,這條消息我已經(jīng)收到了搁凸,這個(gè)動(dòng)作媚值,對(duì)于后續(xù)需要做的離線消息會(huì)有作用。如果不需要支持離線消息功能护糖,這一步可以省略褥芒。最后,調(diào)用消息轉(zhuǎn)發(fā)器嫡良,把接收到的消息轉(zhuǎn)發(fā)到應(yīng)用層即可锰扶。

代碼寫(xiě)了這么多,我們先來(lái)看看運(yùn)行后的效果寝受,先貼上缺失的消息發(fā)送代碼及ims關(guān)閉代碼以及一些默認(rèn)配置項(xiàng)的代碼坷牛。
發(fā)送消息:

發(fā)送消息

關(guān)閉ims:
關(guān)閉ims

ims默認(rèn)配置:
ims默認(rèn)配置

還有,應(yīng)用層實(shí)現(xiàn)的ims client啟動(dòng)器:
IMSClientBootstrap

由于代碼有點(diǎn)多很澄,不太方便全部貼上京闰,如果有興趣可以下載demo體驗(yàn)。
額痴怨,對(duì)了忙干,還有一個(gè)簡(jiǎn)易的服務(wù)端代碼器予,如下:
NettyServerDemo1

NettyServerDemo2

NettyServerDemo3


調(diào)試

我們先來(lái)看看連接及重連部分(由于錄制gif比較麻煩浪藻,體積較大,所以我先把重連間隔調(diào)小成3秒乾翔,方便看效果)爱葵。

  • 啟動(dòng)服務(wù)端:
    啟動(dòng)服務(wù)端
  • 啟動(dòng)客戶端:
    啟動(dòng)客戶端

    可以看到,正常的情況下已經(jīng)連接成功了反浓,接下來(lái)萌丈,我們來(lái)試一下異常情況,比如服務(wù)端沒(méi)啟動(dòng)雷则,看看客戶端的重連情況:


    調(diào)試重連

    這次我們先啟動(dòng)的是客戶端辆雾,可以看到連接失敗后一直在進(jìn)行重連,由于錄制gif比較麻煩月劈,在第三次連接失敗后度迂,我啟動(dòng)了服務(wù)端,這個(gè)時(shí)候客戶端就會(huì)重連成功猜揪。

然后惭墓,我們?cè)賮?lái)調(diào)試一下握手認(rèn)證消息即心跳消息:


握手消息及心跳消息測(cè)試

可以看到,長(zhǎng)連接建立成功后而姐,客戶端會(huì)給服務(wù)端發(fā)送一條握手認(rèn)證消息(1001)腊凶,服務(wù)端收到握手認(rèn)證消息會(huì),給客戶端返回了一條握手認(rèn)證狀態(tài)消息,客戶端收到握手認(rèn)證狀態(tài)消息后钧萍,即啟動(dòng)心跳機(jī)制褐缠。gif不太好演示,下載demo就可以直觀地看到划煮。

接下來(lái)送丰,在講完消息重發(fā)機(jī)制及離線消息后,我會(huì)在應(yīng)用層做一些簡(jiǎn)單的封裝弛秋,以及在模擬器上運(yùn)行器躏,這樣就可以很直觀地看到運(yùn)行效果。


消息重發(fā)機(jī)制

消息重發(fā)蟹略,顧名思義登失,即使對(duì)發(fā)送失敗的消息進(jìn)行重發(fā)⊥诰妫考慮到網(wǎng)絡(luò)環(huán)境的不穩(wěn)定性揽浙、多變性(比如從進(jìn)入電梯、進(jìn)入地鐵意敛、移動(dòng)網(wǎng)絡(luò)切換到wifi等)馅巷,在消息發(fā)送的時(shí)候,發(fā)送失敗的概率其實(shí)不小草姻,這時(shí)消息重發(fā)機(jī)制就很有必要了钓猬。
我們先來(lái)看看實(shí)現(xiàn)的代碼邏輯。
MsgTimeoutTimer:

MsgTimeoutTimer1

MsgTimeoutTimer2

MsgTimeoutTimerManager:
MsgTimeoutTimerManager1

MsgTimeoutTimerManager2

然后撩独,我們看看收消息的TCPReadHandler的改造:
加入消息重發(fā)機(jī)制的TCPReadHandler

最后敞曹,看看發(fā)送消息的改造:
加入消息重發(fā)機(jī)制的發(fā)送消息

說(shuō)一下邏輯吧:發(fā)送消息時(shí),除了心跳消息综膀、握手消息澳迫、狀態(tài)報(bào)告消息外,消息都加入消息發(fā)送超時(shí)管理器剧劝,立馬開(kāi)啟一個(gè)定時(shí)器橄登,比如每隔5秒執(zhí)行一次,共執(zhí)行3次讥此,在這個(gè)周期內(nèi)拢锹,如果消息沒(méi)有發(fā)送成功,會(huì)進(jìn)行3次重發(fā)暂论,達(dá)到3次重發(fā)后如果還是沒(méi)有發(fā)送成功面褐,那就放棄重發(fā),移除該消息取胎,同時(shí)通過(guò)消息轉(zhuǎn)發(fā)器通知應(yīng)用層展哭,由應(yīng)用層決定是否再次重發(fā)湃窍。如果消息發(fā)送成功,服務(wù)端會(huì)返回一個(gè)消息發(fā)送狀態(tài)報(bào)告匪傍,客戶端收到該狀態(tài)報(bào)告后您市,從消息發(fā)送超時(shí)管理器移除該消息,同時(shí)停止該消息對(duì)應(yīng)的定時(shí)器即可役衡。
另外茵休,在用戶握手認(rèn)證成功時(shí),應(yīng)該檢查消息發(fā)送超時(shí)管理器里是否有發(fā)送超時(shí)的消息手蝎,如果有榕莺,則全部重發(fā):

握手認(rèn)證成功檢查是否有發(fā)送超時(shí)的消息


離線消息

由于離線消息機(jī)制,需要服務(wù)端數(shù)據(jù)庫(kù)及緩存上的配合棵介,代碼就不貼了钉鸯,太多太多,我簡(jiǎn)單說(shuō)一下實(shí)現(xiàn)思路吧:
客戶端A發(fā)送消息到客戶端B邮辽,消息會(huì)先到服務(wù)端唠雕,由服務(wù)端進(jìn)行中轉(zhuǎn)。這個(gè)時(shí)候吨述,客戶端B存在兩種情況:

  • 1.長(zhǎng)連接正常岩睁,就是客戶端網(wǎng)絡(luò)環(huán)境良好,手機(jī)有電揣云,應(yīng)用處在打開(kāi)的情況聋丝。
  • 2.廢話敌蚜,那肯定就是長(zhǎng)連接不正惩爰梗咯奴艾。這種情況有很多種原因亿笤,比如wifi不可用翎迁、用戶進(jìn)入了地鐵或電梯等網(wǎng)絡(luò)不好的場(chǎng)所、應(yīng)用沒(méi)打開(kāi)或已退出登錄等净薛,總的來(lái)說(shuō)汪榔,就是沒(méi)有辦法正常接收消息。

如果是長(zhǎng)連接正常肃拜,那沒(méi)什么可說(shuō)的痴腌,服務(wù)端直接轉(zhuǎn)發(fā)即可。
如果長(zhǎng)連接不正常燃领,需要這樣處理:服務(wù)端接收到客戶端A發(fā)送給客戶端B的消息后士聪,先給客戶端A回復(fù)一條狀態(tài)報(bào)告,告訴客戶端A猛蔽,我已經(jīng)收到消息剥悟,這個(gè)時(shí)候灵寺,客戶端A就不用管了,消息只要到達(dá)服務(wù)端即可区岗。然后略板,服務(wù)端先嘗試把消息轉(zhuǎn)發(fā)到客戶端B,如果這個(gè)時(shí)候客戶端B收到服務(wù)端轉(zhuǎn)發(fā)過(guò)來(lái)的消息慈缔,需要立馬給服務(wù)端回一條狀態(tài)報(bào)告叮称,告訴服務(wù)端,我已經(jīng)收到消息藐鹤,服務(wù)端在收到客戶端B返回的消息接收狀態(tài)報(bào)告后瓤檐,即認(rèn)為此消息已經(jīng)正常發(fā)送,不需要再存庫(kù)娱节。如果客戶端B不在線距帅,服務(wù)端在做轉(zhuǎn)發(fā)的時(shí)候,并沒(méi)有收到客戶端B返回的消息接收狀態(tài)報(bào)告括堤,那么碌秸,這條消息就應(yīng)該存到數(shù)據(jù)庫(kù),直到客戶端B上線后悄窃,也就是長(zhǎng)連接建立成功后讥电,客戶端B主動(dòng)向服務(wù)端發(fā)送一條離線消息詢問(wèn),服務(wù)端在收到離線消息詢問(wèn)后轧抗,到數(shù)據(jù)庫(kù)或緩存去查客戶端B的所有離線消息恩敌,并分批次返回,客戶端B在收到服務(wù)端的離線消息返回后横媚,取出消息id(若有多條就取id集合)纠炮,通過(guò)離線消息應(yīng)答把消息id返回到服務(wù)端,服務(wù)端收到后灯蝴,根據(jù)消息id從數(shù)據(jù)庫(kù)把對(duì)應(yīng)的消息刪除即可恢口。
以上是單聊離線消息處理的情況,群聊有點(diǎn)不同穷躁,群聊的話耕肩,是需要服務(wù)端確認(rèn)群組內(nèi)所有用戶都收到此消息后,才能從數(shù)據(jù)庫(kù)刪除消息问潭,就說(shuō)這么多猿诸,如果需要細(xì)節(jié)的話,可以私信我狡忙。


不知不覺(jué)梳虽,NettyTcpClient中定義了很多變量,為了防止大家不明白變量的定義灾茁,還是貼上代碼吧:


定義了很多變量的NettyTcpClient

應(yīng)用層封裝

這個(gè)就見(jiàn)仁見(jiàn)智啦窜觉,每個(gè)人代碼風(fēng)格不同是复,我把自己簡(jiǎn)單封裝的代碼貼上來(lái)吧:
MessageProcessor消息處理器:

MessageProcessor1

MessageProcessor2

IMSEventListener與ims交互的listener:
IMSEventListener1

IMSEventListener2

IMSEventListener3

MessageBuilder消息轉(zhuǎn)換器:
MessageBuilder1

MessageBuilder2

MessageBuilder3

AbstractMessageHandler抽象的消息處理handler,每個(gè)消息類型對(duì)應(yīng)不同的messageHandler:
AbstractMessageHandler

SingleChatMessageHandler單聊消息處理handler:
SingleChatMessageHandler

GroupChatMessageHandler群聊消息處理handler:
GroupChatMessageHandler

MessageHandlerFactory消息handler工廠:
MessageHandlerFactory

MessageType消息類型枚舉:
MessageType

IMSConnectStatusListenerIMS連接狀態(tài)監(jiān)聽(tīng)器:
IMSConnectStatusListener

由于每個(gè)人代碼風(fēng)格不同竖螃,封裝代碼都有自己的思路淑廊,所以,在此就不過(guò)多講解特咆,只是把自己簡(jiǎn)單封裝的代碼全部貼上來(lái)季惩,作一個(gè)參考即可。只需要知道腻格,接收到消息時(shí)画拾,會(huì)回調(diào)OnEventListener的dispatchMsg(MessageProtobuf.Msg msg)方法:
應(yīng)用層接收ims消息入口

發(fā)送消息需要調(diào)用imsClient的sendMsg(MessageProtobuf.Msg msg)方法:
應(yīng)用層調(diào)用ims發(fā)送消息入口

即可,至于怎樣去封裝得更好菜职,大家自由發(fā)揮吧青抛。


最后,為了測(cè)試消息收發(fā)是否正常酬核,我們需要改動(dòng)一下服務(wù)端:


改動(dòng)后的服務(wù)端1

改動(dòng)后的服務(wù)端2

改動(dòng)后的服務(wù)端3

改動(dòng)后的服務(wù)端4

改動(dòng)后的服務(wù)端5

可以看到蜜另,當(dāng)有用戶握手成功后,會(huì)保存該用戶對(duì)應(yīng)的channel到容器里嫡意,給用戶發(fā)送消息時(shí)举瑰,根據(jù)用戶id從容器里取出對(duì)應(yīng)的channel,利用該channel發(fā)送消息蔬螟。當(dāng)用戶斷開(kāi)連接后此迅,會(huì)把該用戶對(duì)應(yīng)的channel從容器里移除掉。

運(yùn)行一下旧巾,看看效果吧:


最終運(yùn)行效果
  • 首先耸序,啟動(dòng)服務(wù)端。
  • 然后鲁猩,修改客戶端連接的ip地址為192.168.0.105(這是我本機(jī)的ip地址)坎怪,端口號(hào)為8855,fromId绳匀,也就是userId芋忿,定義成100001炸客,toId為100002疾棵,啟動(dòng)客戶端A。
  • 再然后痹仙,fromId是尔,也就是userId,定義成100002开仰,toId為100001拟枚,啟動(dòng)客戶端B薪铜。
  • 客戶端A給客戶端B發(fā)送消息,可以看到在客戶端B的下面恩溅,已經(jīng)接收到了消息隔箍。
  • 用客戶端B給客戶端A發(fā)送消息,也可以看到在客戶端A的下面脚乡,也已經(jīng)接收到了消息蜒滩。
    至于,消息收發(fā)測(cè)試成功奶稠。至于群聊或重連等功能俯艰,就不一一演示了,還是那句話锌订,下載demo體驗(yàn)一下吧竹握。。辆飘。

由于gif錄制體積較大啦辐,所以只能簡(jiǎn)單演示一下消息收發(fā),具體下載demo體驗(yàn)吧蜈项。昧甘。。

如果有需要應(yīng)用層UI實(shí)現(xiàn)(就是聊天頁(yè)及會(huì)話頁(yè)的封裝)的話战得,我再分享出來(lái)吧充边。

github地址


發(fā)現(xiàn)的bug

  1. MsgTimeoutTimer
    MsgTimeoutTimer bug1

    這個(gè)bug是自己在檢查代碼時(shí)發(fā)現(xiàn)的,可能是連續(xù)熬幾天夜寫(xiě)文章魔怔了常侦。浇冰。。
    修改如下:
    MsgTimeoutTimer bug1 fix

一個(gè)人精力有限聋亡,大家在使用過(guò)程中肘习,如果發(fā)現(xiàn)其它bug,煩請(qǐng)告訴我坡倔,反正我是會(huì)虛心接受漂佩,堅(jiān)決不改,呸罪塔,一定改投蝉,一定改。另外征堪,歡迎fork瘩缆,期待大家與我一起完善。佃蚜。庸娱。


寫(xiě)在最后

終于寫(xiě)完了着绊,這篇文章大概寫(xiě)了10天左右,有很大部分的原因是自己有拖延癥熟尉,每次寫(xiě)完一小段归露,總靜不下心來(lái)寫(xiě)下去,導(dǎo)致一直拖到現(xiàn)在斤儿,以后得改改靶擦。第一次寫(xiě)技術(shù)分享文章,有很多地方也許邏輯不太清晰雇毫,由于篇幅有限玄捕,也只是貼了部分代碼,建議大家把源碼下載下來(lái)看看棚放。一直想寫(xiě)這篇文章枚粘,以前在網(wǎng)上也嘗試過(guò)找過(guò)很多im方面的文章,都找不到一篇比較完善的飘蚯,本文談不上完善馍迄,但包含的模塊很多,希望起到一個(gè)拋磚引玉的作用局骤,也期待著大家跟我一起發(fā)現(xiàn)更多的問(wèn)題并完善攀圈,最后,如果這篇文章對(duì)你有用峦甩,希望在github上給我一個(gè)star哈赘来。。凯傲。

應(yīng)大家要求犬辰,精簡(jiǎn)了netty-all-4.1.33.Final.jar包。原netty-all-4.1.33.Final.jar包大小為3.9M冰单,經(jīng)測(cè)試發(fā)現(xiàn)目前im_lib庫(kù)只需要用到以下jar包:

  • netty-buffer-4.1.33.Final.jar
  • netty-codec-4.1.33.Final.jar
  • netty-common-4.1.33.Final.jar
  • netty-handler-4.1.33.Final.jar
  • netty-resolver-4.1.33.Final.jar
  • netty-transport-4.1.33.Final.jar

所以幌缝,抽取以上jar包,重新打成了netty-tcp-4.1.33-1.0.jar诫欠,目前自測(cè)沒(méi)有問(wèn)題涵卵,如果發(fā)現(xiàn)bug,請(qǐng)告訴我荒叼,謝謝轿偎。

附上原jar及裁剪后jar包的大小對(duì)比:


666

裁剪后netty-tcp-4.1.33-1.0.jar大小

代碼已更新到Github.


接下來(lái),會(huì)抽時(shí)間把下圖想寫(xiě)的文章都寫(xiě)了甩挫,沒(méi)有先后順序贴硫,想到哪就寫(xiě)到哪吧。伊者。英遭。


想寫(xiě)的文章

另外,創(chuàng)建了一個(gè)Android即時(shí)通訊技術(shù)交流QQ群:1015178804亦渗,有需要的同學(xué)可以加進(jìn)來(lái)挖诸,不懂的問(wèn)題,我會(huì)盡量解答法精,一起學(xué)習(xí)多律,一起成長(zhǎng)。

最新新開(kāi)了一個(gè)微信公眾號(hào)搂蜓,方便后續(xù)KulaChat發(fā)布一些系列文章狼荞,同時(shí)也是為了激勵(lì)自己寫(xiě)作。主要發(fā)布一些原創(chuàng)的Android IM相關(guān)的文章(也會(huì)包含其它方向)帮碰,不定時(shí)更新相味。感興趣的同學(xué)可以關(guān)注一下,謝謝殉挽。PS:感覺(jué)鴻洋大神提供的公眾號(hào)文章排版方式丰涉,感激不盡~~

FreddyChen的微信公眾號(hào)

The end.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市斯碌,隨后出現(xiàn)的幾起案子一死,更是在濱河造成了極大的恐慌,老刑警劉巖傻唾,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件投慈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡冠骄,警方通過(guò)查閱死者的電腦和手機(jī)逛裤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)猴抹,“玉大人带族,你說(shuō)我怎么就攤上這事◇案” “怎么了蝙砌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)跋理。 經(jīng)常有香客問(wèn)我择克,道長(zhǎng),這世上最難降的妖魔是什么前普? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任肚邢,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骡湖。我一直安慰自己贱纠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布响蕴。 她就那樣靜靜地躺著谆焊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浦夷。 梳的紋絲不亂的頭發(fā)上辖试,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音劈狐,去河邊找鬼罐孝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛肥缔,可吹牛的內(nèi)容都是我干的莲兢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼辫继,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼怒见!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起姑宽,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤遣耍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后炮车,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體舵变,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年瘦穆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纪隙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扛或,死狀恐怖绵咱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熙兔,我是刑警寧澤悲伶,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站住涉,受9級(jí)特大地震影響麸锉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜舆声,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一花沉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦碱屁、人聲如沸磷脯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)争拐。三九已至腋粥,卻和暖如春晦雨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隘冲。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工闹瞧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人展辞。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓奥邮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親罗珍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子洽腺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344