SSE(Server-Sent-Event)란?
클라이언트에게 서버로부터 실시간 업데이트를 허용하는 웹 기술.
text/event-stream 타입으로 서버에서 클라이언트에 단방향 통신을 할 수 있습니다.
필요 사항
•
클라이언트에서 매번 요청없이 한번의 연결을 통해 서버와 통신해야하는 경우
•
WebSocket 방식보다 라이트한 방식을 원하는 경우
•
이벤트 타입을통해 한번의 연결로 다양한 분기를 수행해야하는 경우
•
지속적인 연결을 유지해야야는 경우
Client (JS)
•
EventSource 객체를통해 서버와 통신이 가능합니다. (단방향)
const eventSource = new EventSource(url)
JavaScript
복사
선언과 동시에 해당 URL로 통신 시도
•
실시간 데이터 스트리밍 방식 (text/event-stream)
•
지속적인 연결 상태 유지
◦
호출을 통해 닫힐 때까지 연결함.
•
서버에서 보내주는 이벤트를 통해 action을 분리할 수 있다.
◦
server에서 eventName을 입력하면 해당 이벤트로 받아야 합니다.
eventSource.addEventListener('eventname', (event) => {
//...
})
JavaScript
복사
◦
server에서 eventName을 비어서 보낸다면 “message” 이벤트로 받으면 됩니다.
eventSource.addEventListener('message', (event) => {
//...
})
// or
eventSource.onmessage = event => {
//...
}
JavaScript
복사
응답값
•
lastEventId 값: 이것을 통해 통신이 끊겨있는동안 받지 못한 이벤트를 받을 수 있음.
•
type : 서버에서 전달해주는 이벤트 이름 (default: “message”)
•
data : 서버에서 전달해주는 값
요청값
SSE는 서버에서 클라이언트로 일반적인 단방향 통신을 지원합니다. 따라서 도중에 클라이언트가 서버에게 요청하는 방법은 없습니다.
예외방법
•
최초 통신 시도시 : query-string
new EventSource('sse-stream?param=Helloworld')
JavaScript
복사
•
서버에서 End-point를 제공하여 쿠키나 검증 코드 실행하고 연결
Server (kotlin)
•
SseEmitter 객체를 통해 emitter 인스턴스를 생성하고 리턴하면 연결 끝
private val emitters: ConcurrentHashMap<String, SseEmitter> = ConcurrentHashMap()
@GetMapping("/stream-sse", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun streamEvents(@RequestParam(value = "user", required = false, defaultValue = "empty") user: String): SseEmitter {
val emitter = SseEmitter(60_000L)
emitters[user] = emitter
emitter.send(
SseEmitter.event().name("message").data(user)
)
emitter.onCompletion { emitters.remove(user) }
emitter.onTimeout { emitters.remove(user) }
return emitter
}
JavaScript
복사
•
concurrentHansMap 방식 또는 CopyOnWriteArrayList 방식중에서 선택
◦
쓰레드로 접근시 값의 충돌이나 점유상태를 안전하게하기 위함
•
client로 발신
private val scheduler: TaskScheduler = ConcurrentTaskScheduler(Executors.newSingleThreadScheduledExecutor())
@PostConstruct
fun startPeriodicTimeEvents() {
scheduler.scheduleWithFixedDelay({
// 모든 사용자 중 랜덤하게 한 명을 선택
if (emitters.isNotEmpty()) {
val users = emitters.keys.toList()
val selectedUser = users[Random.nextInt(users.size)]
// 선택된 사용자에게만 메시지 전송
emitters[selectedUser]?.let { emitter ->
try {
val eventId = System.currentTimeMillis().toString()
val data = jacksonObjectMapper().writeValueAsString(mapOf("time" to eventId))
emitter.send(SseEmitter.event().id(eventId).name("message").data(data, MediaType.APPLICATION_JSON))
} catch (e: IOException) {
println("Error sending message: $e") // 로그 출력
emitters.remove(selectedUser)
}
}
}
}, 2000)
}
JavaScript
복사
예제 코드
client (html, css, JS)
server (spring boot, java 17, gradle, jar, kotlin)
참고