前言
最近閒暇時間研究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