Tích Hợp WebSocket Trong Spring Framework

1. Giới Thiệu Về WebSocket

WebSocket là một giao thức truyền thông song công (full-duplex) được định nghĩa trong HTML5, cho phép thiết lập một kênh giao tiếp liên tục giữa trình duyệt client và máy chủ. Khác với các mô hình yêu cầu/phản hồi truyền thống của HTTP, sau khi kết nối được thiết lập, cả hai bên (server và client) đều có thể chủ động gửi và nhận dữ liệu bất cứ lúc nào, mang lại khả năng tương tác thời gian thực giống như một socket TCP/IP thông thường.

Sự Ra Đời của WebSocket

Giao thức HTTP nổi tiếng với tính chất "không trạng thái" và "không kết nối" (stateless, connectionless), vốn không tối ưu cho các ứng dụng cần giao tiếp liên tục hoặc cập nhật dữ liệu thời gian thực. Để giải quyết hạn chế này, các kỹ thuật như polling hoặc long polling đã được sử dụng nhưng đều kém hiệu quả do overhead lớn từ header HTTP trong mỗi lần trao đổi dữ liệu.

WebSocket được IETF phát triển để khắc phục những nhược điểm này, sau đó W3C cũng phát hành một bộ API WebSocket toàn diện cho các trình duyệt web. Mặc dù HTTP đóng vai trò khởi tạo kết nối WebSocket (thông qua quá trình "bắt tay" - handshake), nhưng sau đó, WebSocket hoạt động độc lập với HTTP, chỉ gửi dữ liệu thô mà không cần lặp lại các header HTTP nặng nề, giúp tăng hiệu suất truyền tải đáng kể.

So Sánh HTTP và WebSocket

  • Giao thức: HTTP và WebSocket là hai giao thức khác biệt. HTTP thường bắt đầu với http:// hoặc https://, trong khi WebSocket sử dụng ws:// hoặc wss://.
  • Kiến trúc kết nối: HTTP là giao thức yêu cầu/phản hồi, thường đóng kết nối sau mỗi giao dịch (hoặc giữ lại trong HTTP Keep-Alive nhưng vẫn là chiều gửi-nhận tuần tự). WebSocket thiết lập một kết nối liên tục, song công, cho phép cả client và server gửi dữ liệu đồng thời.
  • Hiệu suất: HTTP long polling lãng phí băng thông do lặp lại các header. WebSocket, sau khi kết nối được thiết lập, chỉ truyền tải dữ liệu thuần túy, tối ưu hóa hiệu quả truyền thông.
  • Tương thích: HTTP được hỗ trợ rộng rãi. WebSocket là công nghệ mới hơn, dù đã phổ biến nhưng vẫn có thể gặp vấn đề tương thích với các trình duyệt cũ hoặc môi trường mạng nhất định (có thể được giải quyết bằng SockJS).

2. Triển Khai WebSocket Cấp Thấp Với Spring

Phía Client (JavaScript)

Để thiết lập kết nối WebSocket từ phía client, chúng ta sử dụng API WebSocket của trình duyệt. Đoạn mã sau minh họa cách tạo một đối tượng WebSocket và xử lý các sự kiện cơ bản như mở kết nối, nhận tin nhắn, đóng kết nối và xử lý lỗi.


let wsPath = 'ws://localhost:8080/app/echo-socket';
let clientSocket = new WebSocket(wsPath);
let messageCount = 0;

clientSocket.onopen = (event) => {
    console.log("Kết nối WebSocket đã được mở.");
    sendClientMessage("Xin chào từ client!");
};

clientSocket.onmessage = (event) => {
    console.log("Nhận được tin nhắn từ server: " + event.data);
    messageCount++;
    if (messageCount < 5) {
        setTimeout(() => {
            sendClientMessage("Client gửi lại: " + messageCount);
        }, 1500); // Chờ 1.5 giây trước khi gửi tiếp
    } else {
        clientSocket.close();
    }
};

clientSocket.onclose = (event) => {
    console.log("Kết nối WebSocket đã đóng. Mã: " + event.code + ", Lý do: " + event.reason);
};

clientSocket.onerror = (errorEvent) => {
    console.error("Đã xảy ra lỗi WebSocket:", errorEvent);
};

function sendClientMessage(content) {
    if (clientSocket.readyState === WebSocket.OPEN) {
        console.log('Đang gửi tin nhắn: ' + content);
        clientSocket.send(content);
    } else {
        console.warn('Kết nối chưa sẵn sàng để gửi tin nhắn.');
    }
}

