Spring Springboot實現websocket通訊-1

特別說明:1. 本文基於Springboot spring-boot-starter-parent 1.5.1.RELEASE編碼,在不同的版本中部分方法有區別。

第一篇地址:Spring Springboot實現websocket通訊-1

第二篇地址:Spring Springboot實現websocket通訊-2

在spring和spring boot中配置websocket的代碼基本一樣的,只是pom引入的包不一樣,需要注意的是不同的tomcat版本對websocket的支持可能有區別,造成了代碼的區別,這裡本文沒有特別深究,有興趣的朋友可以去看一下。

spring 中需要引入的包

<code>         
        
            org.springframework
            spring-websocket
            ${spring.version}
        
        
            org.springframework
            spring-messaging
            ${spring.version}
           /<code>

spring boot中引入的包

<code>         
        
            org.springframework.boot
            spring-boot-starter-websocket
        /<code>


配置websocket的方式

  • 配置websocket首先是需要運行的容器支持,這個是前提,我們常用的容器,tomcat,jetty,undertow都支持websocket,spring boot 對內嵌的tomcat(7,8),jetty9,undertow提供了支持,源碼在spring-websocket-4.3.6.RELEASE.jar包中。
  • websocket是通過一個socket來實現雙向異步通訊的,websocket屬於(sockJs:websocket協議的模擬,用作瀏覽器使用,增加了當瀏覽器不支持websocket的時候的兼容性支持,也屬於底層協議)。
  • 底層協議配置有兩種方式,使用javax.websocket包中的配置,屬於JavaEE 7中出了JSR-356:Java API for WebSocket規範。還有一種是使用spring websocket api中提供的底層協議使用@EnableWebSocket註解,實現org.springframework.web.socket.config.annotation.WebSocketConfigurer;。
  • 使用底層協議比較繁瑣,需要自己寫大量的代碼進行支持,不過更加靈活,當然我們也可以websocket的子協議STOMP來,它是一個更高級的協議,STOMP是基於幀(frame)格式來定義消息,與http的request和response類似(具有類似@RequestMapping的@MessageMapping),使用@EnableWebSocketMessageBroker 源碼也在org.springframework.web.socket下。

  • 配置websocket

    <code>package com.wzh.demo.domain;
    
    import javax.websocket.Session;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-08 18:49
     * @see [相關類/方法] (可選)
     **/
    public class WebSocketBean {
    
        /**
         * 連接session對象
         */
        private Session session;
    
        /**
         * 連接錯誤次數
         */
        private AtomicInteger erroerLinkCount = new AtomicInteger(0);
    
        public int getErroerLinkCount() {
            // 線程安全,以原子方式將當前值加1,注意:這裡返回的是自增前的值
            return erroerLinkCount.getAndIncrement();
        }
    
        public void cleanErrorNum()
        {
            // 清空計數
            erroerLinkCount = new AtomicInteger(0);
        }
    
        //...... 省略get set toSting方法
    }
    /<code>

    1.javax.websocket 擴展協議配置

    基於Spring搭建,一個公用的websocket配置,使用@ServerEndpoint創立websocket endpoint

    <code>@Configuration
    public class WebSocketConfig {
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
     
    }/<code>

    因為websocket的session和我們常用的httpsession不一樣,所有我們要轉換一下,部分場景會用到httpsession

    <code>package com.wzh.config.utils;
    
    import javax.servlet.http.HttpSession;
    import javax.websocket.HandshakeResponse;
    import javax.websocket.server.HandshakeRequest;
    import javax.websocket.server.ServerEndpointConfig;
    import javax.websocket.server.ServerEndpointConfig.Configurator;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-10 01:02
     * @see [相關類/方法] (可選)
     **/
    public class GetHttpSessionConfigurator extends Configurator{
    
        @Override
        public void modifyHandshake(ServerEndpointConfig sec,HandshakeRequest request, HandshakeResponse response) {
    
            HttpSession httpSession=(HttpSession) request.getHttpSession();
            sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
        }
    }
    /<code>

    websocket業務接口,抽一些共用的方法出來

    <code>package com.wzh.demo.websocket.service;
    
    import javax.websocket.EndpointConfig;
    import javax.websocket.Session;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-08 17:11
     * @see [相關類/方法] (可選)
     **/
    public interface WebSocketServer {
    
        /**
         * 連接建立成功調用的方法
         * @param session session 對象
         */
        public void onOpen(Session session,EndpointConfig config);
    
        /**
         * 斷開連接方法
         */
        public void onClose(Session session);
    
        /**
         * 收到客戶端消息後調用的方法
         * @param session session 對象
         * @param message 返回客戶端的消息
         */
        public void onMessage(Session session, String message);
    
        /**
         * 發生異常時觸發的方法
         * @param session session 對象
         * @param throwable 拋出的異常
         */
        public void onError(Session session,Throwable throwable);
    
        /**
         * 向單個客戶端發送消息
         * @param session session 對象
         * @param message 發送給客戶端的消息
         */
        public void sendMessage(Session session, String message);
    
        /**
         * 向所有在線用戶群發消息
         * @param message 發送給客戶端的消息
         */
        public void batchSendMessage(String message);
    }
    /<code>

    方法的實現類

    <code>package com.wzh.demo.websocket.service.impl;
    
    import com.wzh.config.utils.GetHttpSessionConfigurator;
    import com.wzh.demo.domain.WebSocketBean;
    import com.wzh.demo.websocket.service.WebSocketServer;
    import org.apache.log4j.Logger;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpSession;
    import javax.websocket.*;
    import javax.websocket.server.ServerEndpoint;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-08 17:11
     * @see [相關類/方法] (可選)
     **/
    @ServerEndpoint(value = "/javax/websocket",configurator=GetHttpSessionConfigurator.class)
    @Component("webSocketService")
    public class WebSocketServiceImpl implements WebSocketServer{
    
        private Logger log = Logger.getLogger(WebSocketServiceImpl.class);
    
        /**
         * 錯誤最大重試次數
         */
        private static final int MAX_ERROR_NUM = 10;
    
        /**
         * 用來存放每個客戶端對應的webSocket對象。
         */
        private static Map webSocketInfo;
    
        static
        {
            // concurrent包的線程安全map
            webSocketInfo = new ConcurrentHashMap();
        }
    
        @OnOpen
        @Override
        public void onOpen(Session session,EndpointConfig config) {
    
            // 如果是session沒有激活的情況,就是沒有請求獲取或session,這裡可能會取出空,需要實際業務處理
            HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
            if(httpSession != null)
            {
                log.info("獲取到httpsession" + httpSession.getId());
            }else {
                log.error("未獲取到httpsession");
            }
    
            // 連接成功當前對象放入websocket對象集合
            WebSocketBean bean = new WebSocketBean();
            bean.setSession(session);
            webSocketInfo.put(session.getId(),bean);
    
            log.info("客戶端連接服務器session id :"+session.getId()+",當前連接數:" + webSocketInfo.size());
        }
    
        @OnClose
        @Override
        public void onClose(Session session) {
    
            // 客戶端斷開連接移除websocket對象
            webSocketInfo.remove(session.getId());
            log.info("客戶端斷開連接,當前連接數:" + webSocketInfo.size());
    
        }
    
        @OnMessage
        @Override
        public void onMessage(Session session, String message) {
    
            log.info("客戶端 session id: "+session.getId()+",消息:" + message);
    
            // 此方法為客戶端給服務器發送消息後進行的處理,可以根據業務自己處理,這裡返回頁面
            sendMessage(session, "服務端返回" + message);
    
        }
    
        @OnError
        @Override
        public void onError(Session session, Throwable throwable) {
    
            log.error("發生錯誤"+ throwable.getMessage(),throwable);
        }
    
        @Override
        public void sendMessage(Session session, String message) {
    
            try
            {
                // 發送消息
                session.getBasicRemote().sendText(message);
    
                // 清空錯誤計數
                webSocketInfo.get(session.getId()).cleanErrorNum();
            }
            catch (Exception e)
            {
                log.error("發送消息失敗"+ e.getMessage(),e);
                int errorNum = webSocketInfo.get(session.getId()).getErroerLinkCount();
    
                // 小於最大重試次數重發
                if(errorNum <= MAX_ERROR_NUM)
                {
                    sendMessage(session, message);
                }
                else{
                    log.error("發送消息失敗超過最大次數");
                    // 清空錯誤計數
                    webSocketInfo.get(session.getId()).cleanErrorNum();
                }
            }
        }
    
        @Override
        public void batchSendMessage(String message) {
            Set> set = webSocketInfo.entrySet();
            for (Map.Entry map : set)
            {
                sendMessage(map.getValue().getSession(),message);
            }
        }
    }
    /<code>

    觸發websocket通訊推送的方式很多,這裡做個最簡單的按鈕,寫個簡單的controller和一個html

    <code>package com.wzh.demo.controller;
    
    
    
    import org.springframework.boot.autoconfigure.web.ServerProperties;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import javax.servlet.http.HttpSession;
    
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-09 22:53
     * @see [相關類/方法] (可選)
     **/
    @Controller
    @RequestMapping("/websocket")
    public class WebSocketController {
    
        @RequestMapping(value = "socket.do",method = RequestMethod.GET)
        public String toWebSocket(HttpSession session, Model model)
        {
            model.addAttribute("address","/javax/websocket");
            return "/test/webSocket";
        }
    }
    /<code>

    html,主要是socketjs觸發

    <code>
    
    
    
        Title
         
         
    
    
    
    
    
    /<code>

    測試一下,瀏覽器訪問controller
    瀏覽器控制檯輸出

    Spring Springboot實現websocket通訊-1

    IDEA控制檯

    Spring Springboot實現websocket通訊-1

    頁面點擊按鈕

    Spring Springboot實現websocket通訊-1

    Spring Springboot實現websocket通訊-1

    通過測試可以看到我們使用底層協議創建的websocket通訊就完成了,當然這只是最簡單的通訊,實際開發中還要保證心跳等其他因素。

    上面的例子是通過JAVA的擴展JAR實現的,既然是Spring項目,可定也能用框架提供的方法進行websocket通訊。

    Spring websocket api 地址:

    https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/socket/package-summary.html


    @EnableWebSocket配置

    在 spring 中 使用較低層級的 API 來處理消息,可以通過以下幾個步驟

    • 一個HandshakeInterceptor攔截器,實現org.springframework.web.socket.server.HandshakeInterceptor,在次攔截器中可以做以下握手前後的處理,此步驟可以省略,此攔截器可以在springMVC中的websocket配置類中註冊使用,做一下前置或者後置操作
    <code>@Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            registry.addHandler(new WebSocketHander(),"/xxxx").addInterceptors(new HandshakeInterceptor());
        }
    }/<code>
    • WebSocketHandler 一個消息處理中心,用於處理websocket的通訊具體服務,可以繼承AbstractWebSocketHandler,也可以實現WebSocketHandler
    <code>public interface WebSocketHandler {
    void afterConnectionEstablished(WebSocketSession session) throws Exception;
    void handleMessage(WebSocketSession session, WebSocketMessage> message) throws Exception;
    void handleTransportError(WebSocketSession session,
     Throwable exception) throws Exception; 
    void afterConnectionClosed(WebSocketSession session,
     CloseStatus closeStatus) throws Exception; 
    boolean supportsPartialMessages();
    }/<code>
    <code>public class ChatTextHandler extends AbstractWebSocketHandler {
     
     @Override
     protected void handleTextMessage(WebSocketSession session,
       TextMessage message) throws Exception {
      session.sendMessage(new TextMessage("xxxx"));
     }
    }/<code>
    • 一個SpringMVC的配置,其中registerWebSocketHandlers註冊消息處理器,此方法可以完成websocket路徑的註冊,消息處理器的註冊,攔截器的註冊
    <code>@Configuration
    @EnableWebSocket//開啟websocket
    public class WebSocketConfig implements WebSocketConfigurer {
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
            // WebSocketHander 為消息處理器,HandshakeInterceptor為攔截器
            registry.addHandler(new WebSocketHander(),"/xxx").addInterceptors(new HandshakeInterceptor());
        }
    }/<code>

    下面我們來開始正式配置基於Spring底層API的websocket通訊

    WebSocketHandler 攔截器

    在這一步可以做一些初始化操作,例如獲取httpSession,此步驟不是開啟websocket的必要步驟,根據自身的業務邏輯決定是否添加攔截器。攔截器我們可以直接使用HttpSessionHandshakeInterceptor這個Spring提供的攔截器,也可以實現HandshakeInterceptor 這個接口進行自定義。
    攔截器HttpSessionHandshakeInterceptor將HttpSession中的值保存到了一個Map裡面,在後期的WebSocketHandler消息處理類中可以獲取存入httpsession中的信息,通過WebSocketSession的getAttributes()下提供get方法獲取。

    HttpSessionHandshakeInterceptor源碼

    <code>//
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package org.springframework.web.socket.server.support;
    
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Enumeration;
    import java.util.Map;
    import javax.servlet.http.HttpSession;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.http.server.ServletServerHttpRequest;
    import org.springframework.web.socket.WebSocketHandler;
    import org.springframework.web.socket.server.HandshakeInterceptor;
    
    public class HttpSessionHandshakeInterceptor implements HandshakeInterceptor {
        public static final String HTTP_SESSION_ID_ATTR_NAME = "HTTP.SESSION.ID";
        private final Collection attributeNames;
        private boolean copyAllAttributes;
        private boolean copyHttpSessionId = true;
        private boolean createSession;
    
        public HttpSessionHandshakeInterceptor() {
            this.attributeNames = Collections.emptyList();
            this.copyAllAttributes = true;
        }
    
        public HttpSessionHandshakeInterceptor(Collection attributeNames) {
            this.attributeNames = Collections.unmodifiableCollection(attributeNames);
            this.copyAllAttributes = false;
        }
    
        public Collection getAttributeNames() {
            return this.attributeNames;
        }
    
        public void setCopyAllAttributes(boolean copyAllAttributes) {
            this.copyAllAttributes = copyAllAttributes;
        }
    
        public boolean isCopyAllAttributes() {
            return this.copyAllAttributes;
        }
    
        public void setCopyHttpSessionId(boolean copyHttpSessionId) {
            this.copyHttpSessionId = copyHttpSessionId;
        }
    
        public boolean isCopyHttpSessionId() {
            return this.copyHttpSessionId;
        }
    
        public void setCreateSession(boolean createSession) {
            this.createSession = createSession;
        }
    
        public boolean isCreateSession() {
            return this.createSession;
        }
    
        // 在握手完成前(連接建立階段)
        public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
            HttpSession session = this.getSession(request);
            if (session != null) {
                if (this.isCopyHttpSessionId()) {
                    // 保存 sessionid
                    attributes.put("HTTP.SESSION.ID", session.getId());
                }
    
                Enumeration names = session.getAttributeNames();
    
                while(true) {
                    String name;
                    do {
                        if (!names.hasMoreElements()) {
                            return true;
                        }
    
                        name = (String)names.nextElement();
                    } while(!this.isCopyAllAttributes() && !this.getAttributeNames().contains(name));
                    // 保存HttpSession中的信息
                    attributes.put(name, session.getAttribute(name));
                }
            } else {
                return true;
            }
        }
    
        // 獲取HttpSession
        private HttpSession getSession(ServerHttpRequest request) {
            if (request instanceof ServletServerHttpRequest) {
                ServletServerHttpRequest serverRequest = (ServletServerHttpRequest)request;
                return serverRequest.getServletRequest().getSession(this.isCreateSession());
            } else {
                return null;
            }
        }
    
        // 完成握手後業務
        public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
        }
    }
    /<code>

    spring 框架提供的攔截器在org.springframework.web.socket.server.support下,如果不能滿足業務需求,我們也可以直接去實現接口

    Spring Springboot實現websocket通訊-1

    實現HandshakeInterceptor接口,這裡因為是操作httpsession,就演示繼承 HttpSessionHandshakeInterceptor 並重寫beforeHandshake 方法

    <code>package com.wzh.demo.websocket.interceptor;
    
    import org.apache.log4j.Logger;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.http.server.ServletServerHttpRequest;
    import org.springframework.web.socket.WebSocketHandler;
    import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
    
    import javax.servlet.http.HttpSession;
    import java.util.Map;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-21 20:05
     * @see [相關類/方法] (可選)
     */
    public class WebSocketHandshakeInterceptor extends HttpSessionHandshakeInterceptor
    {
        
        private Logger log = Logger.getLogger(WebSocketHandshakeInterceptor.class);
    
        @Override
        public boolean beforeHandshake(ServerHttpRequest request,
            ServerHttpResponse response,
            WebSocketHandler webSocketHandler, Map map)
            throws Exception
        {
            // websocket握手建立前調用,獲取httpsession
            if(request instanceof ServletServerHttpRequest)
            {
                ServletServerHttpRequest servletRequset = (ServletServerHttpRequest) request;
    
                // 這裡從request中獲取session,獲取不到不創建,可以根據業務處理此段
                HttpSession httpSession = servletRequset.getServletRequest().getSession(false);
                if (httpSession != null)
                {
                    // 這裡打印一下session id 方便等下對比和springMVC獲取到httpsession是不是同一個
                    log.info("httpSession key:" + httpSession.getId());
    
                    // 獲取到httpsession後,可以根據自身業務,操作其中的信息,這裡只是單純的和websocket進行關聯
                    map.put("HTTP_SESSION",httpSession);
    
                }
                else
                {
                    log.warn("httpSession is null");
                }
            }
    
            // 調用父類方法
            return super.beforeHandshake(request,response,webSocketHandler,map);
        }
        
        @Override
        public void afterHandshake(ServerHttpRequest serverHttpRequest,
            ServerHttpResponse serverHttpResponse,
            WebSocketHandler webSocketHandler, Exception e)
        {
            // websocket握手建立後調用
            log.info("websocket連接握手成功");
        }
    }
    /<code>

    WebSocketHandler 消息處理中心

    建立一個websocket消息處理中心,我們可以編寫一個類實現WebSocketHandler接口,此接口提供5個方法,用於處理websocket的消息。

    Spring Springboot實現websocket通訊-1

    <code>//
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package org.springframework.web.socket;
    
    public interface WebSocketHandler {
        // 在WebSocket協商成功並且WebSocket連接打開並準備好使用後調用。
        void afterConnectionEstablished(WebSocketSession var1) throws Exception;
        
        // 在新的WebSocket消息到達時調用,也就是接受客戶端信息併發發送
        void handleMessage(WebSocketSession var1, WebSocketMessage> var2) throws Exception;
        
        // 處理底層WebSocket消息傳輸中的錯誤,連接出現異常時觸發
        void handleTransportError(WebSocketSession var1, Throwable var2) throws Exception;
    
        // 在任何一方關閉WebSocket連接之後或在發生傳輸錯誤之後調用。
        void afterConnectionClosed(WebSocketSession var1, CloseStatus var2) throws Exception;
        
        // WebSocketHandler是否處理部分消息,API文檔描述說是拆分消息,多次處理,沒有實際使用過
        boolean supportsPartialMessages();
    }
    /<code>

    API路徑:

    https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/socket/WebSocketHandler.html

    除了實現接口外,我們也可以繼承Spring已經給我提供的實現類,簡化操作,因為有的時候我們只需要此接口中的一個或幾個方法,並不需要全部關注,spring提供的handler都在org.springframework.web.socket.handler這個路徑下

    Spring Springboot實現websocket通訊-1

    這裡我們繼承一個抽象類AbstractWebSocketHandler,重寫我們關注的方法,並擴展我們自己的業務方法

    一個公用的websocket類,存一些連接用到的基本信息,可以根據業務添加刪除屬性

    <code>package com.wzh.demo.domain;
    
    import org.springframework.web.socket.WebSocketSession;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-29 18:24
     * @see [相關類/方法] (可選)
     **/
    public class WebSocketBeanSpring
    {
        
        private WebSocketSession session;
        
        /**
         * 連接錯誤次數
         */
        private AtomicInteger erroerLinkCount = new AtomicInteger(0);
        
        public int getErroerLinkCount()
        {
            // 線程安全,以原子方式將當前值加1,注意:這裡返回的是自增前的值
            return erroerLinkCount.getAndIncrement();
        }
        
        public void cleanErrorNum()
        {
            // 清空計數
            erroerLinkCount = new AtomicInteger(0);
        }
        
       // 省略get set 方法
    }
    /<code>

    一個簡單的消息處理中心,繼承AbstractWebSocketHandler

    <code>package com.wzh.demo.websocket.handler;
    
    import com.wzh.demo.domain.WebSocketBeanSpring;
    import org.apache.log4j.Logger;
    import org.springframework.stereotype.Component;
    import org.springframework.web.socket.*;
    import org.springframework.web.socket.handler.AbstractWebSocketHandler;
    
    import java.io.IOException;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-24 23:11
     * @see [相關類/方法] (可選)
     **/
    @Component("webSocketHander")
    public class WebSocketHander extends AbstractWebSocketHandler{
    
        private Logger log = Logger.getLogger(WebSocketHander.class);
    
        /**
         * 用來存放每個客戶端對應的webSocket對象。
         */
        private static Map webSocketInfo;
    
        static
        {
            // concurrent包的線程安全map
            webSocketInfo = new ConcurrentHashMap();
        }
    
        // 服務器與客戶端初次websocket連接成功執行
        @Override
        public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    
            log.debug("websocket 連接成功......");
    
            // 連接成功當前對象放入websocket對象集合
            WebSocketBeanSpring bean = new WebSocketBeanSpring();
            bean.setSession(session);
    
            webSocketInfo.put(session.getId(),bean);
    
            log.info("客戶端連接服務器session id :"+session.getId()+",當前連接數:" + webSocketInfo.size());
    
        }
    
        // 接受消息處理消息
        @Override
        public void handleMessage(WebSocketSession webSocketSession,
            WebSocketMessage> webSocketMessage)
            throws Exception
        {
            /*
            獲取客戶端發送的消息,這裡使用文件消息,也就是字符串進行接收
            消息可以通過字符串,或者字節流進行接收
            TextMessage String/byte[]接收均可以
            BinaryMessage byte[]接收
            */
            log.info("客戶端發送消息" + webSocketMessage.getPayload().toString());
            TextMessage message = new TextMessage(webSocketMessage.getPayload().toString());
            /*
            這裡直接是字符串,做群發,如果要指定發送,可以在前臺平均ID,後臺拆分後獲取需要發送的人。
            也可以做一個單獨的controller,前臺把ID傳遞過來,調用方法發送,在登錄的時候把所有好友的標識符傳遞到前臺,
            然後通過標識符發送私信消息
            */
            this.batchSendMessage(message);
    
        }
    
        // 連接錯誤時觸發
        @Override
        public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
            if(webSocketSession.isOpen()){
                webSocketSession.close();
            }
    
            log.debug("鏈接出錯,關閉鏈接......");
            webSocketInfo.remove(webSocketSession.getId());
        }
    
        // 關閉websocket時觸發
        @Override
        public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
    
            log.debug("鏈接關閉......" + closeStatus.toString());
            webSocketInfo.remove(webSocketSession.getId());
        }
    
        /**
         * 給所有在線用戶發送消息(這裡用的文本消息)
         * @param message
         */
        public void batchSendMessage(TextMessage message)
        {
            
            Set> setInfo =
                webSocketInfo.entrySet();
            for (Map.Entry entry : setInfo)
            {
                WebSocketBeanSpring bean = entry.getValue();
                try
                {
                    bean.getSession().sendMessage(message);
                }
                catch (IOException e)
                {
                    log.error(e.getMessage(),e);
                }
            }
        }
    
        /**
         * 給指定用戶發送消息
         * @param userId
         * @param message
         */
        public void sendMessage(String userId, TextMessage message)
        {
            WebSocketBeanSpring bean = webSocketInfo.get(userId);
            try
            {
                bean.getSession().sendMessage(message);
            }
            catch (IOException e)
            {
                log.error(e.getMessage(), e);
            }
        }
    
    }
    /<code> 

    WebSocketConfig 配置

    此步驟是在SpringMVC中註冊消息處理中心,因為基於SpringBoot搭建,這裡使用@Configuration註解配置,當然也可以xml配置,這個根據自身項目風格進行配置,這裡我們實現WebSocketConfigurer接口

    Spring Springboot實現websocket通訊-1

    <code>package com.wzh.demo.websocket.config;
    
    import com.wzh.demo.websocket.handler.WebSocketHander;
    import com.wzh.demo.websocket.interceptor.WebSocketHandshakeInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.config.annotation.EnableWebSocket;
    import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
    import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-08-05 22:59
     * @see [相關類/方法] (可選)
     **/
    @Configuration //標記為spring 配置類
    @EnableWebSocket //開啟websocket支持
    public class WebSocketConfig implements WebSocketConfigurer{
    
        // 註冊消息處理器,並映射連接地址
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry registry)
        {
            // 註冊消息處理器,並添加自定義攔截器,支持websocket的連接訪問
            registry.addHandler(new WebSocketHander(), "/spring/websocket")
                .addInterceptors(new WebSocketHandshakeInterceptor());
    
            /*
            註冊消息處理器,並添加自定義攔截器,添加不支持websocket的連接訪問
            SockJs是一個WebSocket的通信js庫,Spring對這個js庫進行了後臺的自動支持,
            也就是說,如果使用SockJs,那麼我們就不需要對後臺進行更多的配置,只需要加上withSockJS()這一句就可以了
             */
            registry.addHandler(new WebSocketHander(), "/spring/sockjs/websocket")
                    .addInterceptors(new WebSocketHandshakeInterceptor()).withSockJS();
        }
    }
    /<code> 

    代碼都準備好了,下面進行測試,這裡測試就做兩個簡單的方法,一個通過消息處理中心公告,一個通過controller進行私信

    一個簡單的controller 用於跳轉到html頁面

    <code>// 跳轉websocket界面
        @RequestMapping(value = "/spring/socket.do",method = RequestMethod.GET)
        public String toSpringWebSocket(HttpSession session, Model model)
        {
            model.addAttribute("address","/spring/websocket");
            System.out.println("進入websocket");
            return "/test/springWebSocket";
        }/<code>

    html頁面就用上面寫的界面,按鈕用於觸發websocket的公告方法

    Spring Springboot實現websocket通訊-1

    登錄可以看見後臺控制檯打印

    Spring Springboot實現websocket通訊-1

    點擊頁面按鈕,瀏覽器控制檯輸出

    Spring Springboot實現websocket通訊-1

    在寫個測試私信的controller,這裡為了簡單就不寫頁面了,直接get請求訪問控制器,攜帶websession id

    <code>package com.wzh.demo.controller;
    
    import com.wzh.demo.websocket.handler.WebSocketHander;
    import org.springframework.boot.autoconfigure.web.ServerProperties;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.socket.TextMessage;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    
    
    /**
     * 
     * 
     * @author wzh
     * @version 2018-07-09 22:53
     * @see [相關類/方法] (可選)
     **/
    @Controller
    @RequestMapping("/websocket")
    public class WebSocketController {
    
        @Resource
        private WebSocketHander webSocketHander;
    
        // 測試私信發送
        @RequestMapping(value = "/spring/socketById.do",method = RequestMethod.GET)
        public void toSpringWebSocketByid(HttpSession session, HttpServletRequest request, Model model)
        {
            String id = request.getParameter("id");
            webSocketHander.sendMessage(id,new TextMessage("測試指定人員發送"));
    
        }
    }
    /<code>

    瀏覽器直接訪問http://localhost:8080/SpringBootDemo/websocket/spring/socketById.do?id=1
    查看第一個session id 的界面成功收到消息,其他界面沒有消息

    Spring Springboot實現websocket通訊-1


    分享到:


    相關文章: