Spring WebSocket 初探

Spring WebSocket API

參考文檔:https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html

1.spring 4.0及以上增加了WebSocket的支持(這裏使用4.3.8.RELEASE) 
2.spring 支持STOMP協議的WebSocket通信 
3.應對不支持 WebSocket 的場景,許多瀏覽器不支持 WebSocket 協議;SockJS 是 WebSocket 技術的一種模擬。SockJS 會 儘可能對應 WebSocket API,但如果 WebSocket 技術 不可用的話,會從如下 方案中挑選最優可行方案: 

XHR streaming
XDR streaming
iFrame event source
iFrame HTML file
XHR polling
XDR polling
iFrame XHR polling
JSONP polling

4.WebSocket 是發送和接收消息的 底層API,而SockJS 是在 WebSocket 之上的 API;最後 STOMP(面向消息的簡單文本協議)是基於 SockJS 的高級API 
5.SockJS 所處理的URL 是 “http:” 或 “https:” 模式 
6.WebSocket 所處理的URL 是“ws:” or “wss:” 模式 

WebSocket 實現消息功能

web.xml配置修改

<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         metadata-complete="true">

DispatcherServlet配置中加入

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value></param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>

pom.xm添加依賴

     <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-websocket</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-messaging</artifactId>
      <version>${spring.version}</version>
    </dependency>

創建一個WebSocketHandler

1.可以擴展 AbstractWebSocketHandler 2.也可以擴展 TextWebSocketHandler(文本 WebSocket 處理器),TextWebSocketHandler 繼承 AbstractWebSocketHandler;

public class TestHandler extends TextWebSocketHandler {

    private final static Logger LOGGER = Logger.getLogger(TestHandler.class);

    //已建立連接的用戶
    private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();;


    /**
     * 處理前端發送的文本信息
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session,
                                     TextMessage message) throws Exception {
        // 獲取提交過來的消息詳情                             
        LOGGER.debug("-------handleMessage" + message.toString());
        //回覆一條信息,
        session.sendMessage(new TextMessage("reply msg:" +message.getPayload()));

    }

    /**
     * 當新連接建立的時候,被調用;
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        LOGGER.info("Connection Established");
        users.add(session);
        session.sendMessage(new TextMessage("connect"));
        session.sendMessage(new TextMessage("new_msg"));
        super.afterConnectionEstablished(session);
    }

    /**
     * 當連接關閉時被調用;
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        LOGGER.info("Connection closed. Status: " + status);
        users.remove(session);
        super.afterConnectionClosed(session, status);
    }

    /**
     * 給所有在線用戶發送消息
     *
     * @param message
     */
    public void sendMessageToUsers(TextMessage message) {
        for (WebSocketSession user : users) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 給某個用戶發送消息
     *
     * @param userName
     * @param message
     */
    public void sendMessageToUser(String userName, TextMessage message) {
        for (WebSocketSession user : users) {
            if (user.getAttributes().get(Constants.WEBSOCKET_USERNAME).equals(userName)) {
                try {
                    if (user.isOpen()) {
                        user.sendMessage(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }
}

創建WebSocketConfigurer

@Configuration
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

    @Override
    public  void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
        // 將 TestHandler 處理器 映射到  /webSocketServer路徑下.
        registry.addHandler(testHandler(),"/webSocketServer")
                .addInterceptors(new WebSocketHandshakeInterceptor()); 
        
        // 將 SockJS的連接映射到  /webSocketServer/sockjs路徑下.
        registry.addHandler(testHandler(),"/webSocketServer/sockjs")
                .addInterceptors(new WebSocketHandshakeInterceptor()) 
                .withSockJS(); //開啓SockJS的支持

        //WebSocketHandshakeInterceptor 自定義websocket握手攔截器,在這裏可以對用戶名進行登記或進行用戶分組,以便定向發送消息
    }

    @Bean
    public WebSocketHandler testHandler(){
        return  new TestHandler();
    }
}

自定義握手攔截器

public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {

    private final static Logger LOGGER = Logger.getLogger(WebSocketHandshakeInterceptor.class);
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpSession session = servletRequest.getServletRequest().getSession(false);
            if (session != null) {
                //這裏可以合用不同的userName區分WebSocketHandler,也可以將用戶分組或貼標籤,以便定向發送消息
                //String userName = (String) session.getAttribute(Constants.SESSION_USERNAME);
                //這裏是模擬測試,沒有userName,用sessionId代爲表示
                attributes.put(Constants.WEBSOCKET_USERNAME,session.getId());
            }
        }
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

    }
}

推送消息

    private TestHandler testHandler = new TestHandler();

    @RequestMapping(value = "sendMessage")
    @ResponseBody
    public String sendMessage(String msg){
        //無關代碼都省略了
        testHandler.sendMessageToUsers(new TextMessage("using controller to send msg:"+msg));
        return "done";
    }

前端頁面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path ;
    String serverPath = request.getServerName() + ":" + request.getServerPort() + path ;
%>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Websocket</title>

    <script src="<%=basePath%>/resources/js/jquery-2.1.4.min.js"></script>
    <!--引入sockjs-->
    <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

    <script type="text/javascript">
        var socket = null;

        function connect() {
            //方式一 SockJS的連接
            //socket = new SockJS("<%=basePath%>/webSocketServer/sockjs");
            
            //方式二 Websocket的連接
            socket = new WebSocket('ws://<%=serverPath%>/webSocketServer'); 
            socket.onopen = function () {
                log('建立連接');
            };

            socket.onmessage = function (event) {
                log('接收消息: ' + event.data);
                $("#connect").hide();
                $("#disconnect").show();
            };

            socket.onclose = function (event) {
                log('連接關閉:'+event.data);
                $("#connect").show();
                $("#disconnect").hide();
            };
        }

        function sendMsg() {
            if (socket != null) {
                socket.send($("#message").val());
            } else {
                alert('請先建立連接.');
            }
        }

        function disconnect() {
            if (ws != null) {
                ws.close();
                ws = null;
            }
        }

        function log(message) {
            var html = '<p style="word-wrap: break-word;">'+message+'</p>';
            $("#console").append(html);
        }
    </script>
</head>
<body>

<div>
    <div id="connect-container" style="text-align: center;margin: 40px;">
        <div>
            <button id="connect" onclick="connect();">連接</button>
            <button id="disconnect" style="display: none" onclick="disconnect()">關閉連接</button>
            <input id="message" style="width: 300px" placeholder="輸入消息內容"/>
            <button onclick="sendMsg();">發送消息</button>
        </div>
        <div id="console-container">
            <div id="console"></div>
        </div>
    </div>
</div>
</body>
</html>

界面效果


STOMP 實現消息功能

public class Greeting {
    private String content;

    public Greeting(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}
public class HelloMessage {
    private String name;

    public String getName() {
        return name;
    }
}
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //啓用 STOMP 代理中繼功能: 並將其目的地前綴設置爲 "/topic" or "/greetings" ;
        // spring就能知道 所有目的地前綴爲 "/topic" or "/greetings" 的消息都會發送到 STOMP 代理中;
        config.enableSimpleBroker("/topic", "/greetings");

        //設置應用的前綴爲 "app":所有目的地以 "/app" 打頭的消息(發送消息url not 連接url)都會路由到 帶有 @MessageMapping 註解的方法中,而不會發布到 代理隊列中;
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").withSockJS();
        // 在網頁上我們就可以通過這個鏈接 /server/hello 來和服務器的WebSocket連接
    }
}
@Controller
public class GreetingController {

