從根上理解高性能、高并發(fā)(三):深入操作系統(tǒng),徹底理解I/O多路復(fù)用
Posted on 2021-01-05 15:06 Jack Jiang 閱讀(225) 評(píng)論(0) 編輯 收藏本文原題“終于明白了,一文徹底理解I/O多路復(fù)用”,轉(zhuǎn)載請(qǐng)聯(lián)系作者。
1、系列文章引言
1.1 文章目的
作為即時(shí)通訊技術(shù)的開發(fā)者來說,高性能、高并發(fā)相關(guān)的技術(shù)概念早就了然與胸,什么線程池、零拷貝、多路復(fù)用、事件驅(qū)動(dòng)、epoll等等名詞信手拈來,又或許你對(duì)具有這些技術(shù)特征的技術(shù)框架比如:Java的Netty、Php的workman、Go的nget等熟練掌握。但真正到了面視或者技術(shù)實(shí)踐過程中遇到無法釋懷的疑惑時(shí),方知自已所掌握的不過是皮毛。
返璞歸真、回歸本質(zhì),這些技術(shù)特征背后的底層原理到底是什么?如何能通俗易懂、毫不費(fèi)力真正透徹理解這些技術(shù)背后的原理,正是《從根上理解高性能、高并發(fā)》系列文章所要分享的。
1.2 文章源起
我整理了相當(dāng)多有關(guān)IM、消息推送等即時(shí)通訊技術(shù)相關(guān)的資源和文章,從最開始的開源IM框架MobileIMSDK,到網(wǎng)絡(luò)編程經(jīng)典巨著《TCP/IP詳解》的在線版本,再到IM開發(fā)綱領(lǐng)性文章《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》,以及網(wǎng)絡(luò)編程由淺到深的《網(wǎng)絡(luò)編程懶人入門》、《腦殘式網(wǎng)絡(luò)編程入門》、《高性能網(wǎng)絡(luò)編程》、《不為人知的網(wǎng)絡(luò)編程》系列文章。
越往知識(shí)的深處走,越覺得對(duì)即時(shí)通訊技術(shù)了解的太少。于是后來,為了讓開發(fā)者門更好地從基礎(chǔ)電信技術(shù)的角度理解網(wǎng)絡(luò)(尤其移動(dòng)網(wǎng)絡(luò))特性,我跨專業(yè)收集整理了《IM開發(fā)者的零基礎(chǔ)通信技術(shù)入門》系列高階文章。這系列文章已然是普通即時(shí)通訊開發(fā)者的網(wǎng)絡(luò)通信技術(shù)知識(shí)邊界,加上之前這些網(wǎng)絡(luò)編程資料,解決網(wǎng)絡(luò)通信方面的知識(shí)盲點(diǎn)基本夠用了。
對(duì)于即時(shí)通訊IM這種系統(tǒng)的開發(fā)來說,網(wǎng)絡(luò)通信知識(shí)確實(shí)非常重要,但回歸到技術(shù)本質(zhì),實(shí)現(xiàn)網(wǎng)絡(luò)通信本身的這些技術(shù)特征:包括上面提到的線程池、零拷貝、多路復(fù)用、事件驅(qū)動(dòng)等等,它們的本質(zhì)是什么?底層原理又是怎樣?這就是整理本系列文章的目的,希望對(duì)你有用。
1.3 文章目錄
《從根上理解高性能、高并發(fā)(一):深入計(jì)算機(jī)底層,理解線程與線程池》
《從根上理解高性能、高并發(fā)(二):深入操作系統(tǒng),理解I/O與零拷貝技術(shù)》
《從根上理解高性能、高并發(fā)(三):深入操作系統(tǒng),徹底理解I/O多路復(fù)用》(* 本文)
《從根上理解高性能、高并發(fā)(四):深入操作系統(tǒng),徹底理解同步與異步 (稍后發(fā)布..)》
《從根上理解高性能、高并發(fā)(五):高并發(fā)高性能服務(wù)器到底是如何實(shí)現(xiàn)的 (稍后發(fā)布..)》
1.4 本篇概述
接上篇《深入操作系統(tǒng),理解I/O與零拷貝技術(shù)》,本篇是高性能、高并發(fā)系列的第3篇文章,上篇里我們講到了I/O技術(shù),本篇將以更具象的文件這個(gè)話題入手,帶你一步步理解高性能、高并發(fā)服務(wù)端編程時(shí)無法回避的I/O多路復(fù)用及相關(guān)技術(shù)。
本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號(hào),歡迎關(guān)注。公眾號(hào)上的鏈接是:點(diǎn)此進(jìn)入。
2、本文作者
應(yīng)作者要求,不提供真名,也不提供個(gè)人照片。
本文作者主要技術(shù)方向?yàn)榛ヂ?lián)網(wǎng)后端、高并發(fā)高性能服務(wù)器、檢索引擎技術(shù),網(wǎng)名是“碼農(nóng)的荒島求生”,公眾號(hào)“碼農(nóng)的荒島求生”。感謝作者的無私分享。
3、什么是文件?
在正式展開本文的內(nèi)容之前,我們需要先預(yù)習(xí)一下文件以及文件描述符的概念。
程序員使用I/O最終都逃不過文件這個(gè)概念。
在Linux世界中文件是一個(gè)很簡單的概念,作為程序員我們只需要將其理解為一個(gè)N byte的序列就可以了:
b1, b2, b3, b4, ....... bN
實(shí)際上所有的I/O設(shè)備都被抽象為了文件這個(gè)概念,一切皆文件(Everything is File),磁盤、網(wǎng)絡(luò)數(shù)據(jù)、終端,甚至進(jìn)程間通信工具管道pipe等都被當(dāng)做文件對(duì)待。

