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>