제6장 WebSocket 컨테이너

내용 목차

6.1. 개요
6.2. 제약 사항
6.3. WebSocket 컨테이너 기능
6.3.1. WebSocket Session Failover
6.3.2. 기타
6.4. WebSocket 컨테이너 설정
6.5. Spring WebSocket 사용

본 장에서는 WebSocket 컨테이너의 개념과 기능, 설정 방법에 대해 설명한다.

6.1. 개요

WebSocket 컨테이너는 웹 엔진에 내부적으로 포함되어 있는 형태이며 웹 컨텍스트마다 하나씩 존재한다. WebSocket 컨테이너를 사용하기 위해서는 반드시 jeus-web-dd.xml에 <websocket>을 설정해야 한다.

JEUS에서 제공하는 WebSocket 컨테이너는 JSR 356, Java API for WebSocket을 준수한다. 따라서 서버의 WebSocket 서비스는 해당 표준에서 정의한 API에 따라 작성한다. 단, 본 장에서는 WebSocket RFC6455 및 Java API for WebSocket 표준에 대한 설명은 포함하지 않는다.

참고

1. HTML5 WebSocket API는 클라이언트 라이브러리이므로 WebtoB 및 JEUS와는 관계없다.

2. Java API for WebSocket은 Java EE 7에 포함되어 있다. Java EE 6 API 클래스들을 참조해서 개발할 경우 WebSocket API를 참조할 수 없다. 이때는 lib/system/javaee.jar를 classpath로 지정한다. 여기에 javax.websocket 패키지가 포함되어 있다.

WebSocket 컨테이너의 기본적인 역할은 웹 컨텍스트에 포함된 WebSocket Server Endpoint 개체들을 deploy해주고, 클라이언트로부터 WebSocket Handshake 요청이 왔을 때 이에 매핑되는 Server Endpoint 개체를 연결시켜주는 것이다. 클라이언트와 WebSocket 연결이 맺어지면 WebSocket Session이 생성되는데 이 Session 개체에 대한 라이프 사이클도 관리해준다.

서비스 개발자의 역할은 WebSocket Server Endpoint 클래스(javax.websocket.Endpoint) 및 Configuration 클래스(javax.websocket.server.ServerApplicationConfig)를 작성해서 웹 애플리케이션에 패키징하는 것이다.

6.2. 제약 사항

JEUS 7에서 WebSocket 컨테이너를 사용할 때는 다음과 같은 제약 사항이 존재한다.

  • HTTP 리스너에서만 사용 가능하다. 그러므로 WebtoB 4.1.8 이상 버전의 Reverse Proxy와 결합해서 사용하는 것을 권장한다. WebtoB Reverse Proxy에서의 WebSocket 지원에 관한 내용은 WebtoB 매뉴얼을 참고한다.

  • jeus-web-dd.xml의 <websocket> 설정이 반드시 필요하다.

  • Java API for WebSocket 표준에서 제공하는 Client API는 제공하지 않는다. Client API는 반드시 Java SE 7을 필요로 한다. JEUS 7은 Java EE 6 기반 제품으로 Java SE 6로 컴파일해서 제공하므로 Client API를 제공할 수 없다. Client API에 해당하는 메소드를 호출하는 경우에는 java.lang.UnsupportedOperationException이 발생한다.

    참고

    Client API는 javax.websocket.WebSocketContainer#connectToServer() 메소드들이다.

6.3. WebSocket 컨테이너 기능

본 절에서는 WebSocket 컨테이너는 부가적인 기능에 대해 설명한다.

6.3.1. WebSocket Session Failover

WebSocket 컨테이너는 WebSocket Session별로 메모리에 데이터를 저장할 수 있는 공간을 제공한다. javax.websocket.Session.getUserProperties() API를 호출해서 얻은 Map 객체(이하 UserProperties)가 그 공간을 나타낸다.