所有的I/O操作也都可以通過文件讀寫來實(shí)現(xiàn),這一非常優(yōu)雅的抽象可以讓程序員使用一套接口就能對(duì)所有外設(shè)I/O操作。
常用的I/O操作接口一般有以下幾類:
- 1)打開文件,open;
- 2)改變讀寫位置,seek;
- 3)文件讀寫,read、write;
- 4)關(guān)閉文件,close。
程序員通過這幾個(gè)接口幾乎可以實(shí)現(xiàn)所有I/O操作,這就是文件這個(gè)概念的強(qiáng)大之處。
4、什么是文件描述符?
在上一篇《深入操作系統(tǒng),理解I/O與零拷貝技術(shù)》中我們講到:要想進(jìn)行I/O讀操作,像磁盤數(shù)據(jù),我們需要指定一個(gè)buff用來裝入數(shù)據(jù)。
一般都是這樣寫的:
read(buff);
但是這里我們忽略了一個(gè)關(guān)鍵問題:那就是,雖然我們指定了往哪里寫數(shù)據(jù),但是我們?cè)搹哪睦镒x數(shù)據(jù)呢?
從上一節(jié)中我們知道,通過文件這個(gè)概念我們能實(shí)現(xiàn)幾乎所有I/O操作,因此這里少的一個(gè)主角就是文件。
那么我們一般都怎樣使用文件呢?
舉個(gè)例子:如果周末你去比較火的餐廳吃飯應(yīng)該會(huì)有體會(huì),一般周末人氣高的餐廳都會(huì)排隊(duì),然后服務(wù)員會(huì)給你一個(gè)排隊(duì)序號(hào),通過這個(gè)序號(hào)服務(wù)員就能找到你,這里的好處就是服務(wù)員無需記住你是誰、你的名字是什么、來自哪里、喜好是什么、是不是保護(hù)環(huán)境愛護(hù)小動(dòng)物等等,這里的關(guān)鍵點(diǎn)就是:服務(wù)員對(duì)你一無所知,但依然可以通過一個(gè)號(hào)碼就能找到你。
同樣的:在Linux世界要想使用文件,我們也需要借助一個(gè)號(hào)碼,根據(jù)“弄不懂原則”,這個(gè)號(hào)碼就被稱為了文件描述符(file descriptors),在Linux世界中鼎鼎大名,其道理和上面那個(gè)排隊(duì)號(hào)碼一樣。
因此:文件描述僅僅就是一個(gè)數(shù)字而已,但是通過這個(gè)數(shù)字我們可以操作一個(gè)打開的文件,這一點(diǎn)要記住。

