SpringBoot整合WebSocket【基於純H5】進行點對點[一對一]和廣播[一對多]實時推送
之前實現WebSocket基於STOMP的,覺得SpringBoot封裝的太高,不怎麼靈活,現在實現一個純H5的,也大概瞭解webSocket在內部是怎麼傳輸的。
1.環境搭建
因為在上一篇基於STOMP協議實現的WebSocket裡已經有大概介紹過Web的基本情況了,所以在這篇就不多說了,我們直接進入正題吧,在SpringBoot中,我們還是需要匯入WebSocket的包。
在pox.xml加上對springBoot對WebSocket的支援:
<!-- webSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
這裡大概說一下自己的一點小見解:客戶端與伺服器建立WebSocket連線,實際上是建立了一個Socket,這個Socket是共享與客戶端和伺服器的。兩者只要往對應的Socket裡操作,就可以實現雙方實時通訊了
2.編碼實現
一、在SpringBoot中,新增WebSocket的配置
package com.cloud.sbjm.configure; 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; import com.cloud.sbjm.security.WebSocketInterceptor; import com.cloud.sbjm.service.Imp.MyHandler; //實現介面來配置Websocket請求的路徑和攔截器。 @Configuration @EnableWebSocket public class WebSocketH5Config implements WebSocketConfigurer{ @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { //handler是webSocket的核心,配置入口 registry.addHandler(new MyHandler(), "/myHandler/{ID}").setAllowedOrigins("*").addInterceptors(new WebSocketInterceptor()); } }
1.@Configuration:註解標識該類為Spring的配置類
2.@EnableWebSocket:開啟註解接收和傳送訊息
3.實現WebSocketConfigurer介面,重寫registerWebSocketHandlers方法,這是一個核心實現方法,配置websocket入口,允許訪問的域、註冊Handler、定義攔截器。客戶端通過“/myHandler/{ID}”直接訪問Handler核心類,進行socket的連線、接收、傳送等操作,這裡由於還加了個攔截器,所以建立新的socket訪問時,都先進來攔截器再進去Handler類,“new WebSocketInterceptor()”是我實現的攔截器,“new MyHandler()”是我實現的一個Handler類。
二、WebSocketInterceptor攔截器的實現:
package com.cloud.sbjm.security;
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 WebSocketInterceptor implements HandshakeInterceptor {
//進入hander之前的攔截
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (request instanceof ServletServerHttpRequest) {
String ID = request.getURI().toString().split("ID=")[1];
System.out.println("當前session的ID="+ID);
ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
HttpSession session = serverHttpRequest.getServletRequest().getSession();
map.put("WEBSOCKET_USERID",ID);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
System.out.println("進來webSocket的afterHandshake攔截器!");
}
}
1.實現了HandshakeInterceptor 介面,並實現了beforeHandshake該方法,該方法是在進入Handler核心類之前進行攔截。
這裡主要實現的邏輯是:
擷取客戶端建立webSocket連線時傳送的URL地址字串,並通過對該字串進行特殊標識擷取操作,獲取客戶端傳送的唯一標識(由自己定義的,一般是系統使用者ID唯一標識,用以標識該使用者),並把它以鍵值對的形式放到Session裡,這樣後期可以通過該session獲取它對應的使用者ID了。【一個session對應著一個webSocketSession】
三、MyHandler核心類的實現
package com.cloud.sbjm.service.Imp;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import net.sf.json.JSONObject;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
@Service
public class MyHandler implements WebSocketHandler {
//線上使用者列表
private static final Map<String, WebSocketSession> users;
static {
users = new HashMap<>();
}
//新增socket
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("成功建立連線");
String ID = session.getUri().toString().split("ID=")[1];
System.out.println(ID);
if (ID != null) {
users.put(ID, session);
session.sendMessage(new TextMessage("成功建立socket連線"));
System.out.println(ID);
System.out.println(session);
}
System.out.println("當前線上人數:"+users.size());
}
//接收socket資訊
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
try{
JSONObject jsonobject = JSONObject.fromObject(webSocketMessage.getPayload());
System.out.println(jsonobject.get("id"));
System.out.println(jsonobject.get("message")+":來自"+(String)webSocketSession.getAttributes().get("WEBSOCKET_USERID")+"的訊息");
sendMessageToUser(jsonobject.get("id")+"",new TextMessage("伺服器收到了,hello!"));
}catch(Exception e){
e.printStackTrace();
}
}
/**
* 傳送資訊給指定使用者
* @param clientId
* @param message
* @return
*/
public boolean sendMessageToUser(String clientId, TextMessage message) {
if (users.get(clientId) == null) return false;
WebSocketSession session = users.get(clientId);
System.out.println("sendMessage:" + session);
if (!session.isOpen()) return false;
try {
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 廣播資訊
* @param message
* @return
*/
public boolean sendMessageToAllUsers(TextMessage message) {
boolean allSendSuccess = true;
Set<String> clientIds = users.keySet();
WebSocketSession session = null;
for (String clientId : clientIds) {
try {
session = users.get(clientId);
if (session.isOpen()) {
session.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
allSendSuccess = false;
}
}
return allSendSuccess;
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (session.isOpen()) {
session.close();
}
System.out.println("連接出錯");
users.remove(getClientId(session));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("連線已關閉:" + status);
users.remove(getClientId(session));
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 獲取使用者標識
* @param session
* @return
*/
private Integer getClientId(WebSocketSession session) {
try {
Integer clientId = (Integer) session.getAttributes().get("WEBSOCKET_USERID");
return clientId;
} catch (Exception e) {
return null;
}
}
}
1.實現了WebSocketHandler介面,並實現了關鍵的幾個方法。
① afterConnectionEstablished(介面提供的):建立新的socket連線後回撥的方法。主要邏輯是:將成功建立連線的webSocketSssion放到定義好的常量[private static final Map<String, WebSocketSession> users;]中去。這裡也擷取客戶端訪問的URL的字串,拿到標識,以鍵值對的形式講每一個webSocketSession存到users裡,以記錄每個Socket。
② handleMessage(介面提供的):接收客戶端傳送的Socket。主要邏輯是:獲取客戶端傳送的資訊。這裡之所以可以獲取本次Socket的ID,是因為客戶端在第一次進行連線時,攔截器進行攔截後,設定好ID,這樣也說明,雙方在相互通訊的時候,只是對第一次建立好的socket持續進行操作。
③ sendMessageToUser(自己定義的):傳送給指定使用者資訊。主要邏輯是:根據使用者ID從常量users(記錄每一個Socket)中,獲取Socket,往該Socket裡傳送訊息,只要客戶端還線上,就能收到該訊息。
④sendMessageToAllUsers (自己定義的):這個廣播訊息,傳送資訊給所有socket。主要邏輯是:跟③型別,只不過是遍歷整個users獲取每一個socket,給每一個socket傳送訊息即可完廣播發送
⑤handleTransportError(介面提供的):連接出錯時,回撥的方法。主要邏輯是:一旦有連接出錯的Socket,就從users裡進行移除,有提供該Socket的引數,可直接獲取ID,進行移除。這個在客戶端沒有正常關閉連線時,會進來,所以在開發客戶端時,記得關閉連線
⑥afterConnectionClosed(介面提供的):連線關閉時,回撥的方法。主要邏輯:一旦客戶端/伺服器主動關閉連線時,將個socket從users裡移除,有提供該Socket的引數,可直接獲取ID,進行移除。
後臺的開發就開發完了,大家有沒有發現比基於STOMP協議實現要靈活得多?
四、客戶端頁面的實現【基於H5】
不需要加入任何的JS包
<!DOCTYPE html>
<html>
<head>
<title>socket.html</title>
<meta name="keywords" content="keyword1,keyword2,keyword3">
<meta name="description" content="this is my page">
<meta name="content-type" content="text/html" charset="UTF-8">
<!--<link rel="stylesheet" type="text/css" href="./styles.css">-->
</head>
<body>
Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
<!-- 公共JS -->
<script type="text/javascript" src="../webSocket/jquery.min.js"></script>
<script type="text/javascript">
var userID="888";
var websocket=null;
$(function() {
//建立WebSocket
connectWebSocket();
})
//強制關閉瀏覽器 呼叫websocket.close(),進行正常關閉
window.onunload = function() {
//關閉連線
closeWebSocket();
}
//建立WebSocket連線
function connectWebSocket(){
console.log("開始...");
//建立webSocket連線
websocket = new WebSocket("ws://127.0.0.1:9091/cloud-sbjm/myHandler/ID="+userID);
//開啟webSokcet連線時,回撥該函式
websocket.onopen = function () {
console.log("onpen");
}
//關閉webSocket連線時,回撥該函式
websocket.onclose = function () {
//關閉連線
console.log("onclose");
}
//接收資訊
websocket.onmessage = function (msg) {
console.log(msg.data);
}
}
//傳送訊息
function send(){
var postValue={};
postValue.id=userID;
postValue.message=$("#text").val();
websocket.send(JSON.stringify(postValue));
}
//關閉連線
function closeWebSocket(){
if(websocket != null) {
websocket.close();
}
}
</script>
</body>
</html>
頁面比較簡單,簡單解釋一下:
1.new WebSocket("ws://127.0.0.1:9091/cloud-sbjm/myHandler/ID="+userID),與伺服器建立webSocket連線,後面的ID="+userID,是動態引數,跟伺服器配置Handler的訪問地址時對應"/myHandler/{ID}"。
2.H5也提供多個回撥函式
onopen:開啟webSokcet連線時,回撥該函式
onclose:關閉webSocket連線時,回撥該函式
onmessage:伺服器給該socket傳送訊息時,回撥該函式,獲取訊息
websocket.send(JSON.stringify(postValue));:給Socket傳送訊息,伺服器獲取
websocket.close();客戶端主要關閉連線,會觸發客戶端的onclose方法和伺服器的afterConnectionClosed方法
到此服務端的開發也完成了,下面執行一下程式效果圖:
一、建立連線
客戶端:
伺服器:
二、傳送訊息
客戶端:
伺服器:
三、伺服器主動推送訊息
伺服器程式碼:
到此已經完成了,各位可以根據自己需求進行修改,這會靈活多了!
如有錯漏,請各位指導。