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()
}
}