阿里IM技術(shù)分享(五):閑魚億級IM消息系統(tǒng)的及時(shí)性優(yōu)化實(shí)踐

本文由阿里閑魚技術(shù)團(tuán)隊(duì)有攸分享,原題“向消息延遲說bybye:閑魚消息及時(shí)到達(dá)方案”,有修訂和改動,感謝作者的分享。
1、引言
IM消息作為閑魚用戶重要的交易咨詢工具,核心目標(biāo)有兩點(diǎn):
1)第一是保證用戶的消息不丟失;
2)第二是保證用戶的消息及時(shí)送達(dá)接收方。
IM消息根據(jù)消息的接收方設(shè)備是否在線,分為離線和在線推送。數(shù)據(jù)顯示目前閑魚每天有超過一半以上的IM消息是走在線通道的,而在線消息的到達(dá)率、及時(shí)性是直接影響用戶體驗(yàn)的。
本文將根據(jù)閑魚IM消息系統(tǒng)在消息及時(shí)性方面的優(yōu)化實(shí)踐,詳細(xì)分析了IM在線通道面臨的各種技術(shù)問題,并通過相應(yīng)的技術(shù)手段來優(yōu)化從而保證用戶消息的及時(shí)到達(dá)。
PS:如果您對IM消息可靠性還沒有概念,建議先閱讀這篇入門文章《零基礎(chǔ)IM開發(fā)入門(二):什么是IM系統(tǒng)的實(shí)時(shí)性?》。

