基於STOMP over Websocket協議實現實時在線通信的springboot項目
springboot集成Websocket通過stomp協議實現在線通信
Websocket協議
我們都知道,websocket協議是基於TCP的一種網絡通信協議。比常見的http協議不同,http協議是單工的,只能有客戶端發送請求,服務端響應並返回結果,這種協議足以應對通常場景,但是在需要客戶端和服務端實時通信時卻會大量佔用資源,效率低下。爲了提高效率,需要讓服務端主動發送消息給客戶端,這就是websocket協議。
STOMP協議
stomp是一個面向文本/流的消息協議,提供了能夠協作的報文格式,因此stomp客戶端可以與任何的stomp消息代理進行通信,從而可以爲多語言、多平臺和Brokers集羣提供簡單普遍的消息協作。
STOMP over Websocket
stomp overWebsocket既是通過Websocket建立stomp連接,即在Websocket的連接基礎上再建立stomp連接
springboot集成stomp構建服務端
1、引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
2、添加WebSocket相關配置類
import com.websocket.demo.interceptor.UserInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @Author hezhan
* @Date 2019/9/24 11:31
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 註冊stomp的端點
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//添加一個/stomp端點,客戶端可以通過這個端點進行連接,withSockJS的作用是添加SockJS支持
registry.addEndpoint("/stomp").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//客戶端發送消息的請求前綴
registry.setApplicationDestinationPrefixes("/webSocket");
//客戶端訂閱消息的請求前綴,topic一般用於廣播推送,user用於點對點推送,點對點推送的訂閱前綴必須與下面定義的通知客戶端的前綴保持一致
registry.enableSimpleBroker("/topic", "/user");
//服務端通知客戶端的前綴,可以不設置,默認爲user
registry.setUserDestinationPrefix("/user/");
}
/**
* 配置客戶端入站通道攔截器
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration){
registration.interceptors(createUserInterceptor());
}
/**
* 將自定義的客戶端渠道攔截器加入IOC容器中
* @return
*/
@Bean
public UserInterceptor createUserInterceptor(){
return new UserInterceptor();
}
}
自定義的客戶端通道攔截器代碼如下:
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
/**
* @Author hezhan
* @Date 2019/9/25 15:25
* 客戶端渠道攔截適配器
*/
public class UserInterceptor implements ChannelInterceptor {
/**
* 獲取包含在stomp中的用戶信息
*/
public Message<?> preSend(Message<?> message, MessageChannel channel){
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())){
String userName = accessor.getNativeHeader("name").get(0);
accessor.setUser(new FastPrincipal(userName));
}
return message;
}
}
其中有一個FastPrincipal類是自定義的用戶保存用戶信息的類:
import java.security.Principal;
/**
* @Author hezhan
* @Date 2019/9/24 16:51
* 權限驗證類
*/
public class FastPrincipal implements Principal {
private final String name;
public FastPrincipal(String name){
this.name = name;
}
@Override
public String getName() {
return name;
}
}
3、Controller層代碼編寫
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal;
/**
* @Author hezhan
* @Date 2019/9/24 17:28
*/
@RestController
public class StompController {
@Autowired
SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/message")
public void subscription(Message message, Principal principal) throws Exception {
simpMessagingTemplate.convertAndSendToUser(message.getTo(), "/queue", new Message(message.getMessage(), message.getDatetime(), message.getFrom(), message.getTo()));
System.out.println(principal.getName() + "發送了一條消息給:" + message.getTo());
}
}
SimpMessagingTemplate爲springboot封裝的操作WebSocket的類,使用convertAndSendToUser(參數一,參數二,參數三)方法,可以向指定的用戶發送消息,其中參數一爲指定的用戶標識,參數二爲自定義的客戶端訂閱信息的URL,參數三爲發送的具體消息。
若想不指定用戶推送,則可以使用convertAndSend(參數一,參數二)方法,其中參數一爲制定的訂閱URL,參數二爲推送的消息主體,其中指定的訂閱URL前綴必須爲WebSocketConfig配置類中指定的訂閱前綴。
4、消息實體類
爲了方便消息的推送,我們這裏可以規範一下消息的格式:
/**
* @Author hezhan
* @Date 2019/9/25 15:40
* 通信消息規範格式
*/
public class Message {
private String message;//消息內容
private String datetime;//發送時間
private String from;//消息來源ID
private String to;//發送消息給ID
public Message(String message, String datetime, String from, String to) {
this.message = message;
this.datetime = datetime;
this.from = from;
this.to = to;
}
public Message() {
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDatetime() {
return datetime;
}
public void setDatetime(String datetime) {
this.datetime = datetime;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
}
以上,服務端的代碼已經編寫完畢,挺簡單的吧,畢竟springboot已經封裝了大部分底層實現。接下來是客戶端的代碼編寫。
使用sockjs實現客戶端
在這裏具體我們用html+sockjs來實現客戶端的構建
客戶端引入sockjs
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
具體HTML頁面代碼
話不多說,直接上代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>stomp測試</title>
<link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
<div class="row">
<div class="col-md-6">
<form class="form-inline">
<div class="form-group">
<label for="connect">註冊用戶,請輸入你的賬號:</label>
<input type="text" id="userName" class="form-control" value="tony" placeholder="請輸入賬號"/>
<button id="connect" class="btn" type="submit">連接</button>
</div>
</form>
</div>
<div>
<form class="form-inline">
<div class="form-group">
<label for="name">接收人:</label>
<input type="text" id="name" class="form-control"/>
<label for="message">發送消息:</label>
<input type="text" id="message"/>
</div>
<button id="send" class="btn" type="submit">發送</button>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="conversation" class="table">
<thead>
<tr>
<th>接收到的消息</th>
</tr>
</thead>
<tbody id="greetings"></tbody>
</table>
</div>
</div>
</div>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", connected);
if (connected){
$("#conversation").show();
} else {
$("#conversation").hide();
}
$("#greetings").html("");
}
var url = "http://localhost:8080";
function connect() {
var userName = $("#userName").val();
var socket = new SockJS(url + "/stomp?name=" + userName);
stompClient = Stomp.over(socket);
stompClient.connect({
name:$("#userName").val()
}, function (frame) {
setConnected(true);
console.log("connected:" + frame);
stompClient.subscribe("/user/" + $("#userName").val() +"/queue", function (data) {
var mes = data.body;
showGreeting(mes);
});
});
}
function send() {
stompClient.send("/webSocket/message", {}, JSON.stringify({
message:$("#message").val(),
datetime:"2019-09-25",
from:$("#userName").val(),
to:$("#name").val()
}));
}
function showGreeting(message) {
console.log("顯示信息:" + message);
var json = JSON.parse(message).message;
$("#greetings").append("<tr><td>" + json + "</td></tr>");
}
$(function () {
$("form").on("submit", function (e) {
e.preventDefault();
});
$("#connect").click(function () {
connect();
});
$("#send").click(function () {
send();
});
});
</script>
</body>
</html>
代碼分析
代碼上完後,接下來分析一波。
先看一下客戶端與服務端連接已經訂閱客戶端消息的方法
var url = "http://localhost:8080";
function connect() {
var userName = $("#userName").val();
var socket = new SockJS(url + "/stomp?name=" + userName);
stompClient = Stomp.over(socket);
stompClient.connect({
name:$("#userName").val()//這裏必須要攜帶用戶信息,這樣服務端UserInterceptor客戶端攔截器才能從getNativeHeader("name")中拿到用戶信息
}, function (frame) {
setConnected(true);
console.log("connected:" + frame);
/*這裏是客戶端訂閱服務端的消息,
訂閱路徑爲/user/指定的用戶名/queue,
就是對應上面服務端controller層中convertAndSendToUser方法制定的路徑,
由於這裏是點對點通信,所以制定的路徑前面會加上/user/前綴,之後還要加上指定的用戶信息,
如果是不指定用戶名的推送的訂閱,則URL直接爲服務端convertAndSend方法中定義的URL*/
stompClient.subscribe("/user/" + $("#userName").val() +"/queue", function (data) {
var mes = data.body;//這裏獲取服務端推送的消息主體
showGreeting(mes);
});
});
}
接下來說明一下客戶端發送消息的代碼
function send() {
/*
這裏發送的路徑爲客戶端請求前綴加contronller層方法的路由,
再加上遵循上面自定義的消息規範參數,
這樣就將這些這些消息發送給指定的用戶了
*/
stompClient.send("/webSocket/message", {}, JSON.stringify({
message:$("#message").val(),
datetime:"2019-09-25",
from:$("#userName").val(),
to:$("#name").val()
}));
}
測試效果