제2장 웹 커넥션 관리

내용 목차

2.1. 개요
2.2. 구성 요소
2.2.1. 리스너
2.2.2. 커넥터
2.2.3. Worker Thread Pool
2.3. 웹 커넥션 설정
2.3.1. 리스너 공통 설정
2.3.2. AJP 리스너 설정
2.3.3. HTTP 리스너 설정
2.3.4. TCP 리스너 설정
2.3.5. WebtoB 커넥터 설정
2.3.6. Tmax 커넥터 설정
2.4. 부하 분산을 위한 웹 서버 설정
2.4.1. 부하 분산 구조
2.4.2. Apache 웹 서버
2.4.3. IIS 웹 서버 및 Iplanet 웹 서버 설정
2.4.4. WebtoB 부하 분산 설정
2.5. TCP 리스너 사용
2.5.1. 맞춤 통신 프로토콜 정의
2.5.2. Dispatcher Config Class 구현
2.5.3. TCP 서블릿 구현
2.5.4. 맞춤 프로토콜 코드를 위한 TCP 리스너 설정
2.5.5. TCP 클라이언트 구현
2.5.6. TCP 클라이언트 컴파일과 실행
2.6. HTTP/2 사용
2.6.1. HTTP/2 사용 설정
2.6.2. Server Push
2.7. 리스너 튜닝

본 장에서는 웹 엔진에서 제공하는 리스너 또는 커넥터의 관리 및 설정 방법 등에 대해 설명한다.

JEUS에서는 웹 리스너와 웹 커넥터를 통칭해서 웹 커넥션이라고 한다.

웹 엔진에서는 WebtoB 및 다른 웹 서버와의 커넥션, HTTP/TCP 클라이언트와의 직접적인 커넥션, 또는 Tmax와의 커넥션을 제공한다. 웹 서버는 클라이언트의 HTTP 요청을 받아 조건에 맞는 경우 웹 엔진으로 요청을 전달한다.

대표적인 웹 서버로 WebtoB와 Apache를 사용하고 연결을 위해 다음과 같은 각각의 커넥터를 제공한다.

웹 엔진은 클라이언트와 직접 커넥션을 맺고, 관리를 위해 각각의 클라이언트별로 다음과 같은 리스너 및 커넥터를 제공한다.

리스너는 JEUS 서버의 설정을 기반으로 동작한다. 따라서 보안(SSL) 리스너를 사용하려면 JEUS 서버에 해당 설정을 하고 이를 각각의 웹 관련 리스너들이 사용하도록 설정한다.

본 절에서는 웹 엔진에서 제공하는 리스너 및 커넥터에 대해 설명한다.

커넥터는 웹 엔진에서 WebtoB 및 Tmax에 연결하기 위한 채널이다.

웹 엔진의 커넥터와 각 클라이언트, 프로토콜의 연결을 나타내면 다음과 같다.


다음은 각 커넥터에 대한 설명이다.

참고

JEUS가 클라이언트 역할을 하는 것은 연결해서 커넥션을 맺을 때 뿐이다. 실제로 외부 클라이언트의 요청은 WebtoB, Tmax로부터 전달받는 것이며, JEUS 내부적으로 WebtoB나 Tmax에 요청을 보내는 경우는 없다. 즉, 실제 서비스 중에는 JEUS가 서버 역할을 한다.

리스너 또는 커넥터에는 클라이언트로의 요청을 처리하기 위한 Worker Thread Pool이 존재하는데, 이는 Worker Thread들을 관리하는 개체이다.

JEUS 21 이전에는 리스너 및 커넥터가 Worker Thread Pool을 관리하는 주체였지만, JEUS 21부터는 WebContainer, VirtualHost, Context가 주체가 되어 Worker Thread Pool을 관리한다. 자세한 내용은 “제4장 스레드 풀” Chapter을 참고한다.

리스너의 경우 요청이 오면 그 요청을 처리하기 위한 Worker Thread가 할당된다. HTTP, AJP13, TCP 리스너인 경우에는 Worker Thread에 작업을 넘기기 전에 요청을 파싱하여 Context와 Host를 찾기 때문에, Context와 VirtualHost 레벨의 Workter Thread Pool에 넘길 수 있다. 커넥터의 경우 WebtoB 또는 Tmax와 JEUS가 client으로서 커넥션을 맺는다. 이전과 달리 Worker Thread와 Connection이 서로 종속되지 않으며, Connection을 순서대로 Worker Thread에 할당하여 요청을 처리된다. 커넥터에 경우에는 Worker Thread에서 요청을 파싱하기 때문에 오직 WebContainer 레벨의 스레드 풀에서 처리된다. Connection 갯수보다 WebContainer Thread Pool의 크기가 작으면 다른 요청이 처리하기 전까지 다른 Connection을 대기할 수 밖에 없기 Connection 갯수보다 크게 설정한다.

Worker Thread Pool의 최솟값, 최댓값 등은 서비스 처리 성능에 큰 영향을 미치기 때문에 리스너 또는 커넥터를 설정할 때는 Worker Thread Pool을 주의해서 설정해야 한다.

참고

JEUS는 각 Thread Pool이 자신의 상태를 직접 관리하며, 모니터링 Thread는 주기적으로 그 결과만 Logging해주는 방식을 사용한다. 따라서 로그에 남겨진 Thread 수 증감 변화와 실제 Thread Pool의 상태 변화가 서로 일치하지 않을 수 있으니 주의한다.

Active-Management와 상태 통보

Worker Thread Pool에는 Active-Management에 관한 설정이 포함되어 있다. Active-Management는 관리자가 지정한 특정 상태에 이르면 웹 엔진이 경고 메시지를 이메일(e-mail)로 통지하거나 웹 엔진이 속한 서버의 재시작을 권고하는 설정이다. 설정 가능한 값은 Worker Thread가 블록되었다고 판단하는 기준시간이다. 이에 따라 블록된 Worker Thread가 증가할 경우 몇 개 이상이면 경고 이메일 또는 엔진 재시작 권고 메시지 출력과 같은 특정 작업의 수행을 설정한다.

주의

재시작 권고가 출력되면 서버의 요청 처리가 순조롭지 못할 가능성이 크기 때문에 관리자는 메시지를 확인하여 서버의 재시작 여부를 판단해야 한다.

AJP 리스너, WebtoB 커넥터, Tmax 커넥터를 사용하는 경우 각 연동 제품별로 설정이 필요하다. 모든 웹 커넥션은 웹 엔진에서 관리하므로 본 절에서는 웹 엔진의 설정에 대해서만 설명한다. WebtoB와 다른 웹 서버의 해당 설정에 대한 자세한 내용은 “2.4. 부하 분산을 위한 웹 서버 설정”에서 설명한다.

참고

1. JEUS 6의 컨텍스트 그룹이 없어졌다. 컨텍스트 그룹이 관리하던 대부분의 사항들은 웹 엔진이 관리한다.

2. 웹 리스너의 경우 서버에서 제공하는 통합된 서비스 리스너를 사용한다. 따라서 AJP, HTTP, TCP 리스너의 경우 서버에 리스너를 설정을 하고, 그 서버 리스너를 참조하는 방식으로 설정한다. 이에 따라 'Server Listener Ref'라는 설정이 추가되었다. 하지만 WebtoB, Tmax에 대한 커넥터는 JEUS가 클라이언트로서 직접 연결하므로 기존 설정 방식을 유지한다.

커넥터의 추가, 수정, 삭제는 WebAdmin과 콘솔 툴을 사용할 수 있다. 커넥터를 설정할 때에는 WebAdmin을 사용할 것을 권장한다. WebAdmin을 사용한 설정 방법은 본 절에서 설명한다. 콘솔 툴을 사용하여 설정하는 방법은 JEUS Reference 안내서”의 “4.2.8. 웹 엔진 관련 명령어”를 참고한다.

본 절에서는 각 리스너에서 공통적으로 사용하는 주요 설정이다.

WebAdmin과 콘솔 툴을 사용하여 AJP 리스너를 추가하거나 수정 및 삭제할 수 있다.

참고

AJP 리스너는 AJP 버전 1.3을 준수한다.

WebAdmin 사용

콘솔 툴 사용

다음은 콘솔 툴을 사용해서 AJP 리스너를 추가, 수정, 삭제하는 방법이다.

WebAdmin과 콘솔 툴을 사용하여 HTTP 리스너를 추가하거나 수정 및 삭제할 수 있다. HTTP 리스너는 내부 관리 용도로만 사용할 것을 권장한다.

WebAdmin 사용

콘솔 툴 사용

콘솔 툴을 사용하여 HTTP 리스너를 추가, 수정, 삭제하는 방법은 AJP 리스너의 방법과 동일하나 HTTP 리스너를 추가, 수정할 경우에 대해서만 HTTP/2를 사용할지 결정할 수 있는 옵션을 제공한다. 자세한 내용은 “2.3.2. AJP 리스너 설정”"콘솔 툴 사용"을 참고한다.

TCP 리스너는 커스텀 프로토콜로 상호 간에 통신할 수 있도록 제공하는 특수한 리스너이다. TCP 리스너는 기본적으로 서버 리스너를 다른 웹 리스너들과 공유할 수 없기 때문에 사용을 위해서는 서버에 TCP 리스너를 위한 전용 리스너를 추가해야 한다. WebAdmin과 콘솔 툴을 사용하여 TCP 리스너를 추가하거나 수정 및 삭제할 수 있다.

WebAdmin 사용

콘솔 툴 사용

콘솔 툴을 사용하여 TCP 리스너를 추가, 수정, 삭제하는 방법은 AJP 리스너의 방법과 동일하다. 자세한 내용은 “2.3.2. AJP 리스너 설정”"콘솔 툴 사용"을 참고한다.

WebtoB 커넥터는 커넥션을 생성할 때 JEUS가 클라이언트 역할을 하게 된다. 따라서 WebtoB 주소와 연결 포트가 필요하다. WebAdmin과 콘솔 툴을 사용하여 WebtoB 커넥터를 추가하거나 수정 및 삭제할 수 있다.

참고

WebtoB와 JEUS 사이의 통신 프로토콜인 WJP 버전이 1에서 2(이하 WJPv1 및 WJPv2)로 업그레이드되었다. WJPv2의 경우 WJPv1에 비교해서 좀더 적은 사이즈의 패킷을 사용하며, 여러 가지 부가 기능들을 제공한다. JEUS에서 연결할 WebtoB가 WJPv2를 지원하지 않는 버전일 경우에는 WJPv1을 사용하면 된다. WJP 버전 정보의 경우 WebtoB에서 자동으로 파악할 수 없기 때문에 JEUS에 <wjp-version>으로 설정해야 한다. WJP는 WebtoB-JEUS Protocol을 의미한다.

