톰캣 미션- servlet,요청처리

ttomy·2023년 9월 17일
0
post-thumbnail

servlet이란?

톰캣은 servlet container이다. servlet을 관리(등록,생성,호출소멸 등)한다.
그럼 servlet은 뭘까? servlet에 대한 이해를 위해 웹서버의 발전을 살펴보자.

초기 웹은 연구소끼리 자료를 주고 받기 위해 정적 리소스를 요청/응답하는 네트워크 였다.
여기서 발전해 동적 리소스를 주고받고자 했고, 이에 대한 방법으로 CGI(common gateway interface)가 사용되었다.

CGI는 서버와 프로그램간 데이터를 주고받는 규격이다. CGI 통해 요청을 처리하면 그림처럼 서버는 동적으로 처리될 부분은 프로그램에게 요청해서 응답한다.
이 방법의 단점으로는 매번 요청마다 프로그램에게 프로세스를 할당해야 하기에 속도가 느리다는 것이다.

text 출처 protechtraining

이를 개선한 servlet은 java진영의 웹서버 프로그램을 표준을 말하며, 아파치 톰캣은 이 serlvet,servlet container를 구현한 오픈소스 프로젝트이다. CGI와 servlet의 차이점은 확장성,이식성의 개선이 있지만 눈에 띄는 점은 servlet은 외부 프로세스가 아닌 스레드를 실행해 요청을 처리하기에 서버의 자원을 더 효율적으로 사용할 수 있다는 점이다.

즉 servlet은 웹 요청을 받아 응답하는 자바 프로그램을 말하고, 톰캣은 이 servlet을 관리하는 servlet container를 포함하는 구현체이며 스레드를 통해 요청을 기존의 CGI방식보다 효율적으로 처리한다.

톰캣의 구조

  • Coyote: HTTP 요청과 응답을 처리하는 컴포넌트. 클라이언트로부터의 연결을 수락하고, HTTP 프로토콜을 통해 데이터를 주고받는다.

  • Catalina: Tomcat의 서블릿 컨테이너(Engine) 부분으로, 실제 웹 애플리케이션 로직을 실행한다.

    • Service: 하나 이상의 Connector와 하나의 Engine(서블릿 컨테이너) 사이의 연결 역할을 한다. 즉, 특정 네트워크 요청이 어떤 서블릿 컨테이너에서 처리될 것인지를 매핑해 결정한다.

    • Valve: 파이프라인 내에서 순차적으로 실행되며, 일반적으로 전처리(pre-processing), 후처리(post-processing), 보안 체크(security checks), 로깅(logging) 등과 같은 작업을 한다.

톰캣의 메인 기능인 servlet container로서의 역할은 catalina에서 수행한다. 여기에 coyote로 http요청을 받아줄 수 있기에 톰캣이 was의 기능도 일부 가지고 있다고 표현한다.

톰캣의 요청 처리 과정

톰캣은 어떤 식으로 http통신을 할까?
요청을 수신하는 작업, 요청을 처리하는 작업을 별개의 스레드에서 진행한다.

요청 수신

클라이언트로부터의 연결 요청을 수락하고, 그 연결에 대한 I/O 이벤트를 감지하는 작업은 NioEndpoint 같은 EndPoint클래스가 주로 담당한다. 이 컴포넌트는 별도의 Acceptor 스레드에서 실행되며, 주로 네트워크 소켓 관리와 관련된 저수준 작업들을 처리한다.

톰캣을 시작하면 초기화 과정에서 웹요청을 수신하는 스레드가 실행된다.
NioEndPoint.startInternal()에서 startAcceptorThread()에 디버깅을 걸어보자.
acceptor가 스레드에 담겨 실행되는 걸 볼 수 있다. Acceptor.run()내부를 보면 loop를 돌며 socket연결을 하고 endPoint.setSocketOptions()을 실행한다. 이 메서드 안에서 poller.register(socketWrapper) 을 실행하며 poller에 socket을 등록해 소켓을 읽고 처리하는 작업은 다른 스레드에서 비동기적으로 수행한다.

  • EndPoint.setSocketOptions메서드

-> Acceptor가 연결을 수락하고 EndPoint를 통해 socket을 poller에 등록한다.

요청 처리

poller에서 새로운 작업 스레드 할당

등록된 task를 실행해 poller.run()이 실행되면 processSocket작업을 실행한다.
poller는 요청의 처리를 위해 socketProcessor객체를 만들고 새로운 스레드에 넘기며 작업을 할당한다. AbstractEndpoint에서 sockerprocessor를 executor에 넘겨 실행하는 걸 볼 수 있다.

요청의 실제 처리

소켓을 통해 웹요청 정보를 읽어 요청의 처리를 맡기고, 응답을 쓰는 작업은 이 SocketProcessor가 담당한다. 다.