WebSocket 애플리케이션(WebSocket Server Endpoint가 포함된 웹 컨텍스트)을 서로 다른 두 서버에 deploy한 상황이라고 가정하면 한쪽 서버에 WebSocket을 연결해서 UserProperties에 데이터를 저장하면서 사용했는데 그 서버가 비정상 종료되면 데이터가 모두 사라지게 된다. 이를 만약 다른 서버에 백업해둔다면 한쪽 서버가 비정상 종료되더라도 다른 서버로 WebSocket을 연결해서 UserProperties를 복원할 수 있다. WebSocket 서비스 사용자 입장에서는 아무 문제없이 서비스를 사용할 수 있다. 이를 WebSocket Session FailoverDistributed WebSocket Session이라고 한다.

이 기능을 사용하기 위해서는 다음과 같은 조건들을 만족해야 한다.

  • WebSocket Session Failover는 HTTP 세션 서비스를 기반으로 동작한다. WebSocket 애플리케이션이 반드시 HTTP 세션 클러스터에 deploy되어야 한다. HTTP 세션 클러스터에 대한 자세한 내용은 JEUS 세션 관리 안내서”의 “2.8. 세션 클러스터링”을 참고한다.

  • WebSocket 애플리케이션의 jeus-web-dd.xml 설정 파일에 다음과 같이 기술되어야 한다.

    <websocket>
        <distributed-session enabled="true"/>
    </websocket>
    

    즉, 기본적으로는 WebSocket Session과 HTTP Session을 연동하지 않는다.

  • UserProperties에 put하는 데이터는 Serializable해야 한다.

  • WebSocket 연결은 HTTP 요청으로 시작하는데 이때 WebSocket Handshake 요청의 Cookie 헤더에 JSESSIONID가 있어야 한다. JSESSIONID가 가리키는 HTTP 세션은 Server Endpoint가 포함된 웹 컨텍스트에 생성되어 있어야 한다.

    예를 들어 HTML5 WebSocket API를 호출하도록 작성된 JSP를 호출하면 HTTP 세션이 생성되어 응답 Cookie 헤더로 JESSIONID가 전달된다. 웹 브라우저에서 HTML5 WebSocket JavaScript를 실행하면서 JEUS로 WebSocket Handshake 요청을 보내는데, 그 요청 헤더에 Cookie 헤더가 들어가야 한다. 이를 지원하는 않는 WebSocket Client를 사용하면 WebSocket Session Failover 기능을 사용할 수 없다.

    FireFox 30.0은 HTML5 WebSocket API를 사용할 때 WebSocket Handshake 요청에 Cookie 헤더를 붙여준다.

    GET /service/chat HTTP/1.1
    Connection: keep-alive, Upgrade
    Cookie: JSESSIONID=FheDy8e0bOTPO7KNqdeJ7Eps8j51CaQqcRWHvpYo9mdVw1BCSwlwrFiyrclsolkr.amV1czcvc2VydmVyMg==
    Sec-WebSocket-Key: Csv/FCQo1g1eZfqMtPd8+g==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0 FirePHP/0.7.4

참고

WebtoB Reverse Proxy와 함께 이 기능을 사용할 경우 Reverse Proxy 절에서 Session Routing 설정을 해야 한다. 이에 대한 자세한 사항은 WebtoB 매뉴얼을 참고한다.

주의사항

이 기능을 사용할 때는 다음과 같은 사항을 주의해야 한다.

  • HTTP Session의 timeout과 WebSocket Session의 timeout은 서로 독립적이다. 특히 WebSocket Session의 경우 HTTP Session에 의존적으로 동작하는 것이기 때문에 WebSocket 컨테이너 차원에서 HTTP Session의 속성을 마음대로 바꿀 수는 없다. 상황에 따라서는 UserProperties에 Serialzable한 데이터를 put하는 시점에 HTTP Session이 먼저 timeout 처리돼서 Failover가 되지 않을 수 있다. 이러한 상황을 고려해서 HTTP Session의 timeout을 적절하게 조정해야 한다.

    참고

    UserProperties에 Serialzable한 데이터를 put하는 시점에 HTTP Session이 timeout된 경우에는 WARNING 레벨의 로그를 남긴다.

  • 서로 다른 웹 컨텍스트 간에 WebSocket Session의 UserProperties는 공유되지 않는다.

6.3.2. 기타

javax.websocket.Session API를 통해서 얻을 수 없는 정보

Session.getUserProperties()의 리턴값인 Map<String, Object>에서 아래와 같은 정보들을 얻을 수 있다.

항목설명
jeus.websocket.remoteAddr(String)

HTTP 요청 헤더에 Forwarded 또는 X-Forwarded-For가 있는 경우에는 해당 헤더의 값을 사용한다.

없는 경우에는 HttpServletRequest.getRemoteAddr() 값을 사용한다.

jeus.websocket.remoteHost(String)

HTTP 요청 헤더에 Forwarded 또는 X-Forwarded-For가 있는 경우에는 해당 헤더의 값을 사용한다.

없는 경우에는 HttpServletRequest.getRemoteHost()와 같다.

jeus.websocket.remotePort(String)

HTTP 요청 헤더에 Forwarded 또는 X-Forwarded-For가 있는 경우에는 해당 헤더의 값에 포함된 포트를 사용한다. 만약 헤더는 있는데 포트 정보가 없다면 이 프로퍼티는 제공하지 않는다.

헤더가 없는 경우에는 HttpServletRequest.getRemotePort() 값을 사용하되, 0보다 큰 경우에만 유효하다.

6.4. WebSocket 컨테이너 설정

WebSocket 컨테이너의 정보는 각 웹 애플리케이션의 jeus-web-dd.xml에 설정한다.

[예 6.1] 웹 컨텍스트 설정 파일 : <<jeus-web-dd.xml>>

<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="7.0">
  <websocket>
      <max-incoming-binary-message-buffer-size>8192</max-incoming-binary-message-buffer-size>
      <max-incoming-text-message-buffer-size>8192</max-incoming-text-message-buffer-size>
      <max-session-idle-timeout-in-millis>1800000</max-session-idle-timeout-in-millis>
      <monitoring-period-in-millis>300000</monitoring-period-in-millis>
      <blocking-send-timeout-in-millis>10000</blocking-send-timeout-in-millis>
      <async-send-timeout-in-millis>30000</async-send-timeout-in-millis>
      <websocket-executor>
          <min>10</min>
          <max>30</max>
          <keep-alive-time>60000</keep-alive-time>
          <queue-size>4096</queue-size>
      </websocket-executor>
      <distributed-session>
          <enabled>false</enabled>
          <use-write-through-policy>false</use-write-through-policy>
      </distributed-session>
      <init-param>
          <name>name</name>
          <value>value</value>
      </init-param>
      <batching-buffer-size>655536</batching-buffer-size>
      <websocket-timeout-min-threads>1</websocket-timeout-min-threads>
  </websocket>
</jeus-web-dd>


다음은 설정 태그에 대한 설명이다.

태그설명
<max-incoming-binary-message-buffer-size>

클라이언트로부터 전달되는 바이너리 메시지를 버퍼링할 때 사용하는 버퍼의 최댓값을 의미한다.

설정된 값보다 큰 메시지가 전달되는 경우 1009 에러를 내고 WebSocket Session을 닫는다.

<max-incoming-text-message-buffer-size>

클라이언트로부터 전달되는 텍스트 메시지를 버퍼링할 때 사용하는 버퍼의 최댓값을 의미한다.

설정된 값보다 큰 메시지가 전달되는 경우 1009 에러를 내고 WebSocket Session을 닫는다.

<max-session-idle-timeout-in-millis>

유휴 상태의 WebSocket 세션을 언제 닫을 것인지 결정하는 값이다.

설정된 값이 0보다 크고 1000보다 작을 경우에는 무조건 1000으로 취급한다. (기본값: 30분(1800000ms))

<monitoring-period-in-millis>

WebSocket 세션 타임아웃 여부를 체크하기 위한 주기를 설정한다.

설정된 값이 1000보다 작을 경우에는 무조건 1000으로 취급한다. (기본값: 5분(300000ms))

<blocking-send-timeout-in-millis>

Synchronous Send을 사용하는 경우 대기할 시간을 설정한다.

javax.websocket.RemoteEndpoint.Basic을 사용할 때 적용된다.

(기본값: 10초)

<async-send-timeout-in-millis>

Asynchronous Send을 사용하는 경우 서버 상에서 보내지 못하고 있는 메시지에 대한 타임아웃을 나타낸다.

javax.websocket.WebSocketContainer.getDefaultAsyncSendTimeout()에서 리턴된다.

<websocket-executor>

WebSocket 컨테이너 내부적으로 사용하는 Thread Pool 관련 설정이다.

주로 asynchronous send를 처리하기 위해서 사용한다.

다음의 하위 항목을 설정한다.

  • <keep-alive-time>

    Min을 초과하는 스레드에 대해서 설정된 시간 동안 사용되지 않는다면 자동적으로 Thread Pool에서 제거된다. 0이면 제거하지 않는다. (기본값: 1분(60000ms))

  • <queue-size>

    Thread Pool이 처리하는 Task를 저장하는 Queue의 크기를 지정한다. (기본값: 4096)

<distributed-session>

javax.websocket.Session.getUserProperties()에 정의된 내용에 따라 제공하는 WebSocket Session Failover 관련 설정이다.

다음의 하위 항목을 설정한다.

  • <enabled>

    WebSocket Session Failover 사용 여부를 결정한다. HTTP Session과 연동해야 하기 때문에 기본적으로는 사용하지 않는다.

  • <use-write-through-policy>

    WebSocket Session의 UserProperties에 put/remove할 때 백업 서버로의 동기화가 끝날 때까지 기다릴 것인지 그 여부를 선택한다. 기본적으로는 기다리지 않고 백그라운드에서 동기화가 일어나도록 한다.

<init-param>

WebSocket Container에서 사용하는 추가 설정을 나타낸다.

<name> 항목에 parameter name과 <value>에 설정할 값을 적는다

<batching-buffer-size>

RemoteEndpoint.setBatchingAllowed()를 통해 batch send 기능을 사용할 때 설정한다.

여기에 설정된 값보다 batching buffer에 쌓인 frame의 payload들의 크기가 커지는 경우 RemoteEndpoint.flushBatch()로 버퍼를 비운다.

(기본값: 64KB(65536Byte))

<websocket-timeout-min-threads>

WebSocket Container 내에서 발생하는 Timeout 처리를 하기 위한 Thread Pool의 최소 개수를 설정한다.

0이면 Timeout이 제대로 동작하지 않을 수 있으므로 항상 1 이상으로 설정해야 한다.

6.5. Spring WebSocket 사용

본 절에서는 JEUS에서 Spring WebSocket을 사용하는 방법에 대해서 설명한다.

Spring WebSocket은 웹 엔진에서 제공하는 WebSocket 컨테이너를 사용하면서 추가적인 기능을 제공하는 프레임워크이다. JSR 356, Java API for WebSocket을 준수하는 WebSocket 컨테이너는 동일한 인터페이스를 제공하나 WebSocket 컨테이너를 얻는 방법은 각 벤더별로 차이가 있다.

Spring WebSocket에서는 현재 특정 벤더에 대해서만 WebSocket 컨테이너를 얻는 방법을 지원한다. 따라서 JEUS에서는 Spring WebSocket을 사용하기 위한 별도의 라이브러리를 제공하며 이 라이브러리를 사용하기 위한 추가 설정이 필요하다.

참고

현재 JEUS와 함께 제공되는 라이브러리는 Spring 프레임워크 4.2.0 이상의 버전을 지원한다.

애플리케이션에 Spring WebSocket 지원 라이브러리를 추가하기 위한 설정을 한다.

[예 6.2] 웹 컨텍스트 설정 파일 : <<jeus-web-dd.xml>>

<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="7.0">
    <library-ref>
        <library-name>spring-support</library-name>
        <specification-version>
            <value>4.2.0.RELEASE</value>
        </specification-version>
        <failonerror>true</failonerror>
    </library-ref>
    <websocket/>
</jeus-web-dd>


Spring WebSocket 지원 라이브러리를 사용하기 위하여 websocket handshake-handler로 jeus.spring.websocket.JeusHandshakeHandler를 지정해준다.

[예 6.3] 스프링 컨텍스트 설정 파일 : <<spring-context.xml>>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xmlns:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.2.xsd">

    <websocket:handlers>
        <websocket:mapping path="/chat" handler="chatHandler"/>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>

    <bean id="chatHandler" class="com.tmax.jeus.ChatHandler"/>
    <bean id="handshakeHandler" class="jeus.spring.websocket.JeusHandshakeHandler"/>
</beans>