WebAdmin 사용

  • 추가 및 수정

    다음은 WebAdmin을 사용해서 WebtoB 커넥터를 추가 및 수정하는 방법이다.

    1. WebAdmin 메인 화면에서 Master Server를 선택하면 설정 화면으로 이동한다.

    2. 설정 화면 상단의 탭에서 [서버]를 선택하면 서버 목록 조회 화면으로 이동한다. 서버 목록에서 실행할 서버를 선택하면 서버 설정 화면으로 이동한다. 설정 화면에서 [Engine] 탭을 선택한 후 설정 메뉴에서 [Web Engine] > [Web Connections]을 선택하면 웹 커넥션 목록 조회 화면으로 이동한다. ([그림 2.3] 참고)

      WebtoB 커넥터를 추가하려면 [추가] 버튼을 클릭한 후 메뉴에서 [WebtoB Connector]를 선택하고 수정하려면 웹 커넥션 목록에서 'WebtoB' 타입의 리스너를 클릭한 후 WebtoB Connector 화면에서 정보를 수정한다.

    3. WebtoB Connector 추가 화면에서 정보들을 설정하고 [추가] 버튼을 클릭한다.


      다음은 주요 설정 항목에 대한 설명이다. 그 외의 항목에 대한 설명은 “2.3.1. 리스너 공통 설정”을 참고한다.

      • 기본 설정

        항목설명
        WJP VersionWebtoB와 통신할 때 사용하는 WJP 버전을 결정하는 설정이다. WJPv1, v2를 지원한다.
        Registration IdWebtoB 설정 파일의 SERVER 절의 값과 일치해야 한다. WJPv1을 사용하는 경우 반드시 15자 이내로 설정한다. (필수 입력항목)
        Hth CountWebtoB 설정 파일의 NODE 절에 지정된 HTH 프로세스 개수와 일치해야 한다. “2.4. 부하 분산을 위한 웹 서버 설정”을 참고한다.
        Webtob Address'Network Address''Domain Socket Address' 항목을 서로 배타적으로 설정한다. 한 항목을 설정하면 다른 항목은 설정할 수 없다. Network Address에 WebtoB 서버와 연결할 포트와 IP 주소를 설정하거나 Domain Socket Address를 설정해야 한다. 이것은 UNIX 도메인 소켓 또는 Windows에서 HTH 프로세스와 IPC 통신을 사용할 때 설정한다. 즉, WebtoB가 같은 머신에 있을 때만 의미가 있다.
        Connection-Count

        기본적으로 WebtoB와의 연결 수를 의미한다. 반드시 WebtoB 설정의 'MinProc''MaxProc' 값을 바탕으로 설정해야 한다. 'MinProc''MaxProc'에 대한 자세한 내용은 “2.4.4. WebtoB 부하 분산 설정”을 참고한다.

        [참고]

        JEUS 21 이전 까지는 Thread Pool 하위에 Number로 설정하였다.

      • 고급 선택사항

        항목설명
        Read Timeout

        WebtoB는 지속적으로 웹 엔진에게 WebtoB의 설정 파일에 정의된 svrchktime 변수값의 간격으로 ping 메시지를 보낸다.

        웹 엔진은 여기에서 설정된 시간 간격으로 WebtoB의 상태 보고 결과를 체크한다. WebtoB의 ping이 여기에서 설정된 시간 간격 내에서 발견되지 않으면 통신 연결은 끊어진 것으로 간주되어 다시 설정된다. 그러므로 설정값은 svrchktime 변수값보다 커야 한다.

        Request Prefetch

        웹 엔진 측에서 요청을 처리하는 동안 다음 요청을 WebtoB로부터 미리 받아올 것인지 설정한다.

        기능이 활성화되면 웹 엔진은 각 WebtoB Worker Thread마다 하나의 Queue를 할당하고, Queue는 Worker Thread가 현재 요청을 처리하는 동안 WebtoB로부터 오는 요청을 버퍼링한다. 따라서 웹 엔진은 WebtoB로부터 다음 요청을 받는 시간을 단축할 수 있다. 이 기능의 단점은 요청을 처리하는 도중 웹 엔진에 심각한 문제가 발생하면 Queue에 축적된 요청들을 잃어버릴 수 있다는 것이다.

        Reconnect Interval

        WebtoB와의 연결들이 끊어지면 재연결을 시도하는데, 각 재연결 사이의 시간 간격을 설정한다. 1초보다 작게 설정하는 것은 의미가 없다. (기본값: 5000)

        연결이 성공할 때까지 설정한 시간 간격으로 무한히 시도한다.

        Reconnect Count For Backup

        기존에 연결된 WebtoB와 연결이 끊어졌을 때 재연결 시도 횟수를 의미한다.

        횟수를 초과해서 연결이 안 될 경우에는 다른 WebtoB로 연결 시도한다. Primary WebtoB에서 연결이 끊어지면 백업으로 시도하는 것이고, 백업에서 연결이 끊어지면 Primary로 연결한다.

        WebtoB Backup

        Primary WebtoB가 장애 상태가 되었다고 판단했을 때, 백업으로 설정된 WebtoB로 연결을 시도한다. 일단 백업으로 넘어가면 백업 WebtoB로 연결될 때까지 계속 시도한다. 백업 WebtoB로부터 HTTP 요청를 받는 와중에 Primary WebtoB가 살아나게 되면 자동으로 Failback을 수행한다.

        여기에는 WebtoB 커넥터 설정 중 WebtoB가 설치된 호스트에 따라 다를 수 밖에 없는 부분에 대한 설정을 제공한다.

        Thread Pool 설정이 없다면 Primary WebtoB 설정에서 상속 받는다.

    4. 설정을 완료한 후 [추가] 버튼을 클릭하면 'webtob' 타입의 커넥터가 추가된 것을 확인할 수 있다.

  • 삭제

    다음은 WebAdmin을 사용해서 WebtoB 커넥터를 삭제하는 방법이다.

    1. WebAdmin 메인 화면에서 Master Server를 선택하면 설정 화면으로 이동한다.

    2. 설정 화면 상단의 탭에서 [서버]를 선택하면 서버 목록 조회 화면으로 이동한다. 서버 목록에서 실행할 서버를 선택하면 서버 설정 화면으로 이동한다. 설정 화면에서 [Engine] 탭을 선택한 후 설정 메뉴에서 [Web Engine] > [Web Connections]을 선택하면 웹 커넥션 목록 조회 화면으로 이동한다.([그림 2.6] 참고)

    3. 목록에서 삭제할 리스너를 체크한 후 [삭제] 버튼을 클릭하여 삭제한다.

