สร้างระบบ Notification (Push Event) ด้วย SSE หรือ WebSocket

Sharing is caring!

สารบัญ

  1. ทำไมต้อง Push Notification แบบ Realtime?
  2. รู้จัก SSE และ WebSocket
  3. สแตกเทคโนโลยีที่ใช้
  4. เขียน SSE Backend (Spring WebFlux)
  5. เขียน WebSocket Backend (Spring WebSocket)
  6. โค้ด Frontend รับข้อความ
  7. การ Deploy & Scaling
  8. เปรียบเทียบ SSE กับ WebSocket
  9. Security & Best Practices
  10. Monitoring & Benchmark
  11. FAQ
  12. สรุป

1. ทำไมต้อง Push Notification แบบ Realtime?

ประสบการณ์ผู้ใช้ยุคปัจจุบันคาดหวังข้อมูล “เด้ง” มาทันทีไม่ต้องรีเฟรช ไม่ว่าจะเป็น ออเดอร์ใหม่, ข้อความแชท, ราคาหุ้น หรือ สถานะงาน หากยังใช้วิธี polling (ไถวนถามเซิร์ฟเวอร์ทุก X วินาที) นอกจากสิ้นเปลืองแบนด์วิธยังเพิ่มโหลด CPU แบบไร้สาระ Server-Sent Events (SSE) และ WebSocket จึงกลายเป็นรากฐานของแอป Realtime สมัยใหม่

ภาพรวม Dev กำลังพัฒนาระบบ Push Notification

2. รู้จัก SSE และ WebSocket

2.1 Server-Sent Events (SSE)

การไหลของ SSE: Client ขอครั้งเดียว Server ส่ง event ต่อเนื่อง
  • เชื่อมต่อ HTTP ที่ฝั่ง Client เปิดค้างไว้ (long-lived connection)
  • เหมาะกับ “one-way stream” – server ➜ client
  • เบา (ใช้ text/event-stream) ไม่ต้องทำ handshake พิเศษ
  • Reconnect อัตโนมัติด้วย header Last-Event-ID

2.2 WebSocket

ขั้นตอน WebSocket Handshake เปลี่ยน HTTP เป็นช่องทาง Full-Duplex
  • เปลี่ยน HTTP/1.1 เป็น wss:// บน TCP ซึ่งรับ-ส่งสองทาง
  • Frame binary/text ส่งได้ทั้ง server➜client และ client➜server
  • เหมาะกับแอปแชท เกมออนไลน์ หรือ collaborative editing
  • ต้องมี load balancer ที่รองรับ sticky-session / L4

3. สแตกเทคโนโลยีที่ใช้

  • Java 17+ + Spring Boot 3.5.x
  • Spring WebFlux (Reactive) สำหรับ SSE
  • Spring WebSocket + STOMP สำหรับ WebSocket
  • Frontend สาธิตด้วย Vanilla JS & Vite
  • Database ทดลอง: Redis Stream เป็น event-source

4. เขียน SSE Backend (Spring WebFlux)

4.1 Controller + Event Flux

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class SseController {

  private final Sinks.Many<ServerSentEvent<String>> sink =
      Sinks.many().multicast().onBackpressureBuffer();

  @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  public Flux<ServerSentEvent<String>> stream() {
      return sink.asFlux();
  }

  @PostMapping("/notify")
  public Mono<Void> send(@RequestBody String message) {
      sink.tryEmitNext(ServerSentEvent.builder(message).build());
      return Mono.empty();
  }
}
  

4.2 ทดสอบส่ง cURL

curl -N http://localhost:8080/api/stream
# แท็บใหม่
curl -XPOST http://localhost:8080/api/notify \
     -H "Content-Type:text/plain" -d "Hello SSE!"
  

5. เขียน WebSocket Backend (Spring WebSocket + STOMP)

5.1 WebSocket Config

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
      registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
  }

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

5.2 Controller ส่งข้อความ

@Controller
@RequiredArgsConstructor
public class WsController {

