Xây dựng ứng dụng chat Realtime sử dụng WebSocket
NỘI DUNG BÀI VIẾT
Bạn có muốn làm một ứng dụng Chat giống như Messenger của Facebook, có thể có nhiều người cùng truy cập vào cuộc hội thoại để nói chuyện với nhau hay không ? Nếu bạn muốn làm một ứng dụng Chat như vậy thì bài viết này có lẽ sẽ có ích với bạn. Bài viết này mình sẽ hướng dẫn mọi người cách tạo một ứng dụng Chat Realtime bằng WebSocket trong SpringBoot cùng tham khảo nhé.
Chuẩn bị
Khái niệm WebSocket và cấu trúc của nó đã được mình giới thiệu trong bài viết trước. Mọi người có thể truy cập đường link này để xem lại. Ở đây mình sẽ giới thiệu qua với mọi người về khái niệm STOMP.
Chức năng | Mô tả |
Connect | Đưa ra cách thức làm sao để client và server có thể kết nối với nhau. |
Subscribe | Đưa ra cách thức để client đăng ký (subscribe) nhận tin nhắn của một chủ đề nào đó. |
Unsubscribe | Đưa ra cách thức để client hủy đăng ký (unsubscribe) nhận tin nhắn của một chủ đề nào đó. |
Send | Làm sao client gửi nhắn gửi tới server. |
Message | Làm sao gửi tin nhắn gửi từ server đến client. |
Transaction management | Quản lý giao dịch trong quá trình truyền dữ liệu (BEGIN, COMMIT, ROLLBACK,…) |
Một số khái niệm liên quan
MessageBroker
MessageBroker giống như một người trung gian nó sẽ tiếp nhận những tin nhắn trước khi chuyển các tin nhắn tới các địa chỉ khác cần thiết.
Mô tả cấu trúc của MessageBroker:
MessageBroker phơi bầy ra một endpoint (Điểm cuối) để client có thể liên lạc và hình thành một kết nối. Để liên lạc client sử dụng thư viện SockJS để làm việc này.
Đồng thời MessageBroker cũng phơi bẩy ra 2 loại điểm đến (destination) (1) & (2)
- Điểm đến (1) là các chủ đề (topic) mà client có thể “đăng ký theo dõi” (subscribe), khi một chủ đề có tin nhắn, các tin nhắn sẽ được gửi đến cho những client đã đăng ký chủ đề này.
- Điểm đến (2) là các nơi mà client có thể gửi tin nhắn tới WebSocket Server.
MessageBroker phơi bầy ra một endpoint (Điểm cuối) để client có thể liên lạc và hình thành một kết nối. Để liên lạc client sử dụng thư viện SockJS để làm việc này.
Đồng thời MessageBroker cũng phơi bẩy ra 2 loại điểm đến (destination) (1) & (2)
- Điểm đến (1) là các chủ đề (topic) mà client có thể “đăng ký theo dõi” (subscribe), khi một chủ đề có tin nhắn, các tin nhắn sẽ được gửi đến cho những client đã đăng ký chủ đề này.
- Điểm đến (2) là các nơi mà client có thể gửi tin nhắn tới WebSocket Server.
Cài đặt thư viện
Mọi người tạo ứng dụng SpringBoot và thêm một số thư viện như sau:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Code language: JavaScript (javascript)
Chuẩn bị
Model
Chúng ta sẽ tạo một class có tên là Chat như sau:
Chat.java
package com.example.demo.model;
import lombok.Data;
@Data
public class Chat {
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;
}
}
Code language: JavaScript (javascript)
Listener
Sau khi tạo xong class Chat.java chúng ta sẽ tiến hành tạo class WebSocketEventListener
WebSocketEventListener.java
package com.example.demo.listener;
import com.example.demo.model.Chat;
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;
@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);
Chat chat = new Chat();
chat.setType(Chat.MessageType.LEAVE);
chat.setSender(username);
messagingTemplate.convertAndSend("/topic/publicChatRoom", chat);
}
}
}
Code language: JavaScript (javascript)
Controller
Chúng ta sẽ tiến hành tạo 2 Controller lần lượt là MainController và WebSocketController. MainController sẽ chứa các mapping đến trang login và để logout tài khoản. WebSocketController sẽ chuyển người dùng tới hội thoại chat sau khi login thành công.
MainController.java
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
@Controller
public class MainController {
@RequestMapping("/")
public String index(HttpServletRequest request, Model model) {
String username = (String) request.getSession().getAttribute("username");
if (username == null || username.isEmpty()) {
return "redirect:/login";
}
model.addAttribute("username", username);
return "chat";
}
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String showLoginPage() {
return "login";
}
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String doLogin(HttpServletRequest request, @RequestParam(defaultValue = "") String username) {
username = username.trim();
if (username.isEmpty()) {
return "login";
}
request.getSession().setAttribute("username", username);
return "redirect:/";
}
@RequestMapping(path = "/logout")
public String logout(HttpServletRequest request) {
request.getSession(true).invalidate();
return "redirect:/login";
}
}
Code language: JavaScript (javascript)
WebSocketController.java
package com.example.demo.controller;
import com.example.demo.model.Chat;
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;
@Controller
public class WebSocketController {
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/publicChatRoom")
public Chat sendMessage(@Payload Chat chat) {
return chat;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/publicChatRoom")
public Chat addUser(@Payload Chat chat, SimpMessageHeaderAccessor headerAccessor) {
// Add username in web socket session
headerAccessor.getSessionAttributes().put("username", chat.getSender());
return chat;
}
}
Code language: JavaScript (javascript)
Cấu hình WebSocket
Cấu hình MessageBroker
Chúng ta sẽ cấu hình MessageBroker bằng đoạn mã JavaScript như sau:
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
Code language: JavaScript (javascript)
SockJS là một thư viện của JavaScript và chúng ta chỉ cần sử dụng nó
Cấu hình HTTP Handshake
Chúng ta sẽ tạo một class có tên là HttpHandshakeInterceptor. Lớp này sẽ được sử dụng để xử lý các sự kiện trước và sau khi WebSocket bắt tay với HTTP.
HttpHandshakeInterceptor.java
package com.example.demo.intercepter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Component
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
private static final Logger logger = LoggerFactory.getLogger(HttpHandshakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
logger.info("Call before Handshake");
if (request instanceof ServerHttpRequest) {
ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
HttpSession session = serverHttpRequest.getServletRequest().getSession();
attributes.put("sessionId", session.getId());
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
logger.info("Call after handshake");
}
}
Code language: JavaScript (javascript)
Cấu hình WebSocket
Để cấu hình WebSocket chúng ta sẽ tạo một class có tên WebSocketConfiguration như sau:
WebSocketConfiguration.java
package com.example.demo.configuration;
import com.example.demo.intercepter.HttpHandshakeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Autowired
private HttpHandshakeInterceptor handshakeInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS().setInterceptors(handshakeInterceptor);
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
Code language: JavaScript (javascript)
Annotation @EnableWebSocketMessageBroker được sử dụng để enable WebSocket Server.
Giao diện hiển thị
HTML
Trang login
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<div id="login-container">
<h1 class="title">Enter your username</h1>
<form id="loginForm" name="loginForm" method="POST">
<input type="text" name="username" />
<button type="submit">Login</button>
</form>
</div>
</body>
</html>
Code language: HTML, XML (xml)
Phòng chat
chat.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Boot WebSocket</title>
<link rel="stylesheet" th:href="@{/css/main.css}" />
<!-- https://cdnjs.com/libraries/sockjs-client -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<!-- https://cdnjs.com/libraries/stomp.js/ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
</head>
<body>
<div id="chat-container">
<div class="chat-header">
<div class="user-container">
<span id="username" th:utext="${username}"></span>
<a th:href="@{/logout}">Logout</a>
</div>
<h3>Spring WebSocket Chat Demo</h3>
</div>
<hr/>
<div id="connecting">Connecting...</div>
<ul id="messageArea">
</ul>
<form id="messageForm" name="messageForm">
<div class="input-message">
<input type="text" id="message" autocomplete="off"
placeholder="Type a message..."/>
<button type="submit">Send</button>
</div>
</form>
</div>
<script th:src="@{/js/main.js}"></script>
</body>
</html>
Code language: HTML, XML (xml)
CSS + JS
main.css
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
}
#login-page {
text-align: center;
}
.nickname {
color:blue;margin-right:20px;
}
.hidden {
display: none;
}
.user-container {
float: right;
margin-right:5px;
}
#login-container {
background: #f4f6f6 ;
border: 2px solid #ccc;
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;
}
#chat-container {
position: relative;
height: 100%;
}
#chat-container #messageForm {
padding: 20px;
}
#chat-container {
border: 2px solid #d5dbdb;
background-color: #d5dbdb ;
max-width: 500px;
margin-left: auto;
margin-right: auto;
margin-top: 30px;
height: calc(100% - 60px);
max-height: 600px;
position: relative;
}
#chat-container 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-container #messageForm {
padding: 20px;
}
#chat-container ul li {
line-height: 1.5rem;
padding: 10px 20px;
margin: 0;
border-bottom: 1px solid #f4f4f4;
}
#chat-container ul li p {
margin: 0;
}
#chat-container .event-message {
width: 100%;
text-align: center;
clear: both;
}
#chat-container .event-message p {
color: #777;
font-size: 14px;
word-wrap: break-word;
}
#chat-container .chat-message {
position: relative;
}
#messageForm .input-message {
float: left;
width: calc(100% - 85px);
}
.connecting {
text-align: center;
color: #777;
width: 100%;
}
Code language: CSS (css)
main.js
'use strict';
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;
function connect() {
username = document.querySelector('#username').innerText.trim();
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
}
// Connect to WebSocket Server.
connect();
function onConnected() {
// Subscribe to the Public Topic
stompClient.subscribe('/topic/publicChatRoom', 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 usernameElement = document.createElement('strong');
usernameElement.classList.add('nickname');
var usernameText = document.createTextNode(message.sender);
var usernameText = document.createTextNode(message.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
}
var textElement = document.createElement('span');
var messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
messageArea.appendChild(messageElement);
messageArea.scrollTop = messageArea.scrollHeight;
}
messageForm.addEventListener('submit', sendMessage, true);
Code language: JavaScript (javascript)
Leave a Reply