콘솔 툴 사용

다음은 콘솔 툴을 사용해서 WebtoB 커넥터를 추가, 수정, 삭제하는 방법이다.

Tmax 커넥터 역시 WebtoB 커넥터와 마찬가지로 커넥션을 생성할 때 JEUS가 클라이언트 역할을 하게 된다. 따라서 Tmax 주소와 연결 포트가 필요하다. WebAdmin과 콘솔 툴을 사용하여 Tmax 커넥터를 추가하거나 설정 및 삭제할 수 있다.

WebAdmin 사용

콘솔 툴 사용

다음은 콘솔 툴을 사용해서 Tmax 커넥터를 추가, 수정, 삭제하는 방법이다.

본 절에서는 JEUS와의 연동 사례가 많은 웹 서버들의 설정 방법과 웹 엔진을 웹 커넥션과 연동할 수 있는 방법에 대해서 설명한다.

웹 엔진은 시스템의 HTTP 처리의 성능 향상을 위해 웹 서버와 웹 엔진(서블릿 엔진)으로 구성하여 서비스할 수 있다. 각 웹 엔진으로의 HTTP 요청을 웹 엔진 앞의 웹 서버에서 1차로 부하를 분산시킨다. 이후 같은 연결이나 세션의 요청에 대해 HTTP 세션 클러스터링 서비스를 이용하여 처음 요청을 처리한 웹 엔진으로 할당하여 서비스 처리 효율을 높인다.

처음 요청을 처리한 웹 엔진에 장애가 발생한 경우, 장애 발생 이후에 들어온 요청은 웹 엔진 앞의 웹 서버에서 장애가 발생하지 않은 웹 엔진으로 전달하여 끊김 없는 서비스를 가능하게 한다. 이 서비스는 HTTP 세션 클러스터링을 사용한다는 전제 조건이 필요하다. HTTP 세션 클러스터링에 대한 자세한 내용은 JEUS 세션 관리 안내서”의 “제2장 분산 세션 서버”를 참고한다.

만약 웹 서버를 사용하지 않고 웹 엔진만을 사용할 경우에 요청을 처리한 웹 엔진으로 이후의 요청을 전달할 수 있는 장비나 소프트웨어를 사용하면 웹 서버를 사용할 때와 같이 효율적인 서비스가 가능할 것이다. 그러나 JEUS에서는 이에 필요한 소프트웨어를 제공하지 않고, 이런 사용을 권장하지 않는다.

주의

각 웹 서버들의 설정과 mod_jk 설정은 웹 서버 버전에 따라서 차이가 있을 수 있다. 본 절의 설정 방법은 JEUS 사용자들의 이해를 돕기 위해서 제공하는 것이므로 실제로 설정할 때에는 반드시 각 웹 서버들의 문서, 국내외 커뮤니티에서 제공하는 설정 사례들을 참고해야 한다.

Apache를 웹 엔진과 연동하려면 다음과 같은 과정을 수행해야 한다.

  1. Apache에 mod_jk 라이브러리를 설치한다.

  2. JEUS 웹 엔진에 AJP13 리스너를 설정한다.

  3. workers.properties 파일을 생성하고 편집한다.

  4. Apache 웹 서버의 httpd.conf 파일에 연동 구절을 삽입한다.

  5. Apache 웹 서버를 재시작한다.

참고

Apache의 경우 2.2.4, mod_jk의 경우 1.2.20을 기준으로 설명한다.

mod_jk 라이브러리 설치

Apache 웹 서버를 웹 엔진의 앞 단에서 사용하려면 "mod_jk"라는 모듈을 Apache 설치 모듈에 추가해야 한다. "mod_jk" 모듈은 서버와 엔진 간의 통신 프로토콜을 구현한 것으로, 이 프로토콜은 Apache JServ Protocol 1.3(AJP 1.3)이다.

AJP13 리스너 설정

mod_jk 라이브러리 설치가 완료되면 JEUS 웹 엔진에 AJP13 리스너를 설정한다. AJP13 리스너 설정 방법의 자세한 내용은 “2.3.2. AJP 리스너 설정”을 참고한다.

workers.properties 설정

workers.properties 파일은 mod_jk를 위한 설정 파일이다. 예를 들어 JEUS는 server1, server2라는 2대의 서버가 있다. 호스트 이름이 각각 server1, server2라고 가정한다. 각 웹 엔진에 설정된 AJP 리스너들은 9901, 9902로 설정했다고 가정하자.

다음은 가정한 JEUS 환경에서의 workers.properties 예제이다.


