首頁>技術>

前言

本文中搭建了一個簡易的多人聊天室,使用了WebSocket的基礎特性。

本文內容摘要:

初步理解WebSocket的前後端互動邏輯手把手使用 SpringBoot + WebSocket 搭建一個多人聊天室Demo程式碼原始碼及其解釋前端展示頁面

此外,在下一篇文章中,我們將做到:

對該WebSocket聊天室進行分散式改造,同時部署多臺機器來作為叢集,支撐高併發。儲存使用者session,並且在叢集上實現session同步,比如實時展示當前線上的使用者!正文WebSocket多人線上聊天室

本文工程原始碼:

https://github.com/qqxx6661/springboot-websocket-demo

新建工程

我們新建一個SpringBoot2的專案工程,在預設依賴中,新增websocket依賴:

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-websocket</artifactId></dependency>
WebSocket 配置

我們先來設定websocket的配置,新建config資料夾,在裡面新建類WebSocketConfig

import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.*;@Configuration@EnableWebSocketMessageBrokerpublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {    @Override    public void registerStompEndpoints(StompEndpointRegistry registry) {        registry.addEndpoint("/ws").withSockJS();    }    @Override    public void configureMessageBroker(MessageBrokerRegistry registry) {        registry.setApplicationDestinationPrefixes("/app");        registry.enableSimpleBroker("/topic");    }}

程式碼解釋:

@EnableWebSocketMessageBroker用於啟用我們的WebSocket伺服器。

我們實現了WebSocketMessageBrokerConfigurer介面,並實現了其中的方法。

在第一種方法中,我們註冊一個websocket端點,客戶端將使用它連線到我們的websocket伺服器。

withSockJS()是用來為不支援websocket的瀏覽器啟用後備選項,使用了SockJS。

方法名中的STOMP是來自Spring框架STOMP實現。 STOMP代表簡單文字導向的訊息傳遞協議。它是一種訊息傳遞協議,用於定義資料交換的格式和規則。為啥我們需要這個東西?因為WebSocket只是一種通訊協議。它沒有定義諸如以下內容:如何僅向訂閱特定主題的使用者傳送訊息,或者如何向特定使用者傳送訊息。我們需要STOMP來實現這些功能。

在configureMessageBroker方法中,我們配置一個訊息代理,用於將訊息從一個客戶端路由到另一個客戶端。

第一行定義了以“/app”開頭的訊息應該路由到訊息處理方法(之後會定義這個方法)。

第二行定義了以“/topic”開頭的訊息應該路由到訊息代理。訊息代理向訂閱特定主題的所有連線客戶端廣播訊息。

在上面的示例中,我們使用的是記憶體中的訊息代理。

之後也可以使用RabbitMQ或ActiveMQ等其他訊息代理。

建立 ChatMessage 實體

ChatMessage用來在客戶端和服務端中互動

我們新建model資料夾,建立實體類ChatMessage。

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;    }}

實體中,有三個欄位:

type:訊息型別content:訊息內容sender:傳送者

型別有三種:

CHAT: 訊息JOIN:加入LEAVE:離開建立Controller來接收和傳送訊息

建立controller資料夾,在controller資料夾新增類ChatController

import com.example.websocketdemo.model.ChatMessage;import org.springframework.messaging.handler.annotation.MessageMapping;import org.springframework.messaging.handler.annotation.Payload;import org.springframework.messaging.handler.annotation.SendTo;import org.springframework.messaging.simp.SimpMessageHeaderAccessor;import org.springframework.stereotype.Controller;@Controllerpublic class ChatController {    @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;    }}

程式碼解釋:

我們在websocket配置中,從目的地以/app開頭的客戶端傳送的所有訊息都將路由到這些使用@MessageMapping註釋的訊息處理方法。

例如,具有目標/app/chat.sendMessage的訊息將路由到sendMessage()方法,並且具有目標/app/chat.addUser的訊息將路由到addUser()方法

新增WebSocket事件監聽

完成了上述程式碼後,我們還需要對socket的連線和斷連事件進行監聽,這樣我們才能廣播使用者進來和出去等操作。

建立listener資料夾,新建WebSocketEventListener類

import com.example.websocketdemo.model.ChatMessage;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.event.EventListener;import org.springframework.messaging.simp.SimpMessageSendingOperations;import org.springframework.messaging.simp.stomp.StompHeaderAccessor;import org.springframework.stereotype.Component;import org.springframework.web.socket.messaging.SessionConnectedEvent;import org.springframework.web.socket.messaging.SessionDisconnectEvent;@Componentpublic 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);        }    }}

程式碼解釋:

我們已經在ChatController中定義的addUser()方法中廣播了使用者加入事件。因此,我們不需要在SessionConnected事件中執行任何操作。

在SessionDisconnect事件中,編寫程式碼用來從websocket會話中提取使用者名稱,並向所有連線的客戶端廣播使用者離開事件。

建立前端聊天室頁面

我們在src/main/resources檔案下建立前端檔案,結構類似這樣:

static  └── css       └── main.css  └── js       └── main.js  └── index.html   

1. HTML檔案 index.html

HTML檔案包含用於顯示聊天訊息的使用者介面。 它包括sockjs和stomp 兩個js庫。

SockJS是一個WebSocket客戶端,它嘗試使用本機WebSockets,併為不支援WebSocket的舊瀏覽器提供支援。 STOMP JS是javascript的stomp客戶端。

筆者在檔案裡使用了國內的CDN源

新增連線到websocket端點以及傳送和接收訊息所需的javascript。

'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)

程式碼解釋:

