Spring MVC整合WebSocket通信
目錄
============================================================================
1、使用 Spring 的低層級 WebSocket API
2、使用JSR356定義的WebSocket規范
3、
4、
============================================================================
轉載:http://www.cnblogs.com/yangchongxing/p/8474256.html
WebSocket是HTML5開始提供的一種瀏覽器與服務器間進行全雙工通訊的網絡技術。依靠這種技術可以實現客戶端和服務器端的長連接,雙向實時通信。
特點:事件驅動、異步,使用ws或者wss協議的客戶端socket,能夠實現真正意義上的推送功能
缺點:少部分瀏覽器不支持,瀏覽器支持的程度與方式有區別。
參考資料:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
構造函數
WebSocket(url[, protocols]) 返回一個 WebSocket 對象
常量
名稱 | 值 | 作用 |
WebSocket.CONNECTING | 0 | 正嘗試與服務器建立連接 |
WebSocket.OPEN | 1 | 與服務器已經建立連接 |
WebSocket.CLOSING | 2 | 正在關閉與服務器的連接 |
WebSocket.CLOSED | 3 | 已經關閉了與服務器的連接 |
WebSocket實例的readyState屬性對照判斷狀態
屬性
binaryType | 使用二進制的數據類型連接 |
bufferedAmount | 只讀 未發送至服務器的字節數 |
extensions | 只讀 服務器選擇的擴展 |
onclose | 用於指定連接關閉后的回調函數 |
onerror | 用於指定連接失敗后的回調函數 |
onmessage | 用於指定當從服務器接受到信息時的回調函數 |
onopen | 用於指定連接成功后的回調函數 |
protocol | 只讀 服務器選擇的下屬協議 |
readyState | 只讀 當前的鏈接狀態 |
url | 只讀 WebSocket 的絕對路徑 |
方法
WebSocket.close([code[, reason]]) 關閉當前鏈接
WebSocket.send(data) 向服務器發送數據
Java服務端:
JSR356定義了WebSocket的規范,JSR356 的 WebSocket 規范使用 javax.websocket.*的 API,可以將一個普通 Java 對象(POJO)使用 @ServerEndpoint 注釋作為 WebSocket 服務器的端點。
1、使用 Spring 的低層級 WebSocket API
實現WebSocketHandler接口、該接口包含5個方法,用於處理不同的事件
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(); }
比實現接口更為簡單的方式是擴展AbstractWebSocketHandler抽象類,下面是抽象類的代碼
public abstract class AbstractWebSocketHandler implements WebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { if (message instanceof TextMessage) { handleTextMessage(session, (TextMessage) message); } else if (message instanceof BinaryMessage) { handleBinaryMessage(session, (BinaryMessage) message); } else if (message instanceof PongMessage) { handlePongMessage(session, (PongMessage) message); } else { throw new IllegalStateException("Unexpected WebSocket message type: " + message); } } protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { } protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception { } protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception { } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { } @Override public boolean supportsPartialMessages() { return false; } }
我們以文本消息為例直接繼承TextWebSocketHandler類
TextWebSocketHandler繼承自AbstractWebSocketHandler類用來處理文本消息。
BinaryWebSocketHandler繼承自AbstractWebSocketHandler類用來處理二進制消息。
public class CommoditySocket extends TextWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("open..."); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { System.out.println("Closed"); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { super.handleTextMessage(session, message); System.out.println("收到消息:" + message.getPayload()); } }
消息處理類完成了,來看配置文件
首先追加websocket命名空間
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation="http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.3.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
再追加如下配置,注意path的路徑這個是請求的地址,ws://域名:端口/項目名/socket/text
<websocket:handlers> <websocket:mapping handler="textHandler" path="/socket/text"/> </websocket:handlers> <bean id="textHandler" class="cn.ycx.web.socket.TextHandler"></bean>
瀏覽器端使用標准的WebSocket
var url = 'ws://' + window.location.host + '/ycxxml/socket/text'; //配置文件中配的path有關 var socket = new WebSocket(url); socket.onopen = function() { console.log("open..."); socket.send("start talk...") } socket.onmessage = function(e) { console.log("服務器發來:" + e.data); document.write("" + e.data + "<br/>"); } socket.onclose = function() { console.log("close..."); }
2、使用JSR356定義的WebSocket規范
@ServerEndpoint(value="/websocket/commodity/{userId}", configurator = SpringConfigurator.class)
特別注意:configurator = SpringConfigurator.class,若要進行對象注入此段代碼必須加
表示將普通的Java對象注解為WebSocket服務端點,運行在ws://[Server端IP或域名]:[Server端口]/項目/websocket/commodity/{userId}的訪問端點,客戶端瀏覽器已經可以對WebSocket客戶端API發起HTTP長連接了。
@OnOpen
在新連接建立時被調用。@PathParam可以傳遞url參數,滿足業務需要。Session表明兩個WebSocket端點對話連接的另一端,可以理解為類似HTTPSession的概念。
@OnClose
在連接被終止時調用。參數closeReason可封裝更多細節,如為什么一個WebSocket連接關閉。
@OnMessage
注解的Java方法用於接收傳入的WebSocket信息,這個信息可以是文本格式,也可以是二進制格式。
服務器端代碼:
package cn.ycx.web.socket; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.websocket.CloseReason; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import org.springframework.web.socket.server.standard.SpringConfigurator; import com.alibaba.fastjson.JSON; /** * 交易 * @author 楊崇興 * */ @ServerEndpoint(value="/websocket/commodity/{fromUserId}/{toUserId}", configurator = SpringConfigurator.class) public class WebSocketServer { // 已經建立鏈接的對象緩存起來 private static ConcurrentMap<Integer, WebSocketServer> serverMap = new ConcurrentHashMap<Integer, WebSocketServer>(); // 當前session private Session currentSession; @OnOpen public void onOpen(Session session, @PathParam("fromUserId") int fromUserId, @PathParam("toUserId") int toUserId) throws IOException { this.currentSession = session; serverMap.put(fromUserId, this);//建立鏈接時,緩存對象 } @OnClose public void onClose(Session session, CloseReason reason) { System.out.println(reason.toString()); if (serverMap.containsValue(this)) { Iterator<Integer> keys = serverMap.keySet().iterator(); int userId = 0; while(keys.hasNext()) { userId = keys.next(); if (serverMap.get(userId) == this) { serverMap.remove(userId, this);//關閉鏈接時,刪除緩存對象 } } } this.currentSession = null; try { session.close(); } catch (IOException e) { e.printStackTrace(); } } @OnMessage() @SuppressWarnings("unchecked") public void onMessage(String json) { HashMap<String, String> map = JSON.parseObject(json, HashMap.class); int fromUserId = Integer.parseInt(map.get("fromUserId")); int toUserId = Integer.parseInt(map.get("toUserId")); String content = map.get("content").toString(); WebSocketServer server = serverMap.get(toUserId);//若存在則用戶在線,否在用戶不在線 if (server != null && server.currentSession.isOpen()) { if (fromUserId != toUserId) { try { server.currentSession.getBasicRemote().sendText(content); } catch (IOException e) { e.printStackTrace(); } } } } @OnError public void onError(Throwable t) { t.printStackTrace(); } }
注意:修改了原來的問題,serverMap對象全局緩存了已經鏈接上的對象,通過這對象也能判斷用戶是否在線。
注意:使用spring boot是要定義ServerEndpointExporter
If you want to use @ServerEndpoint in a Spring Boot application that used an embedded container, you must declare a single ServerEndpointExporter @Bean, as shown in the following example:
如果想要在使用嵌入式容器的Spring Boot應用中使用@ServerEndpoint,則必須聲明單個ServerEndpointExporter @Bean,如下例所示:
@Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); }
The bean shown in the preceding example registers any @ServerEndpoint annotated beans with the underlying WebSocket container. When deployed to a standalone servlet container, this role is performed by a servlet container initializer, and the ServerEndpointExporter bean is not required.
前面示例中所示任何在WebSocket容器中使用@ServerEndpoint注解標注的beans。當部署到獨立的servlet容器時,此角色由servlet容器初始值設定項執行,並且不需要 ServerEndpointExporter bean
瀏覽器端:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>socketjs</title> </head> <body> 發送者:<input id="fromUserId" value="2"> 接收者:<input id="toUserId" value="3"> <button type="button" onclick="openClick();">打開</button> <button type="button" onclick="closeClick();">關閉</button><br/> <input id="message" value="---"/> <button type="button" onclick="sendClick();">發送</button> <div id="content"></div> <script> var socket; var t; function openClick() { connection(); } function closeClick() { if (socket) { socket.close(); } } function sendClick() { var fromUserId = document.getElementById("fromUserId").value; var toUserId = document.getElementById("toUserId").value; var content = document.getElementById("message").value; var obj = { "fromUserId":fromUserId, "toUserId":toUserId, "content":content }; document.getElementById("content").innerHTML = document.getElementById("content").innerHTML + '<br/>' + fromUserId + "說:" + content; socket.send(JSON.stringify(obj)); console.log(fromUserId + "說:" + JSON.stringify(content)); } var connection = function() { console.log("connection()"); var fromUserId = document.getElementById("fromUserId"); var toUserId = document.getElementById("toUserId"); var url = 'ws://' + window.location.host + '/ycxcode/websocket/commodity/{' + fromUserId.value + '}/{' + toUserId.value + '}'; socket = new WebSocket(url); socket.onopen = onopen; socket.onmessage = onmessage; socket.onclose = onclose; socket.onerror = onerror; } //重連 var reconnection = function() { //與服務器已經建立連接 if (socket && socket.readyState == 1) { clearTimeout(t); } else { //已經關閉了與服務器的連接 if (socket.readyState == 3) { connection(); } //0正嘗試與服務器建立連接,2正在關閉與服務器的連接 t = setTimeout(function() { reconnection(); }, 1000); } } var onopen = function() { console.log("onopen()"); } var onclose = function() { console.log("onclose()"); reconnection(); } var onmessage = function(e) { console.log("onmessage()"); if (e.data === "") return; var toUserId = document.getElementById("toUserId").value; document.getElementById("content").innerHTML = document.getElementById("content").innerHTML + '<br/>' + toUserId + "說:" + e.data; console.log(toUserId + "說:" + e.data); } var onerror = function() { console.log("error..."); reconnection(); } </script> </body> </html>