workers.properties 파일은 몇몇 정의를 제외하고 일반적으로 "worker.<worker_name>.<directive>=<value>"의 형식으로 정의한다.

다음은 중요한 설정에 대한 설명이다.

httpd.conf 설정

httpd.conf 파일에는 mod_jk 모듈을 사용하기 위해 다음 사항을 추가해야 한다.


주의

Apache 버전에 따라 deprecated될 수도 있으므로 여기에서 제시한 것은 참고 용도로만 사용한다. 반드시 Apache 매뉴얼에 따라 설정한다.

본 절에서는 예제를 통하여 WebtoB와 JEUS의 부하 분산 처리 환경을 구성한다.

다음은 예제의 서버 구조를 나타낸다. 각 엔진이 각각의 WebtoB 서버를 갖는 구조에 대한 설정으로 예제는 2대의 WebtoB가 각각 2대의 웹 엔진에 연결된다.


Server A부터 D에는 동일한 웹 컨텍스트를 deploy해야 한다. 위와 같은 구성에서는 일반적으로 Server A부터 D가 하나의 클러스터를 이루게 되며, 그에 따라 분산식 세션 서버를 사용하게 된다. 만약 사용자 세션이 필요없는 경우에는 클러스터를 구성하지 않아도 무방하다.

다음은 WebtoB A와 Server A, Server B가 연결된 모습을 좀 더 자세히 표현한 것으로 각각의 WebtoB 커넥터 설정 상태를 보여준다.


설정할 때에는 WebtoB의 HTH 프로세스 수를 고려해야 한다.

WebtoB HTH 프로세스는 몇 개의 하위 프로세스를 가지고 있는 엔진처럼 행동한다. 이는 WebtoB 커넥터의 Worker Thread와 1대 1로 연결된다. 따라서 WebtoB 커넥터의 'connection-count' 설정값과 WebtoB 설정의 'MaxProc' 값을 주의해서 설정해야 한다.

HTH 프로세스의 개수는 WebtoB 설정에 존재하는 HTH 프로세스 개수를 그대로 WebtoB 커넥터의 'Hth Count' 설정에 반영한다. WebtoB의 'MaxProc' 값과 WebtoB 커넥터의 'connection-count' 값은 다음과 같은 공식으로 표현될 수 있다.

MinProc = Listener A connection-counts + Listener B connection-counts + … + Listener X connection-counts setting.
MaxProc = Listener A connection-counts + Listener B connection-counts + … + Listener X connection-counts setting.

WebtoB 커넥터의 'Hth Count' 값은 WebtoB 설정 파일의 NODE 절의 요소 중 HTH 프로세스 개수와 반드시 동일해야 한다. WebtoB 커넥터 설정은 “2.3.5. WebtoB 커넥터 설정”을 참고한다.

다음은 WebtoB A가 위의 예제와 같이 설정되기 위한 http.m 파일의 설정 예이다.


connection count는 6과 25 사이여야 한다.

TCP 리스너는 통신 프로토콜을 직접 정의해서 TCP 클라이언트와 TCPServlet 간에 통신할 수 있다. 단, TCP 리스너는 HTTP 또는 HTTPS 프로토콜로 만족할 수 없는 사항이 있을 경우에만 사용하기를 권장한다.

TCP 리스너를 사용하기 위해서는 다음과 같은 과정을 수행해야 한다.

  1. 맞춤 통신 프로토콜을 정의한다.

  2. Dispatcher Config Class를 구현한다(프로토콜 설정 정보).

  3. TCP 핸들러 서블릿을 구현한다(프로토콜 구현).

  4. 맞춤 프로토콜 구현을 위한 TCP 리스너를 설정한다.

  5. TCP 클라이언트를 구현한다.

  6. TCP 클라이언트를 컴파일하고 구동한다.

본 절에서 사용할 용어는 다음과 같다.

용어설명
프로토콜(통신 프로토콜)TCP 클라이언트와 TCP 핸들러 사이에서(TCP 리스너가 중간역할) 교환되는 메시지의 구조와 내용을 정의한다.
메시지TCP 클라이언트와 TCP 핸들러 사이에서 교환되는 데이터이다. 프로토콜에서 정의한 구조에 맞아야 한다.
TCP 클라이언트TCP 리스너와 교신하며 메시지를 주고받는 외부의 애플리케이션(TCP 핸들러에 의해 처리된다)이다. 메시지를 주고받는 것은 표준 소켓을 사용한다.
TCP 핸들러

TCP 리스너를 통해 메시지를 받고 구현된 방법대로 메시지를 처리한다.

TCP 핸들러는 jeus.servlet.tcp.TCPServlet의 하위 클래스의 형태로 구현되고 웹 엔진에 보통 서블릿처럼 등록된다. TCP 핸들러는 TCP 프로토콜 위에 존재하는 맞춤 프로토콜의 "제공자" 또는 "구현체"처럼 동작한다고 할 수 있다.

TCP Dispatcher Configuration Class

TCP Dispatcher Configuration Class는 jeus.servlet.tcp.TCPDispatcherConfig를 확장한 클래스이다. 이 클래스는 맞춤 프로토콜에 대한 정보를 TCP 리스너로 전달하여 이 non-HTTP 메시지를 해석하여 처리한다.

이 클래스는 DOMAIN_HOME/lib/application 디렉터리 아래에 위치시키고, 웹 엔진 설정 하위의 TCP 리스너 설정 항목 중 'Dispatcher Config Class' 항목에 설정한다(“2.3.4. TCP 리스너 설정” 참조).

TCP 리스너

맞춤 메시지에 대한 해석과 라우팅 인프라를 제공한다. TCP 클라이언트와 TCP 핸들러 사이에서 non-HTTP 중간 매개체로 역할을 한다.

이것은 다른 HTTP 리스너와 마찬가지로 웹 엔진 내에 존재한다.

Dispatcher Config Class는 jeus.servlet.tcp.TCPDispatcherConfig Class의 하위 클래스이다. 이 클래스의 abstract 메소드는 TCP 리스너가 알맞은 TCP 핸들러에게 메시지를 전달하기 위해 필요한 여러 가지 정보들이 구현되어야 한다. 이 메소드들은 JEUS_HOME/docs/api/jeus-servlet/index.html에 설명되어 있다.

JEUS 7 Fix#1부터 getBodyLengthLong 메소드가 추가되었다. TCP 바디의 크기가 2GB 이상이라면 해당 메소드를 사용해서 바디 길이를 8Byte로 나타내면 된다.

다음은 정의된 프로토콜에 기반하여 Dispatcher Config Class를 어떻게 구현했는지를 보여준다. 특정 메소드들이 어떻게 사용되었는지는 주석을 참고한다.

[예 2.4] TCPDispatcherConfig 구현 예 : <<SampleConfig.java>>

package sample.config;

import javax.servlet.*;
import javax.servlet.http.*;
import jeus.servlet.tcp.*;

/*
  This class extends the abstract class jeus.servlet.tcp.
  TCPDispatcherConfig class. This implementation provides
  routing and handling information to the TCP listener
  according to our defined communications protocol.
*/
public class SampleConfig extends TCPDispatcherConfig {
    /*
      Any init code goes into this method. This method will be
      Called once after starting the TCP listener.
      We leave this method empty for our simple example.
    */
    public void init() {}

    /*
      This method returns the fixed-length of the header so
      that the TCP listener knows when to stop
      reading the header. The header length for
      our example is 41 (bytes): 4 (magic) + 1
      (type) + 4 (body length) + 32 (service name).
    */
    public int getHeaderLength() {
        return 41;
    }

    /*
      This method must return the length of the body.
      For our example, this length is represented as an
      “int” in the header, starting at position “5”
      (see the protocol definition above).
    */
    public int getBodyLength(byte[] header) {
        return getInt(header, 5);
    }

    /**)
     * Returns the long-size length of request body of
     * incoming request packet.
     * If you don't need to support long, you may map to
     * {@link #getBodyLength(byte[])}.
     */
    public long getBodyLengthLong(byte[] header) {
        return getBodyLength(header);
    }

    /*
      This method must return the context path so that the
      request can be routed by the TCP listener to the context
      that contains the TCP handler (TCPServlet implementation).
      For our example, we always use the context path “/tcptest”.
    */
    public String getContextPath(byte[] header) {
        return "/tcptest";
    }

    /*
      This method must return the name (path) of the TCP
      handler(TCPServlet) relative to the context path.
      For our example, we fetch this name from the 9th position
      in the header.
    */
    public String getServletPath(byte[] header) {
        return "/" + getString(header, 9, 32);
    }

    /*
      This method returns some path info from the header.
      It is not used in our example and thus returns “null”.
    */
    public String getPathInfo(byte[] header) {
        return null;
    }

    /*
      This method returns any session ID embedded in the header.
      It is not used in our example and thus returns “null”.
    */
    public String getSessionId(byte[] header) {
        return null;
    }

    /*
      This method determines whether the TCP listener
      should keep the socket connection open after the TCP handler
      has delivered its response. If it returns “false”, the
      connection will be dropped like in HTTP communications.
      If it returns “true” the connection will be kept open
      like in the Telnet or FTP protocols. For our example,
      we choose to make it persistent (connection not closed
      by the TCP listener).
    */
    public boolean isPersistentConnection() {
        return true;
    }
}


TCP 서블릿은 항상 jeus.servlet.tcp.TCPServlet의 하위 클래스이다. 여기에는 항상 overridden되는 abstract void service(TCPServletRequest req, TCPServletResponse res) 메소드가 존재한다.

service 메소드는 맞춤 프로토콜에 준하는 메시지를 처리하도록 구현해야 한다. 웹 컨테이너는 헤더를 읽어서 TCPServletRequest 객체에 전달하며, TCP 서블릿에서는 TCPServletResponse 객체의 output stream으로 응답을 쓴다.

다음 예제는 맞춤 프로토콜에 준하는 메시지를 처리하는 TCP 서블릿의 구현 코드이다.

[예 2.5] TCP 서블릿 구현 예 : <<SampleTCPServlet.java>>

package sample.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import jeus.servlet.tcp.*;

/**
 * Sample TCPServlet implementation
 *
 * Protocol scheme:
 *
 * common header (for request and response) : total 41 byte
 *
 *   magic field: length = 4 byte, value = 7777
 *   type field : length = 1 byte, 0 : request, 1:response
 *   body length field : length = 4, value = 128
 *   service name field : length = 32
 *
 * request and response body
 *
 *   message1 field : length = 64
 *   message2 field : length = 64
 *
 */

public class SampleTCPServlet extends TCPServlet {

    public void init(ServletConfig config) throws ServletException {
    }

    public void service(TCPServletRequest req, TCPServletResponse res)
        throws ServletException, IOException {
        ServletContext context = req.getServletContext();
        byte[] header = req.getHeader();
        byte[] body = new byte[req.getContentLength()];
        int read = req.getInputStream().read(body);
        if (read < body.length) {
            throw new IOException("The client sent the wrong content.");
        }

        String encoding = res.getCharacterEncoding();
        if (encoding == null)
            encoding = "euc-kr";

        DataInputStream in = new DataInputStream(new ByteArrayInputStream(header));
        int magic = in.readInt();
        context.log("[SampleTCPServlet] received magic = " + magic);

        byte type = (byte)in.read();
        context.log("[SampleTCPServlet] received type = " + type);

        int len = in.readInt();
        context.log("[SampleTCPServlet] received body length = " + len);

        byte[] svcname = new byte[32];
        in.readFully(svcname);
        context.log("[SampleTCPServlet] received service name = "
                     + (new String(svcname)).trim());

        String rcvmsg = null;
        rcvmsg = (new String(body, 0, 64)).trim();
        context.log("[SampleTCPServlet] received msg1 = " + rcvmsg);

        try {
            rcvmsg = (new String(body, 64, 64, encoding)).trim();
        } catch (Exception e) {}
        context.log("[SampleTCPServlet] received msg2 = " + rcvmsg);

        String msg1 = "test response";
        String msg2 = "test response2";

        byte[] result1 = null;
        byte[] result2 = null;
        if (encoding != null) {
            try {
                result1 = msg1.getBytes(encoding);
                result2 = msg2.getBytes(encoding);
            } catch (UnsupportedEncodingException uee) {
                result1 = msg1.getBytes();
                result2 = msg2.getBytes();
            }
        } else {
            result1 = msg1.getBytes();
            result2 = msg2.getBytes();
        }

        header[4] = (byte)1; // mark as response

        ServletOutputStream out = res.getOutputStream();
        out.write(header);

        byte[] buf1 = new byte[64];
        System.arraycopy(result1, 0, buf1, 0, result1.length);
        out.write(buf1);

        byte[] buf2 = new byte[64];
        System.arraycopy(result2, 0, buf2, 0, result2.length);
        out.write(buf2);

        out.flush();
    }

    public void destroy() {
    }
}


참고

JEUS 7 Fix#2부터는 TCPServletRequest.getBody() 메소드 사용을 권장하지 않는다. 이 메소드는 요청 바디가 매우 큰 경우 메모리 문제를 일으킬 수 있다. 따라서 서블릿 표준에 정의된 ServletInputStream을 사용하는 것을 권장한다. 이는 TCPServletRequest.getInputStream()으로 얻을 수 있다.

TCP 클라이언트는 TCP 리스너와 소켓 연결을 맺고 이 연결을 통하여 메시지를 전송한다. 이 메시지는 “2.5.1. 맞춤 통신 프로토콜 정의”에서 정의한 맞춤 통신 프로토콜에 준하는 Byte 스트림이다.

설정된 TCP 리스너는 메시지를 받아서 SampleConfig 클래스에 정의된 dispatch 정보를 바탕으로 SampleTCPServlet의 service() 메소드를 호출한다. SampleTCPServlet은 클라이언트로부터 전달된 데이터를 바탕으로 응답을 생성하여 전송한다. 이 응답은 클라이언트가 받아 System.out으로 출력한다.

다음은 그에 대한 예이다.

[예 2.7] TCP 클라이언트 구현 예 : <<Client.java>>

package sample.client;

import java.io.*;
import java.net.*;

public class Client {
    private String address;
    private int port;

    private int magic = 7777;
    private byte type = 0;
    private int bodyLength = 128;
    private byte[] serviceName="sample".getBytes();

    public Client(String host, int port) {
        this.address = host;
        this.port = port;
    }

    public void test()
        throws IOException, UnsupportedEncodingException {
        Socket socket = new Socket(address, port);
        DataOutputStream out = new DataOutputStream(
            new BufferedOutputStream(socket.getOutputStream()));
        DataInputStream in = new DataInputStream(
            new BufferedInputStream(socket.getInputStream()));

        out.writeInt(7777);
        out.write(type);
        out.writeInt(bodyLength);
        byte[] buf = new byte[32];
        System.arraycopy(serviceName, 0, buf, 0, serviceName.length);
        out.write(buf);
        byte[] msg1 = "test request".getBytes();
        byte[] msg2 = "test request2".getBytes();
        buf = new byte[64];
        System.arraycopy(msg1, 0, buf, 0, msg1.length);
        out.write(buf);
        buf = new byte[64];
        System.arraycopy(msg2, 0, buf, 0, msg2.length);
        out.write(buf);

        out.flush();

        // rx msg
        int magic = in.readInt();
        System.out.println("[Client] received magic = " + magic);

        byte type = (byte)in.read();
        System.out.println("[Client] received type = " + type);

        int len = in.readInt();
        System.out.println("[Client] received body length = " + len);

        byte[] svcname = new byte[32];
        in.readFully(svcname);
        System.out.println("[Client] received service name = " +
                           (new String(svcname)).trim());

        byte[] body = new byte[128];
        in.readFully(body);
        String rcvmsg = null;
        rcvmsg = (new String(body, 0, 64)).trim();
        System.out.println("[Client] received msg1 = " + rcvmsg);
        rcvmsg = (new String(body, 64, 64, "euc-kr")).trim();
        System.out.println("[Client] received msg2 = " + rcvmsg);

        out.close();
        in.close();
        socket.close();
    }

    public static void main(String[] argv) throws Exception {
        Client client = new Client("localhost", 5555);
        client.test();
    }
}


위의 클라이언트 코드에서 프로토콜에 필요한 다양한 헤더의 필드들의 설정을 주의 깊게 확인한다.

