Backend

99 Ngày Spring — Ngày 84: SSE và WebSocket

SSite Admin
19 tháng 10, 2026 3 phút đọc 5 lượt xem
99 Ngày Spring — Ngày 84: SSE và WebSocket

Polling vài giây một lần vừa trễ vừa tốn request. Hai cách phổ biến để server chủ động đẩy dữ liệu là Server-Sent Events và WebSocket. SSE là một response HTTP không kết thúc, chỉ đi từ server xuống. WebSocket nâng cấp kết nối thành kênh hai chiều. Chọn đúng loại tiết kiệm rất nhiều công vận hành.

SSE và WebSocket

1. SSE: một dòng sự kiện qua HTTP

Server trả Content-Type text/event-stream và giữ kết nối mở; mỗi sự kiện là vài dòng văn bản. Phía trình duyệt, EventSource tự kết nối lại và gửi header Last-Event-ID để server phát tiếp từ chỗ đứt. Với WebFlux, chỉ cần trả một Flux; với Spring MVC, dùng SseEmitter. Vì là HTTP thường, SSE đi qua proxy và hạ tầng xác thực sẵn có dễ hơn WebSocket.

@GetMapping(path = "/orders/{id}/events",
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<ServerSentEvent<OrderStatus>> events(@PathVariable String id) {
    return statusStream.forOrder(id)
        .map(status -> ServerSentEvent.builder(status)
            .id(String.valueOf(status.version()))
            .event("status")
            .build());
}

2. WebSocket và STOMP

WebSocket chỉ cho một ống truyền khung dữ liệu hai chiều; nó không định nghĩa ai nhận tin nào. STOMP là giao thức nhắn tin đơn giản chạy trên đó, thêm khái niệm destination, subscribe và send. Với spring-boot-starter-websocket, bạn khai báo endpoint bắt tay, tiền tố cho tin gửi vào ứng dụng và tiền tố của broker phát tin ra.

@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").setAllowedOrigins("https://shop.example");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }
}

3. Nhận và phát tin

Phương thức có @MessageMapping nhận tin client gửi tới /app/...; @SendTo phát giá trị trả về tới mọi client đã subscribe destination đó. Từ code không nằm trong luồng tin nhắn, ví dụ một listener Kafka, dùng SimpMessagingTemplate để đẩy tin. Tin gửi riêng cho một người dùng đi qua convertAndSendToUser và cần Principal được thiết lập lúc bắt tay.

@Controller
class ChatController {
    @MessageMapping("/rooms/{room}")
    @SendTo("/topic/rooms/{room}")
    ChatMessage send(@DestinationVariable String room, ChatMessage message) {
        return message.withSentAt(Instant.now());
    }
}

// elsewhere: template.convertAndSend("/topic/orders/42", status);

4. Vận hành và lựa chọn

Simple broker giữ danh sách subscription trong bộ nhớ của từng instance. Chạy hai instance thì client nối vào instance A không nhận tin phát từ instance B; khi đó cần broker relay tới RabbitMQ hoặc một kênh pub/sub chung. Kết nối dài còn đòi hỏi heartbeat, timeout của proxy đủ lớn, giới hạn số kết nối và kiểm tra origin. Cả SSE lẫn WebSocket đều phải xác thực ngay từ request đầu tiên.

  • Thông báo, tiến độ xử lý, bảng số liệu: SSE là đủ và đơn giản hơn.

  • Chat, cộng tác, game: cần hai chiều với độ trễ thấp thì dùng WebSocket.

  • Client gửi lên không thường xuyên: SSE cho chiều xuống cộng với REST cho chiều lên.

Bắt đầu bằng câu hỏi dữ liệu chảy một chiều hay hai chiều, rồi mới chọn công nghệ. Ngày 85 đổi hẳn kiểu API: GraphQL với Spring.

Tài liệu đối chiếu

  • https://docs.spring.io/spring-framework/reference/web/websocket.html

  • https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 84: Text Block

Cú pháp ba dấu nháy, cách compiler bỏ thụt lề, các escape mới và cách dùng an toàn cho JSON, SQL.

19 thg 10, 20263 phút5
99 Ngày Spring — Ngày 83: R2DBC

Driver non-blocking, ReactiveCrudRepository, transaction reactive và những đánh đổi so với JPA.

18 thg 10, 20262 phút6
99 Ngày Java — Ngày 83: Sealed Classes

sealed và permits, ba lựa chọn của lớp con, switch đầy đủ không cần default và cách mô hình hóa domain.

18 thg 10, 20263 phút4