프로젝트 중 알림 기능이 필요해져 검색하던 와중 SSE에 대해 알게 되었다.
SSE vs 웹소켓
간단하게!
웹소켓 : 양방향으로 서버와 클라이언트 간의 데이터를 주고 받을 수 있다, 자동재접속은 안됨
SSE : 클라이언트는 데이터를 받을 수만 있게 된다 (일방향), 자동재접속 가능
이제 안드로이드 스튜디오에서 SSE를 구현해보겠습니다.
dependencies {
implementation("com.launchdarkly:okhttp-eventsource:4.1.0")
}
먼저 dependencies를 implementation 해준다
class SseEventHandler : BackgroundEventHandler {
override fun onOpen() {
// SSE 연결 성공시 처리 로직 작성
}
override fun onClosed() {
// SSE 연결 종료시 처리 로직 작성
}
override fun onMessage(event: String, messageEvent: MessageEvent) {
// SSE 이벤트 도착시 처리 로직 작성
// event: String = 이벤트가 속한 채널 또는 토픽 이름
// messageEvent.lastEventId: String = 도착한 이벤트 ID
// messageEvent.data: String = 도착한 이벤트 데이터
}
override fun onComment(comment: String) {
}
override fun onError(t: Throwable) {
// SSE 연결 전 또는 후 오류 발생시 처리 로직 작성
// 서버가 2XX 이외의 오류 응답시 com.launchdarkly.eventsource.StreamHttpErrorException: Server returned HTTP error 401 예외가 발생
// 클라이언트에서 서버의 연결 유지 시간보다 짧게 설정시 error=com.launchdarkly.eventsource.StreamIOException: java.net.SocketTimeoutException: timeout 예외가 발생
// 서버가 연결 유지 시간 초과로 종료시 error=com.launchdarkly.eventsource.StreamClosedByServerException: Stream closed by server 예외가 발생
}
}
// EventSource 오브젝트 생성
val eventSource: BackgroundEventSource = BackgroundEventSource
.Builder(
SseEventHandler(),
EventSource.Builder(
ConnectStrategy
.http(URL("{server-url}"))
// 커스텀 요청 헤더를 명시
.header(
"Authorization",
"Bearer {token}"
)
.connectTimeout(3, TimeUnit.SECONDS)
// 최대 연결 유지 시간을 설정, 서버에 설정된 최대 연결 유지 시간보다 길게 설정
.readTimeout(600, TimeUnit.SECONDS)
)
)
.threadPriority(Thread.MAX_PRIORITY)
.build()
// EventSource 연결 시작
eventSource.start()
위 코드에서 오류가 발생하면 Logcat에서 오류를 확인할 수 있습니다.
서버 연결 유지 시간은 모두 끝나거나 실패 시 자동으로 재접속합니다
핸들러 같은 경우 알림 발생 시 notification을 띄우고 싶다면
import com.launchdarkly.eventsource.MessageEvent
import com.launchdarkly.eventsource.background.BackgroundEventHandler
class SseHandler(private val context: Context) : BackgroundEventHandler {
private val channelId = "channelId"
private val channelName = "Channel Name"
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
override fun onOpen() {
// SSE 연결 성공시 처리 로직 작성
Log.d("SSE", "SSE연결 성공")
}
override fun onClosed() {
// SSE 연결 종료시 처리 로직 작성
Log.d("SSE", "SSE 안전 종료")
}
override fun onMessage(event: String?, messageEvent: MessageEvent?) {
val messageData = messageEvent?.data ?: return
Log.e("SSE", "Received data: $messageData")
val eventId = messageEvent.lastEventId
val eventType = messageEvent.eventName
val eventData = messageEvent.data
println("Event ID: $eventId")
println("Event Type: $eventType")
println("Event Data: $eventData")
val comment = if (eventData.contains(":")) {
messageEvent.data.split(":").map { it.toString() }.last()
} else {
eventData
}
Log.e("SSE Comment", comment.toString())
if (!comment.contains("EventStream")) {
// Android 8.0 이상에서는 Notification Channel을 설정해야 합니다.
val tapResultIntent = Intent(context, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
tapResultIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(channelId, channelName, importance).apply {
description = "descriptionText"
}
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.top_icon_vector)
.setContentTitle("알림")
.setContentText(comment)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
try {
notificationManager.notify(1, builder.build())
Log.d("Notification", "Notification created successfully")
} catch (e: Exception) {
Log.e("Notification", "Failed to create notification: ${e.message}")
}
}
}
override fun onComment(comment: String?) {
}
override fun onError(t: Throwable?) {
Log.d("SSE", "SSE연결 실패")
Log.e("SSE", t.toString())
//java.net.SocketTimeoutException: timeout
}
}
들어오는 messageEvent의 이벤트를 잘 확인하고 처리하는 방식을 신경써서 들어오는 메세지 대로 알림을 띄우는 방식입니다!
참조
black_han26님의 velog
surviveasdev.tistory.com
json-object.github.io/Implementing-SSE-logic
medium.com/@anugrahasb1997/implementing-server-sent-events-sse-in-android