WebSocket的故事系列計劃分五大篇六章,旨在由淺入深的介紹WebSocket以及在Springboot中如何快速構建和使用WebSocket提供的能力。本系列計劃包含以下幾篇文章:javascript
第一篇,什麼是WebSocket以及它的用途
第二篇,Spring中如何利用STOMP快速構建WebSocket廣播式消息模式
第三篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(1)
第四篇,Springboot中,如何利用WebSocket和STOMP快速構建點對點的消息模式(2)
第五篇,Springboot中,實現網頁聊天室之自定義WebSocket消息代理
第六篇,Springboot中,實現更靈活的WebSockethtml
本篇將經過一個接近真實的網頁聊天室Demo,來詳細講述如何利用WebSocket來實現一些具體的產品功能。本篇將只採用WebSocket自己,再也不使用STOMP等這些封裝。親自動手實現消息的接收、處理、發送以及WebSocket的會話管理。這也是本系列的最重要的一篇,無論大家激不激動,反正我是激動了。下面咱們就開始。java
想了解如何在Springboot上自定義實現更爲複雜的WebSocket產品邏輯的同窗以及各路有志青年。git
爲了可以目標明確的表達本文中所要講述的技術要點,我設計了一個小小聊天室產品,先列出需求,這樣你們在看後面的實現時可以知其因此然。github
以上就是咱們本篇要實現的需求。簡單說,就是:web
用戶可加入,退出某房間,加入後可向房間內全部人發送消息,也可向某我的發送悄悄話消息。json
很容易想到咱們設計的主體就是用戶、會話和房間,那麼在用戶管理上,咱們就能夠用下面這個圖來表示他們之間的關係:瀏覽器
這樣咱們就能夠用一個簡單的Map來存儲房間<->用戶組
這樣的映射關係,在用戶組內咱們再使用一個Map來存儲用戶名<->會話Session
這樣的映射關係(假設沒有重名)。這樣,咱們就解決了房間和用戶組、用戶和會話,這些關係的存儲和維護。服務器
有兄弟看到這說了,「你講這麼半天了,跟以前幾篇講的什麼STOMP,什麼消息代理,有毛線的關係?」大兄弟你先消消氣,咱們學STOMP,學消息代理,學點對點消息,重要的是學思想,你說對不?下面咱們就用上了。session
當用戶加入到某房間以後,房間裏有任何風吹草動,即有人加入、退出或者發公屏消息,都會「通知」給該用戶。到此,咱們就能夠將建立房間理解成「建立消息代理」,將用戶加入房間,當作是對房間這個「消息代理」的一個「訂閱」,將用戶退出房間,當作是對房間這個「消息代理」的一個「解除訂閱」。
那麼,第一個加入房間的人,咱們定義爲「建立房間」,即建立了一個消息代理。爲了好理解,上圖:
其中紅色的小人表示第一個加入房間的用戶,即建立房間的人。當某用戶發送消息時,若是選擇將消息發送給聊天室的全部人,即至關於在房間裏發送了一個廣播,全部訂閱這個房間的用戶,都會收到這個廣播消息;若是選擇發送悄悄話,則只將消息發送給特定用戶名的用戶,即點對點消息。
總結一下咱們要實現的要點:
大致設計就到此爲止,還有一些細節,咱們先來看一下演示效果,再來看經過代碼來說解實現。
用瀏覽器打開客戶端頁面後,展現輸入框和加入按鈕。輸入房間號1和用戶名小銘,點擊進入房間。
進入房間成功後,展現當前房間人數和歡迎語。
當有其餘人加入或退出房間時,展現通知信息。能夠發送公屏消息和私聊消息。
下面就讓咱們看一下這些主要功能如何來實現吧。
按照咱們上述的設計,我會着重介紹重點部分的代碼設計和技術要點。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/webSocket/{INFO}").setAllowedOrigins("*")
.addInterceptors(new WebSocketInterceptor());
}
}
複製代碼
要點解析:
WebSocketHandler
(MyHandler
),這是用來處理WebSocket創建以及消息處理的類,後面會詳細介紹。WebSocketInterceptor
攔截器,此攔截器用來在客戶端向服務端發起初次鏈接時,記錄客戶端攔截信息。{INFO}
參數,用來註冊的時候攜帶用戶信息。以上都會在後續代碼中詳細介紹。
public class WebSocketInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
if (serverHttpRequest instanceof ServletServerHttpRequest) {
String INFO = serverHttpRequest.getURI().getPath().split("INFO=")[1];
if (INFO != null && INFO.length() > 0) {
JSONObject jsonObject = new JSONObject(INFO);
String command = jsonObject.getString("command");
if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
System.out.println("當前session的ID="+ jsonObject.getString("name"));
ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
HttpSession session = request.getServletRequest().getSession();
map.put(MessageKey.KEY_WEBSOCKET_USERNAME, jsonObject.getString("name"));
map.put(MessageKey.KEY_ROOM_ID, jsonObject.getString("roomId"));
}
}
}
return true;
}
}
複製代碼
要點解析:
HandshakeInterceptor
用來攔截客戶端第一次鏈接服務端時的請求,即客戶端鏈接/webSocket/{INFO}
時,咱們能夠獲取到對應INFO
的信息。beforeHandshake
方法,進行用戶信息保存,這裏咱們將用戶名和房間號保存到Session
上。public class MyHandler implements WebSocketHandler {
//用來保存用戶、房間、會話三者。使用雙層Map實現對應關係。
private static final Map<String, Map<String, WebSocketSession>> sUserMap = new HashMap<>(3);
//用戶加入房間後,會調用此方法,咱們在這個節點,向其餘用戶發送有用戶加入的通知消息。
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("成功創建鏈接");
String INFO = session.getUri().getPath().split("INFO=")[1];
System.out.println(INFO);
if (INFO != null && INFO.length() > 0) {
JSONObject jsonObject = new JSONObject(INFO);
String command = jsonObject.getString("command");
String roomId = jsonObject.getString("roomId");
if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
Map<String, WebSocketSession> mapSession = sUserMap.get(roomId);
if (mapSession == null) {
mapSession = new HashMap<>(3);
sUserMap.put(roomId, mapSession);
}
mapSession.put(jsonObject.getString("name"), session);
session.sendMessage(new TextMessage("當前房間在線人數" + mapSession.size() + "人"));
System.out.println(session);
}
}
System.out.println("當前在線人數:" + sUserMap.size());
}
//消息處理方法
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) {
try {
JSONObject jsonobject = new JSONObject(webSocketMessage.getPayload().toString());
Message message = new Message(jsonobject.toString());
System.out.println(jsonobject.toString());
System.out.println(message + ":來自" + webSocketSession.getAttributes().get(MessageKey.KEY_WEBSOCKET_USERNAME) + "的消息");
if (message.getName() != null && message.getCommand() != null) {
switch (message.getCommand()) {
//有新人加入房間信息
case MessageKey.ENTER_COMMAND:
sendMessageToRoomUsers(message.getRoomId(), new TextMessage("【" + getNameFromSession(webSocketSession) + "】加入了房間,歡迎!"));
break;
//聊天信息
case MessageKey.MESSAGE_COMMAND:
if (message.getName().equals("all")) {
sendMessageToRoomUsers(message.getRoomId(), new TextMessage(getNameFromSession(webSocketSession) +
"說:" + message.getInfo()
));
} else {
sendMessageToUser(message.getRoomId(), message.getName(), new TextMessage(getNameFromSession(webSocketSession) +
"悄悄對你說:" + message.getInfo()));
}
break;
//有人離開房間信息
case MessageKey.LEAVE_COMMAND:
sendMessageToRoomUsers(message.getRoomId(), new TextMessage("【" + getNameFromSession(webSocketSession) + "】離開了房間,歡迎下次再來"));
break;
default:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/** * 發送信息給指定用戶 */
public boolean sendMessageToUser(String roomId, String name, TextMessage message) {
if (roomId == null || name == null) return false;
if (sUserMap.get(roomId) == null) return false;
WebSocketSession session = sUserMap.get(roomId).get(name);
if (!session.isOpen()) return false;
try {
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/** * 廣播信息給某房間內的全部用戶 */
public boolean sendMessageToRoomUsers(String roomId, TextMessage message) {
if (roomId == null) return false;
if (sUserMap.get(roomId) == null) return false;
boolean allSendSuccess = true;
Collection<WebSocketSession> sessions = sUserMap.get(roomId).values();
for (WebSocketSession session : sessions) {
try {
if (session.isOpen()) {
session.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
allSendSuccess = false;
}
}
return allSendSuccess;
}
//退出房間時的處理
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
System.out.println("鏈接已關閉:" + closeStatus);
Map<String, WebSocketSession> map = sUserMap.get(getRoomIdFromSession(webSocketSession));
if (map != null) {
map.remove(getNameFromSession(webSocketSession));
}
}
}
複製代碼
要點解析:
sUserMap
這個靜態變量來保存用戶信息。對應咱們上述的關係圖。afterConnectionEstablished
方法,當用戶進入房間成功後,保存用戶信息到Map
,並調用sendMessageToRoomUsers
廣播新人加入信息。handleMessage
方法,處理用戶加入,離開和發送信息三類消息。afterConnectionClosed
方法,用來處理當用戶離開房間後的信息銷燬工做。從Map
中清除該用戶。sendMessageToUser
、sendMessageToRoomUsers
兩個向客戶端發送消息的方法。直接經過Session
便可發送結構化數據到客戶端。sendMessageToUser
實現了點對點消息的發送,sendMessageToRoomUsers
實現了廣播消息的發送。客戶端咱們就使用HTML5爲咱們提供的WebSocket JS接口便可。
<html>
<script type="text/javascript"> function ToggleConnectionClicked() { if (SocketCreated && (ws.readyState == 0 || ws.readyState == 1)) { lockOn("離開聊天室..."); SocketCreated = false; isUserloggedout = true; var msg = JSON.stringify({'command':'leave', 'roomId':groom , 'name': gname, 'info':'離開房間'}); ws.send(msg); ws.close(); } else if(document.getElementById("roomId").value == "請輸入房間號!") { Log("請輸入房間號!"); } else { lockOn("進入聊天室..."); Log("準備鏈接到聊天服務器 ..."); groom = document.getElementById("roomId").value; gname = document.getElementById("txtName").value; try { if ("WebSocket" in window) { ws = new WebSocket( 'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}'); } else if("MozWebSocket" in window) { ws = new MozWebSocket( 'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}'); } SocketCreated = true; isUserloggedout = false; } catch (ex) { Log(ex, "ERROR"); return; } document.getElementById("ToggleConnection").innerHTML = "斷開"; ws.onopen = WSonOpen; ws.onmessage = WSonMessage; ws.onclose = WSonClose; ws.onerror = WSonError; } }; function WSonOpen() { lockOff(); Log("鏈接已經創建。", "OK"); $("#SendDataContainer").show(); var msg = JSON.stringify({'command':'enter', 'roomId':groom , 'name': "all", 'info': gname + "加入聊天室"}) ws.send(msg); }; </html> 複製代碼
要點解析:
'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}'
,這裏咱們在INFO
後加入了用戶我的信息,服務端收到後,便可根據這個信息標記此會話。ws.send()
方法實現。至此代碼部分就介紹完了,過多的代碼就再也不堆疊了,更詳細的代碼,請參見後面的Github地址。
經過一個相對完整的網頁聊天室例子,介紹了咱們本身使用WebSocket時的幾個細節:
HandshakeInterceptor
。WebSocketHandler
。WebSocketSession
便可向客戶端發送消息,經過用戶和Session
的綁定,實現對應關係。想加深理解的同窗,仍是要深刻到代碼中仔細體會。限於篇幅,並且在文章中加入大量代碼自己也不容易讀下去。因此你們仍是要實際對着代碼理解比較好。
歡迎持續關注原創,喜歡的別忘了收藏關注,碼字實在太累,大家的鼓勵就是我堅持的動力!
小銘出品,必屬精品
歡迎關注xNPE技術論壇,更多原創乾貨每日推送。