SpringBoot 整合 Tomcat 的 WebSocket 服務端
傳統 Tomcat 開發 WebSocket 回顧
WebSocket 的出現是基於 Web 應用的實時性需要而產生的,在淘寶、京東等網頁客服、網頁賣家聊天等需求上應用廣泛。對於前端網頁可以使用 H5 開發 WebSocket 客戶端,也可以使用 SockJS 庫開發 WebSocket 客戶端。
對於Java 開發者而言,後臺 WebSocket 服務端開發通常有以下常用的選擇:
Tomcat7 以後開始支援 websocket 協議
Spring4 以後開始支援 WebSocket
Netty3 以後支援開發 WebSocket
Tomcat8 如下所示,自身已經支援 WebSocket 服務端開發,它的 lib 目錄下有自己實現 WebSocket 協議的開發包,如果是傳統的 Java Web 專案,則只需要將 tomcat-websocket.jar、websocket-api.jar 匯入應用中即可進行程式碼開發。
Tomcat 自身也提供了 WebSocket 開發的示例,在 webapps/exampls下。
一共提供了 4 個示例,可以啟動 Tomcat 進行訪問測試以及學習。
對於傳統 Java Web 導包式應用開發,這裡不再過多進行說明,它的基本流程是:
1)新建 Java Web 應用後,匯入 Tomcat 伺服器 lib 目錄下的 websocket-api.jar 、tomcat-websocket.jar開發包,前者是瀏覽器 webSocket 規範的介面,後者是 Tomcat 對它的實現
2)建立後臺 webSocket 服務端類,標識 @ServerEndpoint( javax.websocket.server.ServerEndpoint)註解,表示當前類是 webSocket 服務終端,同時在裡面實現客戶端連線建立、傳送訊息、接收訊息等通訊業務。
2)自己實現 javax.websocket.server.ServerApplicationConfig 介面,掃描整個應用所有的 @ServerEndpoint 服務終端
SpringBoot 整合 Tomcat 的 WebSocket 開發
本文的重點是 SpringBoot 專案如何再使用 Tomcat 的 webSocket 服務端開發。
本文使用的是 springboot 2.1.0 版本。
既然是使用 Tomcat 的 webSocket ,所以應該參加 web 專案,而且要麼使用 SpringBoot 預設內建的 Tomcat 伺服器,要麼就是自己是有外接的 Servelt 容器(Tomcat)。專案結構如下:
pom.xml 引入 springboot websocket 元件
預設情況 springboot 建立的 web 應用,在 web 啟動模組是已經依賴了 tomcat-websocket 模組的,所以不再需要自己在 pom.xml 檔案中重複匯入。
但是必須匯入 springboot 的 websocket 啟動模組:
<!-- Spring boot WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@ServerEndpoint 建立 websocket 服務終端
建立後臺 webSocket 服務端類,標識 @ServerEndpoint( javax.websocket.server.ServerEndpoint)註解,表示當前類是 webSocket 服務終端,同時在裡面實現客戶端連線建立、傳送訊息、接收訊息等通訊業務。
這與傳統導包式開發 Tomcat WebSocket 服務端是一樣的,區別就是:傳統方式 @ServerEndpoint 類上不需要加 @Component 交由 Spring 管理,而現在需要加上 @Component 將此元件交由 spring 管理。
package com.example.demo.tomcatWebSocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
/**
* Created by Administrator on 2018/11/28 0028.
* @ServerEndpoint :標識此類為 Tomcat 的 websocket 服務終端,/websocket/yy.action 是客戶端連線請求的路徑
* @Component :將本類交由 spring IOC 容器管理
*/
@ServerEndpoint(value = "/websocket/yy.action")
@Component
public class ServerEnpoint {
private static Logger logger = LoggerFactory.getLogger(ServerEnpoint.class);
/**
* 用 Set 來 儲存 客戶端 連線
*/
private static Set<Session> sessionSet = new HashSet<>();
/**
* 連線成功後自動觸發
*
* @param session
*/
@OnOpen
public void afterConnectionEstablished(Session session) {
/**
* session 表示一個連線會話,整個連線會話過程中它都是固定的,每個不同的連線 session 不同
* String queryString = session.getQueryString();//獲取請求地址中的查詢字串
* Map<String, List<String>> parameterMap = session.getRequestParameterMap();//獲取請求地址中引數
* Map<String, String> stringMap = session.getPathParameters();
* URI uri = session.getRequestURI();
*/
sessionSet.add(session);
logger.info("新客戶端加入,session id=" + session.getId() + ",當前客戶端格個數為:" + sessionSet.size());
/**
* session.getBasicRemote().sendText(textMessage);同步傳送
* session.getAsyncRemote().sendText(textMessage);非同步傳送
*/
session.getAsyncRemote().sendText("我是伺服器,你連線成功!");
}
/**
* 連線斷開後自動觸發,連線斷開後,應該清楚掉 session 集合中的值
*
* @param session
*/
@OnClose
public void afterConnectionClosed(Session session) {
sessionSet.remove(session);
logger.info("客戶端斷開,session id=" + session.getId() + ",當前客戶端格個數為:" + sessionSet.size());
}
/**
* 收到客戶端訊息後自動觸發
*
* @param session
* @param textMessage :客戶端傳來的文字訊息
*/
@OnMessage
public void handleMessage(Session session, String textMessage) {
try {
logger.info("接收到客戶端資訊,session id=" + session.getId() + ":" + textMessage);
/**
* 原樣回覆文字訊息
* getBasicRemote:同步傳送
* session.getAsyncRemote().sendText(textMessage);非同步傳送
* */
session.getBasicRemote().sendText(textMessage);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 訊息傳輸錯誤後
*
* @param session
* @param throwable
*/
@OnError
public void handleTransportError(Session session, Throwable throwable) {
System.out.println("shake client And server handleTransportError,session.getId()=" + session.getId() + " -- " + throwable.getMessage());
logger.error("與客戶端 session id=" + session.getId() + " 通訊錯誤...");
}
}
注入 ServerEndpointExporter
注入 org.springframework.web.socket.server.standard.ServerEndpointExporter,這個 bean 會自動註冊使用了@ServerEndpoint 註解宣告的 Websocket endpoint 。
如果使用獨立的 servlet 容器,而不是使用 springboot 的內建容器,就不要注入ServerEndpointExporter,因為它將由 Tomcat 容器自己提供和管理。
因為傳統導包式 Tomcat websocket 開發時,是需要實現 javax.websocket.server.ServerApplicationConfig 介面的,然後由它去掃描整個應用中的 @ServerEndpoint,而現在這一步就由 springboot 的 ServerEndpointExporter 取代了。
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* Created by Administrator on 2018/11/28 0028.
*/
@Configuration
public class WebSocketConfig {
/**
* 建立 ServerEndpointExporter 元件,交由 spring IOC 容器管理,
* 它會自動掃描註冊應用中所有的 @ServerEndpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
前端 H5 webSocket 客戶端
前端 webSocket 客戶端為了方便直接使用 H5 的 webSocket 方式,不清楚的可以參考《HTML5 WebSocket》。
頁面的 html 與 css 樣式就不做提供了,直接提供客戶端 webSocket 的 js 程式碼。
/**
* web socket 繫結
*/
var ws = null;
function webSocketBind() {
/**主流瀏覽器現在都支援 H5 d的 webSocket 通訊,但建議還是要判斷*/
if ("WebSocket" in window) {
/**建立 web socket 例項
* 如果連線失敗,瀏覽器控制檯報錯,連線失敗
* 字首 ws:// 必須正確,yyServer 是應用名稱,websocket/yy.action 是後臺訪問路徑
* 192.168.1.20:websocket 伺服器地址
* */
ws = new WebSocket("ws://192.168.1.20/yyServer/websocket/yy.action");
/**onopen:伺服器連線成功後,自動觸發*/
ws.onopen = function () {
/** Web Socket 已連線上,使用 send() 方法傳送資料*/
//ws.send("connect success...");
console.log("伺服器連線成功,併發送資料到後臺...");
};
/**伺服器傳送資料後,自動觸發此方法,客戶端進行獲取資料,使用 evt.data 獲取資料*/
ws.onmessage = function (evt) {
var received_msg = evt.data;
console.log("接收到伺服器資料:" + received_msg);
showClientMessage(received_msg);
};
/**客戶端與伺服器資料傳輸錯誤時觸發*/
ws.onerror = function (evt) {
console.log("客戶端 與 伺服器 資料傳輸錯誤...");
};
/**web Socket 連線關閉時觸發*/
ws.onclose = function () {
console.log("web scoket 連線關閉...");
};
} else {
alert("您的瀏覽器不支援 WebSocket!");
}
}
/**
* 顯示伺服器傳送的訊息
* @param message
*/
let showServerMessage = function (message) {
if (message != undefined && message.trim() != "") {
/**
* 往伺服器傳送訊息
*/
ws.send(message.trim());
/**
* scrollHeight:div 區域內文件的高度,只能 DOM 操作,JQuery 沒有提供相應的方法
* @type {string}
*/
let messageShow = "<div class='messageLine server'><div class='messageContent serverCon'>" + message + "</div><span>:我</span>";
$(".centerTop").append(messageShow + "<br>");
$(".messageArea").val("");
let scrollHeight = $(".centerTop")[0].scrollHeight;
$(".centerTop").scrollTop(scrollHeight - $(".centerTop").height());
}
};
/**
* 顯示客戶端的訊息
* @param message
*/
let showClientMessage = function (message) {
if (message != undefined && message.trim() != "") {
/**
* scrollHeight:div 區域內文件的高度,只能 DOM 操作,JQuery 沒有提供相應的方法
* @type {string}
*/
let messageShow = "<div class='messageLine client'><span>伺服器:</span><div class='messageContent clientCon'>" + message + "</div>";
$(".centerTop").append(messageShow + "<br>");
$(".messageArea").val("");
let scrollHeight = $(".centerTop")[0].scrollHeight;
$(".centerTop").scrollTop(scrollHeight - $(".centerTop").height());
}
};
$(function () {
/**初始化後清空訊息傳送區域*/
$(".messageArea").val("");
/**
* 為 訊息 傳送按鈕繫結事件
*/
$(".sendButton").click(function () {
let message = $(".messageArea").val();
showServerMessage(message);
});
/**
* 繫結鍵盤敲擊事件 —— 用於按 回車鍵 傳送訊息
*/
$(window).keydown(function (event) {
if (event.keyCode === 13) {
let message = $(".messageArea").val();
showServerMessage(message);
}
});
/**
* 繫結 webSocket,連線 伺服器
*/
webSocketBind();
});
執行測試