    // @MessageMapping defines the sending addr for client.
    // 消息發送地址: /app/hello
    @MessageMapping("/hello")
    @SendTo("/topic")
    public Greeting greeting(HelloMessage message) throws Exception {
        System.out.println("receiving " + message.getName());
        System.out.println("connecting successfully.");
        return new Greeting("Hello, " + message.getName() + "!");
    }

    @SubscribeMapping("/macro")
    public Greeting handleSubscription() {
        Greeting greeting = new Greeting("SubscribeMapping macro");
        return greeting;
    }

    private SimpMessageSendingOperations  template;

    @Autowired
    public GreetingController(SimpMessageSendingOperations  template) {
        this.template = template;
    }

    @RequestMapping(path="/send")
    @ResponseBody
    public String send(@RequestParam String message) {
        //發送消息
        this.template.convertAndSend("/topic", new Greeting(message));
        return "done";
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path ;
%>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Websocket</title>

    <script src="<%=basePath%>/resources/js/jquery-2.1.4.min.js"></script>
    <!--引入sockjs-->
    <script src="<%=basePath%>/resources/js/sockjs-1.1.1.js"></script>
    <script src="<%=basePath%>/resources/js/stomp.js"></script>
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

    <script type="text/javascript">

        var stompClient = null;

        //this line.
        function connect() {
            var socket = new SockJS("<%=basePath%>/hello");
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                stompClient.subscribe('/topic', function(greeting){
                    log("Received:"+JSON.parse(greeting.body).content);
                });

                stompClient.subscribe('/app/macro',function(greeting){
                    log("Received:"+JSON.parse(greeting.body).content);
                });
            });
        }

        function sendMsg() {
            stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#message").val()}));
            log("send:name="+$("#message").val());
        }

        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
        }

        function log(message) {
            var html = '<p style="word-wrap: break-word;">'+message+'</p>';
            $("#console").append(html);
        }
    </script>
</head>
<body>

<div>
    <div id="connect-container" style="text-align: center;margin: 40px;">
        <div>
            <button id="connect" onclick="connect();">連接</button>
            <button id="disconnect" style="display: none" onclick="disconnect()">關閉連接</button>
            <input id="message" style="width: 300px" placeholder="輸入消息內容"/>
            <button onclick="sendMsg();">發送消息</button>
        </div>
        <div id="console-container">
            <div id="console"></div>
        </div>
    </div>
</div>
</body>
</html>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章