學(xué)習(xí)交流:
- 即時(shí)通訊/推送技術(shù)開發(fā)交流5群:215477170?[推薦]
- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK
(本文同步發(fā)布于:http://www.52im.net/thread-3726-1-1.html)
2、系列文章
本文是系列文章的第5篇,總目錄如下:
《阿里IM技術(shù)分享(一):企業(yè)級IM王者——釘釘在后端架構(gòu)上的過人之處》
《阿里IM技術(shù)分享(二):閑魚IM基于Flutter的移動端跨端改造實(shí)踐》
《阿里IM技術(shù)分享(三):閑魚億級IM消息系統(tǒng)的架構(gòu)演進(jìn)之路》
《阿里IM技術(shù)分享(四):閑魚億級IM消息系統(tǒng)的可靠投遞優(yōu)化實(shí)踐》
《阿里IM技術(shù)分享(五):閑魚億級IM消息系統(tǒng)的及時(shí)性優(yōu)化實(shí)踐》(* 本文)
3、當(dāng)前面臨的問題
3.1 端內(nèi)長連接中斷
在IM場景中,用戶與云端通信頻繁,且為了實(shí)現(xiàn)用戶的消息及時(shí)到達(dá),往往采用云端下推消息的方式觸達(dá)用戶,所以用戶在線時(shí)設(shè)備與云端會維持一條TCP長連接通道,可以更輕量級的與服務(wù)端進(jìn)行交互,現(xiàn)代IM即時(shí)通訊的下行消息都是通過長連下發(fā)的。
當(dāng)前閑魚IM消息系統(tǒng)使用的是ACCS長連接,ACCS是淘寶無線提供的全雙工、低延時(shí)、高安全的通道服務(wù)。
但由于用戶設(shè)備網(wǎng)絡(luò)狀態(tài)的不確定性,可能會發(fā)生各種各樣的網(wǎng)絡(luò)異常情況導(dǎo)致ACCS長連接通道中斷。而長連接一旦意外中斷,就會導(dǎo)致用戶無法及時(shí)收到在線消息。

針對這個(gè)問題,我們需要盡可能及時(shí)的感知到長連中斷并嘗試重連。具體的優(yōu)化思路會在本文后面的內(nèi)容中分享。
3.2 下推的消息未達(dá)
感知長連中斷并重連只能在大多數(shù)時(shí)間保證長連接的有效性,但是在長連接無效或不穩(wěn)定期間下推的消息客戶端可能根本收不到。
簡單說就是僅僅有重連機(jī)制無法保證下行消息必達(dá),可能有以下場景導(dǎo)致下行消息失敗:
1)服務(wù)端發(fā)送下行消息時(shí)長連暢通,消息在傳輸路上通道斷掉,客戶端無法收到;
2)設(shè)備的在線狀態(tài)存在延遲,服務(wù)端下行消息時(shí)認(rèn)為設(shè)備在線,實(shí)際上設(shè)備已經(jīng)離線,無法收到;
3)客戶端收到了下行消息,但端上后續(xù)處理失敗(比如落庫失敗,消息沒有成功展示給用戶)。
我們通過數(shù)據(jù)埋點(diǎn)統(tǒng)計(jì)得出,ACCS長連接的下行成功率在97%左右。
ACCS長連接的下行成功率的統(tǒng)計(jì)方法如下:
ACCS下行成功率 = 通過ACCS成功下行且客戶端收到的消息量 / 服務(wù)端認(rèn)為通過ACCS成功下行的消息量
有心急的同學(xué)就要問了,丟了3%的消息嗎?
并沒有!這3%的消息不會丟失,只是不保證及時(shí)觸達(dá)給用戶。
我們的消息同步模型是推拉結(jié)合模式,在用戶拉取消息時(shí)會拉取到設(shè)備當(dāng)前位點(diǎn)與服務(wù)端最新位點(diǎn)的所有消息,ACCS下行失敗的消息會通過主動拉模式獲取到,但客戶端主動拉取消息的觸發(fā)時(shí)機(jī)有限。
當(dāng)前客戶端主動拉取消息的觸發(fā)時(shí)機(jī)主要有以下幾個(gè):
1)用戶冷啟動APP,主動同步消息;
2)用戶主動下拉刷新;
3)APP后臺切換前臺;
4)收到一條推送消息,客戶端發(fā)現(xiàn)新消息的位點(diǎn)跟本地最新的位點(diǎn)有g(shù)ap,觸發(fā)同步。
可見:上述主動同步消息的觸發(fā)很大程度上依賴用戶行為或者有沒有收到新消息,難以保證消息及時(shí)到達(dá)。
如果是用戶高頻打開的IM軟件,這樣也不會有太大的問題。但是閑魚APP的活躍度較低,有時(shí)候甚至依賴IM消息拉活,而且一條延遲的消息觸達(dá)可能導(dǎo)致用戶錯(cuò)過一筆交易,閑魚消息不允許有這樣的延遲發(fā)生。
基于上述分析,我們先描述一個(gè)數(shù)據(jù)指標(biāo)來反映現(xiàn)狀。
通過上面的描述可知:ACCS消息并不全都是推下來的,也可能是主動拉下來的。如果是推,必定可以及時(shí)到達(dá);如果是拉,則受限于用戶行為。
拉的這部分消息,我們定義為ACCS消息補(bǔ)償?shù)竭_(dá),然后計(jì)算ACCS消息補(bǔ)償?shù)竭_(dá)耗時(shí),消息范圍限定為服務(wù)端ACCS成功下行但是客戶端通過主動拉取同步到的消息,以往的版本這個(gè)數(shù)據(jù)在60分鐘左右。
注意:這個(gè)數(shù)據(jù)并不是消息觸達(dá)到用戶的耗時(shí),因?yàn)槿绻诰€轉(zhuǎn)離線觸達(dá),拉取到消息的時(shí)間取決于用戶行為(用戶何時(shí)打開了APP),但這個(gè)數(shù)據(jù)也能大致反映在線消息的到達(dá)延遲狀況。
ACCS長連接的消息補(bǔ)償?shù)竭_(dá)耗時(shí)的統(tǒng)計(jì)方法如下:
ACCS消息補(bǔ)償?shù)竭_(dá)耗時(shí) = 客戶端通過拉獲取到ACCS消息的時(shí)間 - 服務(wù)端ACCS下行時(shí)間
接下來本文將從長連接的重連和未達(dá)消息重發(fā)兩個(gè)方面詳細(xì)講述我們是如何優(yōu)化在線通道穩(wěn)定性的,從而優(yōu)化并保證消息的及時(shí)到達(dá)。
4、優(yōu)化手段1:增加長連接重連機(jī)制
4.1 長連接為什么會中斷?
有因必有果,我們先來分析下有哪些原因會導(dǎo)致連接中斷。
對于IM這種場景下來說,通常可能有以下原因:
1)用戶設(shè)備斷網(wǎng);
2)設(shè)備發(fā)生了網(wǎng)絡(luò)切換;
3)設(shè)備處于弱網(wǎng)環(huán)境,網(wǎng)絡(luò)不穩(wěn)定;
4)設(shè)備網(wǎng)絡(luò)正常,TCP連接由于NAT超時(shí)導(dǎo)致連接被運(yùn)營商中斷。
對于APP來說,如果是用戶操作導(dǎo)致網(wǎng)絡(luò)狀態(tài)變化的情況,會有網(wǎng)絡(luò)狀態(tài)變化事件通知,這種情況可以監(jiān)聽事件并主動嘗試重連。但現(xiàn)實(shí)中的大多數(shù)情況都是“意料之外”(正如上面列舉的這些斷網(wǎng)可能性一樣)。
那么既然“意料之外”的斷網(wǎng)無法預(yù)知,技術(shù)上可以如何有效的感知到各種異常狀況呢?
PS:如果要透徹理解斷網(wǎng)、弱網(wǎng)、TCP鏈接有效性,并不是本文能講的清楚的,可以參照下面的資料深入理解一下,值得好好學(xué)習(xí)。
關(guān)于TCP鏈接本身的有效性問題,可以讀以下兩篇:
《為何基于TCP協(xié)議的移動端IM仍然需要心跳?;顧C(jī)制?》
《不為人知的網(wǎng)絡(luò)編程(十二):徹底搞懂TCP協(xié)議層的KeepAlive?;顧C(jī)制》
關(guān)于移動網(wǎng)絡(luò)的復(fù)雜性問題,可以從以下幾篇入門的科普文章學(xué)習(xí)一下:
《IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門(十一):為什么WiFi信號差?一文即懂!》
《IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門(十二):上網(wǎng)卡頓?網(wǎng)絡(luò)掉線?一文即懂!》
《IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門(十三):為什么手機(jī)信號差?一文即懂!》
《IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門(十四):高鐵上無線上網(wǎng)有多難?一文即懂!》
關(guān)于移動弱網(wǎng)帶來的各種問題、優(yōu)化方案等,可以通過以下幾篇系統(tǒng)學(xué)習(xí)一下:
《現(xiàn)代移動端網(wǎng)絡(luò)短連接的優(yōu)化手段總結(jié):請求速度、弱網(wǎng)適應(yīng)、安全保障》
《移動端IM開發(fā)者必讀(一):通俗易懂,理解移動網(wǎng)絡(luò)的“弱”和“慢”》
《移動端IM開發(fā)者必讀(二):史上最全移動弱網(wǎng)絡(luò)優(yōu)化方法總結(jié)》
《百度APP移動端網(wǎng)絡(luò)深度優(yōu)化實(shí)踐分享(三):移動端弱網(wǎng)優(yōu)化篇》
4.2 心跳檢測機(jī)制
像大多數(shù)鏈路保活場景一樣,IM這種場景下最有效的檢測手段就是心跳檢測(如果你對TCP鏈路保活還沒有什么概念,建議先讀《為何基于TCP協(xié)議的移動端IM仍然需要心跳?;顧C(jī)制?》)。
原理就是:客戶端通過定時(shí)發(fā)送心跳包,服務(wù)端收到心跳包后再反饋給客戶端,通過客戶端和服務(wù)端這一來一去的配合,就可以實(shí)現(xiàn)客戶服和服務(wù)端各自都能感知到連接是否中斷。
從及時(shí)性效果來看:心跳間隔越短越好,而頻繁的心跳檢測勢必會帶來用戶流量以及電量的損耗,所以我們的實(shí)現(xiàn)目標(biāo)是如何盡可能少的心跳檢測而又盡量及時(shí)地感知到長連中斷的意外情況。
狀態(tài)機(jī)+消息心跳隊(duì)列:

