내용 목차
본 장에서는 WebSocket 컨테이너의 개념과 기능, 설정 방법에 대해 설명한다.
WebSocket 컨테이너는 웹 엔진에 내부적으로 포함되어 있는 형태이며 웹 컨텍스트마다 하나씩 존재한다. JEUS에서 제공하는 WebSocket 컨테이너는 JSR 356, Java API for WebSocket을 준수한다. 따라서 서버의 WebSocket 서비스는 해당 표준에서 정의한 API에 따라 작성한다. 단, 본 장에서는 WebSocket RFC6455 및 Jakarta API for WebSocket 표준에 대한 설명은 포함하지 않는다.
1. HTML5 WebSocket API는 클라이언트 라이브러리이므로 WebtoB 및 JEUS와는 관계없다.
2. Jakarta API for WebSocket은 Jakarta EE 8에 포함되어 있다. 따라서 Jakarta EE 8 기반의 개발을 한다면 기본적인 Jakarta EE 8 라이브러리를 참조해서 개발한다.
WebSocket 컨테이너의 기본적인 역할은 웹 컨텍스트에 포함된 WebSocket Server Endpoint 개체들을 deploy해주고, 클라이언트로부터 WebSocket Handshake 요청이 왔을 때 이에 매핑되는 Server Endpoint 개체를 연결시켜주는 것이다. 클라이언트와 WebSocket 연결이 맺어지면 WebSocket 세션이 생성되는데 이 Session 개체에 대한 라이프 사이클도 관리해 준다.
서비스 개발자의 역할은 WebSocket Server Endpoint 클래스(javax.websocket.Endpoint) 및 Configuration 클래스(javax.websocket.server.ServerApplicationConfig)를 작성해서 웹 애플리케이션에 패키징하는 것이다. 또한 annotation을 이용한 pojo style로 개발할 수도 있다.
JEUS 8.5에서 WebSocket 컨테이너를 사용할 때는 다음과 같은 제약 사항이 존재한다.
기본적으로 HTTP 리스너에서 사용 가능하다. WebtoB와 함께 사용하기 위해서는 WebtoB 매뉴얼을 참고한다.
jeus-web-dd.xml의 <websocket> 항목에 자세한 설정이 가능하나, 해당 설정이 없어도 WebSocket을 사용할 수 있다.
Java API for WebSocket 표준에서 제공하는 클라이언트 API는 lib/system/jeus-websocket-client.jar로 제공한다. 만약 다른 벤더사의 클라이언트 모듈을 사용하고 싶을 때는 위의 jar 파일의 META-INF/service/javax.websocket.ContainerProvider 파일 안에 타 벤더의 Provider class의 Full name을 지정하면 Service loader를 통해 사용할 수 있다.
본 절에서는 WebSocket 컨테이너는 부가적인 기능에 대해 설명한다.
WebSocket 컨테이너는 WebSocket 세션별로 메모리에 데이터를 저장할 수 있는 공간을 제공한다. javax.websocket.Session.getUserProperties() API를 호출해서 얻은 Map 객체(이하 UserProperties)가 그 공간을 나타낸다.
WebSocket 애플리케이션(WebSocket Server Endpoint가 포함된 웹 컨텍스트)을 서로 다른 두 서버에 deploy한 상황이라고 가정하면 한쪽 서버에 WebSocket을 연결해서 UserProperties에 데이터를 저장하면서 사용했는데 그 서버가 비정상 종료되면 데이터가 모두 사라지게 된다. 이를 만약 다른 서버에 백업해둔다면 한쪽 서버가 비정상 종료되더라도 다른 서버로 WebSocket을 연결해서 UserProperties를 복원할 수 있다. WebSocket 서비스 사용자 입장에서는 아무 문제없이 서비스를 사용할 수 있다. 이를 WebSocket UserProperties Failover나 Distributed WebSocket UserProperties이라고 한다.
기존의 사용자는 미리 저장해 놓은 UserProperties 안의 데이터는 가져올 수는 있으나, failover된 WebSocket Endpoint는 기존 서버와는 다른 서버에 존재하는 endpoint이고, WebSocket 세션은 새롭게 생성된다. 따라서 이 기능은 매우 한정적인 케이스에 한해서 사용되어져야 한다.
이 기능을 사용하기 위해서는 다음과 같은 조건들을 만족해야 한다.
WebSocket UserProperties Failover는 HTTP 세션 서비스를 기반으로 동작한다. WebSocket 세션 클러스터를 지원해야 한다. 세션 클러스터에 대한 자세한 내용은 “JEUS 세션 관리 안내서”의 “2.6. 세션 클러스터 모드”를 참고한다.
WebSocket 애플리케이션의 jeus-web-dd.xml 설정 파일에 다음과 같이 기술되어야 한다.
[예 6.1] WebSocket UserProperties Failover 설정 : <<jeus-web-dd.xml>>
<websocket> <distributed-userProperties> <enabled>true</enabled> </distributed-userProperties> </websocket>
즉, 기본적으로는 WebSocket 세션과 HTTP 세션을 연동하지 않는다.
UserProperties에 put하는 데이터는 Serializable해야 한다.
WebSocket 연결은 HTTP 요청으로 시작하는데 이때 WebSocket Handshake 요청의 Cookie 헤더에 JSESSIONID가 있어야 한다. JSESSIONID가 가리키는 HTTP 세션은 Server Endpoint가 포함된 웹 컨텍스트에 생성되어 있어야 한다.
예를 들어 HTML 5 WebSocket API를 호출하도록 작성된 JSP를 호출하면 HTTP 세션이 생성되어 응답 Cookie 헤더로 JESSIONID가 전달된다. 웹 브라우저에서 HTML 5 WebSocket JavaScript를 실행하면서 JEUS로 WebSocket Handshake 요청을 보내는데, 그 요청 헤더에 Cookie 헤더가 들어가야 한다. 이를 지원하는 않는 WebSocket Client를 사용하면 WebSocket UserProperties Failover 기능을 사용할 수 없다.
FireFox 30.0은 HTML 5 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 세션의 타임아웃과 WebSocket 세션의 타임아웃은 서로 독립적이다.
특히 WebSocket 세션의 경우 HTTP 세션에 의존적으로 동작하는 것이기 때문에 WebSocket 컨테이너 차원에서 HTTP 세션의 속성을 마음대로 바꿀 수는 없다. 상황에 따라서는 UserProperties에 Serialzable한 데이터를 put하는 시점에 HTTP 세션이 먼저 타임아웃 처리가 되어서 Failover가 되지 않을 수 있다. 이러한 상황을 고려해서 HTTP 세션의 타임아웃을 적절하게 조정해야 한다.
UserProperties에 Serialzable한 데이터를 put하는 시점에 HTTP 세션이 타임아웃된 경우에는 WARNING 레벨의 로그를 남긴다.
서로 다른 웹 컨텍스트 간에 WebSocket 세션의 UserProperties는 공유되지 않는다.
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보다 큰 경우에만 유효하다. |
WebSocket 컨테이너의 정보는 각 웹 애플리케이션의 jeus-web-dd.xml에 설정한다.
[예 6.2] 웹 컨텍스트 설정 파일 : <<jeus-web-dd.xml>>
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="8.5"> <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-userProperties> <enabled>false</enabled> <use-write-through-policy>false</use-write-through-policy> </distributed-userProperties> <init-param> <name>name</name> <value>value</value> </init-param> </websocket> </jeus-web-dd>
다음은 설정 태그에 대한 설명이다.
태그 | 설명 |
---|---|
<max-incoming-binary-message-buffer-size> | 클라이언트로부터 전달되는 바이너리 메시지를 버퍼링할 때 사용하는 버퍼의 최댓값을 의미한다. 설정된 값보다 큰 메시지가 전달되는 경우 1009 에러를 내고 WebSocket 세션을 닫는다. |
<max-incoming-text-message-buffer-size> | 클라이언트로부터 전달되는 텍스트 메시지를 버퍼링할 때 사용하는 버퍼의 최댓값을 의미한다. 설정된 값보다 큰 메시지가 전달되는 경우 1009 에러를 내고 WebSocket 세션을 닫는다. |
<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를 처리하기 위해서 사용한다. 다음의 하위 항목을 설정한다.
|
<distributed-userProperties> | javax.websocket.Session.getUserProperties()에 정의된 내용에 따라 제공하는 WebSocket 세션 Failover 관련 설정이다. 다음의 하위 항목을 설정한다.
|
<init-param> | WebSocket Container에서 사용하는 추가 설정을 나타낸다. <name> 항목에 parameter name과 <value>에 설정할 값을 적는다 |
본 절에서는 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.3] 웹 컨텍스트 설정 파일 : <<jeus-web-dd.xml>>
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="8.5"> <library-ref> <library-name>spring-support</library-name> <specification-version> <value>4.2.0.RELEASE</value> </specification-version> <failon-error>true</failon-error> </library-ref> </jeus-web-dd>
Spring WebSocket 지원 라이브러리를 사용하기 위하여 websocket handshake-handler로 jeus.spring.websocket.JeusHandshakeHandler를 지정해준다.
[예 6.4] 스프링 컨텍스트 설정 파일 : <<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>