前言
距離上一篇已經比較久的時間了,項目也是開了個頭。並且,由於網上的關於Spring Boot的websocket講解也比較多。於是我采用了另外的一個通訊框架 t-io 來實現LayIM中的通訊功能。本篇會着重介紹我在研究與開發過程中踩過的坑和比較花費的時間的部分。
WebSocket
在研究 t-io 的時候,我已經寫過關於t-io框架的一些簡單例子分析以及框架中關於 websocket 中的編解碼代碼分析等,有興趣的同學可以先看一下。因為 在LayIM項目中我會是用到 Showcase Demo 中的設計思路。
通訊框架 t-io 學習——給初學者的Demo:ShowCase設計分析
通訊框架 t-io 學習——websocket 部分源碼解析
如果你潛心想學到這些東西的話,本人還是建議靜下心來看看。為什么不用Spring Boot 封裝好的websocket呢?因為它封裝的太完備,許多業務不能定制。而通過t-io框架自己開發websocket端,就比較靈活了。甚至可以打造專門為LayIM定制的websocket服務,在講解我的開發之路之前,也向大家推薦更完備的解決方案 tio-im,當然,我也是借鑒該源代碼的設計思路。不過它的實現更加強大,由於我的水平有限,我只能照貓畫虎,胡亂寫了一通。不過也還是能用的。
tio-im 地址:https://gitee.com/xchao/tio-im
項目實戰
前幾篇已經實現了LayIM主要界面的數據加載功能。接下來就是最核心的部分,通訊。實現思路很多,這里呢我使用了 基於 t-io 通訊框架的 websocket。在進入詳細代碼之前,我們先分析LayIM中用到的一些功能點。
- 登錄功能
- 單聊功能
- 群聊功能
- 其他自定義消息提醒功能
- 等等。。。。
登錄的目的是過濾非法請求,如果有一個非法用戶請求websocket服務,直接返回403或者401即可。
單聊,群聊這個就不用解釋了
其他自定義消息提醒,比如:時時加好友消息,廣播消息,審核消息等。
t-io 中的對外發送消息接口在 Aio.java 中實現。(下文中只列取部分接口,以及在LayIM項目中用到的)
//綁定用戶 public static void bindUser(ChannelContext channelContext, String userid) //發送給用戶 public static Boolean sendToUser(GroupContext groupContext, String userid, Packet packet) //發送到群組 public static void sendToGroup(GroupContext groupContext, String group, Packet packet) //發送給所有人 public static void sendToAll(GroupContext groupContext, Packet packet) //發送到指定channel public static Boolean send(ChannelContext channelContext, Packet packet)
開工之前呢,我們還要開發消息的編解碼類(框架中已經實現),消息監聽事件的處理,由於對於LayIM我們有基於業務的定制開發,所以會改一部分源代碼。那我這里呢就把框架中部分源碼粘貼到項目中,然后進行代碼修改。不過像比如:握手流程,升級Websocket連接,解析byte[] 這些功能我們就沒必要自己去做了,想要學習的話,可以看着源代碼自己去研究。好,我們進入代碼部分。
代碼剖析
首先實現 IWsMsgHandler接口。這個接口定義在 org.tio.websocket.server.handler 包中,代碼如下。
public interface IWsMsgHandler { /** * 對httpResponse參數進行補充並返回,如果返回null表示不想和對方建立連接,框架會斷開連接,如果返回非null,框架會把這個對象發送給對方 */ public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception; /** * @return 可以是WsResponse、byte[]、ByteBuffer、String或null,如果是null,框架不會回消息 */ Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception; /** * @return 可以是WsResponse、byte[]、ByteBuffer、String或null,如果是null,框架不會回消息 */ Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception; /** * @return 可以是WsResponse、byte[]、ByteBuffer、String或null,如果是null,框架不會回消息 */ Object onText(WsRequest wsRequest, String text, ChannelContext channelContext) throws Exception; }
一般我們會在公開的這些接口實現中做些事情,比如
@Override public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception { logger.info("接收到text消息"); //消息業務處理邏輯 return "消息發送成功"; }
不過既然這次我們可以自己寫websocket內部的業務邏輯,所以,這些接口我們就不在處理主要業務邏輯。那么主要業務邏輯在哪里處理呢? 我把他放在了 decode 方法之后。可能,大伙看到這里有些暈,下面我畫一張圖來從大局上介紹一個消息的發送處理流程。這里我以單聊發送消息舉例。
首先是,客戶端連接服務器。先走握手流程。
if (!wsSessionContext.isHandshaked()) { HttpRequest request = HttpRequestDecoder.decode(buffer, channelContext); if (request == null) { return null; } //升級到websokcet協議 HttpResponse httpResponse = Protocol.updateToWebSocket(request, channelContext); if (httpResponse == null) { throw new AioDecodeException("http協議升級到websocket協議失敗"); } wsSessionContext.setHandshakeRequestPacket(request); wsSessionContext.setHandshakeResponsePacket(httpResponse); WsRequest wsRequestPacket = new WsRequest(); wsRequestPacket.setHandShake(true); return wsRequestPacket; }
WsSessionContext wsSessionContext = (WsSessionContext) channelContext.getAttribute(); HttpRequest request = wsSessionContext.getHandshakeRequestPacket(); HttpResponse httpResponse = wsSessionContext.getHandshakeResponsePacket();
//這里通過handshake接口實現的返回值,判斷是否同意握手 HttpResponse r = wsMsgHandler.handshake(request, httpResponse, channelContext); if (r == null) { Aio.remove(channelContext, "業務層不同意握手"); return; }
上文第二段代碼中的 wsMsgHandler.handshake 方法,這里一般直接返回默認的 httpReponse即可,代表(框架層)握手成功。但是我們可以在接口中自定義一些業務邏輯,比如用戶判斷之類的邏輯,然后決定是否同意握手流程。
這里有一個小細節需要注意,無論是握手還是業務登錄請求,成功之后,都需要將用戶綁定到當前的上下文(channelContext)中。調用 Aio.bindUser 即可。
下圖為簡版的聊天發送消息流程:客戶端A 發送消息到客戶端B。
正如上文中所說,編解碼我們不用過多的關心,那么我們需要關注的部分就是業務處理了。設計思路呢也很容易想到,首先,我們有不同的消息類型。這個消息類型由客戶端決定。如果傳入了錯誤的消息類型,就拋出異常或者返回未知消息處理即可。消息處理類結構設計如下:
是不是很簡單,一個通用業務處理入口,將消息轉化為友好的類實體,然后在具體的消息處理器中處理業務邏輯即可。
LayimAbsMsgProcessor 核心代碼如下:
/** * 這里采用showcase中的設計思路(反序列化消息之后,由具體的消息處理器處理) * */ @Override public WsResponse process(WsRequest layimPacket, ChannelContext channelContext) throws Exception { Class<T> clazz = getBodyClass(); T body = null; if (layimPacket.getBody() != null) { //獲取json格式的數據 String json = ByteUtil.toText(layimPacket.getBody()); //將字符串轉化為具體類型的對象 body = Json.toBean(json, clazz); } //通過具體處理類處理消息對象 return process(layimPacket, body, channelContext); } public abstract WsResponse process(WsRequest layimPacket,T body,ChannelContext channelContext) throws Exception;
ClientToClientMsgProcessor 核心代碼如下:
@Override public WsResponse process(WsRequest layimPacket, ChatRequestBody body, ChannelContext channelContext) throws Exception { //requestBody 轉化為接收端的消息類型 ClientToClientMsgBody msgBody = BodyConvert.getInstance().convertToMsgBody(body,channelContext); //消息包裝,返回WsResponse WsResponse response = BodyConvert.getInstance().convertToTextResponse(msgBody); //得到對方的channelContext ChannelContext toChannelContext = Aio.getChannelContextByUserid(channelContext.getGroupContext(),body.getToId()); //發送給對方 Aio.send(toChannelContext,response); return null; }
對接spring boot
那么如何啟動websocket服務呢,一般框架中都是綁定好的。這里呢,我們特殊處理一下,剛開始我是手動調用start方法,后來研究了一下spring boot starter。下面簡單介紹一下starter的用法。
首先建立一個配置類。
@ConfigurationProperties("layim.websocket") public class LayimServerProperties { public LayimServerProperties(){ port = 8081; heartBeatTimeout = 0; ip = null; } // getter setter private int port; private int heartBeatTimeout; private String ip; }
第二部,新建一個 AutoConfig類
@Configuration @EnableConfigurationProperties(LayimServerProperties.class) public class LayimWebsocketServerAutoConfig { @Autowired LayimServerProperties properties; @Bean LayimWebsocketStarter layimWebsocketStarter() throws Exception{ //初始化配置信息 LayimServerConfig config = new LayimServerConfig(properties.getPort()); config.setBindIp(properties.getIp()); config.setHeartBeatTimeout(properties.getHeartBeatTimeout()); LayimWebsocketStarter layimWebsocketStarter = new LayimWebsocketStarter(config); //啟動服務 layimWebsocketStarter.start(); //返回 return layimWebsocketStarter; } }
第三步,在resources文件夾下,新建META-INF文件夾,在新建一個spring.factories文件,文件內容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.fyp.layim.im.server.LayimWebsocketServerAutoConfig
OK,到這里我們配置一下。
然后啟動程序。
啟動成功!
項目演示
啰啰嗦嗦的講了這么多,還是給大家看一下演示。
用戶 1,2 鏈接服務器。
用戶2給用戶1發送消息:
看上面的只是演示消息能夠順利發送,下面的日志打印圖可以看出來服務器的處理流程。
總結
到此為止我們已經可以實現通訊了,但是這些還不夠還有更多的業務去處理。不過沒關系,通訊實現了,后邊的就不難了。其實更多的是細節的把握,比如用戶退群,用戶下線,統計用戶在線個數等。
下期預告:從零一起學Spring Boot之LayIM項目長成記(六)用戶登錄驗證和單聊群聊的實現