Phía Server (Spring)

2.1. Thêm Phụ Thuộc Maven

Để triển khai WebSocket với Spring, cần bổ sung các phụ thuộc sau vào file pom.xml:


<!-- Spring WebSocket -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>4.3.13.RELEASE</version>
</dependency>

<!-- Hỗ trợ JSON (ví dụ: nếu cần gửi/nhận đối tượng JSON qua WebSocket) -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.10</version>
</dependency>

2.2. Triển Khai Dịch Vụ WebSocket

Có hai cách chính để triển khai dịch vụ WebSocket trong Spring:

Sử Dụng API spring-websocket

Spring cung cấp giao diện WebSocketHandler để xử lý các sự kiện WebSocket. Cách đơn giản hơn là kế thừa từ TextWebSocketHandler (cho tin nhắn văn bản) hoặc BinaryWebSocketHandler (cho tin nhắn nhị phân), là các lớp triển khai trừu tượng của WebSocketHandler.


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class EchoTextWebSocketHandler extends TextWebSocketHandler {

    private static final Logger serverLogger = LoggerFactory.getLogger(EchoTextWebSocketHandler.class);

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        serverLogger.info("Client <{}> đã kết nối tới WebSocket.", session.getId());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage receivedMessage) throws Exception {
        serverLogger.info("Nhận tin nhắn từ client <{}>: {}", session.getId(), receivedMessage.getPayload());
        // Giả lập xử lý và gửi lại tin nhắn sau một khoảng thời gian
        Thread.sleep(1000);
        String response = "Server đã nhận: " + receivedMessage.getPayload();
        session.sendMessage(new TextMessage(response));
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        serverLogger.info("Client <{}> đã ngắt kết nối. Trạng thái: {}", session.getId(), status.getReason());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        serverLogger.error("Lỗi truyền tải cho client <{}>: {}", session.getId(), exception.getMessage(), exception);
    }
}
Sử Dụng Annotation Dựa Trên JSR-356 (javax.websocket-api)

Đây là cách tiếp cận dựa trên tiêu chuẩn Java EE, sử dụng các annotation để định nghĩa các điểm cuối WebSocket. Cách này đòi hỏi phụ thuộc javax.websocket-api.

i. Phụ Thuộc Maven

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope> <!-- Thường là provided vì server ứng dụng đã có -->
</dependency>
ii. Dịch Vụ WebSocket (Với Annotation)

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ServerEndpoint("/annotation-ws") // Định nghĩa đường dẫn WebSocket
public class JsrWebSocketAnnotatedService {

    private static final Logger log = LoggerFactory.getLogger(JsrWebSocketAnnotatedService.class);

    @OnOpen
    public void onConnect(Session userSession) {
        log.info("Client ID <{}> đã kết nối.", userSession.getId());
    }

    @OnClose
    public void onDisconnect(Session userSession) {
        log.info("Client ID <{}> đã ngắt kết nối.", userSession.getId());
    }

    @OnMessage
    public void onReceiveMessage(String messageContent, Session userSession) {
        try {
            log.info("Nhận tin nhắn từ <{}>: {}", userSession.getId(), messageContent);
            Thread.sleep(800); // Giả lập xử lý
            String replyMessage = "Server: " + messageContent.toUpperCase();
            userSession.getBasicRemote().sendText(replyMessage);
        } catch (IOException e) {
            log.error("Lỗi khi gửi tin nhắn cho client <{}>", userSession.getId(), e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Xử lý ngắt luồng
            log.warn("Luồng xử lý tin nhắn bị gián đoạn.", e);
        }
    }

    @OnError
    public void onErrorOccurred(Session userSession, Throwable error) {
        log.error("Lỗi trong phiên WebSocket <{}>: {}", userSession.getId(), error.getMessage(), error);
    }
}

2.3. Cấu Hình Ánh Xạ WebSocket

Sau khi có các lớp xử lý, chúng ta cần cấu hình để Spring biết cách ánh xạ các URL WebSocket tới các handler tương ứng.

i. Cấu Hình Java (Java Config)

Sử dụng annotation @EnableWebSocket trên một lớp cấu hình và triển khai giao diện WebSocketConfigurer.


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
          registry.addHandler(echoHandler(), "/app/echo-socket") // Đăng ký handler và đường dẫn
                  .setAllowedOrigins("*"); // Cho phép tất cả các origin
    }

    @Bean
    public EchoTextWebSocketHandler echoHandler(){
        return new EchoTextWebSocketHandler();
    }
}
ii. Cấu Hình XML