有了文件描述符,進(jìn)程可以對(duì)文件一無所知,比如文件在磁盤的什么位置、加載到內(nèi)存中又是怎樣管理的等等,這些信息統(tǒng)統(tǒng)交由操作系統(tǒng)打理,進(jìn)程無需關(guān)心,操作系統(tǒng)只需要給進(jìn)程一個(gè)文件描述符就足夠了。
因此我們來完善上述程序:
int fd = open(file_name); // 獲取文件描述符
read(fd, buff);
怎么樣,是不是非常簡單。
5、文件描述符太多了怎么辦?
經(jīng)過了這么多的鋪墊,終于要到高性能、高并發(fā)這一主題了。
從前幾節(jié)我們知道,所有I/O操作都可以通過文件樣的概念來進(jìn)行,這當(dāng)然包括網(wǎng)絡(luò)通信。
如果你有一個(gè)IM服務(wù)器,當(dāng)三次握手建議長連接成功以后,我們會(huì)調(diào)用accept來獲取一個(gè)鏈接,調(diào)用該函數(shù)我們同樣會(huì)得到一個(gè)文件描述符,通過這個(gè)文件描述符就可以處理客戶端發(fā)送的聊天消息并且把消息轉(zhuǎn)發(fā)給接收者。
也就是說,通過這個(gè)描述符我們就可以和客戶端進(jìn)行通信了:
// 通過accept獲取客戶端的文件描述符
int conn_fd = accept(...);
Server端的處理邏輯通常是接收客戶端消息數(shù)據(jù),然后執(zhí)行轉(zhuǎn)發(fā)(給接收者)邏輯:
if(read(conn_fd, msg_buff) > 0) {
do_transfer(msg_buff);
}
是不是非常簡單,然而世界終歸是復(fù)雜的,當(dāng)然也不是這么簡單的。
接下來就是比較復(fù)雜的了。

既然我們的主題是高并發(fā),那么Server端就不可能只和一個(gè)客戶端通信,而是可能會(huì)同時(shí)和成千上萬個(gè)客戶端進(jìn)行通信。這時(shí)你需要處理不再是一個(gè)描述符這么簡單,而是有可能要處理成千上萬個(gè)描述符。
為了不讓問題一上來就過于復(fù)雜,我們先簡單化,假設(shè)只同時(shí)處理兩個(gè)客戶端的請(qǐng)求。
有的同學(xué)可能會(huì)說,這還不簡單,這樣寫不就行了:
if(read(socket_fd1, buff) > 0) { // 處理第一個(gè)
do_transfer();
}
if(read(socket_fd2, buff) > 0) { // 處理第二個(gè)
do_transfer();
在上一篇《深入操作系統(tǒng),理解I/O與零拷貝技術(shù)》中我們討論過,這是非常典型的阻塞式I/O,如果此時(shí)沒有數(shù)據(jù)可讀那么進(jìn)程會(huì)被阻塞而暫停運(yùn)行。這時(shí)我們就無法處理第二個(gè)請(qǐng)求了,即使第二個(gè)請(qǐng)求的數(shù)據(jù)已經(jīng)就位,這也就意味著處理某一個(gè)客戶端時(shí)由于進(jìn)程被阻塞導(dǎo)致剩下的所有其它客戶端必須等待,在同時(shí)處理幾萬客戶端的server上。這顯然是不能容忍的。
聰明的你一定會(huì)想到使用多線程:為每個(gè)客戶端請(qǐng)求開啟一個(gè)線程,這樣一個(gè)客戶端被阻塞就不會(huì)影響到處理其它客戶端的線程了。注意:既然是高并發(fā),那么我們要為成千上萬個(gè)請(qǐng)求開啟成千上萬個(gè)線程嗎,大量創(chuàng)建銷毀線程會(huì)嚴(yán)重影響系統(tǒng)性能。
那么這個(gè)問題該怎么解決呢?
這里的關(guān)鍵點(diǎn)在于:我們事先并不知道一個(gè)文件描述對(duì)應(yīng)的I/O設(shè)備是否是可讀的、是否是可寫的,在外設(shè)的不可讀或不可寫的狀態(tài)下進(jìn)行I/O只會(huì)導(dǎo)致進(jìn)程阻塞被暫停運(yùn)行。
因此要優(yōu)雅的解決這個(gè)問題,就要從其它角度來思考這個(gè)問題了。

6、“不要打電話給我,有需要我會(huì)打給你”
大家生活中肯定會(huì)接到過推銷電話,而且不止一個(gè),一天下來接上十個(gè)八個(gè)推銷電話你的身體會(huì)被掏空的。
這個(gè)場(chǎng)景的關(guān)鍵點(diǎn)在于:打電話的人并不知道你是不是要買東西,只能來一遍遍問你。因此一種更好的策略是不要讓他們打電話給你,記下他們的電話,有需要的話打給他們,這樣推銷員就不會(huì)一遍一遍的來煩你了(雖然現(xiàn)實(shí)生活中這并不可能)。
在這個(gè)例子中:你,就好比內(nèi)核,推銷者就好比應(yīng)用程序,電話號(hào)碼就好比文件描述符,和你用電話溝通就好比I/O。
現(xiàn)在你應(yīng)該明白了吧,處理多個(gè)文件描述符的更好方法其實(shí)就存在于推銷電話中。
因此相比上一節(jié)中:我們通過I/O接口主動(dòng)問內(nèi)核這些文件描述符對(duì)應(yīng)的外設(shè)是不是已經(jīng)就緒了,一種更好的方法是,我們把這些感興趣的文件描述符一股腦扔給內(nèi)核,并霸氣的告訴內(nèi)核:“我這里有1萬個(gè)文件描述符,你替我監(jiān)視著它們,有可以讀寫的文件描述符時(shí)你就告訴我,我好處理”。而不是弱弱的問內(nèi)核:“第一個(gè)文件描述可以讀寫了嗎?第二個(gè)文件描述符可以讀寫嗎?第三個(gè)文件描述符可以讀寫了嗎?。。。”
這樣:應(yīng)用程序就從“繁忙”的主動(dòng)變?yōu)榱饲彘e的被動(dòng),反正文件描述可讀可寫了內(nèi)核會(huì)通知我,能偷懶我才不要那么勤奮。

