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