Starlette 源码阅读 (一) ASGI应用

为了深入了解FastAPI框架,笔者决定将其基础库Starlette的源码进行阅读,未来也可能阅读uvicorn库的ASGI原理。个人水平十分有限,有错误的地方还请见谅。


Starlette文件结构

从applications.py开始

applications只包含了Starlette一个类

 class Starlette:
  """
        创建一个APP
        参数:
            (1) debug: 布尔值, 用于设置在出现错误时是否应返回调试回溯.

            (2) routers: 一个路由列表, 提供HTTP和WebSocket服务.

            (3) middleware: 一个中间件列表, 应用于每个request请求.
                一个starlette应用, 最少包含两个中间件:
                1. ServerErrorMiddleware中间件处于最外层, 以处理在整个堆栈中任何地方发生的未捕获错误.
                2. ExceptionMiddleware在最内层以处理处理路由或终结点中发生的异常情况.

            (4) exception_handlers: 一个字典, 键为状态码或者异常类, 值为其对应的调用函数.
                调用函数应该是handler(request, exc) -> response, 可以是标准函数, 也可以是异步函数.

            (5) on_startup: 一个启动时的调用项列表, 启动处理调用项, 不接受任何参数, 可以是同步函数, 也可以是异步函数.

            (6) on_shutdown: 一个关闭时的调用项列表, 同上.
    """
    def __init__(
        self,
        debug: bool = False,
        routes: typing.Sequence[BaseRoute] = None,
        middleware: typing.Sequence[Middleware] = None,
        exception_handlers: typing.Dict[
            typing.Union[int, typing.Type[Exception]], typing.Callable
        ] = None,
        on_startup: typing.Sequence[typing.Callable] = None,
        on_shutdown: typing.Sequence[typing.Callable] = None,
        lifespan: typing.Callable[["Starlette"], typing.AsyncGenerator] = None,
    ) -> None:
        """
            lifespan上下文函数是一种较新的样式,
            它取代了on_startup/on_shutdown处理程序。
            两种方式任选其一,切勿一起使用。
        """
        assert lifespan is None or (
            on_startup is None and on_shutdown is None
        ), "Use either 'lifespan' or 'on_startup'/'on_shutdown', not both."

        self._debug = debug
        self.state = State()
        self.router = Router(
            routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan
        )
        self.exception_handlers = (
            {} if exception_handlers is None else dict(exception_handlers)
        )
        self.user_middleware = [] if middleware is None else list(middleware)
        self.middleware_stack = self.build_middleware_stack()
        # build_middleware_stack函数用于构建中间件堆栈
    def build_middleware_stack(self) -> ASGIApp:
        """
            构建中间件堆栈
            返回值是ServerErrorMiddleware, 也就是最外层的中间件.其app属性指向上一个中间件, 以此类推
        """
        debug = self.debug
        error_handler = None
        exception_handlers = {}

        for key, value in self.exception_handlers.items():
            if key in (500, Exception):
                error_handler = value
            else:
                exception_handlers[key] = value
        # 将异常类和状态码分离出来保存

        middleware = (
            [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug,)]
            + self.user_middleware
            + [
                Middleware(
                    ExceptionMiddleware, handlers=exception_handlers, debug=debug,
                )
            ]
        )
        # 创建中间件元祖, 顺序为:
        # (1)最外层ServerErrorMiddleware(异常类)
        # (2)用户的中间件列表
        # (3)最内层ExceptionMiddleware(状态码字典)

        app = self.router
        for cls, options in reversed(middleware):
            app = cls(app=app, **options)
        # cls, options为middleware类的两个属性
        # 分别对应上面Middleware的第一个参数, 和后续参数.
        # 随后进行套娃, 迭代中第一个app中的app属性为self.router, 随后每个中间件app中的app属性, 皆指向上一个中间件app
        # 把最后一个套娃(应该是最外层的ServerErrorMiddleware)返回

        return app
    @property
    def routes(self) -> typing.List[BaseRoute]:
        return self.router.routes

    @property
    def debug(self) -> bool:
        return self._debug

    @debug.setter
    def debug(self, value: bool) -> None:
        self._debug = value
        self.middleware_stack = self.build_middleware_stack()

    def url_path_for(self, name: str, **path_params: str) -> URLPath:
        return self.router.url_path_for(name, **path_params)

App的启动入口

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        scope["app"] = self
        # 将自身装入到传过来的scope字典中, 然后传给中间件堆栈
        await self.middleware_stack(scope, receive, send)
        # 前面提到, middleware_stack实际上是最外层中间件ServerErrorMiddleware
        # 调用ServerErrorMiddleware的__call__
errors.py → ServerErrorMiddleware类

与上文相关的部分

class ServerErrorMiddleware:
    """
        当服务器发生错误时, 返回500响应

        如果debug设置了, 那么将返回traceback, 否则将调用指定的handler

        这个中间件类通常应该用于包含其他所有中间件, 这样保证任何地方出现异常
        都可以返回 500 response
    """

    def __init__(
        self, app: ASGIApp, handler: typing.Callable = None, debug: bool = False
    ) -> None:
        self.app = app
        self.handler = handler
        self.debug = debug

        # if key in (500, Exception):
        #     error_handler = value
        # [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug,)]
        # 中间件堆栈定义时的参数, handler为用户自定义的500处理:

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return
        # 如果请求类型不是http, 则转入下一个中间件

        response_started = False
        async def _send(message: Message) -> None:
            nonlocal response_started, send

            if message["type"] == "http.response.start":
                response_started = True
            await send(message)
        # 做一个闭包传给下一个中间件, 让后续中间件使用send时,
        # 能够修改自身的response_started为True

        try:
            await self.app(scope, receive, _send)
        except Exception as exc:
            # 如果调用过_send, response_started为True
            # 下面代码将不会执行
            if not response_started:
                request = Request(scope)
                if self.debug:
                    # 在debug模式中, 将返回traceback
                    response = self.debug_response(request, exc)
                elif self.handler is None:
                    # 使用默认的handler处理
                    response = self.error_response(request, exc)
                else:
                    # 使用用户自定义的500 handler
                    if asyncio.iscoroutinefunction(self.handler):
                        response = await self.handler(request, exc)
                    else:
                        response = await run_in_threadpool(self.handler, request, exc)
                    # 判断是否为协程, 不是则以线程池方式运行
                await response(scope, receive, send)

            # 我们总是不断地抛出异常。
            # 这允许服务器记录错误,
            # 或者允许测试客户端在测试用例中选择性地提出错误。
            raise exc from None

调用

uvicorn.run(app, host="127.0.0.1", port=8000)
运行时将app传给ASGI服务器, 服务器调用时会沿着
app.__call__ → middleware1.__call__ → middleware2.__call__...
查看最内层中间件ExceptionMiddleware.__call__的部分代码

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        response_started = False

        async def sender(message: Message) -> None:
            nonlocal response_started

            if message["type"] == "http.response.start":
                response_started = True
            await send(message)

        try:
            await self.app(scope, receive, sender)
        except Exception as exc:

会发现和最外层的ServerErrorMiddleware有极为相似, 暂且推测. 整个中间件堆栈中, 多次使用了将send闭包传给下层, 下层可能再做一次闭包将自己的response_started也包括进去.

其结构抽象化如下:
    async def send3(message):
        nonlocal response_started
        if message["type"] == "http.response.start":
            response_started = True
            # ExceptionMiddleware.response_started
        await def send2(message):
                nonlocal response_started
                if message["type"] == "http.response.start":
                    response_started = True
                    # XXXMiddleware.response_started
                await def send1(message):
                        nonlocal response_started
                        if message["type"] == "http.response.start":
                            response_started = True
                            # ServerErrorMiddleware.response_started
                        await send(message)

回到applications.py → Starlette类

英文注释在这里做了警告, 以下都是修改Starlette自身属性的方法

    # The following usages are now discouraged in favour of configuration
    #  during Starlette.__init__(...)
    # 以下方法, 现在不支持在__init__中配置使用
    def on_event(self, event_type: str) -> typing.Callable:
        return self.router.on_event(event_type)

    def mount(self, path: str, app: ASGIApp, name: str = None) -> None:
        self.router.mount(path, app=app, name=name)

    def host(self, host: str, app: ASGIApp, name: str = None) -> None:
        self.router.host(host, app=app, name=name)

    def add_middleware(self, middleware_class: type, **options: typing.Any) -> None:
        self.user_middleware.insert(0, Middleware(middleware_class, **options))
        self.middleware_stack = self.build_middleware_stack()

    def add_exception_handler(
        self,
        exc_class_or_status_code: typing.Union[int, typing.Type[Exception]],
        handler: typing.Callable,
    ) -> None:
        self.exception_handlers[exc_class_or_status_code] = handler
        self.middleware_stack = self.build_middleware_stack()

    def add_event_handler(self, event_type: str, func: typing.Callable) -> None:
        self.router.add_event_handler(event_type, func)

    def add_route(
        self,
        path: str,
        route: typing.Callable,
        methods: typing.List[str] = None,
        name: str = None,
        include_in_schema: bool = True,
    ) -> None:
        self.router.add_route(
            path, route, methods=methods, name=name, include_in_schema=include_in_schema
        )

    def add_websocket_route(
        self, path: str, route: typing.Callable, name: str = None
    ) -> None:
        self.router.add_websocket_route(path, route, name=name)

以及对上述方法的一些包装

    def exception_handler(
        self, exc_class_or_status_code: typing.Union[int, typing.Type[Exception]]
    ) -> typing.Callable:
        def decorator(func: typing.Callable) -> typing.Callable:
            self.add_exception_handler(exc_class_or_status_code, func)
            return func

        return decorator

    def route(
        self,
        path: str,
        methods: typing.List[str] = None,
        name: str = None,
        include_in_schema: bool = True,
    ) -> typing.Callable:
        def decorator(func: typing.Callable) -> typing.Callable:
            self.router.add_route(
                path,
                func,
                methods=methods,
                name=name,
                include_in_schema=include_in_schema,
            )
            return func

        return decorator

    def websocket_route(self, path: str, name: str = None) -> typing.Callable:
        def decorator(func: typing.Callable) -> typing.Callable:
            self.router.add_websocket_route(path, func, name=name)
            return func

        return decorator

    def middleware(self, middleware_type: str) -> typing.Callable:
        assert (
            middleware_type == "http"
        ), 'Currently only middleware("http") is supported.'

        def decorator(func: typing.Callable) -> typing.Callable:
            self.add_middleware(BaseHTTPMiddleware, dispatch=func)
            return func

        return decorator

以上为Starlette库中applications.py为主的源码解读, 如有错误欢迎指正.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342