前言
前兩周經常有大學生小伙伴私信給我,問我可否有償提供畢設幫助,我說暫時沒有這個打算,因為工作實在太忙,現階段無法投入到這樣的領域內,其中有兩個小伙伴又問到我websocket該怎么使用,想給自己的項目中加入這樣的技術。
剛好我所在的公司有做問診服務,里面就使用了websocket實現聊天通訊,就在閑暇之余專門把部分代碼摘取出來,做了一個簡單的demo分享給他們了,之后想想這塊可以再豐富一下,就花時間又做了一個更完整的小項目出來,且加了詳細的注釋說明,分享給對websocket感興趣的小伙伴們。
案例展示

技術棧
考慮到不同群體對vue等前端技術的接受程度,本案例采用了HTML+CSS+JQuery來實現,代碼直接復制到vue項目中也是一樣的,只是賦值和取值的方式改變而已,很多Java程序員其實對於一門簡單案例的學習不喜歡牽扯太多前端技術,而是單純學習想知道的這門技術就好,太多其他的引入反而影響跟蹤調試,而原始的HTML+JS方式更有利於我們學習和理解,只需要右鍵HTML頁面在瀏覽器打開進行F12調試即可。
| 技術 | 版本 |
|---|---|
| Java | 1.8 |
| SpringBoot | 2.3.12.RELEASE |
| WebSocket | 2.3.12.RELEASE |
| Hutools | 5.8.0.M1 |
| SockJS | 1.6.0 |
| StompJS | 1.7.1 |
實現過程
1、引入依賴
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- Hutools工具類 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0.M1</version>
</dependency>
2、訂閱常量類
后面的websocket配置類會用到這幾個常量
stomp端點地址: 連接websocket時的后綴地址,比如127.0.0.1:8888/websocket。
websocket前綴:前端調服務端消息接口時的URL都加上了這個前綴,比如默認是/send,變成/app/send。
點對點代理地址:如果websocket配置類中設置了代理路徑,一般點對點訂閱路徑喜歡用/queue。
廣播代理地址:如果websocket配置類中設置了代理路徑,一般廣播訂閱路徑喜歡用這個/topic。
package com.simple.ws.constants;
/**
* <p>
* websocket常量
* </p>
*
* @author 福隆苑居士,公眾號:【Java分享客棧】
* @since 2022-04-02 10:11
*/
public class WsConstants {
// stomp端點地址
public static final String WEBSOCKET_PATH = "/websocket";
// websocket前綴
public static final String WS_PERFIX = "/app";
// 消息訂閱地址常量
public static final class BROKER {
// 點對點消息代理地址
public static final String BROKER_QUEUE = "/queue/";
// 廣播消息代理地址
public static final String BROKER_TOPIC = "/topic";
}
}
3、WebSocket配置類
核心內容講解:
1)、@EnableWebSocketMessageBroker:用於開啟stomp協議,這樣就能支持@MessageMapping注解,類似於@requestMapping一樣,同時前端可以使用Stomp客戶端進行通訊;
2)、registerStompEndpoints實現:主要用來注冊端點地址、開啟跨域授權、增加攔截器、聲明SockJS,這也是前端選擇SockJS的原因,因為spring項目本身就支持;
3)、configureMessageBroker實現:主要用來設置客戶端訂閱消息的路徑(可以多個)、點對點訂閱路徑前綴的設置、訪問服務端@MessageMapping接口的前綴路徑、心跳設置等;
package com.simple.ws.config;
import com.simple.ws.constants.WsConstants;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
/**
* <p>
* websocket核心配置類
* </p>
*
* @author 福隆苑居士,公眾號:【Java分享客棧】
* @since 2022/4/1 22:57
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注冊stomp端點
*
* @param registry stomp端點注冊對象
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(WsConstants.WEBSOCKET_PATH)
.setAllowedOrigins("*")
.withSockJS();
}
/**
* 配置消息代理
*
* @param registry 消息代理注冊對象
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 配置服務端推送消息給客戶端的代理路徑
registry.enableSimpleBroker(WsConstants.BROKER.BROKER_QUEUE, WsConstants.BROKER.BROKER_TOPIC);
// 定義點對點推送時的前綴為/queue
registry.setUserDestinationPrefix(WsConstants.BROKER.BROKER_QUEUE);
// 定義客戶端訪問服務端消息接口時的前綴
registry.setApplicationDestinationPrefixes(WsConstants.WS_PERFIX);
}
}
特別說明:如果對於配置類中這幾個路徑的設置看不明白,沒關系,后面的前端部分你一看就懂了。
4、消息接口
說明:
1)、消息接口使用@MessageMapping注解,前面講的配置類@EnableWebSocketMessageBroker注解開啟后才能使用這個;
2)、這里稍微提一下,真正線上項目都是把websocket服務做成單獨的網關形式,提供rest接口給其他服務調用,達到共用的目的,本項目因為不涉及任何數據庫交互,所以直接用@MessageMapping注解,后續完整IM項目接入具體業務后會做一個獨立的websocket服務,敬請關注哦!
package com.simple.ws.controller;
import com.simple.ws.constants.WsConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* <p>
* 消息接口
* </p>
*
* @author 福隆苑居士,公眾號:【Java分享客棧】
* @since 2022-04-02 12:00
*/
@RestController
@RequestMapping("/api")
@Slf4j
public class MsgController {
private final SimpMessagingTemplate messagingTemplate;
public MsgController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
/**
* 發送廣播消息
* -- 說明:
* 1)、@MessageMapping注解對應客戶端的stomp.send('url');
* 2)、用法一:要么配合@SendTo("轉發的訂閱路徑"),去掉messagingTemplate,同時return msg來使用,return msg會去找@SendTo注解的路徑;
* 3)、用法二:要么設置成void,使用messagingTemplate來控制轉發的訂閱路徑,且不能return msg,個人推薦這種。
*
* @param msg 消息
*/
@MessageMapping("/send")
public void sendAll(@RequestParam String msg) {
log.info("[發送消息]>>>> msg: {}", msg);
// 發送消息給客戶端
messagingTemplate.convertAndSend(WsConstants.BROKER.BROKER_TOPIC, msg);
}
5、前端項目結構
很簡單,就是HTML+CSS和幾個js文件,sockjs和stompjs就是和服務端通信的實現,可以從GitHub官網下載,而websocket.js是我們自己封裝的和服務端通信的內容。

6、Stomp客戶端使用
頁面結構樣式這里就省略不講了,直接開始正文。
stompjs,是對websocket原生使用的一層封裝,提供了更簡單的調用方法。
這里先看看我們自己封裝的websocket.js的實現:
1)、聲明URL
也就是服務端配置類的端點地址
var url = "http://127.0.0.1:8888/websocket"; // 改成自己的服務端地址
2)、建立websocket連接
stompClient = Stomp.over(socket)就是覆蓋了sockjs使用自己的客戶端來操作ws;
進入stompClient.connect()中就代表連接成功,可以進行自己的業務處理比如廣播通知某人上線等等,重要的是連接成功后要聲明訂閱列表,這樣服務端轉發的消息才會根據這些訂閱地址發送過來,否則收不到;
最后就是有一個回調可以捕獲異常情況,在里面可以做一些操作比如重連等等。
/**
* 連接
*/
function connect() {
userId = GetUrlParam("userId");
var socket = new SockJS(url, null, { timeout: 15000});
stompClient = Stomp.over(socket); // 覆蓋sockjs使用stomp客戶端
stompClient.connect({}, function (frame) {
console.log('frame: ' + frame)
// 連接成功后廣播通知
sendNoticeMsg(userId, "in");
/**
* 訂閱列表,訂閱路徑和服務端發消息路徑一致就能收到消息。
* -- /topic: 服務端配置的廣播訂閱路徑
* -- /queue/: 服務端配置的點對點訂閱路徑
*/
stompClient.subscribe("/topic", function (response) {
showMsg(response.body);
});
stompClient.subscribe("/queue/" + userId + "/topic", function (response) {
showMsg(response.body);
});
// 異常時進行重連
}, function (error) {
console.log('connect error: ' + error)
if (reConnectCount > 10) {
console.log("溫馨提示:您的連接已斷開,請退出后重新進入。")
reConnectCount = 0;
} else {
wsReconnect && clearTimeout(wsReconnect);
wsReconnect = setTimeout(function () {
console.log("開始重連...");
connect();
console.log("重連完畢...");
reConnectCount++;
}, 1000);
}
}
)
}
3)、斷開websocket連接
斷開很簡單,但要注意一點,不要根據關閉窗口或瀏覽器的事件來控制斷開,這是一個誤區,首先瀏覽器兼容性差異較大,傳統的js在監聽窗口關閉事件的兼容性上是很差的,這個可以自己試驗就知道了,有些瀏覽器可以有些不可以;
其次,可以參考QQ,你自己在退出一個群聊的時候實際上你就單純是關閉了,並沒有離線,而是你退出QQ時才真正離線,所以真正控制這個斷開方法的位置應該是點擊退出按鈕時,這一點不要理解錯了。
/**
* 斷開
*/
function disconnect() {
if (stompClient != null) {
// 斷開連接時進行廣播通知
sendNoticeMsg(userId, "out");
// 斷開連接
stompClient.disconnect(function(){
// 有效斷開的回調
console.log(userId + "斷開連接....")
});
}
}
4)、消息滾動到底部
這個沒什么說的,在進入頁面以及發送消息后渲染頁面時使用即可。
// 消息窗口滾動到底部
function scrollBotton(){
var div = document.getElementById("content");
div.scrollTop = div.scrollHeight;
}
5)、聊天消息渲染到頁面
這里就是單純的JQuery操作了,注意的一點是這里加了個type判斷是系統消息還是聊天消息,在本案例中,系統消息就是某人上下線的提示,聊天消息就是發送出來的內容。
在vue這樣的框架中,這部分的操作其實會很簡單。
/**
* 聊天消息渲染到頁面中
*/
function showMsg(obj) {
obj = JSON.parse(obj);
var userId = obj.userId;
var sendTime = obj.sendTime;
var info = obj.info;
var type = obj.type;
if (1 === type) {
// 聊天消息
console.log("聊天消息...")
var msgHtml = "<div class=\"msg\" id=\"msg\">" +
" <div class=\"first-line\">" +
" <div class=\"userName\" id=\"userName\">" + userId + "</div>" +
" <div class=\"sendTime\" id=\"sendTime\">" + sendTime + "</div>" +
" </div>" +
" <div class=\"second-line\">" +
" <div class=\"sendMsg\" id=\"sendMsg\">" + info + "</div>" +
" </div>" +
"</div>";
// 渲染到頁面
$("#content").html($("#content").html() + "\n" + msgHtml);
} else if (2=== type) {
// 系統消息
console.log("系統消息...")
var msgHtml = "<div class=\"notice\">" +
"<div class=\"notice-info\">" + info + "</div>" +
"</div>";
// 渲染到頁面
$("#content").html($("#content").html() + "\n" + msgHtml);
}
// 消息窗口滾動到底部
scrollBotton();
}
6)、發送群聊消息
這里傳遞的obj定義了一個消息體,就是一個對象,真正項目中也是這般使用,而不是單純傳遞一個文本;
stompClient.send中的url,其中/app是服務端配置類中設置的ApplicationDestinationPrefixes,而/send就是controller接口中@MessageMapping("/send")的路徑,兩個加在一起就是這里前端發送的路徑,少一個或多一個斜杠都會導致服務端收不到消息。
/**
* 發送群聊消息
* -- 這里我們傳遞消息體對象
* {
* "userId": userId, // 發送者
* "sendTime": sendTime, // 發送時間
* "info": info, // 發送內容
* "type": 1 // 消息類型,1-聊天消息,2-系統消息
* }
*/
function sendAll(obj) {
stompClient.send("/app/send", {}, JSON.stringify(obj));
}
7)、發送系統消息
就是傳遞type=2即可,info做了下判斷返回不同的消息內容。
/**
* 發送系統通知消息
* @param userId 用戶id
*/
function sendNoticeMsg(userId, action) {
var obj = {
"userId": userId,
"sendTime": new Date().Format("HH:mm:ss"),
"info": "in" === action ? userId + "進入房間" : userId + "離開房間",
"type": 2
}
sendAll(obj);
}
7、聊天頁發消息
index.html就是聊天主頁面,直接調用我們前面封裝好的websocket.js方法即可。
主要步驟為:進入頁面時建立websocket連接 --> 獲取登錄用戶信息 --> 監聽按鈕點擊事件和鍵盤事件 --> 發送websocket消息 --> 清空文本框內容
這樣,一旦發送消息成功,服務端就可以看到接收到的消息體並根據發送路徑進行轉發,前端websocket.js中訂閱列表中的路徑一旦和服務端轉發的路徑匹配上,就會收到消息,我們把消息渲染到頁面上即可。
這個過程其實也就是websocket全雙工通信的原理
<script>
$(function() {
// 啟動websocket
connect();
// 獲取用戶信息
getUser();
// 消息窗口滾動到底部
scrollBotton();
// 監聽鍵盤Enter鍵,要用keyup,否則無法清除換行符。
$("#send-info").keyup(function(e) {
var eCode = e.keyCode ? e.keyCode : e.which ? e.which : e.charCode;
if (eCode == 13){
$("#send-btn").click();
}
});
// 監聽發送按鈕點擊事件
$("#send-btn").click(function() {
send();
});
// 監聽退出按鈕點擊事件
$("#exit-btn").click(function() {
layer.confirm('你確定要退出嗎?', {
time: 0, // 不自動關閉
btn: ['確定退出', '再玩玩'],
yes: function(index){
layer.close(index);
disconnect();
window.location.href = 'login.html';
}
});
});
});
// 獲取用戶信息
function getUser() {
var userId = GetUrlParam("userId");
$("#userId").text(userId);
}
// 發送廣播消息,這里定義一個type:1-聊天消息,2-系統消息。
function send() {
var userId = $("#userId").text().trim();
var sendTime = new Date().Format("HH:mm:ss");
var info = $("#send-info").val().replace("\n", "");
var msg = {
"userId": userId,
"sendTime": sendTime,
"info": info,
"type": 1
}
// 發送消息
sendAll(msg);
// 清空文本域內容
$("#send-info").val("");
}
</script>
避坑指南
1)、版本問題,經本人專門花時間測試,SpringBoot2.4.0以下版本才能整合SockJS和StompJS成功,以上的版本都不行,會報 Main site uses: "1.6.0", the iframe: "1.0.0" 這樣的錯誤,將StompJS換成低版本也不行,所以這里整合時用了SpringBoot2.3.12.RELEASE版本,但這個沒關系,websocket服務一般都是單獨做成一個服務的,如果是微服務,你的其他業務服務使用高版本的SpringBoot就行了;
2)、監聽窗口關閉事件不可取,這個在前面已經講過了,瀏覽器兼容性差,我試過好幾個瀏覽器監聽效果都各不相同甚至完全無效,其次本身這樣操作也不合理,我們只要保證退出時觸發斷開事件即可,無需在這樣的事情上浪費時間,可以參考QQ;
3)、服務端編寫消息接口時推薦使用SimpMessagingTemplate來控制發送,而不是@SendTo注解,因為前者更符合程序員開發思路,后期獨立websocket服務暴露rest接口時也更簡單;
4)、配置類中其實還有很多其他配置項,比如心跳配置、攔截器配置等,本案例沒有加入進來,因為我自己公司的項目中其實使用過心跳,但后來又去掉了,因為對這塊了解不深入的話貿然使用容易出現稀奇古怪的問題。
講個趣事,我們前端工程師當初就因為心跳這塊調試了挺久,上線后依然會出現時好時壞的情況,因為他之前也沒做過websocket都是現學的,而且線上環境和測試環境差異難明,包含程序缺陷、網絡環境因素等等,后來我們決定去掉心跳檢測,之后兩年也沒出任何問題。
所以有時候保證項目穩定性反而更有用,但處於學習的角度而言,心跳檢測是一定需要的,否則所有的socket框架也不會專門提供這樣的方案了。
總結
SpringBoot+websocket的實現其實不難,你可以使用原生的實現,也就是websocket本身的OnOpen、OnClosed等等這樣的注解來實現,以及對WebSocketHandler的實現,類似於netty的那種使用方式,而且原生的還提供了對websocket的監聽,服務端能更好的控制及統計。
但根據我個人的經驗而言,真實項目中還是使用Stomp實現的居多,因為獨立服務更方便,便於后期搭建集群環境做橫向擴展,且內置的方法也很簡單,既然如此,我們還是以主流實現方式為准來學習吧。
源碼
鏈接: https://pan.baidu.com/doc/share/tt1_DVkvVQ9BQoXJiM07hg-793418840542403
提取碼: agiu
后續會根據本案例進行優化,設計具體的業務表,實現群聊、單聊、心跳檢測,同時前端以vue3來搭建,實現一個完整的IM應用,有興趣的可以關注下本人以獲取最新資訊哦~
本人原創文章純手打,覺得有一滴滴幫助的話,就請點個贊和推薦吧,鞠躬~