在心跳協(xié)議設(shè)計(jì)上,要注意心跳包的核心目標(biāo)是檢測長連通道是否暢通,客戶端主動上行心跳包且能收到服務(wù)端回包,就認(rèn)為長連通道健康。所以心跳的上行消息以及回包的數(shù)據(jù)包應(yīng)盡可能小。一般來說,通過協(xié)議頭標(biāo)識心跳包及響應(yīng)即可(這樣就能節(jié)省協(xié)議包大?。?。
PS:關(guān)于心跳機(jī)制的入門文章可以詳讀《一文讀懂即時(shí)通訊應(yīng)用中的網(wǎng)絡(luò)心跳包機(jī)制:作用、原理、實(shí)現(xiàn)思路等》。
4.3 心跳策略
心跳策略是實(shí)現(xiàn)我們上述目標(biāo)的核心機(jī)制,本文僅簡單列舉幾種心跳策略。
比如以下這幾種:
1)短心跳檢測 初始狀態(tài)連續(xù) ping 3次 收到 ACK 后,可以認(rèn)為進(jìn)入穩(wěn)定狀態(tài);
2)常規(guī)固定時(shí)長心跳(根據(jù)APP狀態(tài)不同,頻率可調(diào)Mid+,Mid-, Long);
3)自適應(yīng)心跳 根據(jù)設(shè)備網(wǎng)絡(luò)狀態(tài)變化自動適應(yīng)的心跳間隔;
4)冗余心跳,APP后臺切前臺,主動心跳一次。
關(guān)于心跳策略的詳細(xì)設(shè)計(jì)甚至可以單獨(dú)寫一篇文章,有興趣的同學(xué)可以閱讀以下推薦的文章繼續(xù)深入研究。
《微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺保活實(shí)戰(zhàn)分享(網(wǎng)絡(luò)?;钇?》
《移動端IM實(shí)踐:實(shí)現(xiàn)Android版微信的智能心跳機(jī)制》
《移動端IM實(shí)踐:WhatsAPP、Line、微信的心跳策略分析》
《融云技術(shù)分享:融云安卓端IM產(chǎn)品的網(wǎng)絡(luò)鏈路?;罴夹g(shù)實(shí)踐》
《Web端即時(shí)通訊實(shí)踐干貨:如何讓你的WebSocket斷網(wǎng)重連更快速?》
《正確理解IM長連接的心跳及重連機(jī)制,并動手實(shí)現(xiàn)(有完整IM源碼)》
《一種Android端IM智能心跳算法的設(shè)計(jì)與實(shí)現(xiàn)探討(含樣例代碼)》
《手把手教你用Netty實(shí)現(xiàn)網(wǎng)絡(luò)通信程序的心跳機(jī)制、斷線重連機(jī)制》
5、優(yōu)化手段2:消息ACK應(yīng)答與重發(fā)機(jī)制
5.1 概述
為了解決上面的問題,我們同時(shí)也引入了消息ACK應(yīng)答與重發(fā)機(jī)制。
整體思路是:客戶端在收到ACCS消息并處理成功后,給服務(wù)端回一個(gè)ACK應(yīng)答包,服務(wù)端下發(fā)ACCS消息時(shí)將消息加入重試隊(duì)列,收到ACK應(yīng)答包后更新消息到達(dá)狀態(tài),并終止重試。
整體設(shè)計(jì)流程圖如下:

該方案的難點(diǎn)即重試處理器的實(shí)現(xiàn)設(shè)計(jì),接下來我們將重點(diǎn)講述這部分的詳細(xì)設(shè)計(jì)。
5.2 重試隊(duì)列存儲設(shè)計(jì)
我們采用阿里云表格存儲TimeLine模型來存儲下行消息的到達(dá)狀態(tài)。Timeline 模型是針對消息數(shù)據(jù)場景所設(shè)計(jì)的數(shù)據(jù)模型,它能滿足消息數(shù)據(jù)場景對消息保序、海量消息存儲、實(shí)時(shí)同步的特殊需求,在IM、Feed流等消息場景應(yīng)用廣泛。(關(guān)于TimeLine模型,這里有篇詳細(xì)的文章可以學(xué)習(xí)一下《現(xiàn)代IM系統(tǒng)中聊天消息的同步和存儲方案探討》)
我們給每個(gè)用戶設(shè)備定義一個(gè)TimeLine,timeline-id定義為userId_deviceId,sequenceId自定義為消息位點(diǎn)。
存儲結(jié)構(gòu)如下:

每通過ACCS成功下行一條消息,則插入到接收用戶設(shè)備的TimeLine中,收到ACK后根據(jù)消息id更新消息到達(dá)狀態(tài)。
同時(shí)由于重試動作只發(fā)生在下行消息后較短的一段時(shí)間內(nèi),所以我們設(shè)置一個(gè)比較短的全局過期時(shí)間即可,避免數(shù)據(jù)膨脹。
5.3 延遲重試設(shè)計(jì)

如上圖所示:
1)每通過ACCS下發(fā)一條消息,先插入到Timeline中,初始狀態(tài)為未達(dá),然后生產(chǎn)一條延遲N秒的延遲消息;
2)每次消費(fèi)到延遲消息后,讀取tablestore中該消息的到達(dá)狀態(tài),如到達(dá)則終止延遲,否則繼續(xù);
3)每次重試先判斷設(shè)備是否在線,如果設(shè)備不在線,轉(zhuǎn)發(fā)離線通道并終止重試,如果設(shè)備在線,則重推未到達(dá)的消息,并再次延遲N秒消費(fèi);
4)每條消息的重試生命周期中用的同一條延遲消息,最多重試消費(fèi)M次,超過次數(shù)不再重試并打日志埋點(diǎn)(后續(xù)可以監(jiān)控這種情況并基于這個(gè)數(shù)據(jù)進(jìn)行優(yōu)化)。
5.4 延遲重發(fā)策略
延遲重發(fā)策略是指在重發(fā)流程中,如何選擇合適的延遲時(shí)間來使得重發(fā)的效率最高。
不同用戶在不同時(shí)間、地點(diǎn)所處的網(wǎng)絡(luò)環(huán)境差別較大,網(wǎng)絡(luò)恢復(fù)到穩(wěn)定態(tài)所需要的時(shí)間也有差異,需要選用合適的延遲策略來保證重發(fā)效率。
最優(yōu)的延遲策略的目標(biāo)是在最短的時(shí)間內(nèi),使用最少的重發(fā)次數(shù)將消息投遞成功。以下是幾種可選的方案。
5.4.1)固定延遲時(shí)間:
要想找到最優(yōu)的延遲策略,必須從數(shù)據(jù)中通過分析得到答案,天馬行空的想象往往離實(shí)際相差甚遠(yuǎn)。
我們先采用固定的延遲時(shí)間(10s)最大重試6次來分析一波數(shù)據(jù):

