通訊框架 t-io 學習——給初學者的Demo:ShowCase設計分析

山有木兮,木有枝發表於2024-03-14

  前言

  最近閒暇時間研究Springboot,正好需要用到即時通訊部分了,雖然springboot 有websocket,但是我還是看中了 t-io框架。看了部分原始碼和示例,先把helloworld敲了一遍,又把showcase程式碼敲了一遍,決定做一個總結。本篇文章並不會解釋T-io是如何通訊的,而是從showcase這個給t-io初學者寫的demo分析showcase的設計思路,以及為什麼這麼設計等。不過都是我個人理解,疏漏之處在所難免。

T-io簡單介紹

  t-io 原始碼:https://gitee.com/tywo45/t-io/

  程式碼結構很簡單,首先我們知道通訊有客戶端(client)和服務端(server).它們之間又會存在一些重複的業務處理邏輯,於是就有common的存在。那麼整體的showcase下包含三部分:client,server,common。在程式碼分析之前呢,先簡單介紹一下關於使用tio實現通訊的基礎思路。我從tio官方截了兩個圖:

  server端,我們只看紅色部分。沒錯,要實現AioHandler中的encode,decode,handler方法。然後建立ServerGroupContext,最後呼叫start方法開啟服務端。

  client端,同樣也需要實現AioHandler中的encode,decode,handler方法。不過客戶端可以看到,多了一個心跳包(heartbeatPacket)。

通訊流程

  我們知道,最基本的通訊流程就是,客戶端傳送訊息到服務端,服務端處理之後,返回響應結果到客戶端。或者服務端主動推送訊息到客戶端。因為客戶端傳送的訊息格式不固定,所以t-io把編解碼的權利交給開發者,這樣可以自定義訊息結構。那麼Demo中由於採用的是同樣的編解碼方式。所以會有一個在common中的一個基礎實現類。當然,由於訊息型別的不同,具體的handler方法實現還是得區分不同的處理。

結構分析

  以下的圖都是根據我自己的理解畫的,錯誤之處歡迎指正。  

  首先,我們看一下介面,類關係圖:

  

  首先,AioHandler,ClientAioHandler,ServerAioHandler 都是t-io中的Hander介面。我們從ShowcaseAbsAioHander開始看。上文中說道編解碼屬於通用部分,於是ShowcaseAbsAioHander 實現了AioHandler 介面中的 encode,decode方法。就是說不管客戶端,服務端編碼,解碼方式都是同樣的。其中有一個基礎包 ShowcasePacket 。 它是貫穿整個通訊流程的。我們看一下程式碼:

public class ShowcasePacket extends Packet {
        
    private byte type;//訊息型別(用於訊息處理)

