<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Jack Jiang

    我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
    posts - 494, comments - 13, trackbacks - 0, articles - 1


    0、引言

    站長提示:本文適合IM新手閱讀,但最好有一定的網絡編程經驗,必竟實踐性的代碼上手就是網絡編程。如果你對網絡編程,以及IM的一些理論知識知之甚少,請務必首先閱讀:新手入門一篇就夠:從零開發移動端IM,該文為IM小白分類整理了詳盡的理論資料,請按需補充相關知識。

    配套源碼:本文寫的比較淺顯但不太易懂,建議結合代碼一起來讀,文章配套的完整源碼 請從本文文末 “11、完整源碼下載” 處下載!

    學習交流:

    - 即時通訊/推送技術開發交流5群:215477170 [推薦]

    - 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

    (本文同步發布于:http://www.52im.net/thread-2768-1-1.html

    1、內容概述

    首先講講IM(即時通訊)技術可以用來做什么:

    1)聊天:qq、微信;

    2)直播:斗魚直播、抖音;

    3)實時位置共享、游戲多人互動等等。

    可以說幾乎所有高實時性的應用場景都需要用到IM技術。

    本篇將帶大家從零開始搭建一個輕量級的IM服務端。

    麻雀雖小,五臟俱全,我們搭建的IM服務端實現以下功能:

    1)一對一的文本消息、文件消息通信;

    2)每個消息有“已發送”/“已送達”/“已讀”回執;

    3)存儲離線消息;

    4)支持用戶登錄,好友關系等基本功能;

    5)能夠方便地水平擴展。

    通過這個項目能學到很多后端必備知識:

    1)rpc通信;

    2)數據庫;

    3)緩存;

    4)消息隊列;

    5)分布式、高并發的架構設計;

    6)docker部署。

    2、相關文章

    更多實踐性代碼參考:


    相關IM架構方面的文章:

    3、消息通信

    3.1 文本消息

    我們先從最簡單的特性開始實現:一個普通消息的發送。

    消息格式如下:

    message ChatMsg{

        id= 1;

        //消息id

        fromId = Alice

        //發送者userId

        destId = Bob

        //接收者userId

        msgBody = hello

        //消息體

    }

    如上圖,我們現在有兩個用戶:Alice和Bob連接到了服務器,當Alice發送消息message(hello)給Bob,服務端接收到消息,根據消息的destId進行轉發,轉發給Bob。

    3.2 發送回執

    那我們要怎么來實現回執的發送呢?

    我們定義一種回執數據格式ACK,MsgType有三種,分別是sent(已發送),delivered(已送達), read(已讀)。

    消息格式如下:

    message AckMsg {

        id;

        //消息id

        fromId;

        //發送者id

        destId;

        //接收者id

        msgType;

        //消息類型

        ackMsgId;

        //確認的消息id

    }


    enum MsgType {

        DELIVERED;

        READ;

    }

    當服務端接受到Alice發來的消息時:

    1)向Alice發送一個sent(hello)表示消息已經被發送到服務器:

    message AckMsg {

        id= 2;

        fromId = Alice;

        destId = Bob;

        msgType = SENT;

        ackMsgId = 1;

    }

    2)服務器把hello轉發給Bob后,立刻向Alice發送delivered(hello)表示消息已經發送給Bob:

    message AckMsg {

        id= 3;

        fromId = Bob;

        destId = Alice;

        msgType = DELIVERED;

        ackMsgId = 1;

    }

    3)Bob閱讀消息后,客戶端向服務器發送read(hello)表示消息已讀:

    message AckMsg {

        id= 4;

        fromId = Bob;

        destId = Alice;

        msgType = READ;

        ackMsgId = 1;

    }

    這個消息會像一個普通聊天消息一樣被服務器處理,最終發送給Alice。

    在服務器這里不區分ChatMsg和AckMsg,處理過程都是一樣的:解析消息的destId并進行轉發。

    4、水平擴展

    當用戶量越來越大,必然需要增加服務器的數量,用戶的連接被分散在不同的機器上。此時,就需要存儲用戶連接在哪臺機器上。

    我們引入一個新的模塊來管理用戶的連接信息。

    4.1 管理用戶狀態

    模塊叫做user status,共有三個接口:

    public interface UserStatusService {


        /**

         * 用戶上線,存儲userId與機器id的關系

         *

         * @param userId

         * @param connectorId

         * @return 如果當前用戶在線,則返回他連接的機器id,否則返回null

         */

        String online(String userId, String connectorId);


        /**

         * 用戶下線

         *

         * @param userId

         */

        voidoffline(String userId);


        /**

         * 通過用戶id查找他當前連接的機器id

         *

         * @param userId

         * @return

         */

        String getConnectorId(String userId);

    }

    這樣我們就能夠對用戶連接狀態進行管理了,具體的實現應考慮服務的用戶量、期望性能等進行實現。

    此處我們使用redis來實現,將userId和connectorId的關系以key-value的形式存儲。

    4.2 消息轉發

    除此之外,還需要一個模塊在不同的機器上轉發消息,如下結構:

    此時我們的服務被拆分成了connector和transfer兩個模塊,connector模塊用于維持用戶的長鏈接,而transfer的作用是將消息在多個connector之間轉發。

    現在Alice和Bob連接到了兩臺connector上,那么消息要如何傳遞呢?

    1)Alice上線,連接到機器[1]上時:

    1.1)將Alice和它的連接存入內存中。

    1.2)調用user status的online方法記錄Alice上線。

    2)Alice發送了一條消息給Bob:

    2.1)機器[1]收到消息后,解析destId,在內存中查找是否有Bob。

    2.2)如果沒有,代表Bob未連接到這臺機器,則轉發給transfer。

    3)transfer調用user status的getConnectorId(Bob)方法找到Bob所連接的connector,返回機器[2],則轉發給機器[2]。

    流程圖:

    4.3 總結

    引入user status模塊管理用戶連接,transfer模塊在不同的機器之間轉發,使服務可以水平擴展。為了滿足實時轉發,transfer需要和每臺connector機器都保持長鏈接。

    5、離線消息

    如果用戶當前不在線,就必須把消息持久化下來,等待用戶下次上線再推送,這里使用mysql存儲離線消息。

    為了方便地水平擴展,我們使用消息隊列進行解耦:

    1)transfer接收到消息后如果發現用戶不在線,就發送給消息隊列入庫;

    2)用戶登錄時,服務器從庫里拉取離線消息進行推送。

    6、用戶登錄、好友關系

    用戶的注冊登錄、賬戶管理、好友關系鏈等功能更適合使用http協議,因此我們將這個模塊做成一個restful服務,對外暴露http接口供客戶端調用。

    至此服務端的基本架構就完成了:

    7、中場休息 ... ...

    本文以上內容,本篇幫大家構建了IM服務端的架構,但還有很多細節需要我們去思考。

    例如:

    1)如何保證消息的順序和唯一

    2)多個設備在線如何保證消息一致性

    3)如何處理消息發送失敗

    4)消息的安全性

    5)如果要存儲聊天記錄要怎么做

    6)數據庫分表分庫

    7)服務高可用

    ……

    更多細節實現請繼續讀下半部分啦~

    8、可靠性

    什么是可靠性?對于一個IM系統來說,可靠的定義至少是不丟消息、消息不重復、不亂序,滿足這三點,才能說有一個好的聊天體驗。

    8.1 不丟消息

    我們先從不丟消息開始講起。

    首先復習一下上面章節中設計的服務端架構:

    我們先從一個簡單例子開始思考:當Alice給Bob發送一條消息時,可能要經過這樣一條鏈路:

    1)client-->connecter

    2)connector-->transfer

    3)transfer-->connector

    4)connector-->client

    在這整個鏈路中的每個環節都有可能出問題,雖然tcp協議是可靠的,但是它只能保證鏈路層的可靠,無法保證應用層的可靠。

    例如在第一步中,connector收到了從client發出的消息,但是轉發給transfer失敗,那么這條消息Bob就無法收到,而Alice也不會意識到消息發送失敗了。

    如果Bob狀態是離線,那么消息鏈路就是:

    1)client-->connector

    2)connector-->transfer

    3)transfer-->mq

    如果在第三步中,transfer收到了來自connector的消息,但是離線消息入庫失敗,那么這個消息也是傳遞失敗了。

    為了保證應用層的可靠,我們必須要有一個ack機制,使發送方能夠確認對方收到了這條消息。

    具體的實現,我們模仿tcp協議做一個應用層的ack機制。

    tcp的報文是以字節(byte)為單位的,而我們以message單位。

    發送方每次發送一個消息,就要等待對方的ack回應,在ack確認消息中應該帶有收到的id以便發送方識別。

    其次,發送方需要維護一個等待ack的隊列。 每次發送一個消息之后,就將消息和一個計時器入隊。

    另外存在一個線程一直輪詢隊列,如果有超時未收到ack的,就取出消息重發。

    超時未收到ack的消息有兩種處理方式:

    1)和tcp一樣不斷發送直到收到ack為止。

    2)設定一個最大重試次數,超過這個次數還沒收到ack,就使用失敗機制處理,節約資源。例如如果是connector長時間未收到client的ack,那么可以主動斷開和客戶端的連接,剩下未發送的消息就作為離線消息入庫,客戶端斷連后嘗試重連服務器即可。

    8.2 不重復、不亂序

    有的時候因為網絡原因可能導致ack收到較慢,發送方就會重復發送,那么接收方必須有一個去重機制。

    去重的方式是給每個消息增加一個唯一id。這個唯一id并不一定是全局的,只需要在一個會話中唯一即可。例如某兩個人的會話,或者某一個群。如果網絡斷連了,重新連接后,就是新的會話了,id會重新從0開始。

    關于消息ID的生成算法方面的文章,請詳細參考:

    融云技術分享:解密融云IM產品的聊天消息ID生成策略

    微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)

    微信技術分享:微信的海量IM聊天消息序列號生成實踐(容災方案篇)

    美團技術分享:深度解密美團的分布式ID生成算法

    接收方需要在當前會話中維護收到的最后一個消息的id,叫做lastId。

    每次收到一個新消息, 就將id與lastId作比較看是否連續,如果不連續,就放入一個暫存隊列 queue中稍后處理。

    例如:

    1)當前會話的lastId=1,接著服務器收到了消息msg(id=2),可以判斷收到的消息是連續的,就處理消息,將lastId修改為2;

    2)但是如果服務器收到消息msg(id=3),就說明消息亂序到達了,那么就將這個消息入隊,等待lastId變為2后,(即服務器收到消息msg(id=2)并處理完了),再取出這個消息處理。

    因此,判斷消息是否重復只需要判斷msgId>lastId && !queue.contains(msgId)即可。如果收到重復的消息,可以判斷是ack未送達,就再發送一次ack。

    接收方收到消息后完整的處理流程如下:

    偽代碼如下:

    class ProcessMsgNode{

        /**

         * 接收到的消息

         */

        privateMessage message;

        /**

         * 處理消息的方法

         */

        privateConsumer<Message> consumer;

    }


    public CompletableFuture<Void> offer(Long id,Message     message,Consumer<Message> consumer) {

        if(isRepeat(id)) {

        //消息重復

            sendAck(id);

            return null;

        }

        if(!isConsist(id)) {

        //消息不連續

            notConsistMsgMap.put(id, newProcessMsgNode(message, consumer));

            return null;

        }

        //處理消息

        returnprocess(id, message, consumer);

    }


    private CompletableFuture<Void> process(Long id, Message message, Consumer<Message> consumer) {

        return CompletableFuture

            .runAsync(() -> consumer.accept(message))

            .thenAccept(v -> sendAck(id))

            .thenAccept(v -> lastId.set(id))

            .thenComposeAsync(v -> {

                Long nextId = nextId(id);

                if(notConsistMsgMap.containsKey(nextId)) {

                    //隊列中有下個消息

                    ProcessMsgNode node = notConsistMsgMap.get(nextId);

                    returnprocess(nextId, node.getMessage(), consumer);

                } else{

                    //隊列中沒有下個消息

                    CompletableFuture<Void> future = newCompletableFuture<>();

                    future.complete(null);

                    returnfuture;

                }

            })

            .exceptionally(e -> {

                logger.error("[process received msg] has error", e);

                returnnull;

            });

    }

    9、安全性

    無論是聊天記錄還是離線消息,肯定都會在服務端存儲備份,那么消息的安全性,保護客戶的隱私也至關重要。

    因此所有的消息都必須要加密處理。

    在存儲模塊里,維護用戶信息和關系鏈有兩張基礎表,分別是im_user用戶表和im_relation關系鏈表。

    im_user表用于存放用戶常規信息,例如用戶名密碼等,結構比較簡單。

    im_relation表用于記錄好友關系。

    結構如下:

    CREATE TABLE `im_relation` (

      `id` bigint(20) COMMENT '關系id',

      `user_id1` varchar(100) COMMENT '用戶1id',

      `user_id2` varchar(100) COMMENT '用戶2id',

      `encrypt_key` char(33) COMMENT 'aes密鑰',

      `gmt_create` timestamp DEFAULT CURRENT_TIMESTAMP,

      `gmt_update` timestamp DEFAUL TCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

      PRIMARYKEY(`id`),

      UNIQUE KEY `USERID1_USERID2` (`user_id1`,`user_id2`)

    );

    1)user_id1和user_id2是互為好友的用戶id,為了避免重復,存儲時按照user_id1<user_id2的順序存,并且加上聯合索引;

    2)encrypt_key是隨機生成的密鑰。當客戶端登錄時,就會從數據庫中獲取該用戶的所有的relation,存在內存中,以便后續加密解密;

    3)當客戶端給某個好友發送消息時,取出內存中該關系的密鑰,加密后發送。同樣,當收到一條消息時,取出相應的密鑰解密。

    客戶端完整登錄流程如下:

    1)client調用rest接口登錄;

    2)client調用rest接口獲取該用戶所有relation;

    3)client向connector發送greet消息,通知上線;

    4)connector拉取離線消息推送給client;

    5)connector更新用戶session。

    那為什么connector要先推送離線消息再更新session呢?

    我們思考一下如果順序倒過來會發生什么:

    1)用戶Alice登錄服務器;

    2)connector更新session;

    3)推送離線消息;

    4)此時Bob發送了一條消息給Alice。

    如果離線消息還在推送的過程中,Bob發送了新消息給Alice,服務器獲取到Alice的session,就會立刻推送。這時新消息就有可能夾在一堆離線消息當中推過去了,那這時,Alice收到的消息就亂序了。

    而我們必須保證離線消息的順序在新消息之前。

    那么如果先推送離線消息,之后才更新session。在離線消息推送的過程中,Alice的狀態就是“未上線”,這時Bob新發送的消息只會入庫im_offline,im_offline表中的數據被讀完之后才會“上線”開始接受新消息。這也就避免了亂序。

    10、存儲設計

    10.1 存儲離線消息

    當用戶不在線時,離線消息必然要存儲在服務端,等待用戶上線再推送。理解了上一個小節后,離線消息的存儲就非常容易了。

    增加一張離線消息表im_offline,表結構如下:

    CREATE TABLE `im_offline` (

      `id` int(11) COMMENT '主鍵',

      `msg_id` bigint(20) COMMENT '消息id',

      `msg_type` int(2) COMMENT '消息類型',

      `content` varbinary(5000) COMMENT '消息內容',

      `to_user_id` varchar(100) COMMENT '收件人id',

      `has_read` tinyint(1) COMMENT '是否閱讀',

      `gmt_create` timestamp COMMENT '創建時間',

      PRIMARY KEY(`id`)

    );

    msg_type用于區分消息類型(chat,ack),content加密后的消息內容以byte數組的形式存儲。

    用戶上線時按照條件to_user_id=用戶id拉取記錄即可。

    10.2 防止離線消息重復推送

    我們思考一下多端登錄的情況,Alice有兩臺設備同時登陸,在這種并發的情況下,我們就需要某種機制來保證離線消息只被讀取一次。

    這里利用CAS機制來實現:

    1)首先取出所有has_read=false的字段;

    2)檢查每條消息的has_read值是否為false,如果是,則改為true。這是原子操作:

    1updateim_offline sethas_read = truewhereid = ${msg_id} andhas_read = false

    3)修改成功則推送,失敗則不推送。

    相信到這里,同學們已經可以自己動手搭建一個完整可用的IM服務端了。

    11、完整源碼下載

     從零開發一個IM服務端(完整源碼)(52im.net).zip (或自行從github下載:https://github.com/52im/IM

    附錄:更多IM開發文章

    [1] 更多IM代碼實踐(適合新手):
    自已開發IM有那么難嗎?手把手教你自擼一個Andriod版簡易IM (有源碼)
    一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)
    手把手教你用Netty實現網絡通信程序的心跳機制、斷線重連機制
    詳解Netty的安全性:原理介紹、代碼演示(上篇)
    詳解Netty的安全性:原理介紹、代碼演示(下篇)
    微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]
    Java NIO基礎視頻教程、MINA視頻教程、Netty快速入門視頻 [有源碼]
    輕量級即時通訊框架MobileIMSDK的iOS源碼(開源版)[附件下載]
    開源IM工程“蘑菇街TeamTalk”2015年5月前未刪減版完整代碼 [附件下載]
    微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]
    NIO框架入門(一):服務端基于Netty4的UDP雙向通信Demo演示 [附件下載]
    NIO框架入門(二):服務端基于MINA2的UDP雙向通信Demo演示 [附件下載]
    NIO框架入門(三):iOS與MINA2、Netty4的跨平臺UDP雙向通信實戰 [附件下載]
    NIO框架入門(四):Android與MINA2、Netty4的跨平臺UDP雙向通信實戰 [附件下載]
    用于IM中圖片壓縮的Android工具類源碼,效果可媲美微信 [附件下載]
    高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]
    一個WebSocket實時聊天室Demo:基于node.js+socket.io [附件下載]
    Android聊天界面源碼:實現了聊天氣泡、表情圖標(可翻頁) [附件下載]
    高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]
    開源libco庫:單機千萬連接、支撐微信8億用戶的后臺框架基石 [源碼下載]
    分享java AMR音頻文件合并源碼,全網最全
    微信團隊原創Android資源混淆工具:AndResGuard [有源碼]
    一個基于MQTT通信協議的完整Android推送Demo [附件下載]
    Android版高仿微信聊天界面源碼 [附件下載]
    高仿手機QQ的Android版鎖屏聊天消息提醒功能 [附件下載]
    高仿iOS版手機QQ錄音及振幅動畫完整實現 [源碼下載]
    Android端社交應用中的評論和回復功能實戰分享[圖文+源碼]
    Android端IM應用中的@人功能實現:仿微博、QQ、微信,零入侵、高可擴展[圖文+源碼]
    仿微信的IM聊天時間顯示格式(含iOS/Android/Web實現)[圖文+源碼]
    Android版仿微信朋友圈圖片拖拽返回效果 [源碼下載]
    適合新手:從零開發一個IM服務端(基于Netty,有完整源碼)
    >> 更多同類文章 …… 

    [2] IM群聊相關的技術文章:
    快速裂變:見證微信強大后臺架構從0到1的演進歷程(一)
    如何保證IM實時消息的“時序性”與“一致性”?
    IM單聊和群聊中的在線狀態同步應該用“推”還是“拉”?
    IM群聊消息如此復雜,如何保證不丟不重?
    微信后臺團隊:微信后臺異步消息隊列的優化升級實踐分享
    移動端IM中大規模群消息的推送如何保證效率、實時性?
    現代IM系統中聊天消息的同步和存儲方案探討
    關于IM即時通訊群聊消息的亂序問題討論
    IM群聊消息的已讀回執功能該怎么實現?
    IM群聊消息究竟是存1份(即擴散讀)還是存多份(即擴散寫)?
    一套高可用、易伸縮、高并發的IM群聊、單聊架構方案設計實踐
    [技術腦洞] 如果把14億中國人拉到一個微信群里技術上能實現嗎?
    IM群聊機制,除了循環去發消息還有什么方式?如何優化?
    網易云信技術分享:IM中的萬人群聊技術方案實踐總結
    >> 更多同類文章 ……

    [3] 有關IM架構設計的文章:
    淺談IM系統的架構設計
    簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端
    一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)
    一套原創分布式即時通訊(IM)系統理論架構方案
    從零到卓越:京東客服即時通訊系統的技術架構演進歷程
    蘑菇街即時通訊/IM服務器開發之架構選擇
    騰訊QQ1.4億在線用戶的技術挑戰和架構演進之路PPT
    微信后臺基于時間序的海量數據冷熱分級架構設計實踐
    微信技術總監談架構:微信之道——大道至簡(演講全文)
    如何解讀《微信技術總監談架構:微信之道——大道至簡》
    快速裂變:見證微信強大后臺架構從0到1的演進歷程(一)
    17年的實踐:騰訊海量產品的技術方法論
    移動端IM中大規模群消息的推送如何保證效率、實時性?
    現代IM系統中聊天消息的同步和存儲方案探討
    IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?
    IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議
    IM開發基礎知識補課(四):正確理解HTTP短連接中的Cookie、Session和Token
    WhatsApp技術實踐分享:32人工程團隊創造的技術神話
    微信朋友圈千億訪問量背后的技術挑戰和實踐總結
    王者榮耀2億用戶量的背后:產品定位、技術架構、網絡方案等
    IM系統的MQ消息中間件選型:Kafka還是RabbitMQ?
    騰訊資深架構師干貨總結:一文讀懂大型分布式系統設計的方方面面
    以微博類應用場景為例,總結海量社交系統的架構設計步驟
    快速理解高性能HTTP服務端的負載均衡技術原理
    子彈短信光鮮的背后:網易云信首席架構師分享億級IM平臺的技術實踐
    知乎技術分享:從單機到2000萬QPS并發的Redis高性能緩存實踐之路
    IM開發基礎知識補課(五):通俗易懂,正確理解并用好MQ消息隊列
    微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)
    微信技術分享:微信的海量IM聊天消息序列號生成實踐(容災方案篇)
    新手入門:零基礎理解大型分布式架構的演進歷史、技術原理、最佳實踐
    一套高可用、易伸縮、高并發的IM群聊、單聊架構方案設計實踐
    阿里技術分享:深度揭秘阿里數據庫技術方案的10年變遷史
    阿里技術分享:阿里自研金融級數據庫OceanBase的艱辛成長之路
    社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等
    社交軟件紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進
    社交軟件紅包技術解密(三):微信搖一搖紅包雨背后的技術細節
    社交軟件紅包技術解密(四):微信紅包系統是如何應對高并發的
    社交軟件紅包技術解密(五):微信紅包系統是如何實現高可用性的
    社交軟件紅包技術解密(六):微信紅包系統的存儲層架構演進實踐
    社交軟件紅包技術解密(七):支付寶紅包的海量高并發技術實踐
    社交軟件紅包技術解密(八):全面解密微博紅包技術方案
    社交軟件紅包技術解密(九):談談手Q紅包的功能邏輯、容災、運維、架構等
    即時通訊新手入門:一文讀懂什么是Nginx?它能否實現IM的負載均衡?
    即時通訊新手入門:快速理解RPC技術——基本概念、原理和用途
    多維度對比5款主流分布式MQ消息隊列,媽媽再也不擔心我的技術選型了
    從游擊隊到正規軍:馬蜂窩旅游網的IM系統架構演進之路
    IM開發基礎知識補課(六):數據庫用NoSQL還是SQL?讀這篇就夠了!
    >> 更多同類文章 ……

    (本文同步發布于:http://www.52im.net/thread-2768-1-1.html



    作者:Jack Jiang (點擊作者姓名進入Github)
    出處:http://www.52im.net/space-uid-1.html
    交流:歡迎加入即時通訊開發交流群 215891622
    討論:http://www.52im.net/
    Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
    本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
    主站蜘蛛池模板: 亚洲乱码一区二区三区在线观看 | 美女网站在线观看视频免费的| aa午夜免费剧场| 青柠影视在线观看免费高清| 久久A级毛片免费观看| 无人影院手机版在线观看免费| 韩国日本好看电影免费看| 亚洲福利精品电影在线观看| 亚洲永久精品ww47| 91久久亚洲国产成人精品性色| 久久亚洲精品国产亚洲老地址| 男人免费视频一区二区在线观看| 日本道免费精品一区二区| 在线看片无码永久免费视频| 免费一级毛片清高播放| 亚洲av日韩av高潮潮喷无码| 亚洲AV成人一区二区三区在线看 | 久久亚洲精品无码gv| aa级女人大片喷水视频免费| 免费成人激情视频| 亚洲精品99久久久久中文字幕 | 欧洲人成在线免费| 丁香花在线观看免费观看| 亚洲无码精品浪潮| 亚洲中文字幕在线无码一区二区| 免费一级毛片在线播放视频免费观看永久| 在线观看片免费人成视频播放| 日韩欧美一区二区三区免费观看| 亚洲高清免费视频| 亚洲人成在久久综合网站| 欧洲乱码伦视频免费国产| 最近免费中文在线视频| 亚洲成网777777国产精品| 亚洲欧洲日产专区| 一级黄色免费毛片| 在线观看特色大片免费视频| 亚洲精品成人片在线观看精品字幕| 亚洲欧美日韩久久精品| 美女视频黄a视频全免费网站色窝 美女被cao网站免费看在线看 | 国产高清不卡免费视频| 国产成人免费高清在线观看|