這是一種更加高效的I/O處理機(jī)制,現(xiàn)在我們可以一次處理多路I/O了,為這種機(jī)制起一個(gè)名字吧,就叫I/O多路復(fù)用吧,這就是 I/O multiplexing。
7、I/O多路復(fù)用(I/O multiplexing)
multiplexing一詞其實(shí)多用于通信領(lǐng)域,為了充分利用通信線路,希望在一個(gè)信道中傳輸多路信號(hào),要想在一個(gè)信道中傳輸多路信號(hào)就需要把這多路信號(hào)結(jié)合為一路,將多路信號(hào)組合成一個(gè)信號(hào)的設(shè)備被稱為Multiplexer(多路復(fù)用器),顯然接收方接收到這一路組合后的信號(hào)后要恢復(fù)原先的多路信號(hào),這個(gè)設(shè)備被稱為Demultiplexer(多路分用器)。
如下圖所示:

回到我們的主題。
所謂I/O多路復(fù)用指的是這樣一個(gè)過程:
- 1)我們拿到了一堆文件描述符(不管是網(wǎng)絡(luò)相關(guān)的、還是磁盤文件相關(guān)等等,任何文件描述符都可以);
- 2)通過調(diào)用某個(gè)函數(shù)告訴內(nèi)核:“這個(gè)函數(shù)你先不要返回,你替我監(jiān)視著這些描述符,當(dāng)這堆文件描述符中有可以進(jìn)行I/O讀寫操作的時(shí)候你再返回”;
- 3)當(dāng)調(diào)用的這個(gè)函數(shù)返回后我們就能知道哪些文件描述符可以進(jìn)行I/O操作了。
也就是說通過I/O多路復(fù)用我們可以同時(shí)處理多路I/O。那么有哪些函數(shù)可以用來進(jìn)行I/O多路復(fù)用呢?
以Linux為例,有這樣三種機(jī)制可以用來進(jìn)行I/O多路復(fù)用:
- 1)select;
- 2)poll;
- 3)epoll。
接下來我們就來介紹一下牛掰的I/O多路復(fù)用三劍客。

