본문으로 바로가기

ASGI와 HTTP

category Coding/Python 2022. 11. 24. 20:25
반응형

ASGI Callable interface

ASGI는 callable interface들로 구성되어있다. 첫 번째 API요청은 새로운 상태를 가진 컨텍스트를 설정하기 위해 수행되는 동기 함수 호출이다. 두 번째는 비동기 함수 호출인데, 이는 서버와 클라이언트간 통신을 위해 사용된다. 대략적인 기본 구조는 아래와 같다.

def asgi_app(scope):
    async def asgi_instance(receive, send):
        ...

    return asgi_instance

Scope

Scope는 어플리케이션의 상태를 세팅하는 인자로 딕셔너리 형태로 구성되어있다. ASGI는 HTTP뿐만 아니라 다양한 인터페이스에서 사용할 수 있는데, 이 때문에 type이라는 중요한 키가 scope 딕셔너리 내부에 포함되어있다. 해당 키를 통해 어떠한 형태의 인터페이스를 사용하는 지 판단한다. 예를 들어 https://www.example.org/ 로 GET요청을 보내는 경우 scope는 아래와 같이 구성된다.

{
    "type": "http",
    "method": "GET",
    "scheme": "https",
    "server": ("www.example.org", 80),
    "path": "/",
    "headers": []
}

Send

Send는 단일 메시지를 인자로 받고 None을 리턴하는 비동기 함수이다. HTTP의 경우 message파라미터는 HTTP응답을 위해 사용된다.

await send({
    "type": "http.response.start",
    "status": 200,
    "headers": [
        [b"content-type", b"text/plain"],
    ],
})
await send({
    "type": "http.response.body",
    "body": b"Hello, world!",
})

응답 유형에는 위처럼 2가지가 있는데, 하나는 응답을 시작하는 것이고 또다른 하나는 응답 body를 보내는 형태이다.

Receive

Receive는 인자를 받지 않고 단일 메시지를 리턴하는 비동기 함수이다. HTTP의 경우 HTTP Request body를 컨슈밍하는 역할을 한다.

# Consume the entire HTTP request body into `body`.
body = b''
more_body = True
while more_body:
    message = await receive()
    assert message["type"] == "http.request.body"
    body += message.get("body", b"")
    more_body = message.get("more_body", False)

Example

def app(scope):
    assert scope["type"] == "http"  # Ignore anything other than HTTP

    async def asgi(receive, send):
        await send({
            "type": "http.response.start",
            "status": 200,
            "headers": [
                [b"content-type", b"text/plain"],
            ],
        })
        await send({
            "type": "http.response.body",
            "body": b"Hello, World!",
        })

    return asgi

uvicorn 파일명:메소드이름 형태로 실행시킬 수 있다.

ASGI 어플리케이션을 구성하는 다른 방법들

1. 클로저

def app(scope):
    assert scope["type"] == "http"

    async def asgi(receive, send):
        ...

    return asgi

2. functools.partial

import functools


async def asgi_instance(receive, send, scope):
    ...

def asgi_application(scope):
    assert scope["type"] == "http"
    return functools.partial(asgi_instance, scope=scope)

3. 클래스 기반의 인터페이스

class ASGIApplication:
    def __init__(self, scope):
        assert scope["type"] == "http"
        self.scope = scope

    async def __call__(self, receive, send):
        ...

클래스 기반의 인터페이스는 ASGI 어플리케이션을 구현할 때 일반적으로 사용되는 방법인데, 이는 단일 Request/Response 라이프사이클에서 상태를 조작할 수 있는 싱글톤 객체를 인스턴스화하기 때문이다.

High level 인터페이스로 작업하기

ASGI의 기본적인 작동원리를 이해하는것은 굉장히 중요하지만 대부분의 상황에서 위에서 설명한 Low level 인터페이스를 가지고 작업하기란 쉽지 않다. Starlette 라이브러리는 Low level 인터페이스를 쉽게 다룰 수 있는 Request/Response에 관련된 고수준 클래스를 제공해준다.

HTTP Requests

Request 클래스는 scope를 인자로 받고 Optional하게 receive도 같이 받는다. 그리고 Request라는 High level 인터페이스를 제공한다.

from starlette.requests import Request


class ASGIApplication:
    def __init__(self, scope):
        assert scope["type"] == "http"
        self.scope = scope

    async def __call__(self, receive, send):
        request = Request(scope=self.scope, receive=receive)
        ...

Request 클래스에서 제공하는 인터페이스는 아래와 같다.

request.method

- HTTP Method

request.url

- 파싱된 문자열 형태의 URL

request.query_params

- 딕셔너리 형태로써 쿼리 파라미터 정보

request.headers

- 대소문자 구분을 하지 않는 딕셔너리로써 헤더 정보

request.cookies

- 문자열 값으로 이루어진 딕셔너리로써 Request에 포함된 모든 쿠키 정보

async request.body()

- Request body를 byte형태로 리턴하는 비동기 메소드

async request.form()

- Request body를 Form data형태로 리턴하는 비동기 메소드

async request.json()

- Request body를 JSON형태로 리턴하는 비동기 메소드

async request.stream()

- Request stream을 한번에 메모리에 올리지 않고 chunk단위로 잘라서 컨슈밍하는 비동기 이터레이터

HTTP Responses

Starlette는 HTTP응답을 내보내기 위해 다양한 기능을 제공하는 Response클래스를 가지고 있다. 아래의 예제는 Request와 Response를 동시에 사용하는 예제이다.

from starlette.requests import Request
from starlette.responses import JSONResponse


class ASGIApplication:
    def __init__(self, scope):
        assert scope["type"] == "http"

    async def __call__(self, receive, send):
        request = Request(scope=self.scope, receive=receive)
        response = JSONResponse({
            "method": request.method,
            "path": request.path,
            "query_params": dict(request.query_params),
        })
        await response(receive, send)

Response 인스턴스는 모든 인터페이스의 ASGI 인스턴스를 나타낸다. 실제로 응답을 보내기 위해 아래와 같이 사용한다.

await response(receive, send)

위 메소드를 호출한다고 해서 실제로 비동기 네트워크 IO가 발생한다거나 Request body를 읽는 등의 행위는 수반되지 않는다. 아래의 예제는 함수를 기반으로 한 예제이다.

from starlette.requests import Request
from starlette.responses import JSONResponse

def app(scope):
    assert scope['type'] == 'http'

    request = Request(scope=scope)
    return JSONResponse({
        "method": request.method,
        "path": request.path,
        "query_params": dict(request.query_params),
    })

정리

우리는 위 내용들을 통해 ASGI 메세징에 대한 기초 지식들에 대해 알아봤다. 하지만 위에 나온 예시들은 일반적으로 다루는 계층이 아니므로 실제로 개발을 진행할 때 사용할 일은 거의 없을 것이다. 이러한 low level 계층보다는 좀 더 높은 수준의 high level로 추상화된 인터페이스를 사용할 수 있기 때문이다. 마지막으로 아래의 간단한 용어들을 정리하고 끝낸다.

ASGI Application

- ASGI 인터페이스를 충족하는 Callable

ASGI Instance

- ASGI 어플리케이션 인스턴스

Scope

- ASGI 어플리케이션을 인스턴스화할 때 사용하는 딕셔너리

Receive, Send

- 서버/어플리케이션에서 발생되는 한쌍의 채널

Message

- Send/Receive를 통해 전송되는 딕셔너리

Reference

https://www.encode.io/articles/asgi-http

반응형

댓글을 달아 주세요