通過這組數(shù)據(jù)可以看到:有約85%的消息在40s內(nèi)重發(fā)可以投遞成功,還有12%的消息在達(dá)到最大重試次數(shù)后依舊沒有收到ACK。在4次重試之后,第5次成功只有2.03%,第6次只有0.92%,繼續(xù)重發(fā)的收益已經(jīng)變得很低。
6次以后還有部分消息沒有收到ACK,這部分消息如果用固定延遲時(shí)間策略,性價(jià)比很低,頻繁重發(fā)浪費(fèi)系統(tǒng)資源,我們需要繼續(xù)改進(jìn)策略。
5.4.2)固定延遲+固定步長遞增:
考慮到部分用戶的網(wǎng)絡(luò)短時(shí)間無法恢復(fù),頻繁的短間隔重發(fā)價(jià)值不大,我們采用4次固定短間隔延遲N秒后,每次延遲時(shí)間都是上一次延遲時(shí)間遞增固定步長M秒的策略。直到收到ACK、用戶設(shè)備離線或者達(dá)到了最大延遲時(shí)間MAX(N)。
這種策略一定程度上可以解決固定延遲時(shí)間重發(fā)策略的問題,但如果用戶短時(shí)間網(wǎng)絡(luò)無法恢復(fù),每次重發(fā)都要重新遞增,也不是一種最優(yōu)解。
5.4.3)自適應(yīng)延遲:
設(shè)計(jì)流程圖:

