1. Tổng quan

Bởi vì spring đóng vai trò là một thư viện cầu nối với trái tim là spring-context, vậy nên đối với websocket thì nó cũng có nhiều phiên bản khác nhau, trong đó có phiên bản sử dụng stomp-websocket và phiên bản sử dụng Netty Websocket. Và ở trong bài viết lần này mình sẽ chỉ sử dụng phiên bản với Netty thôi nhé.

Về cơ bản thì spring cũng chẳng làm gì nhiều, nó cố gắng wrap lại các đối tượng của Netty để có thể sử dụng chung cho hệ sinh thái của mình. Đến khi Dev lập trình thì sẽ chỉ cần quan tâm đến các đối tượng của spring mà thôi.

2. Cài đặt

Để có thể sử dụng được spring websocket, chúng ta sẽ cần thêm các thư viện phụ thuộc vào dự án của chúng ta, ví dụ gradle:

implementation 'org.springframework.boot:spring-boot-starter:' + springBootVersion
implementation 'org.springframework.boot:spring-boot-starter-webflux:' + springBootVersion
implementation 'org.springframework.boot:spring-boot-starter-integration:' + springBootVersion

Với springBootVersion=2.7.0.

3. Ví dụ

Bây giờ hãy bắt tay vào làm một ví dụ đơn giản để chúng ta hiểu được cách lập trình websocket với spring. Giả sử chúng ta đang cần làm một chương trình gửi nhận Event qua websocket giữa client và server, và lớp Event của chúng ta có dạng:

package com.tvd12.example.websocket;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Event {
    private String eventId;
    private String eventData;
}

Giả sử chúng ta cần xử lý event từ client gửi lên server, sau đó bổ sung thêm chuỗi Response và gửi lại cho client, thì lớp EventSocketHandler của chúng ta có thể như sau:

package com.tvd12.example.websocket;

import java.io.IOException;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketSession;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.AllArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
@AllArgsConstructor
public class EventSocketHandler implements WebSocketHandler {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        final Flux<WebSocketMessage> stringFlux = session
            .receive()
            .map(it -> toEvent(
                it.getPayloadAsText()
            ))
            .doOnNext(System.out::println)
            .map(it -> session.textMessage(
                toJson(
                    new Event(
                        it.getEventId(),
                        it.getEventData() + " - Response"
                    )
                )
            ));
        return session.send(stringFlux);
    }

    private Event toEvent(String json) {
        try {
            return objectMapper.readValue(json, Event.class);
        } catch (IOException e) {
            throw new RuntimeException("Invalid JSON:" + json, e);
        }
    }

    private String toJson(Event event) {
        try {
            return objectMapper.writeValueAsString(event);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

Tiếp theo chúng ta sẽ cần tạo 1 lớp để cấu hình cho websocket server:

package com.tvd12.example.websocket;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;

import lombok.AllArgsConstructor;
import lombok.Setter;

@Setter
@Configuration
@AllArgsConstructor
public class EventWebSocketConfiguration {

    private final EventSocketHandler eventSocketHandler;

    @Bean
    public HandlerMapping webSocketHandlerMapping() {
        final Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/chat", eventSocketHandler);

        final SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
        handlerMapping.setOrder(1);
        handlerMapping.setUrlMap(map);
        return handlerMapping;
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

Và tất nhiên chúng ta cần 1 lớp khởi động cho spring boot:

package com.tvd12.example.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class EventWebSocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(EventWebSocketApplication.class, args);
    }
}

Cuối cùng là tạo 1 socket client gửi request định kỳ 1 giây 1 lần lên server:

package com.tvd12.example.websocket;

import java.io.IOException;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketMessage;
import org.springframework.web.reactive.socket.WebSocketSession;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.AllArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
@AllArgsConstructor
public class EventSocketHandler implements WebSocketHandler {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        final Flux<WebSocketMessage> stringFlux = session
            .receive()
            .map(it -> toEvent(
                it.getPayloadAsText()
            ))
            .doOnNext(System.out::println)
            .map(it -> session.textMessage(
                toJson(
                    new Event(
                        it.getEventId(),
                        it.getEventData() + " - Response"
                    )
                )
            ));
        return session.send(stringFlux);
    }

    private Event toEvent(String json) {
        try {
            return objectMapper.readValue(json, Event.class);
        } catch (IOException e) {
            throw new RuntimeException("Invalid JSON:" + json, e);
        }
    }

    private String toJson(Event event) {
        try {
            return objectMapper.writeValueAsString(event);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

Chạy chương trình và chúng ta sẽ nhận được kết quả:

# server
Event(eventId=hello, eventData=message#795)
Event(eventId=hello, eventData=message#796)
Event(eventId=hello, eventData=message#797)

# client
{"eventId":"hello","eventData":"message#795 - Response"}
{"eventId":"hello","eventData":"message#796 - Response"}
{"eventId":"hello","eventData":"message#797 - Response"}