Ktor 서버에서 WebSockets
필수 종속성: io.ktor:ktor-server-websockets
코드 예시: server-websockets
WebSocket은 단일 TCP 연결을 통해 사용자의 브라우저와 서버 간에 전이중 통신 세션을 제공하는 프로토콜입니다. 서버로/부터 실시간 데이터 전송이 필요한 애플리케이션을 생성하는 데 특히 유용합니다.
Ktor는 서버 및 클라이언트 측 모두에서 WebSocket 프로토콜을 지원합니다.
Ktor를 사용하면 다음을 수행할 수 있습니다.
- 기본 WebSocket 설정(예: 프레임 크기, 핑 주기 등)을 구성합니다.
- 서버와 클라이언트 간 메시지 교환을 위한 WebSocket 세션을 처리합니다.
- WebSocket 확장 기능을 추가합니다. 예를 들어, Deflate 확장 기능을 사용하거나 사용자 지정 확장 기능을 구현할 수 있습니다.
클라이언트 측 WebSocket 지원에 대해 알아보려면 WebSockets 클라이언트 플러그인을 참조하세요.
단방향 통신 세션의 경우 Server-Sent Events (SSE) 사용을 고려해 보세요. SSE는 서버가 클라이언트에 이벤트 기반 업데이트를 전송해야 하는 경우에 특히 유용합니다.
의존성 추가
WebSockets
을(를) 사용하려면 빌드 스크립트에 ktor-server-websockets
아티팩트를 포함해야 합니다:
WebSockets 설치
애플리케이션에 WebSockets
플러그인을 설치하려면 지정된
install
함수에 전달하세요. 아래 코드 스니펫은 WebSockets
을(를) 설치하는 방법을 보여줍니다... - ...
embeddedServer
함수 호출 내에서. - ...
Application
클래스의 확장 함수인 명시적으로 정의된module
내에서.
WebSockets 구성
선택적으로, install
블록 내에서 WebSocketOptions을(를) 전달하여 플러그인을 구성할 수 있습니다:
pingPeriod
속성을 사용하여 핑 간의 지속 시간을 지정합니다.timeout
속성을 사용하여 연결이 닫힐 시간 초과를 설정합니다.maxFrameSize
속성을 사용하여 수신하거나 보낼 수 있는 최대Frame
크기를 설정합니다.masking
속성을 사용하여 마스킹을 활성화할지 여부를 지정합니다.contentConverter
속성을 사용하여 직렬화/역직렬화를 위한 컨버터를 설정합니다.
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
WebSockets 세션 처리
API 개요
WebSockets
플러그인을 설치하고 구성한 후 WebSocket 세션을 처리할 엔드포인트를 정의할 수 있습니다. 서버에 WebSocket 엔드포인트를 정의하려면 라우팅 블록 내에서 webSocket
함수를 호출합니다:
routing {
webSocket("/echo") {
// Handle a WebSocket session
}
}
이 예시에서, 기본 구성이 사용될 때 서버는 ws://localhost:8080/echo
로 WebSocket 요청을 수락합니다.
webSocket
블록 내에서 WebSocket 세션에 대한 핸들러를 정의하며, 이는 DefaultWebSocketServerSession 클래스로 표현됩니다. 블록 내에서 다음 함수와 속성을 사용할 수 있습니다:
send
함수를 사용하여 클라이언트에 텍스트 콘텐츠를 보냅니다.incoming
및outgoing
속성을 사용하여 WebSocket 프레임을 수신하고 보내기 위한 채널에 접근합니다. 프레임은Frame
클래스로 표현됩니다.close
함수를 사용하여 지정된 이유와 함께 종료 프레임을 보냅니다.
세션을 처리할 때, 프레임 유형을 확인할 수 있습니다. 예를 들어:
Frame.Text
는 텍스트 프레임입니다. 이 프레임 유형의 경우Frame.Text.readText()
를 사용하여 콘텐츠를 읽을 수 있습니다.Frame.Binary
는 바이너리 프레임입니다. 이 유형의 경우Frame.Binary.readBytes()
를 사용하여 콘텐츠를 읽을 수 있습니다.
incoming
채널에는 핑/퐁 또는 종료 프레임과 같은 제어 프레임이 포함되어 있지 않습니다. 제어 프레임을 처리하고 분할된 프레임을 재조립하려면 webSocketRaw 함수를 사용하여 WebSocket 세션을 처리하세요.
클라이언트에 대한 정보(예: 클라이언트의 IP 주소)를 얻으려면
call
속성을 사용하세요. 일반 요청 정보에 대해 알아보세요.
아래에서 이 API를 사용하는 예시를 살펴보겠습니다.
예시: 단일 세션 처리
아래 예시는 단일 클라이언트와의 세션을 처리하기 위해 echo
WebSocket 엔드포인트를 생성하는 방법을 보여줍니다:
routing {
webSocket("/echo") {
send("Please enter your name")
for (frame in incoming) {
frame as? Frame.Text ?: continue
val receivedText = frame.readText()
if (receivedText.equals("bye", ignoreCase = true)) {
close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE"))
} else {
send(Frame.Text("Hi, $receivedText!"))
}
}
}
}
전체 예시는 server-websockets를 참조하세요.
예시: 다중 세션 처리
여러 WebSocket 세션을 효율적으로 관리하고 브로드캐스팅을 처리하려면 Kotlin의 SharedFlow
를 활용할 수 있습니다. 이 접근 방식은 WebSocket 통신을 관리하기 위한 확장 가능하고 동시성 친화적인 방법을 제공합니다. 이 패턴을 구현하는 방법은 다음과 같습니다:
- 메시지 브로드캐스팅을 위한
SharedFlow
를 정의합니다:
val messageResponseFlow = MutableSharedFlow<MessageResponse>()
val sharedFlow = messageResponseFlow.asSharedFlow()
- WebSocket 라우트에서 브로드캐스팅 및 메시지 처리 로직을 구현합니다:
webSocket("/ws") {
send("You are connected to WebSocket!")
val job = launch {
sharedFlow.collect { message ->
send(message.message)
}
}
runCatching {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
val messageResponse = MessageResponse(receivedText)
messageResponseFlow.emit(messageResponse)
}
}
}.onFailure { exception ->
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
runCatching
블록은 수신 메시지를 처리하고 SharedFlow
로 내보내며, SharedFlow
는 모든 컬렉터에게 브로드캐스팅합니다.
이 패턴을 사용하면 개별 연결을 수동으로 추적하지 않고도 여러 WebSocket 세션을 효율적으로 관리할 수 있습니다. 이 접근 방식은 동시 WebSocket 연결이 많은 애플리케이션에 잘 맞고, 메시지 브로드캐스팅을 처리하는 깔끔하고 반응적인 방법을 제공합니다.
전체 예시는 server-websockets-sharedflow를 참조하세요.
WebSocket API와 Ktor
WebSocket API의 표준 이벤트는 Ktor에 다음과 같이 매핑됩니다:
onConnect
는 블록의 시작 부분에서 발생합니다.onMessage
는 메시지를 성공적으로 읽은 후(예:incoming.receive()
사용) 또는for(frame in incoming)
과 같은 일시 중단된 반복을 사용할 때 발생합니다.onClose
는incoming
채널이 닫힐 때 발생합니다. 이는 일시 중단된 반복을 완료하거나 메시지를 수신하려고 할 때ClosedReceiveChannelException
을 발생시킵니다.onError
는 다른 예외와 동일합니다.
onClose
와 onError
모두에서 closeReason
속성이 설정됩니다.
다음 예시에서 무한 루프는 예외( ClosedReceiveChannelException
또는 다른 예외)가 발생할 때만 종료됩니다:
webSocket("/echo") {
println("onConnect")
try {
for (frame in incoming){
val text = (frame as Frame.Text).readText()
println("onMessage")
received += text
outgoing.send(Frame.Text(text))
}
} catch (e: ClosedReceiveChannelException) {
println("onClose ${closeReason.await()}")
} catch (e: Throwable) {
println("onError ${closeReason.await()}")
e.printStackTrace()
}
}