    private byte[] body;//訊息體
}

  其中,type訊息型別是對應在common中的Type介面,它定義了不同的訊息型別。

 1 public interface Type {
 2 
 3     /**
 4      * 登入訊息請求
 5      */
 6     byte LOGIN_REQ = 1;
 7     /**
 8      * 登入訊息響應
 9      */
10     byte LOGIN_RESP = 2;
11 
12     /**
13      * 進入群組訊息請求
14      */
15     byte JOIN_GROUP_REQ = 3;
16     /**
17      * 進入群組訊息響應
18      */
19     byte JOIN_GROUP_RESP = 4;
20 
21     /**
22      * 點對點訊息請求
23      */
24     byte P2P_REQ = 5;
25     /**
26      * 點對點訊息響應
27      */
28     byte P2P_RESP = 6;
29 
30     /**
31      * 群聊訊息請求
32      */
33     byte GROUP_MSG_REQ = 7;
34     /**
35      * 群聊訊息響應
36      */
37     byte GROUP_MSG_RESP = 8;
38 
39     /**
40      * 心跳
41      */
42     byte HEART_BEAT_REQ = 99;
43 
44 }

  我們繼續看上圖,ShowcaseClientAioHandler 和 ShowCaseServerAioHandler 這兩個類的實現差不多。都是做基礎訊息處理。並且根據訊息型別建立(獲取)不同的訊息處理器(handler)。實現程式碼如下:

    @Override
    public void handler(Packet packet, ChannelContext channelContext) throws Exception {
        //接收到的訊息包
        ShowcasePacket showcasePacket = (ShowcasePacket) packet;
        //獲取訊息型別
        Byte type = showcasePacket.getType();
        //從handleMap中獲取到具體的訊息處理器
        AbsShowcaseBsHandler<?> showcaseBsHandler = handlerMap.get(type);
     //服務端的處理可能由於type型別不正確拿不到相應的訊息處理器,直接return不給客戶端響應。(或者統一返回錯誤訊息)
//處理訊息 showcaseBsHandler.handler(showcasePacket, channelContext); return; }

  下面我們看一下,handler相關介面的設計。

  可以看到,訊息處理類使用了泛型。AbsShowcaseBsHandler<T> 實現了ShowcaseBsHandlerIntf 中的handler方法。並且定義了一個抽象方法 handler,其中多了 T bsBody 引數。可以知道,他對訊息的實現,就是將訊息字元轉換為具體的訊息物件,然後在呼叫具體的訊息處理器處理相應的訊息邏輯。程式碼如下:

  

public abstract class AbsShowcaseBsHandler<T extends BaseBody> implements ShowcaseBsHandlerIntf {
    private static Logger log = LoggerFactory.getLogger(AbsShowcaseBsHandler.class);

    /**
     *
     * @author tanyaowu
     */
    public AbsShowcaseBsHandler() {
    }
    //抽象方法,具體是什麼型別的由子類實現
    public abstract Class<T> bodyClass();

    @Override
    public Object handler(ShowcasePacket packet, ChannelContext channelContext) throws Exception {
        String jsonStr = null;
        T bsBody = null;
        if (packet.getBody() != null) {
            //將body轉化為string
            jsonStr = new String(packet.getBody(), Const.CHARSET);
            //根據型別反序列化訊息,得到具體型別的訊息物件
            bsBody = Json.toBean(jsonStr, bodyClass());
        }
        //呼叫具體的訊息處理的實現
        return handler(packet, bsBody, channelContext);
    }

    //抽象方法,由每個訊息處理類來實現具體的訊息處理邏輯
    public abstract Object handler(ShowcasePacket packet, T bsBody, ChannelContext channelContext) throws Exception;

}

  我們以登入訊息為例,分析具體訊息處理流程。

  首先客戶端發起登入請求。(比如使用者名稱:panzi,密碼:123123)

        LoginReqBody loginReqBody = new LoginReqBody();
            loginReqBody.setLoginname(loginname);
            loginReqBody.setPassword(password);
       //具體的訊息都會包裝在ShowcasePacket中(byte[] body)
            ShowcasePacket reqPacket = new ShowcasePacket();
       //這裡呢就是傳相應的訊息型別
            reqPacket.setType(Type.LOGIN_REQ);
reqPacket.setBody(Json.toJson(loginReqBody).getBytes(ShowcasePacket.CHARSET));        
//呼叫 t-io 傳送訊息方法 Aio.send(clientChannelContext, reqPacket);

  服務端收到訊息。這時候我們回過頭看 ShowcaseServerAioHandler 中的 handle方法。(上文中有介紹)此時訊息型別為Type.LOGIN_REQ.可以很容易的想到,需要用 LoginReqHandler來處理這條訊息。

  

  我們看一下LoginReqHandler的具體實現

  

    @Override
    public Object handler(ShowcasePacket packet, LoginReqBody bsBody, ChannelContext channelContext) throws Exception {
        log.info("收到登入請求訊息:{}", Json.toJson(bsBody));
        //定義響應物件
        LoginRespBody loginRespBody = new LoginRespBody();
        //模擬登入,直接給Success
        loginRespBody.setCode(JoinGroupRespBody.Code.SUCCESS);
        //返回一個模擬的token
        loginRespBody.setToken(newToken());
        
        //登入成功之後繫結使用者
        String userid = bsBody.getLoginname();
        Aio.bindUser(channelContext, userid);

        //給全域性Context設定使用者ID
        ShowcaseSessionContext showcaseSessionContext = (ShowcaseSessionContext) channelContext.getAttribute();
        showcaseSessionContext.setUserid(userid);

        //構造響應訊息包
        ShowcasePacket respPacket = new ShowcasePacket();
        //響應訊息型別為 Type.LOGIN_RESP
        respPacket.setType(Type.LOGIN_RESP);
        //將loginRespBody轉化為byte[]
        respPacket.setBody(Json.toJson(loginRespBody).getBytes(ShowcasePacket.CHARSET));
        //傳送響應到客戶端(告訴客戶端登入結果)
        Aio.send(channelContext, respPacket);

        return null;
    }

  這個時候就要到客戶端處理了。同理,客戶端處理拿到具體的處理器(LoginRespHandler)

  

  看一下客戶端訊息處理程式碼

  @Override
    public Object handler(ShowcasePacket packet, LoginRespBody bsBody, ChannelContext channelContext) throws Exception {
        System.out.println("收到登入響應訊息:" + Json.toJson(bsBody));
        if (LoginRespBody.Code.SUCCESS.equals(bsBody.getCode())) {
            ShowcaseSessionContext showcaseSessionContext = (ShowcaseSessionContext) channelContext.getAttribute();
            showcaseSessionContext.setToken(bsBody.getToken());
            System.out.println("登入成功,token是:" + bsBody.getToken());
        }
        return null;
    }

  這樣,整個訊息流程就結束了。為了更清晰一點,我們將它以流程圖的形式展現。

  

 


 總結

  雖然一個簡單的Showcase,但是作者也是用了心思。通過這個例子可以既讓我們學習到如何使用t-io,又能領略到程式設計的魅力,一個小小demo都這麼多東西,看來讀原始碼之路還是比較遙遠啊。以上是我對Showcase的程式碼理解,多有不當之處敬請指正。

  showcase地址:https://gitee.com/tywo45/t-io/tree/master/src/example/showcase

相關文章