8、I/O多路復(fù)用三劍客
本質(zhì)上:Linux上的select、poll、epoll都是阻塞式I/O,也就是我們常說的同步I/O。
原因在于:調(diào)用這些I/O多路復(fù)用函數(shù)時(shí)如果任何一個(gè)需要監(jiān)視的文件描述符都不可讀或者可寫那么進(jìn)程會(huì)被阻塞暫停執(zhí)行,直到有文件描述符可讀或者可寫才繼續(xù)運(yùn)行。
8.1 select:初出茅廬
在select這種I/O多路復(fù)用機(jī)制下,我們需要把想監(jiān)控的文件描述集合通過函數(shù)參數(shù)的形式告訴select,然后select會(huì)將這些文件描述符集合拷貝到內(nèi)核中。
我們知道數(shù)據(jù)拷貝是有性能損耗的,因此為了減少這種數(shù)據(jù)拷貝帶來的性能損耗,Linux內(nèi)核對(duì)集合的大小做了限制,并規(guī)定用戶監(jiān)控的文件描述集合不能超過1024個(gè),同時(shí)當(dāng)select返回后我們僅僅能知道有些文件描述符可以讀寫了,但是我們不知道是哪一個(gè)。因此程序員必須再遍歷一邊找到具體是哪個(gè)文件描述符可以讀寫了。
因此,總結(jié)下來select有這樣幾個(gè)特點(diǎn):
- 1)我能照看的文件描述符數(shù)量有限,不能超過1024個(gè);
- 2)用戶給我的文件描述符需要拷貝的內(nèi)核中;
- 3)我只能告訴你有文件描述符滿足要求了,但是我不知道是哪個(gè),你自己一個(gè)一個(gè)去找吧(遍歷)。
因此我們可以看到,select機(jī)制的這些特性在高并發(fā)網(wǎng)絡(luò)服務(wù)器動(dòng)輒幾萬幾十萬并發(fā)鏈接的場(chǎng)景下無疑是低效的。
8.2 poll:小有所成
poll和select是非常相似的。
poll相對(duì)于select的優(yōu)化僅僅在于解決了文件描述符不能超過1024個(gè)的限制,select和poll都會(huì)隨著監(jiān)控的文件描述數(shù)量增加而性能下降,因此不適合高并發(fā)場(chǎng)景。
8.3 epoll:獨(dú)步天下
在select面臨的三個(gè)問題中,文件描述數(shù)量限制已經(jīng)在poll中解決了,剩下的兩個(gè)問題呢?
針對(duì)拷貝問題:epoll使用的策略是各個(gè)擊破與共享內(nèi)存。
實(shí)際上:文件描述符集合的變化頻率比較低,select和poll頻繁的拷貝整個(gè)集合,內(nèi)核都快被煩死了,epoll通過引入epoll_ctl很體貼的做到了只操作那些有變化的文件描述符。同時(shí)epoll和內(nèi)核還成為了好朋友,共享了同一塊內(nèi)存,這塊內(nèi)存中保存的就是那些已經(jīng)可讀或者可寫的的文件描述符集合,這樣就減少了內(nèi)核和程序的拷貝開銷。
針對(duì)需要遍歷文件描述符才能知道哪個(gè)可讀可寫這一問題,epoll使用的策略是“當(dāng)小弟”。
在select和poll機(jī)制下:進(jìn)程要親自下場(chǎng)去各個(gè)文件描述符上等待,任何一個(gè)文件描述可讀或者可寫就喚醒進(jìn)程,但是進(jìn)程被喚醒后也是一臉懵逼并不知道到底是哪個(gè)文件描述符可讀或可寫,還要再從頭到尾檢查一遍。
但epoll就懂事多了,主動(dòng)找到進(jìn)程要當(dāng)小弟替大哥出頭。
在這種機(jī)制下:進(jìn)程不需要親自下場(chǎng)了,進(jìn)程只要等待在epoll上,epoll代替進(jìn)程去各個(gè)文件描述符上等待,當(dāng)哪個(gè)文件描述符可讀或者可寫的時(shí)候就告訴epoll,epoll用小本本認(rèn)真記錄下來然后喚醒大哥:“進(jìn)程大哥,快醒醒,你要處理的文件描述符我都記下來了”,這樣進(jìn)程被喚醒后就無需自己從頭到尾檢查一遍,因?yàn)閑poll小弟都已經(jīng)記下來了。
因此我們可以看到:在epoll這種機(jī)制下,實(shí)際上利用的就是“不要打電話給我,有需要我會(huì)打給你”這種策略,進(jìn)程不需要一遍一遍麻煩的問各個(gè)文件描述符,而是翻身做主人了——“你們這些文件描述符有哪個(gè)可讀或者可寫了主動(dòng)報(bào)上來”。
這種機(jī)制實(shí)際上就是大名鼎鼎的事件驅(qū)動(dòng)——Event-driven,這也是我們下一篇的主題。
實(shí)際上:在Linux平臺(tái),epoll基本上就是高并發(fā)的代名詞。