'magic' 수는 '7777'로, 'type'은 '0'(요청), 'body length'는 '128Bytes'(고정 길이), 'service name'은 'sample'로 설정되어 있다(SampleServlet의 이름은 web.xml에 설정되어 있다). 그리고 2개의 메시지들을 생성하여 헤더 정보를 전송한 후에 TCP 리스너에게 그들을 전송한다. 마지막으로 'hostname'을 'localhost'로 포트 번호는 '5555'로 설정하였다.

HTTP/2는 HTTP/1.1에서 성능 향상을 위하여 새로 나온 HTTP 표준의 차기 버전이다.

다음은 HTTP/2의 특징에 대한 설명이다.

참고

본 안내서는 JEUS에서 HTTP/2 사용에 관한 내용만을 주로 다루고 있으며 HTTP/2에 관한 더 자세한 내용은 "RFC 문서"를 참고한다.

HTTP/2는 h2ch2 두 가지 식별자를 정의하고 있다. 각각의 의미는 아래와 같다.

구분설명
h2cHTTP/2를 가리킨다. 'http'로 서비스된다.
h2TLS(Transport Layer Security)를 사용해 동작하는 HTTP/2를 가리킨다. 'https'로 서비스된다.

참고

대부분의 브라우저는 h2c를 지원하지 않고 h2만 지원한다.

HTTP/2를 h2c로 사용하기 위해서는 “2.3.3. HTTP 리스너 설정”을 참고하여 HTTP/2 사용 항목을 체크한다.

HTTP/2를 h2로 사용하기 위해서 TLS(Transport Layer Security)를 사용해 동작하는데 TLS handshake 과정중에 ALPN(Application Layer Protocol Negotiation) 기능을 이용해야 한다. h2는 TLS를 사용해 동작하므로 당연히 보안 리스너로 설정한 포트를 사용해야 한다.

다음은 ALPN을 사용하기 위해 필요한 작업이다.

  1. 다음 주소에서 사용할 JDK 버전에 맞는 alpn-boot 라이브러리를 확인한 후 "MVN Repository"에서 다운받는다.

    http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions
  2. 다운받은 alpn-boot 라이브러리를 bootclasspath에 추가하는 <jvm-option>을 domain.xml에 추가한다.

    <jvm-config>
        <jvm-option>-Xbootclasspath/p:<path_to_alpn_boot_jar> ...</jvm-option>
    </jvm-config> 

주의

1. 위에서 사용한 alpn-boot 라이브러리는 OpenJDK, OracleJDK만 지원한다. 따라서 현재 IBM JDK에서는 HTTP/2를 사용할 수 없다.

2. HTTP/2 스펙에서는 TLS 통신에 필요한 Cipher Suites 중 권장하지 않는 Cipher Suites를 명시하고 있다. (http://httpwg.org/specs/rfc7540.html#BadCipherSuites) 스펙에 따르면 권장하지 않는 Cipher Suites를 사용할 경우 연결이 끊어질 수 있으며 대부분의 브라우저는 이에 따라 연결을 끊고 있다.

JDK 7에서 제공하는 Cipher Suites는 모두 권장하지 않고 있기 때문에 더 강력한 암호화 Suites를 사용하는 JDK 8 버전 사용이 필수이다.

HTTP/2 스펙에서 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256는 필수로 지원하도록 명시하고 있기 때문에 이 암호화 Suite를 Listener SSL 설정에서 설정하길 권장한다.

3. JEUS 서버와 관련된 세부 설정 사항에 대한 자세한 내용은 JEUS Server 안내서”의 “2.3.3. Listener 설정”을 참고한다.

Server Push는 지정된 리소스에 대하여 클라이언트가 요청을 하지 않아도 서버에서 응답을 주는 기능으로 요청 전송에 필요한 시간을 줄여 더 빠른 응답을 받을 수 있다. Server Push 기능을 사용하기 위해서는 웹 애플리케이션을 개발할 때에 Server Push할 리소스를 지정해야 한다. 본 절에서는 해당 기능의 사용법에 대해서 설명한다.

참고

Servlet 4.0 API를 기반으로 Server Push를 구현하였다.

Server Push 사용의 기본적인 흐름은 PushBuilder 객체를 얻은 후 리소스의 경로를 지정하고 push() 메소드를 호출한다.

다음은 Server Push의 기본 흐름에 대한 예제 소스이다. 이외에도 Push될 리소스에 전달된 Query String, Cookie, Header 등을 바꿀 수 있다.


참고

Server Push를 사용하지 않겠다고 설정한 경우 위 예제는 아무 영향을 미치지 않는다.

최적의 성능을 위하여 리스너를 설정할 때 다음의 몇 가지 사항을 고려해야 한다.

  • 시스템 리소스를 많이 사용하거나 대기시간을 길게하여 출력 버퍼의 크기를 증가시킨다.

  • 일반적으로 Worker Thread Pool의 min, max 값을 크게 부여하면 웹 엔진에 많은 클라이언트가 접근할 때 Pool이 좋은 성능을 가지게 된다. 시스템 메모리를 적게 사용하기 위해서는 이들의 값을 낮게 설정한다.

  • 'Server Access Control' 옵션을 비활성화하면 성능 개선을 기대할 수 있다.

  • WebtoB 커넥터에서는 앞에서 언급했던 공식에 따라 웹 엔진의 WebtoB 커넥터의 Worker Thread 개수와 http.m의 값들을 동일하게 설정한다. 이렇게 해야 가장 좋은 성능을 기대할 수 있다.