NioEndPoint의 read메서드에 디버깅을 걸어본다.
sockerProcessorBase는 SocketProcessor의 부모클래스이다. socketProcessor가 별도의 스레드에서 잘 실행되어 소켓으로부터 읽기 작업을 한다. 읽기 작업 뿐 아니라 socketProcessor -> handler -> AbstractProcessor를 거쳐 http11Processor.service()를 실행해 본격적인 요청의 처리를 하고있다.
Http11Processor.service()에서부터는 adapter-> service -> container -> servlet으로 요청을 처리하는 catalina(servlet container)의 영역으로 넘어간다.

-> poller의 웹요청을 처리하는 task를 실행한다. 이 수행은 socketProcessor를 생성해 새 스레드에 할당하는 식으로 처리된다. 새 스레드에서 socketprocessor는 소켓을 통해 웹요청 정보를 읽어 요청의 처리를 맡기고, 응답을 쓰는 작업을 한다.
httpProcessor는 httpProcessor를 통해 servlet container에게 요청의 처리를 넘긴다.

//socketProcessor.doRun()

  @Override
        protected void doRun() {
            /*
             * Do not cache and re-use the value of socketWrapper.getSocket() in
             * this method. If the socket closes the value will be updated to
             * CLOSED_NIO_CHANNEL and the previous value potentially re-used for
             * a new connection. That can result in a stale cached value which
             * in turn can result in unintentionally closing currently active
             * connections.
             */
            Poller poller = NioEndpoint.this.poller;
            if (poller == null) {
                socketWrapper.close();
                return;
            }

            try {
                int handshake = -1;
                try {
                    if (socketWrapper.getSocket().isHandshakeComplete()) {
                        // No TLS handshaking required. Let the handler
                        // process this socket / event combination.
                        handshake = 0;
                    } else if (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT ||
                            event == SocketEvent.ERROR) {
                        // Unable to complete the TLS handshake. Treat it as
                        // if the handshake failed.
                        handshake = -1;
                    } else {
                        handshake = socketWrapper.getSocket().handshake(event == SocketEvent.OPEN_READ, event == SocketEvent.OPEN_WRITE);
                        // The handshake process reads/writes from/to the
                        // socket. status may therefore be OPEN_WRITE once
                        // the handshake completes. However, the handshake
                        // happens when the socket is opened so the status
                        // must always be OPEN_READ after it completes. It
                        // is OK to always set this as it is only used if
                        // the handshake completes.
                        event = SocketEvent.OPEN_READ;
                    }
                } catch (IOException x) {
                    handshake = -1;
                    if (logHandshake.isDebugEnabled()) {
                        logHandshake.debug(sm.getString("endpoint.err.handshake",
                                socketWrapper.getRemoteAddr(), Integer.toString(socketWrapper.getRemotePort())), x);
                    }
                } catch (CancelledKeyException ckx) {
                    handshake = -1;
                }
                if (handshake == 0) {
                    SocketState state = SocketState.OPEN;
                    // Process the request from this socket
                    if (event == null) {
                        state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
                    } else {
                        state = getHandler().process(socketWrapper, event);
                    }
                    if (state == SocketState.CLOSED) {
                        socketWrapper.close();
                    }
                } else if (handshake == -1 ) {
                    getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
                    socketWrapper.close();
                } else if (handshake == SelectionKey.OP_READ){
                    socketWrapper.registerReadInterest();
                } else if (handshake == SelectionKey.OP_WRITE){
                    socketWrapper.registerWriteInterest();
                }
            } catch (CancelledKeyException cx) {
                socketWrapper.close();
            } catch (VirtualMachineError vme) {
                ExceptionUtils.handleThrowable(vme);
            } catch (Throwable t) {
                log.error(sm.getString("endpoint.processing.fail"), t);
                socketWrapper.close();
            } finally {
                socketWrapper = null;
                event = null;
                //return to cache
                if (running && processorCache != null) {
                    processorCache.push(this);
                }
            }
        }

    }

요약

톰캣은 http요청을 받아 스레드를 실행해 처리하고 응답하는 servlet 구현체이다.
catalina로 servlet container역할을 수행하며 coyote로 요청을 받는 것까지 가능하기에 기능을 좀 더 제공하는 servlet container라 볼 수 있다.

톰캣의 구조는 크게 connector인 coyote와 servlet container인 catalina로 나뉜다. coyote - catalina - container - service -valve - filter를 거쳐
요청을 처리해 응답을 가져온다.

톰캣에서 웹 요청은 여러 스레드에 걸쳐서 처리한다.
요청의 수신은 EndPoint클래스와 Acceptor를 통해 루프를 돌며 요청수신을 대기하고, 요청이 오면 poller에 등록하는 방식이다.

요청의 처리는 poller가 SocketProcessor를 생성해 새로운 작업 스레드로 실행하면
요청 읽기, 처리, 응답쓰기 작업을 한다. 요청의 처리는 httpProcessor를 통해
service - container - valve - filter를 거쳐 servlet이 invoke되어 이루어진다.

0개의 댓글