  private final SimpMessagingTemplate template;

  @MessageMapping("/chat")
  public void onChat(String body) {
      template.convertAndSend("/topic/messages", body);
  }
}
  

6. โค้ด Frontend รับข้อความ

6.1 Client SSE

const evtSource = new EventSource("/api/stream");
evtSource.onmessage = (e) => {
  const li = document.createElement("li");
  li.textContent = "📩 " + e.data;
  document.querySelector("#messages").append(li);
};
  

6.2 Client WebSocket ผ่าน STOMP

const socket = new SockJS("/ws");
const stomp  = Stomp.over(socket);
stomp.connect({}, () => {
  stomp.subscribe("/topic/messages", (msg) => {
    const li = document.createElement("li");
    li.textContent = "💬 " + msg.body;
    document.querySelector("#messages").append(li);
  });
  // ส่งข้อความ
  document.querySelector("#send").onclick = () => {
    const txt = document.querySelector("#txt").value;
    stomp.send("/app/chat", {}, txt);
  };
});
  

7. การ Deploy & Scaling

  • เปิด server.tomcat.max-threads สูงไว้สำหรับ WebSocket blocking-IO
  • SSE สามารถใช้ Netty & Reactive Pool ขนาดเล็กรับได้หมื่น connection
  • ถ้า Kubernetes ➜ ใช้ NGINX Ingress v1.9+ เปิด use-http2
  • เพิ่ม proxy_read_timeout ใหญ่กว่า idle timeout ของ Browser

8. เปรียบเทียบ SSE กับ WebSocket

คุณสมบัติSSEWebSocket
ทิศทางข้อมูลServer ➜ ClientFull-Duplex
โปรโตคอลHTTP text/event-streamws/wss Binary or Text
Browser SupportChrome, Edge, Firefox, Safariเหมือนกัน
ฟีเจอร์ Reconnectในตัวต้องเขียนเอง
ใช้ Proxy/LB ทั่วไปอาจต้อง sticky session
เหมาะกับEvent feed, IoT metricsChat, Game, Collab

9. Security & Best Practices

  1. ส่ง JWT ใน Authorization Header สำหรับ SSE / WebSocket Handshake
  2. ใส่ CSRF-token ซ่อนใน query param เพื่อป้องกัน WS Hijack
  3. จำกัดขนาดข้อความ (maxFrameSize) เพื่อกัน big payload attack
  4. แยก Topic ตาม user-id เพื่อกันข้อมูลรั่ว

10. Monitoring & Benchmark

Spring Boot Actuator มี metric เช่น:

  • reactor.netty.connection.active จำนวน Connection SSE
  • tomcat.sessions.active สำหรับ WS in-memory

ทดสอบด้วย wrk ส่ง 50 kpps SSE พบ CPU ใช้เพียง 25 %

11. FAQ

Q: Safari บน iOS ไม่รับ SSE?

A: ใช้ Fetch + ReadableStream Polyfill หรือ Upgrade เป็น iOS 13+

Q: WebSocket บน AWS ALB ตั้งอย่างไร?

A: เปิด idle-timeout = 4000 s และใช้ protocol: HTTP2

12. สรุป

ทั้ง SSE และ WebSocket ทำให้แอปของคุณ พูดคุย กับผู้ใช้แบบเรียลไทม์ เลือกให้เหมาะ: ถ้าเพียง “ส่งแจ้งเตือน” ใช้ SSE ง่าย เบา เสถียร, ถ้า “โต้ตอบสองทาง” เช่น Chat หรือ Game ให้ไป WebSocket อย่าลืมวัดผล & ปรับแต่ง timeout , buffer และ security ให้รอบด้าน!

โค้ดตัวอย่างเต็ม ดาวน์โหลดได้ที่ GitHub: realtime-demo


© 2025 poolsawat.com • ขอบคุณที่ติดตาม หากบทความนี้เป็นประโยชน์ฝาก Share ต่อให้เพื่อน 👍

Leave a Reply

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *