이 글은 Python 웹 프레임워크의 내부 동작을 이해하고자 Flask의 일부 기능을 간략하게 설명합니다. 세부적인 구현은 포함되어 있지 않습니다.
https://ooknimm.tistory.com/6 글에서 이어집니다.
프로덕션 환경에서는 uwsgi, gunicorn과 같은 WSGI로 HTTP 서버가 실행되고, 이 서버가 웹 애플리케이션을 호출합니다. WSGI 툴은 동시성, 안정성 등 기능들을 제공하기 때문에 필수적으로 사용됩니다. 반면 개발 환경에서는 단독으로 HTTP 서버를 실행하여 pdb 및 IDE를 통해 디버깅할 수 있어야 합니다.
app = Flask(__name__)
app.run(port=9000)
위와 같이 app을 직접 실행하면 http서버가 listen 되어 9000 포트로 요청하면 애플리케이션을 호출합니다. 해당 기능을 만들어보겠습니다.
TCP 서버
TCP(Transmission Control Protocol)은 서버와 클라이언트 간 3 way handshake를 통한 논리적 연결 상태에서 데이터를 주고받는 것이 특징입니다.
서버는 최초 가동시 listen용 Socket을 생성하고, port에 bind 하여 listen 합니다.(클라이언트의 요청을 기다립니다.)
그리고 클라이언트의 요청이 발생하면 다음과 같습니다.
- 클라이언트에서 서버의 특정 포트에 연결 요청을 합니다.
- accept()를 호출합니다. accept 내부에서 클라이언트 통신용 소켓을 생성합니다. (1, 2 과정에 3 way handshake가 포함됩니다.)
- 클라이언트 통신용 소켓을 통해 클라이언트와 데이터를 주고 받습니다.
flask의 베이스인 werkzeug의 HTTP 서버 코드를 바탕으로 다음과 같이 구현하였습니다.
class Server:
def __init__(self, server_address: str, application: Callable) -> None:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_address = server_address
self.application = application
self.finish = False
self.bind() # 1
self.activate() # 2
def fileno(self) -> int:
return self.socket.fileno()
def bind(self) -> None:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
self.socket.bind(self.server_address)
def activate(self) -> None:
self.socket.listen()
def close(self) -> None:
self.socket.close()
def accept(self) -> Tuple[socket.socket, Any]:
return self.socket.accept()
def forever(self) -> None: # 3
try:
with selectors.SelectSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while not self.finish:
ready = selector.select(0.5)
if ready:
self.handle_request()
finally:
self.finish = True
self.close()
def handle_request(self) -> None:
request, _ = self.accept() # 4
RequestHandler(request, self.application) # 5
request.close() # 6
1. 객체 생성 시 bind 메소드를 호출하여 소켓을 생성하고 서버 주소를 bind 합니다.
2. activate 메소드로 bind된 소켓을 listen 합니다.
3. 애플리케이션 단독 실행 시 forever 메소드를 호출하여 listen된 소켓을 0.5초마다 "읽기 가능" 상태인지 확인합니다.
4. 읽기 가능 상태가 되면 accept를 호출하고 통신용 소켓을 반환받습니다.
5. 통신용 소켓으로 데이터를 받고 애플리케이션을 호출한 후 데이터를 전달합니다.
6. 통신용 소켓을 종료합니다.
RequestHandler
클라이언트와 논리적 연결이 되면 통신용 소켓 사용하여 데이터를 읽고 쓰거나 wsgi로 애플리케이션을 실행하는 로직입니다.
class RequestHandler:
def __init__(self, request: socket.socket, application: Callable[[Dict[str, str], Callable[[str, str], None]], Iterable[bytes]]) -> None:
self.application = application
self.rfile: io.BufferedReader = request.makefile("rb", -1) # 1
self.wfile: io.BufferedWriter = request.makefile("wb", -1) # 1
self.status: Optional[str] = None
self.headers: List[Tuple[str, str]] = []
try:
self.handle()
finally:
self.finish()
def handle(self) -> None:
raw_requestline = self.rfile.readline(65537)
command, path, headers = self.parse_request(raw_requestline) # 2
self.run_wsgi(command, path, headers) # 3
def finish(self) -> None:
self.wfile.close()
self.rfile.close()
def parse_headers(self) -> None:
header_list: List[bytes] = []
while True:
line = self.rfile.readline(65537)
if line in (b"\r\n", b"\n", b""):
break
header_list.append(line)
headers = dict((header.decode("latin-1").rstrip("\r\n").split(": ") for header in header_list))
return headers
def parse_request(self, raw_requestline: bytes) -> Tuple[str, str, Dict[str, str]]:
request_line = str(raw_requestline, "latin-1").rstrip("\r\n")
words = request_line.split()
command, path = words[:2]
headers = self.parse_headers()
return command, path, headers
def make_environ(self, command: str, path: str, headers: Dict[str, str]) -> Dict[str, str]:
request_url = urlsplit(path)
path_info = request_url.path
environ = {
"PATH_INFO": path_info,
"QUERY_STRING": request_url.query,
"REQUEST_METHOD": command,
"wsgi.input": self.rfile
}
for key, value in headers.items():
key = key.upper().replace("-", "_")
environ[key] = value
return environ
def make_headers(self) -> List[bytes]:
code, msg = self.status.split(None, 1)
headers = [(f"HTTP/1.0 {code} {msg}\r\n").encode("latin-1")]
for h in self.headers:
headers.append("{}: {}\r\n".format(h[0], h[1]).encode("latin-1"))
return headers
def write(self, data: bytes) -> None:
headers = self.make_headers()
self.wfile.sendall(b"".join(headers))
self.wfile.sendall(b"\r\n")
self.wfile.sendall(data)
def start_response(self, status: str, headers: Dict[str, str]) -> None:
self.status = status
self.headers = headers
def run_wsgi(self, command: str, path: str, headers: Dict[str, str]) -> None:
environ = self.make_environ(command, path, headers)
application_iter = self.application(environ, self.start_response)
for data in application_iter:
self.write(data)
1. 소켓과 결합한 파일 객체를 반환합니다. 읽기용, 쓰기용을 각각 생성합니다.
2. rfile에서 http method, url path, header를 읽습니다. "\r\n"으로 header, body 데이터 간 구분이 가능합니다.
3. wsgi 방식으로 애플리케이션을 실행합니다. http method, url path, header로 environ을 만들고 애플리케이션을 호출합니다. wsgi 방식은 https://ooknimm.tistory.com/2를 참고하시기 바랍니다.
서버 실행
애플리케이션 로직에 run 메소드를 추가합니다.
class FakeFlask:
.
.
.
def run(self, port: int) -> None:
from server import Server
httpd = Server(("127.0.0.1", port), self)
httpd.forever()
위에서 작성한 http 서버 객체를 생성하고 forever 메소드를 호출합니다.
app = FakeFlask()
@app.route("/")
def home():
print(request.get_json())
return "hello world"
@app.route("/ping")
def ping():
return "pong"
app.run(port=9000)
IDE의 디버그모드나 직접 실행이 가능한 애플리케이션을 작성할 수 있습니다.
이렇게 Flask를 흉내 내어 웹 프레임워크를 작성해 봤습니다. 실제로 웹 애플리케이션을 작성하는데 크게 도움이 되는 내용은 아니지만 내부 동작 방식이 궁금했던 분들에게 도움이 되었으면 좋겠습니다.
'Python > 웹 프레임워크 만들기' 카테고리의 다른 글
Python 웹 프레임워크 만들기 - Context (0) | 2023.09.24 |
---|---|
Python 웹 프레임워크 만들기 - WSGI (0) | 2023.09.09 |