connect()函式使用SockJS和stomp客戶端連線到我們在Spring Boot中配置的/ws端點。

成功連線後,客戶端訂閱/topic/public,並透過向/app/chat.addUser目的地傳送訊息將該使用者的名稱告知伺服器。

stompClient.subscribe()函式採用一種回撥方法,只要訊息到達訂閱主題,就會呼叫該方法。

其它的程式碼用於在螢幕上顯示和格式化訊息。

3. CSS main.css

* {    -webkit-box-sizing: border-box;    -moz-box-sizing: border-box;    box-sizing: border-box;}html,body {    height: 100%;    overflow: hidden;}body {    margin: 0;    padding: 0;    font-weight: 400;    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;    font-size: 1rem;    line-height: 1.58;    color: #333;    background-color: #f4f4f4;    height: 100%;}body:before {    height: 50%;    width: 100%;    position: absolute;    top: 0;    left: 0;    background: #128ff2;    content: "";    z-index: 0;}.clearfix:after {    display: block;    content: "";    clear: both;}.hidden {    display: none;}.form-control {    width: 100%;    min-height: 38px;    font-size: 15px;    border: 1px solid #c8c8c8;}.form-group {    margin-bottom: 15px;}input {    padding-left: 10px;    outline: none;}h1, h2, h3, h4, h5, h6 {    margin-top: 20px;    margin-bottom: 20px;}h1 {    font-size: 1.7em;}a {    color: #128ff2;}button {    box-shadow: none;    border: 1px solid transparent;    font-size: 14px;    outline: none;    line-height: 100%;    white-space: nowrap;    vertical-align: middle;    padding: 0.6rem 1rem;    border-radius: 2px;    transition: all 0.2s ease-in-out;    cursor: pointer;    min-height: 38px;}button.default {    background-color: #e8e8e8;    color: #333;    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);}button.primary {    background-color: #128ff2;    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);    color: #fff;}button.accent {    background-color: #ff4743;    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);    color: #fff;}#username-page {    text-align: center;}.username-page-container {    background: #fff;    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);    border-radius: 2px;    width: 100%;    max-width: 500px;    display: inline-block;    margin-top: 42px;    vertical-align: middle;    position: relative;    padding: 35px 55px 35px;    min-height: 250px;    position: absolute;    top: 50%;    left: 0;    right: 0;    margin: 0 auto;    margin-top: -160px;}.username-page-container .username-submit {    margin-top: 10px;}#chat-page {    position: relative;    height: 100%;}.chat-container {    max-width: 700px;    margin-left: auto;    margin-right: auto;    background-color: #fff;    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);    margin-top: 30px;    height: calc(100% - 60px);    max-height: 600px;    position: relative;}#chat-page ul {    list-style-type: none;    background-color: #FFF;    margin: 0;    overflow: auto;    overflow-y: scroll;    padding: 0 20px 0px 20px;    height: calc(100% - 150px);}#chat-page #messageForm {    padding: 20px;}#chat-page ul li {    line-height: 1.5rem;    padding: 10px 20px;    margin: 0;    border-bottom: 1px solid #f4f4f4;}#chat-page ul li p {    margin: 0;}#chat-page .event-message {    width: 100%;    text-align: center;    clear: both;}#chat-page .event-message p {    color: #777;    font-size: 14px;    word-wrap: break-word;}#chat-page .chat-message {    padding-left: 68px;    position: relative;}#chat-page .chat-message i {    position: absolute;    width: 42px;    height: 42px;    overflow: hidden;    left: 10px;    display: inline-block;    vertical-align: middle;    font-size: 18px;    line-height: 42px;    color: #fff;    text-align: center;    border-radius: 50%;    font-style: normal;    text-transform: uppercase;}#chat-page .chat-message span {    color: #333;    font-weight: 600;}#chat-page .chat-message p {    color: #43464b;}#messageForm .input-group input {    float: left;    width: calc(100% - 85px);}#messageForm .input-group button {    float: left;    width: 80px;    height: 38px;    margin-left: 5px;}.chat-header {    text-align: center;    padding: 15px;    border-bottom: 1px solid #ececec;}.chat-header h2 {    margin: 0;    font-weight: 500;}.connecting {    padding-top: 5px;    text-align: center;    color: #777;    position: absolute;    top: 65px;    width: 100%;}@media screen and (max-width: 730px) {    .chat-container {        margin-left: 10px;        margin-right: 10px;        margin-top: 10px;    }}@media screen and (max-width: 480px) {    .chat-container {        height: calc(100% - 30px);    }    .username-page-container {        width: auto;        margin-left: 15px;        margin-right: 15px;        padding: 25px;    }    #chat-page ul {        height: calc(100% - 120px);    }    #messageForm .input-group button {        width: 65px;    }    #messageForm .input-group input {        width: calc(100% - 70px);    }    .chat-header {        padding: 10px;    }    .connecting {        top: 60px;    }    .chat-header h2 {        font-size: 1.1em;    }}

整個專案結構如下:

image.png

啟動

啟動SpringBoot專案

效果如下:

image.png

image.png

補充:使用RabbitMQ代替記憶體作為訊息代理

新增依賴:

public void configureMessageBroker(MessageBrokerRegistry registry) {    registry.setApplicationDestinationPrefixes("/app");    // Use this for enabling a Full featured broker like RabbitMQ    registry.enableStompBrokerRelay("/topic")            .setRelayHost("localhost")            .setRelayPort(61613)            .setClientLogin("guest")            .setClientPasscode("guest");}

如此一來,便可以透過RabbitMq進行訊息的訂閱。

13
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 第8天 | 14天搞定Vue3.0,事件處理(詳細)