Trong cấu hình XML, sử dụng namespace websocket: để đăng ký handler.


<!-- Kích hoạt WebSocket handlers -->
<websocket:handlers>
    <websocket:mapping handler="myEchoWebSocketHandler" path="/simple-echo"/>
</websocket:handlers>

<!-- Định nghĩa bean cho WebSocket handler -->
<bean id="myEchoWebSocketHandler" class="com.example.websocket.EchoTextWebSocketHandler"/>

3. Hỗ Trợ WebSocket Bằng SockJS

Tại Sao Cần SockJS?

Mặc dù WebSocket là một giao thức mạnh mẽ, nhưng vẫn có những trường hợp không thể sử dụng trực tiếp:

  1. Khả năng tương thích trình duyệt: Một số trình duyệt cũ hơn hoặc môi trường không hỗ trợ WebSocket.
  2. Tường lửa/Proxy: Nhiều tường lửa hoặc proxy mạng có thể chặn các kết nối WebSocket (thường không chạy trên cổng 80/443 hoặc không được cấu hình để cho phép nâng cấp giao thức từ HTTP).

SockJS Là Gì?

SockJS là một thư viện JavaScript cung cấp một API giống như WebSocket, nhưng rất thông minh ở tầng bên dưới. Nó sẽ ưu tiên sử dụng WebSocket nếu có thể. Nếu WebSocket không khả dụng, SockJS sẽ tự động chuyển sang một trong các phương án dự phòng khác nhau (fallback options) để mô phỏng giao tiếp song công, chẳng hạn như XHR streaming, long polling dựa trên XHR, iframe-based event source, v.v. Điều này đảm bảo rằng ứng dụng vẫn có thể giao tiếp thời gian thực ngay cả trong môi trường hạn chế.

Sử Dụng SockJS

3.1. Phía Client

Để sử dụng SockJS ở phía client, bạn cần nhúng thư viện SockJS và khởi tạo kết nối bằng SockJS thay vì WebSocket. Lưu ý rằng URL cho SockJS bắt đầu với http:// hoặc https://.

i. Nhúng Thư Viện Client

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.0/dist/sockjs.min.js"></script>
ii. Khởi Tạo Kết Nối SockJS

let sockjsUrl = 'http://localhost:8080/app/sockjs-endpoint'; // URL sử dụng HTTP/HTTPS
let sockjsClient = new SockJS(sockjsUrl);

// Các sự kiện onopen, onmessage, onclose, onerror tương tự như WebSocket API
sockjsClient.onopen = () => {
    console.log("Kết nối SockJS đã được mở.");
    sockjsClient.send("Chào server qua SockJS!");
};

sockjsClient.onmessage = (event) => {
    console.log("Nhận tin nhắn từ server qua SockJS: " + event.data);
};

sockjsClient.onclose = () => {
    console.log("Kết nối SockJS đã đóng.");
};

sockjsClient.onerror = (error) => {
    console.error("Lỗi SockJS:", error);
};

3.2. Phía Server (Spring)

Phía server chỉ cần thêm hỗ trợ SockJS vào cấu hình ánh xạ handler WebSocket.

i. Cấu Hình Java (Java Config)

Gọi phương thức .withSockJS() khi đăng ký handler.


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class SockJSConfiguration implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
          registry.addHandler(sockjsCompatibleHandler(), "/app/sockjs-endpoint")
                  .setAllowedOrigins("http://localhost:8080") // Chỉ định origin cụ thể hoặc "*"
                  .withSockJS(); // Kích hoạt hỗ trợ SockJS
    }

    @Bean
    public EchoTextWebSocketHandler sockjsCompatibleHandler(){
        return new EchoTextWebSocketHandler();
    }
}
ii. Cấu Hình XML

Thêm thẻ <websocket:sockjs/> vào trong <websocket:handlers>.


<websocket:handlers>
    <websocket:mapping handler="sockjsMessageProcessor" path="/sockjs-path"/>
    <websocket:sockjs/> <!-- Kích hoạt tính năng SockJS -->
</websocket:handlers>

<!-- Định nghĩa bean cho WebSocket handler -->
<bean id="sockjsMessageProcessor" class="com.example.websocket.EchoTextWebSocketHandler"/>

Thẻ: Spring Framework WebSocket SockJS Java Spring-WebSocket

Đăng vào ngày 11 tháng 10 lúc 12:41