Spring MVC使用WebSocket統計即時在線人數
目前昕力大學網站是用JAVA的Sring MVC框架開發,人數統計系統是利用session來實現,雖然這種方式十分直覺且方便,亦是一般常被使用的方式,但此種統計方式具有某些缺陷:
其一是每個session會保留至少30分鐘,因此統計出來的並非真正的在線人數,而是代表30分鐘內瀏覽過昕力大學的人數。所以昕力大學網頁標頭顯示的人數,看起來總像是灌過水就是這個緣故。
其二為網頁開啟後,只要不刷新頁面,看到的人數就會一直保持在當初開啟網頁時的數字,不會即時更新,結合第一點,我們看到的數字與實際線上人數其實有很大的差距。
為了揭開事情的真相,取得精確的人數,解決數字灌水的問題,因此我捨棄了session的作法,改用WebSocket進行人數統計。詳細作法如下:
一、pom.xml添加依賴
<!-- websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring-framework.version}</version>
</dependency>
二、 建立MessageHandler
public class MessageHandler extends TextWebSocketHandler {
private List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
//統計總共有多少CSRF Token用來代表人數
private HashMap<String , String> userMap = new HashMap<String, String>();
//關閉連線 刪除session
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session); //從List中移除
userMap.remove(session.getId());//從HashMap中移除
}
//連線成功 新增session
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
}
//處理用戶發送的訊息 再回傳給其他用戶
//這裡只回傳人數統計
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//連線成功時 會傳一個包含csrf token的訊息 將其加入HashMap
String id = session.getId();
String token = message.getPayload();
userMap.put(id, token);
//計算總共有多少不同的csrftoken
//將所有CSRF Token放入Set就不會有重複的值
Set<String> csrfSet = new HashSet<String>();
Set<String> keysSet = userMap.keySet();
for(String key : keysSet) {
String csrf = userMap.get(key);
csrfSet.add(csrf);
}
//目前人數 = CSRF Token的數量
String onlineUsersNumber = String.valueOf(csrfSet.size());
//發送在線人數給所有人
for(WebSocketSession wss : sessions) {
wss.sendMessage(new TextMessage(onlineUsersNumber));
}
}
}
此程式用來收發WebSocket的訊息以及統計即時在現人數。其中使用List來儲存用戶WebSocketSession,當人數更新時即發送在線人數給所有用戶。另外再建立一個HashMap來存放用戶ID(包含在WebSocketSession之中)和用戶從前端傳回來的CSRF Token。
至於使用HashMap統計人數的原因,是因為只用WebSocketSession統計人數的話,若同一個人開啟多個分頁,每個分頁會產生各自的WebSocket連線以及WebSocketSession,人數就會重複計算。為避免此種情況,每個用戶勢必要有唯一的辨識方式,剛好昕力大學這專案有使用Spring Security的CSRF Token功能。因此我就設定在網頁成功建立WebSocket連線時,將CSRF Token回傳給WebSocket Server並存在HashMap內。如此一來只要統計CSRF Token就能得知人數。
或許有人會覺得奇怪,為何在WebSocket斷開連線的時候不發送人數變化的訊息給所有使用者?因為昕力大學目前網站目前為MVC架構,而非前後分離,因此使用者在瀏覽網站的時候會時常切換頁面。用戶在離開舊頁面時斷開WebSocket會使在線人數減一,到新頁面後又會使人數加一。當使用者多,許多人在切換頁面的話,就會導致其他用戶畫面上的在線人數一直做無意義的跳動。
為避免此情況,因此只在建立新連線時才發送人數訊息給所有用戶。
三、建立WebSocketConfig
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MessageHandler(), "/websocket")//設定連結
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
四、 在WebApplicationInitializer註冊WebSocketConfig
public class WebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) throws ServletException {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(WebSocketConfig.class);//註冊websocket
...
}
}
五、 在Spring Security中開啟Websocket的路徑權限
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/websocket").permitAll()
...
}
}
六、 前端網頁Javascript加入Websocket的連線程式
var websocket;
function connect(){ //初始化連線
try {
var protocol = window.location.protocol;
if(protocol == "https:"){
websocket = new WebSocket("wss://"+ window.location.host + "/tpu/websocket");
}else{
websocket = new WebSocket("ws://"+ window.location.host + "/tpu/websocket");
}
} catch (ex) {
console.log(ex);
console.log("websocket連接異常");
}
connecting();
window.addEventListener("load", connecting, false);
}
function connecting() {
websocket.onopen = function(evt) {
onOpen(evt)
}
websocket.onclose = function(evt) {
onClose(evt)
}
websocket.onmessage = function(evt) {
onMessage(evt)
}
websocket.onerror = function(evt) {
onError(evt)
}
}
//連線上事件
function onOpen(evt) {
console.log("WebSocket 連線成功");
websocket.send(csrfToken);//將CSRF Token傳送給WebSocket Server
}
//關閉事件
function onClose(evt) {}
//後端推送事件
function onMessage(evt) {
console.log("WebSocket獲得目前在線人數:" + evt.data);
showMessage(evt.data);
}
//發生錯誤
function onError(evt) {}
//瀏覽器主動斷開連線
function wsclose() {
websocket.close();
}
function showMessage(message) {
$("#header_visitors").html('線上人數:' + message);
$("#footer_visitors").html('線上人數:' + message);
}
$(document).ready(function(){
//啟動連線
connect();
});
當連線成功之時會自動將CSRF Token發送給WebSocket Server,WebSocket再將統計出來的人數發送給所有用戶,再顯示在網頁上。
需要注意的是,當網址的開頭從http改為https時,WebSocket連線的開頭也要從ws改為wws,否則會無法建立連線。由於昕力大學的測試機是使用http,但正式主機是https,所以我這邊有做判斷並分別建立連線。
啟動測試伺服器並打開網頁,就可以看到Console打印出「WebSocket連線成功」以及取得在線人數了!而且即使一人開啟多個分頁,也不會重複統計。
沒考慮到connect斷線的情況