1. 前言:
SpringBoot+websocket的實(shí)現(xiàn)其實(shí)不難,你可以使用原生的實(shí)現(xiàn),也就是websocket本身的OnOpen、OnClosed等等這樣的注解來(lái)實(shí)現(xiàn),以及對(duì)WebSocketHandler的實(shí)現(xiàn),類似于netty的那種使用方式,而且原生的還提供了對(duì)websocket的監(jiān)聽(tīng),服務(wù)端能更好的控制及統(tǒng)計(jì)(即上文實(shí)現(xiàn)的方式)。
但是,真實(shí)項(xiàng)目中還是使用Stomp實(shí)現(xiàn)的居多,因?yàn)楠?dú)立服務(wù)更方便,便于后期搭建集群環(huán)境做橫向擴(kuò)展,且內(nèi)置的方法也很簡(jiǎn)單,既然如此,我們還是以主流實(shí)現(xiàn)方式為準(zhǔn)來(lái)學(xué)習(xí)吧。
2. stomp
當(dāng)直接使用WebSocket時(shí)(或SockJS)就很類似于使用TCP套接字來(lái)編寫Web應(yīng)用。因?yàn)闆](méi)有高層級(jí)的線路協(xié)議(wire protocol),因此就需要我們定義應(yīng)用之間所發(fā)送消息的語(yǔ)義,還需要確保連接的兩端都能遵循這些語(yǔ)義。
就像HTTP在TCP套接字之上添加了請(qǐng)求-響應(yīng)模型層一樣,STOMP在WebSocket之上提供了一個(gè)基于幀的線路格式(frame-based wire format)層,用來(lái)定義消息的語(yǔ)義。
與HTTP請(qǐng)求和響應(yīng)類似,STOMP幀由命令、一個(gè)或多個(gè)頭信息以及負(fù)載所組成。例如,如下就是發(fā)送數(shù)據(jù)的一個(gè)STOMP幀:
>>> SEND
transaction:tx-0
destination:/app/marco
content-length:20
{"message":"Marco!"}
3. 需求:
-
登陸:
-
1號(hào)用戶加入,發(fā)送消息:
-
2號(hào):
-
3號(hào):
-
退出:
-
要求使用stomp完成
4. websocket配置:
// websocket核心配置類
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注冊(cè)stomp端點(diǎn)
* @param registry stomp端點(diǎn)注冊(cè)對(duì)象
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 配置stomp端點(diǎn)地址
registry.addEndpoint("/ws").withSockJS();
}
/**
* 配置消息代理
* @param registry 消息代理注冊(cè)對(duì)象
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 定義客戶端訪問(wèn)服務(wù)端消息接口時(shí)的前綴
registry.setApplicationDestinationPrefixes("/app");
// 配置服務(wù)端推送消息給客戶端的代理路徑
registry.enableSimpleBroker("/topic");
// 定義點(diǎn)對(duì)點(diǎn)推送時(shí)的前綴為/queue
registry.setUserDestinationPrefix("/queue");
// Use this for enabling a Full featured broker like RabbitMQ
/*
registry.enableStompBrokerRelay("/topic")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest");
*/
}
}
其中:
-
@EnableWebSocketMessageBroker
:用于開(kāi)啟stomp協(xié)議,這樣就能支持@MessageMapping注解,類似于@requestMapping一樣,同時(shí)前端可以使用Stomp客戶端進(jìn)行通訊; -
WebSocketMessageBrokerConfigurer
接口:實(shí)現(xiàn)了其中的兩個(gè)方法:-
registerStompEndpoints
實(shí)現(xiàn):- 注冊(cè)一個(gè)websocket端點(diǎn),客戶端將使用它連接到我們的websocket服務(wù)器。
- withSockJS()是用來(lái)為不支持websocket的瀏覽器啟用后備選項(xiàng),使用了SockJS。
- 方法名中的STOMP是來(lái)自Spring框架STOMP實(shí)現(xiàn)。 STOMP代表簡(jiǎn)單文本導(dǎo)向的消息傳遞協(xié)議。它是一種消息傳遞協(xié)議,用于定義數(shù)據(jù)交換的格式和規(guī)則。為啥我們需要這個(gè)東西?因?yàn)閃ebSocket只是一種通信協(xié)議。它沒(méi)有定義諸如以下內(nèi)容:如何僅向訂閱特定主題的用戶發(fā)送消息,或者如何向特定用戶發(fā)送消息。我們需要STOMP來(lái)實(shí)現(xiàn)這些功能
-
configureMessageBroker
實(shí)現(xiàn):主要用來(lái)設(shè)置客戶端訂閱消息的路徑(可以多個(gè))、點(diǎn)對(duì)點(diǎn)訂閱路徑前綴的設(shè)置、訪問(wèn)服務(wù)端@MessageMapping接口的前綴路徑、心跳設(shè)置等;- 第一行定義了以“/app”開(kāi)頭的消息應(yīng)該路由到消息處理方法(之后會(huì)定義這個(gè)方法)。
- 第二行定義了以“/topic”開(kāi)頭的消息應(yīng)該路由到消息代理。消息代理向訂閱特定主題的所有連接客戶端廣播消息
-
5. model對(duì)象:
public class ChatMessage {
private MessageType type;
private String content;
private String sender;
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
public MessageType getType() {
return type;
}
public void setType(MessageType type) {
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
@Override
public String toString() {
return "ChatMessage{" +
"type=" + type +
", content='" + content + '\'' +
", sender='" + sender + '\'' +
'}';
}
}
6. controller接收和發(fā)送消息:
/**
* 發(fā)送廣播消息
* -- 說(shuō)明:
* 1)、@MessageMapping注解對(duì)應(yīng)客戶端的stomp.send('url');
* 2)、用法一:要么配合@SendTo("轉(zhuǎn)發(fā)的訂閱路徑"),去掉messagingTemplate,同時(shí)return msg來(lái)使用,return msg會(huì)去找@SendTo注解的路徑;
* 3)、用法二:要么設(shè)置成void,使用messagingTemplate來(lái)控制轉(zhuǎn)發(fā)的訂閱路徑,且不能return msg
*/
@Controller
@Slf4j
public class ChatController {
// 實(shí)現(xiàn)向?yàn)g覽器發(fā)送消息的功能
private final SimpMessagingTemplate messagingTemplate;
public ChatController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// 用法一:
@MessageMapping("/send")
public void sendAll(@RequestParam String msg) {
log.info("[發(fā)送消息]>>>> msg: {}", msg);
// 發(fā)送消息給客戶端
// 第一個(gè)參數(shù)是瀏覽器中訂閱消息的地址,第二個(gè)參數(shù)是消息本身
messagingTemplate.convertAndSend("/topic/public", msg);
}
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return chatMessage;
}
// 用法二:
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
// Add username in web socket session
headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
return chatMessage;
}
}
- 消息接口使用@MessageMapping注解,前面講的配置類@EnableWebSocketMessageBroker注解開(kāi)啟后才能使用這個(gè)
- 我們在websocket配置中,以/app開(kāi)頭的客戶端發(fā)送的所有消息都將路由到這些使用@MessageMapping注釋的消息處理方法。
例如,具有目標(biāo)/app/chat.sendMessage的消息將路由到sendMessage()方法,并且具有目標(biāo)/app/chat.addUser的消息將路由到addUser()方法 - 這里稍微提一下,真正線上項(xiàng)目都是把websocket服務(wù)做成單獨(dú)的網(wǎng)關(guān)形式,提供rest接口給其他服務(wù)調(diào)用,達(dá)到共用的目的,本項(xiàng)目因?yàn)椴簧婕叭魏螖?shù)據(jù)庫(kù)交互,所以直接用@MessageMapping注解,后續(xù)完整IM項(xiàng)目接入具體業(yè)務(wù)后會(huì)做一個(gè)獨(dú)立的websocket服務(wù)
- 發(fā)送消息的兩個(gè)用法:
- 用法一:要么配合@SendTo(“轉(zhuǎn)發(fā)的訂閱路徑”),去掉messagingTemplate,同時(shí)return msg來(lái)使用,return msg會(huì)去找@SendTo注解的路徑;
- 用法二:要么設(shè)置成void,使用messagingTemplate來(lái)控制轉(zhuǎn)發(fā)的訂閱路徑,且不能return msg
7. 添加websocket監(jiān)聽(tīng)事件
完成了上述代碼后,我們還需要對(duì)socket的連接和斷連事件進(jìn)行監(jiān)聽(tīng),這樣我們才能廣播用戶進(jìn)來(lái)和出去等操作。
@Component
public class WebSocketEventListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
logger.info("Received a new web socket connection");
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String username = (String) headerAccessor.getSessionAttributes().get("username");
if(username != null) {
logger.info("User Disconnected : " + username);
ChatMessage chatMessage = new ChatMessage();
chatMessage.setType(ChatMessage.MessageType.LEAVE);
chatMessage.setSender(username);
messagingTemplate.convertAndSend("/topic/public", chatMessage);
}
}
}
我們已經(jīng)在ChatController中定義的addUser()方法中廣播了用戶加入事件。因此,我們不需要在SessionConnected事件中執(zhí)行任何操作。
在SessionDisconnect事件中,編寫代碼用來(lái)從websocket會(huì)話中提取用戶名,并向所有連接的客戶端廣播用戶離開(kāi)事件。
8. 主要前端代碼:
'use strict';
var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('.connecting');
var stompClient = null;
var username = null;
var colors = [
'#2196F3', '#32c787', '#00BCD4', '#ff5652',
'#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];
function connect(event) {
username = document.querySelector('#name').value.trim();
if(username) {
usernamePage.classList.add('hidden');
chatPage.classList.remove('hidden');
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
}
event.preventDefault();
}
function onConnected() {
// Subscribe to the Public Topic
stompClient.subscribe('/topic/public', onMessageReceived);
// Tell your username to the server
stompClient.send("/app/chat.addUser",
{},
JSON.stringify({sender: username, type: 'JOIN'})
)
connectingElement.classList.add('hidden');
}
function onError(error) {
connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
connectingElement.style.color = 'red';
}
function sendMessage(event) {
var messageContent = messageInput.value.trim();
if(messageContent && stompClient) {
var chatMessage = {
sender: username,
content: messageInput.value,
type: 'CHAT'
};
stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
messageInput.value = '';
}
event.preventDefault();
}
function onMessageReceived(payload) {
var message = JSON.parse(payload.body);
var messageElement = document.createElement('li');
if(message.type === 'JOIN') {
messageElement.classList.add('event-message');
message.content = message.sender + ' joined!';
} else if (message.type === 'LEAVE') {
messageElement.classList.add('event-message');
message.content = message.sender + ' left!';
} else {
messageElement.classList.add('chat-message');
var avatarElement = document.createElement('i');
var avatarText = document.createTextNode(message.sender[0]);
avatarElement.appendChild(avatarText);
avatarElement.style['background-color'] = getAvatarColor(message.sender);
messageElement.appendChild(avatarElement);
var usernameElement = document.createElement('span');
var usernameText = document.createTextNode(message.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
var textElement = document.createElement('p');
var messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
messageArea.appendChild(messageElement);
messageArea.scrollTop = messageArea.scrollHeight;
}
function getAvatarColor(messageSender) {
var hash = 0;
for (var i = 0; i < messageSender.length; i++) {
hash = 31 * hash + messageSender.charCodeAt(i);
}
var index = Math.abs(hash % colors.length);
return colors[index];
}
usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)
代碼解釋:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-402719.html
- connect()函數(shù)使用SockJS和stomp客戶端連接到我們?cè)赟pring Boot中配置的/ws端點(diǎn)。
- 成功連接后,客戶端訂閱/topic/public,并通過(guò)向/app/chat.addUser目的地發(fā)送消息將該用戶的名稱告知服務(wù)器。
- stompClient.subscribe()函數(shù)采用一種回調(diào)方法,只要消息到達(dá)訂閱主題,就會(huì)調(diào)用該方法。
- 其它的代碼用于在屏幕上顯示和格式化消息。
參考:
https://blog.csdn.net/xiangyangsanren/article/details/123970860
https://blog.csdn.net/qqxx6661/article/details/98883166
https://blog.csdn.net/qq_53021672/article/details/124313430
https://blog.csdn.net/qq_35387940/article/details/108276136(最全的)文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-402719.html
到了這里,關(guān)于WebSocket(三) -- 使用websocket+stomp實(shí)現(xiàn)群聊功能的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!