如上圖:我們最終衍生出了自適應(yīng)延遲策略。
自適應(yīng)延遲是指:根據(jù)用戶的網(wǎng)絡(luò)狀況,采取自動調(diào)整的延遲時(shí)間,以期望達(dá)到最高的重發(fā)效率。
具體是:新消息先通過4次固定N秒的短延遲來探測設(shè)備的網(wǎng)絡(luò)狀況,一旦網(wǎng)絡(luò)恢復(fù),我們將設(shè)備的N值清空(設(shè)備N值是指根據(jù)上幾次重發(fā)經(jīng)驗(yàn),當(dāng)前設(shè)備網(wǎng)絡(luò)能回復(fù)ACK所需要的最短時(shí)間,默認(rèn)情況該值為空,代表用戶設(shè)備網(wǎng)絡(luò)正常)。4次重發(fā)后依舊收不到ACK,我們嘗試讀取設(shè)備N值,如果為空,則取初始值,以后每次延遲都遞增固定步長M,并在重發(fā)后更新當(dāng)前設(shè)備的N值,直到消息收到ACK或者達(dá)到了最大延遲時(shí)間MAX(N)。
5.5 新老版本兼容性
需要注意的是老版本的APP是不會回ACK的,如果下發(fā)給老版本設(shè)備的消息也加入重試隊(duì)列,那此類消息將一直重試到最大次數(shù)才會終止,無端消耗資源。
所以我們設(shè)計(jì)在ACCS長連建立之后,客戶端主動上行一條設(shè)備信息,其中包含APP的版本號,服務(wù)端存儲一定時(shí)間,在將消息加入重試隊(duì)列之前,先校驗(yàn)接收者設(shè)備APP的版本號,符合要求再加入重試隊(duì)列。
6、 最終優(yōu)化后的效果
消息重連重發(fā)方案上線后,我們上面定義的指標(biāo) ACCS補(bǔ)償?shù)竭_(dá)時(shí)間 從60分鐘大幅降低至15分鐘,降幅達(dá)75%。
從而印證了我們的技術(shù)分析,同時(shí)用戶有關(guān)消息延遲的輿情反饋大幅下降,可見消息重發(fā)機(jī)制對保證用戶消息及時(shí)到達(dá)成效顯著。
7、未來展望
消息在線通道的穩(wěn)定性優(yōu)化至此已告一段落,未來我們將繼續(xù)優(yōu)化閑魚消息的使用體驗(yàn),包括基礎(chǔ)功能的完善以及基礎(chǔ)體驗(yàn)的提升。
基礎(chǔ)功能方面:我們在近期的版本中已經(jīng)支持了消息撤回、草稿功能,后續(xù)將逐步支持發(fā)送定位,會話分組、備注,消息搜索等功能。
基礎(chǔ)體驗(yàn)方面:我們對消息的UI樣式做了優(yōu)化升級,并優(yōu)化了APP消息tab頁的cpu及內(nèi)存使用,后續(xù)將繼續(xù)從流量、電量、性能方面繼續(xù)優(yōu)化消息的使用體驗(yàn)。
附錄:參考資料
[1]?為何基于TCP協(xié)議的移動端IM仍然需要心跳?;顧C(jī)制?
[2]?不為人知的網(wǎng)絡(luò)編程(十二):徹底搞懂TCP協(xié)議層的KeepAlive?;顧C(jī)制
[3]?現(xiàn)代IM系統(tǒng)中聊天消息的同步和存儲方案探討
[4]?現(xiàn)代移動端網(wǎng)絡(luò)短連接的優(yōu)化手段總結(jié):請求速度、弱網(wǎng)適應(yīng)、安全保障
[5]?移動端IM開發(fā)者必讀(二):史上最全移動弱網(wǎng)絡(luò)優(yōu)化方法總結(jié)
[6]?IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門(十二):上網(wǎng)卡頓?網(wǎng)絡(luò)掉線?一文即懂!
[7]?IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門(十三):為什么手機(jī)信號差?一文即懂!
[8]?移動端IM實(shí)踐:實(shí)現(xiàn)Android版微信的智能心跳機(jī)制
[9]?融云技術(shù)分享:融云安卓端IM產(chǎn)品的網(wǎng)絡(luò)鏈路?;罴夹g(shù)實(shí)踐
[10]?Web端即時(shí)通訊實(shí)踐干貨:如何讓你的WebSocket斷網(wǎng)重連更快速?
本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號。
同步發(fā)布鏈接是:http://www.52im.net/thread-3726-1-1.html