目錄
-
-
- WebSocket簡介
- SpringBoot整合WebScoket
-
- 依賴
- 方式一
- 方式二
- 方式三(推薦)
- WebSocket的監聽器
- WebSocket的攔截器
-
WebSocket簡介
WebSocket是基于TCP的一種新的網絡協定,實作了浏覽器、伺服器之間的全雙工通信,允許伺服器主動發送資訊給用戶端。
WebSocket隻需要一次HTTP握手,整個通訊過程建立在一次連接配接狀态中。
WebSocket常用于伺服器推送實時資料給用戶端,常見的應用場景如下
- 彈幕
- 網頁聊天系統
- 公告
- 實時資料監控,eg. 雙11交易資料的實時監控
- 實時資料推送,eg. 伺服器給炒股軟體(用戶端)實時推送k線圖走勢
WebSocket中的廣播分為3類
- 單點傳播:點對點,常用于私信、私聊
- 多點傳播:也叫多點傳播,常用于推送消息給特定人群,eg. 群聊,推送消息給該群中的所有人。常用于多人聊天、釋出訂閱
- 廣播:推送消息給所有人,常用于推送公告、釋出訂閱
SpringBoot整合WebScoket
依賴
Messaging -> 勾選WebSocket,也可以手動添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
websocket前後端的寫法很多,此處介紹3種,第三種簡單強大,推薦。
方式一
背景自己實作Endpoint,前端使用内置的WebSocket。
配置類
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 掃描@ServerEndpoint,将@ServerEndpoint修飾的類注冊為websocket
* 如果使用外置tomcat,則不需要此配置
*/
@Bean
public ServerEndpointExporter serverEndpointExporter()
{
return new ServerEndpointExporter();
}
}
Endpoint 端點
@ServerEndpoint("/websocket")
@Component //放到spring容器中
@Slf4j
public class WebSocketServer{
/**
* 所有連接配接的用戶端
*/
private static ConcurrentHashMap<String,Session> clients = new ConcurrentHashMap<>();
/**
* 建立連接配接時調用的方法
*/
@OnOpen
public void onOpen(Session session) {
clients.put(session.getId(),session);
//向特定使用者發送消息,使用的session是接收方的session
session.getAsyncRemote().sendText("已加入群聊");
}
/**
* 連接配接關閉時調用的方法
*/
@OnClose
public void onClose(Session session) {
clients.remove(session.getId());
session.getAsyncRemote().sendText("已退出群聊");
}
/**
* 收到用戶端發送過來的消息時調用的方法
* @param msg 用戶端使用者發送過來的消息,二進制可以聲明為byte[]
*/
@OnMessage
public void onMessage(String msg) {
//群發消息
for (Session session : clients.values()) {
session.getAsyncRemote().sendText(msg);
}
}
/**
* 發生錯誤時調用的方法
*/
@OnError
public void onError(Session session, Throwable e) {
log.error("發送錯誤的sessionId:"+session.getId()+",錯誤資訊:"+e.getMessage());
}
}
接收到前端傳來的消息時,會自動調用onMessage()方法。
前端
<script>
let socket;
//手動打開連接配接
function openSocket() {
if(typeof(WebSocket) == "undefined") {
console.log("您使用的浏覽器不支援WebSocket");
}else{
//連接配接到websocket的某個endpoint
socket = new WebSocket("ws://127.0.0.1:8080/websocket");
//以下幾個方法相當于事件監聽,在特定事件觸發時會自動調用
socket.onopen = () => {
console.log("已連接配接到websocket");
};
socket.onmessage = resp => {
console.log("接收到服務端資訊:" + resp.data);
};
socket.onclose = () => {
console.log("已斷開websocket連接配接");
};
socket.onerror = () => {
console.log("websocket發生錯誤");
}
}
}
//手動關閉連接配接
function closeSocket() {
socket.close();
}
//發送消息到伺服器
function sendMsg(msg) {
//參數不一定要是字元串類型,可以是任意類型(二進制資料)
socket.send(msg);
}
</script>
說明
1、如果要同時實作單發、群發
- 前端可以将msg寫成對象,設定發送類型、接收方等屬性,将對象轉換為json字元串進行發送,然後在服務端的onMessage()中解析
- 也可以多寫幾個endpoint,一個endpoint作為單發、一個作為群發
2、getBasicRemote()、getAsyncRemote()的差別
getBasicRemote()是同步的,getAsyncRemote()是異步的,getBasicRemote()會抛出異常。
eg. sendText(),sendText() 前後發送2次消息
如果是同步的,會等第一個sendText()執行完畢才執行第二個sendText();如果是異步的,第一個sendText()開始執行後就繼續往下執行代碼。
3、sendText()發送文本内容,sendBinary()發送二進制資料
4、方式一簡單,但功能單一、能接收的資料類型有限,适合隻群發、不單發,消息内容簡單的場景。
方式二
後端使用@MessageMapping、@SendTo指定接受、推送位址,前端使用sockjs+stomp。
sockjs封裝了websocket,stomp是消息隊列模式。
配置類
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注冊端點
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注冊endpoint(服務端點),用于接收用戶端連接配接,用戶端的SockJS通過端點連接配接到websocket
// setAllowedOrigins是配置跨域,withSockJS是啟用SockJS支援
// 可注冊多個端點
registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
}
/**
* 配置伺服器接收消息、推送消息的位址字首
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 用戶端訂閱位址字首,用于用戶端訂閱服務端的某個
// registry.enableSimpleBroker("/topic");
// 服務端接收位址字首,用于服務端接收用戶端的消息
// registry.setApplicationDestinationPrefixes("/app");
}
}
實體類
@Getter
@Setter
@ToString
@AllArgsConstructor
public class Msg implements Serializable {
private String msgContent;
private String fromUserId;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date sendTime;
}
處理消息的controller
@Controller
public class MsgController {
@MessageMapping("/app/serverReceive") //@MessageMapping指定伺服器接收消息的位址,此方法隻處理發給該位址的消息
@SendTo("/topic/serverPush") //@SendTo指定消息的推送位址,會把return傳回的資料推送到指定的位址
public Msg msgHandler(Msg message){
return message;
}
}
前端
要引入2個核心的js檔案:sockjs.js、stomp.js
<noscript>您使用的浏覽器不支援websocket</noscript>
<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="text/javascript">
let stompClient = null;
//連接配接到websocket
function connect() {
//SockJS連接配接的是端點endpoint
let socket = new SockJS('/socket');
stompClient = Stomp.over(socket);
//參數:請求頭設定,連接配接成功的回調函數,連接配接失敗的回調函數
stompClient.connect({},frame => {
console.log("連接配接成功");
//stompClient訂閱的是推送位址。參數:訂閱位址、回調函數
stompClient.subscribe('/topic/serverPush',resp => {
// console.log(resp.body)
});
},error => {
console.log("連接配接失敗")
});
}
//斷開連接配接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
console.log("已斷開連接配接");
}
//發送消息
function sendMsg() {
//消息可以是對象
let msg={ 'fromUserId': $('#fromUserId').val(),'msgContent': $('#msgContent').val(),'sendTime':new Date()};
//參數:伺服器接收位址,請求頭設定,消息内容
stompClient.send("/app/serverReceive", {}, JSON.stringify(msg));
}
</script>
說明
1、不管在配置類中配不配置訂閱位址的字首,@SendTo()中都要寫全全路徑,不能省略字首。
如果在配置中配置了接受位址的字首,@MessageMapping中要省略字首;如果沒配置接受位址的字首,@MessageMapping中必須要寫全路徑。
2、fromUserId給其它使用者推送消息,推送時fromUserId本身也會收到自己發出的消息,但這個消息是本地的msg,其它人收到的msg是服務端推送的msg,可能在原msg的基礎上做了修改。
消息最好在本地就配置好,盡量不要在服務端修改消息,以保證每個使用者收到的消息是一緻的(主要是發送方和接受方收到的消息一緻)。
3、此種方式可以接受多種類型的消息内容,主要是可以接受對象形式的消息内容,但@SendTo适合群發,實作單點發送有點麻煩。此種方式與方式一相比,優點是可以接收對象形式的消息。
方式三(推薦)
在方式二的基礎上改進,使用SimpMessagingTemplate代替@SendTo注解。
配置類
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注冊端點
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
}
/**
* 配置伺服器接收消息、推送消息的位址字首
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 用戶端訂閱位址字首,用于用戶端訂閱服務端的某個
// 需要加上點對點的字首
registry.enableSimpleBroker("/topic","/user");
// 服務端接收位址字首,用于服務端接收用戶端的消息
registry.setApplicationDestinationPrefixes("/app");
//點對點使用的訂閱字首
registry.setUserDestinationPrefix("/user");
}
}
實體類
@Getter
@Setter
@ToString
@AllArgsConstructor
public class Msg implements Serializable {
private String msgContent;
private String fromUserId;
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date sendTime;
}
處理消息的controller
@Controller
public class MsgController {
@Autowired
private SimpMessagingTemplate template;
/**
* 群發
*/
@MessageMapping("/toAll")
public void toAll(Msg msg) {
//convertAndSend代替@SendTo指定目标位址
template.convertAndSend("/topic/toAll", msg);
}
/**
* 點對點
*/
@MessageMapping("/toOne")
// @Scheduled(fixedDelay = 1000L) //可以使用定時器實作定時推送
public void toOne(Msg msg) {
//參數:目前使用者的辨別,目标位址,消息内容。會将消息推送到目前使用者的頻道中,所有訂閱了目前使用者的用戶端都會收到消息
template.convertAndSendToUser(msg.getFromUserId(), "/toOne", msg);
}
}
前端
<noscript>您使用的浏覽器不支援websocket</noscript>
<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script type="text/javascript">
let stompClient = null;
//連接配接到websocket
function connect() {
//SockJS連接配接的是端點endpoint
let socket = new SockJS('/socket');
stompClient = Stomp.over(socket);
//參數:請求頭設定,連接配接成功的回調函數,連接配接失敗的回調函數
stompClient.connect({},frame => {
console.log("連接配接成功");
//訂閱全體消息
stompClient.subscribe('/topic/toAll',resp => {
// console.log(resp.body)
});
//訂閱點對點消息,/user/toUserId/toOne 此處訂閱的是目标使用者的消息
stompClient.subscribe('/user/' + $("#toUserId").val() + '/toOne', resp => {
// console.log("ok:"+resp.body);
});
},error => {
console.log("連接配接失敗")
});
}
//斷開連接配接
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
console.log("已斷開連接配接");
}
//群發消息
function sendMsgToAll() {
let msg={ 'fromUserId': $('#fromUserId').val(),'msgContent': $('#msgContent').val(),'sendDate':new Date()};
//參數:伺服器接收位址,請求頭設定,消息内容
stompClient.send("/app/toAll", {}, JSON.stringify(msg));
}
//點對點
function sendMsgToOne() {
let msg={ 'fromUserId': $('#fromUserId').val(),'msgContent': $('#msgContent').val(),'sendDate':new Date()};
//伺服器接收位址天@MessageMapping映射的即可
stompClient.send("/app/toOne", {}, JSON.stringify(msg));
}
</script>
說明
SimpMessagingTemplate比@SendTo更加靈活,支援多種發送方式,即可以實作群發,又可以實作點對點,且可以接收對象類型的消息,十分強大。
WebSocket的監聽器
WebSocket的監聽器可以監聽以下事件
- SessionSubscribeEvent 訂閱
- SessionUnsubscribeEvent 取消訂閱
- SessionDisconnectEvent 斷開連接配接
- SessionConnectEvent 建立連接配接
使用示例
建立類listener.ConnectEventListener
@Component //放到spring容器中
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> {
@Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("事件類型:"+headerAccessor.getCommand().getMessageType());
}
}
需要實作ApplicationListener接口,泛型指定要監聽的事件類型。一個類隻能監聽一個事件,如果要監聽多個事件,需要寫多個類。
WebSocket的攔截器
WebSocket的一次通信隻需要1次握手,HandshakeInterceptor 握手攔截器可以在握手前後進行攔截,做一些處理。
建立類intecepter.MyHandShakeIntecepter
public class MyHandShakeIntecepter implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request,ServerHttpResponse response,WebSocketHandler wsHandler,Map<String, Object> attributes) {
ServletServerHttpRequest req = (ServletServerHttpRequest) request;
HttpSession session = req.getServletRequest().getSession();
// 傳回的boolean辨別是否繼續往下執行
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request,ServerHttpResponse response, WebSocketHandler wsHandler,Exception exception) {
ServletServerHttpRequest req = (ServletServerHttpRequest) request;
HttpSession session = req.getServletRequest().getSession();
}
}
需要在websocket的配置中添加要使用的攔截器
/**
* 注冊端點
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.addInterceptors(new MyHandShakeIntecepter()) //攔截器
.setAllowedOrigins("*")
.withSockJS();
}