從零一起學Spring Boot之LayIM專案長成記(五)websocket

丶Pz發表於2017-11-20

前言

  距離上一篇已經比較久的時間了,專案也是開了個頭。並且,由於網上的關於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專案長成記(六)使用者登入驗證和單聊群聊的實現

  GitHub:https://github.com/fanpan26/SpringBootLayIM

相關文章