9、本文小結(jié)
基于一切皆文件的設(shè)計(jì)哲學(xué),I/O也可以通過文件的形式實(shí)現(xiàn),高并發(fā)場(chǎng)景下要與多個(gè)文件交互,這就離不開高效的I/O多路復(fù)用技術(shù)。
本文我們?cè)敿?xì)講解了什么是I/O多路復(fù)用以及使用方法,這其中以epoll為代表的I/O多路復(fù)用(基于事件驅(qū)動(dòng))技術(shù)使用非常廣泛,實(shí)際上你會(huì)發(fā)現(xiàn)但凡涉及到高并發(fā)、高性能的場(chǎng)景基本上都能見到事件驅(qū)動(dòng)的編程方法,當(dāng)然這也是下一篇我們要重點(diǎn)講解的主題《從根上理解高性能、高并發(fā)(四):深入操作系統(tǒng),徹底理解同步與異步》,敬請(qǐng)期待!
附錄:更多高性能、高并發(fā)文章精選
《高性能網(wǎng)絡(luò)編程(一):單臺(tái)服務(wù)器并發(fā)TCP連接數(shù)到底可以有多少》
《高性能網(wǎng)絡(luò)編程(二):上一個(gè)10年,著名的C10K并發(fā)連接問題》
《高性能網(wǎng)絡(luò)編程(三):下一個(gè)10年,是時(shí)候考慮C10M并發(fā)問題了》
《高性能網(wǎng)絡(luò)編程(四):從C10K到C10M高性能網(wǎng)絡(luò)應(yīng)用的理論探索》
《高性能網(wǎng)絡(luò)編程(五):一文讀懂高性能網(wǎng)絡(luò)編程中的I/O模型》
《高性能網(wǎng)絡(luò)編程(六):一文讀懂高性能網(wǎng)絡(luò)編程中的線程模型》
《高性能網(wǎng)絡(luò)編程(七):到底什么是高并發(fā)?一文即懂!》
《以網(wǎng)游服務(wù)端的網(wǎng)絡(luò)接入層設(shè)計(jì)為例,理解實(shí)時(shí)通信的技術(shù)挑戰(zhàn)》
《知乎技術(shù)分享:知乎千萬級(jí)并發(fā)的高性能長連接網(wǎng)關(guān)技術(shù)實(shí)踐》
《淘寶技術(shù)分享:手淘億級(jí)移動(dòng)端接入層網(wǎng)關(guān)的技術(shù)演進(jìn)之路》
《一套海量在線用戶的移動(dòng)端IM架構(gòu)設(shè)計(jì)實(shí)踐分享(含詳細(xì)圖文)》
《一套原創(chuàng)分布式即時(shí)通訊(IM)系統(tǒng)理論架構(gòu)方案》
《微信后臺(tái)基于時(shí)間序的海量數(shù)據(jù)冷熱分級(jí)架構(gòu)設(shè)計(jì)實(shí)踐》
《微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡(演講全文)》
《如何解讀《微信技術(shù)總監(jiān)談架構(gòu):微信之道——大道至簡》》
《快速裂變:見證微信強(qiáng)大后臺(tái)架構(gòu)從0到1的演進(jìn)歷程(一)》
《17年的實(shí)踐:騰訊海量產(chǎn)品的技術(shù)方法論》
《騰訊資深架構(gòu)師干貨總結(jié):一文讀懂大型分布式系統(tǒng)設(shè)計(jì)的方方面面》
《以微博類應(yīng)用場(chǎng)景為例,總結(jié)海量社交系統(tǒng)的架構(gòu)設(shè)計(jì)步驟》
本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號(hào)。
▲ 本文在公眾號(hào)上的鏈接是:點(diǎn)此進(jìn)入。同步發(fā)布鏈接是:http://www.52im.net/thread-3287-1-1.html
作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
出處:http://www.52im.net/space-uid-1.html
交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
討論:http://www.52im.net/
Jack Jiang同時(shí)是【原創(chuàng)Java
Swing外觀工程BeautyEye】和【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
本博文
歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。