본문 바로가기

app/python

[python] 서버의 기본 동작 방식 2

- 내용이 길어서 두개로 나눴습니다. 전편을 읽고 오는게 도움이 됩니다. 

- 해당 글은 python 3.6을 기준으로 작성하였습니다. 

 

앞써 서버의 시작과 유저의 요청을 받기 위해 어떠한 동작들이 일어나는지 살펴 보았습니다. 

그러면 유저가 요청을 보냈을 때 서버에서는 어떠한 동작들이 일어나는걸까요?

 

4. 포트에서 request까지

 

다시 공식 문서를 보시면

https://docs.python.org/3.6/library/http.server.html

 

"""

class http.server.BaseHTTPRequestHandler(request, client_address, server)

This class is used to handle the HTTP requests that arrive at the server. By itself, it cannot respond to any actual HTTP requests; it must be subclassed to handle each request method (e.g. GET or POST). BaseHTTPRequestHandler provides a number of class and instance variables, and methods for use by subclasses.

"""

 

핸들러는 BaseHTTPRequestHandler로 구현되어 있는데, "서버에 도착하는 HTTP 요청을 처리하는데 사용 되며, 그 자체로는 실제 HTTP 요청에 응답 할 수 없고 각 요청 메소드 (예 : GET 또는 POST)를 처리하려면 서브 클래싱를 작성해야 한다" 라고 되어 있다. 

 

소스로 돌아와서 

# !/usr/bin/python
from http.server import BaseHTTPRequestHandler, HTTPServer

PORT_NUMBER = 8080
IP_NUMBER = '127.0.0.1'

class myHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write("Hello World !".encode())
        return


if __name__ == '__main__':
    try:
        server = HTTPServer((IP_NUMBER, PORT_NUMBER), myHandler)
        print('Started httpserver on port ', PORT_NUMBER)
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    
    print('^C received, shutting down the web server')
    server.socket.close()

 

위의 소스에서 MyHandle()부분이 바로 사용자가 구현한 서브클래스 입니다. 어떤 요청이 들어오면 메소드에 따라 해당 로직을 실행하게 됩니다. 

 

 

 

위의 코드를 이번엔 유저가 요청을 했을 상황을 pycallgraph으로 실행해봤습니다. 

확대해서 보세요.

 

위의 그림에서 오른쪽 대부분은 유저의 요청 시 데이터를 처리하기 위한 파일 로더, 인코더 등으로 핵심 클래스인 socketserver.server_forever(), Pollselector.selector()가 많이 호출되었습니다. 

 

서버입장에서는 유저의 요청이 언제 들어올지 모릅니다. 그래서 지속적으로 poll을 호출해서 fd가 변경되는지 확인해야 합니다. (이 부분은 앞글에서 다뤘습니다.)

- 한가지 특이한점은 파서의 기능이 email 클래스에 있습니다. 신기합니다...

 

5. select 부터 메시지 리턴까지 

serve_forever() 함수의 일부분 입니다. 바로 아래 에서 select가 호출되어 fd의 변경을 알려줍니다. 그리고 변경된 값이 있다면 ready를 통해 _handle_request_noblock()를 호출 합니다. 

with _ServerSelector() as selector:
    selector.register(self, selectors.EVENT_READ)

    while not self.__shutdown_request:
        ready = selector.select(poll_interval)
        if self.__shutdown_request:
            break
        if ready:
            self._handle_request_noblock()

        self.service_actions()

 

 

_handle_request_noblock() 에서 get_request()를 호출 하게 되는데 여기에서 socket.accept()를 통해 소켓의 연결을 받아들입니다.

    def get_request(self):
        return self.socket.accept()

BaseRequestHandler.handle() -> handle_one_request() -> method() 에서 사용자 요청이 어떤 메소드 호출인지 확인 합니다. 그리고 서버에서 선언한 메소드가 있다면 해당 메소드를 호출 / 없다면 에러를 반환합니다. 

 

사용자 함수 실행 (여기에선 do_GET() 입니다. )을 통해 요청에 대한 리턴을 반환하면 서버에서는 _SocketWriter를 통해  전달할 메시지를 전송합니다. 해당 클래스는 BufferedIOBase의 wrapper로 써 버퍼 텍스트 인터페이스의 구현체 입니다. (https://docs.python.org/ko/3.6/library/io.html#io.BufferedIOBase)   

 

마지막으로 메시지를 모두 전송하면 소켓을 닫음으로써 유저의 호출이 종료되고

shutdown_request()-> request.shutdown()

 

다시 select를 통해 fd의 변화가 있는지 계속 체크 합니다. 

 

- 요청 흐름의 중간인 파서를 통한 헤더 분석과 Content-type, http 버전 검사 등은 제외했습니다. 

 

결국 

1. fd를 계속 체크해서 요청을 확인한다. 

2. 요청에 대한 응답을 만든다.

3. 응답을 전송하고 1번으로 돌아간다.

 

의 순으로 서버는 동작하게 됩니다. 

 

이로써 서버의 실행 부터 유저의 요청까지 대략적인 흐름을 살펴 보았습니다. 언어에서 지원하는 간단한 서버 실행조차 많은 단계를 거치며, 실행되고 있는것을 확인하였습니다. 

 

우리가 흔히 쓰는 프레임워크는 내부적으로 환경설정, 자원 및 스레딩 관리, 템플릿과 로그/디비 연결까지 포함하는 거대한 이벤트 덩어리 입니다. 앞으로 프레임워크의 기초적인 부분을 뜯어보면서 프레임워크의 동작방식에 대해서 다뤄보려 합니다. (시간이 된다면요?!)

 

 

 

 

 

같이 보면 좋은 글 

https://docs.python.org/3.6/library/http.server.html

https://spoqa.github.io/2012/05/07/about-flask-request.html