Django 源码分析(二):wsgi & asgi

Django 源码分析(二):wsgi & asgi

说明:上一节主要讲述了 django 项目的启动,后期主要会根据 django 请求的生命周期进行分析;

参考文章:https://zhuanlan.zhihu.com/p/95942024

参考文章:https://zhuanlan.zhihu.com/p/269456318

附:生命周期参考图;

第一步:浏览器发起请求

补充:第一步和第二步之间可以进行 Nginx 的代理,可以通过配置进行负载均衡与反向代理等信息;

第二步:WSGI创建 socket 服务端,接收请求(HttpRequest)

第三步:中间件处理请求

第四步:url路由,根据当前请求的URL找到视图函数

第五步:view视图,进行业务处理

第六步:中间件处理响应

第七步:WSGI返回响应(HttpResponse)

第八步:浏览器渲染

20181109215619143

1. Wsgi

梗概:本部分主要介绍 wsgi 相关概念,以及在 django 框架中的源码信息。

img

1.1 wsgi 概念

全称 Web Server Gateway Interface,指定了 web 服务器和 Python web应用或web框架之间的标准接口,以提高 web 应用在一系列web 服务器间的移植性。 具体可查看 官方文档

  • WSGI 是一套接口标准协议/规范
  • 通信(作用)区间是 Web 服务器和 Python Web 应用
  • 目的是制定标准,以保证不同Web服务器可以和不同的Python程序之间相互通信

WSGI 接口有服务端和应用端两部分,服务端也可以叫网关端,应用端也叫框架端。服务端调用一个由应用端提供的可调用对象。如何提供这个对象,由服务端决定。例如某些服务器或者网关需要应用的部署者写一段脚本,以创建服务器或者网关的实例,并且为这个实例提供一个应用实例。另一些服务器或者网关则可能使用配置文件或其他方法以指定应用实例应该从哪里导入或获取。

WSGI 对于 application 对象有如下三点要求:

  • 必须是一个可调用的对象
  • 接收两个必选参数 environ、start_response。
    • environ,字典包含请求的所有信息
    • start_reponse,在可调用对象中调用的函数,用来发起响应,参数包括状态码,headers等;
  • 返回值必须是可迭代对象,用来表示http body。

通过以上的知识,在结合 Python 的内置模块,编写一个简单的 wsgi 服务。

""" 编写一个简单 wsgi 请求
"""

from wsgiref.simple_server import make_server, demo_app


def hello_wsgi_app(environ, start_response):
    status = "200 OK"  # 设置响应头状态码
    response_headers = [('Content-Type', 'text/html')]  # 设置响应头的类型
    start_response(status, response_headers)  # 发起响应
    path = environ['PATH_INFO'][1:] or 'hello'  # 
    return [b'<h1>%s</h1>' % path.encode()]


app = make_server('127.0.0.1', 5001, hello_wsgi_app)
app.serve_forever()

image-20231203203016819

image-20231203203226105

1.2 uWSGI

uWSGI 是一个 Web 服务器,他是实现 wsgi 协议、uwsgi、http 等协议。Nginx 中的 HttpUwsgiModule 的作用是与uWSGI服务器进行交换。

  • wsgi 是一种协议
  • uwsgi同WSGI一样是一种通信协议。
  • 而uWSGI是实现了uwsgi和WSGI两种协议的Web服务器。

uwsgi 协议是一个 uWSGI 服务器自有的协议,它用于定义传输信息的类型(type of information),每一个 uwsgi packet 前 4 字节为传输信息类型描述,它与 WSGI 相比是两样东西。

1.3 Django 内部的应用

创建的 django 的项目中存在 wsgi.py,下面逐步分析 django 中的 wsgi 程序进行分析;

"""
WSGI config for djangoProject1 project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

# 设置环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoProject1.settings')

# 获取应用
application = get_wsgi_application()

被调用的函数

import django
from django.core.handlers.wsgi import WSGIHandler


def get_wsgi_application():
    """
    The public interface to Django's WSGI support. Return a WSGI callable.

    Avoids making django.core.handlers.WSGIHandler a public API, in case the
    internal WSGI implementation changes or moves in the future.
    """
    django.setup(set_prefix=False)  # 执行 django.setup() 函数加载 django 项目中的 app;
    return WSGIHandler()  # 返回实例化的对象

WSGIHandler 类的信息如下:

class WSGIHandler(base.BaseHandler):
    # 继承 BaseHandler
    request_class = WSGIRequest  # 聚合 WSGIRequest, 该类继承与 HttpRequest

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)  # 首先执行父类的初始化方法;
        self.load_middleware()  # 加载中间件, 注意生命周期现在还没有进入路由匹配

    def __call__(self, environ, start_response):  # 实现 wsgi 服务, 接收 environ 、 start_response
        set_script_prefix(get_script_name(environ))  # 设置线程
        signals.request_started.send(sender=self.__class__, environ=environ)
        # 请求
        request = self.request_class(environ)  # 执行 WSGIRequest 的 init 函数, 对请求信息进行疯转
        
        # 响应
        response = self.get_response(request)  # 为请求对象初始化返回指定响应对象 HttpResponse

        response._handler_class = self.__class__

        status = '%d %s' % (response.status_code, response.reason_phrase)  # 设置 status
        response_headers = [
            *response.items(),  # 响应字典解析成为 (key,value) 的形式
            *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()),
        ]  # 请求响应头
        start_response(status, response_headers)  # wsgi 的方法
        # 反射查看 file_to_stream 是否为空 和  wsgi.file_wrapper
        if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
            # If `wsgi.file_wrapper` is used the WSGI server does not call
            # .close on the response, but on the file wrapper. Patch it to use
            # response.close instead which takes care of closing all files.
            response.file_to_stream.close = response.close  # 关闭文件流
            # 设置响应头中的流和长度
            response = environ['wsgi.file_wrapper'](response.file_to_stream, response.block_size)
        return response  # 返回 响应对象

这里的模式的与 Flask 相似都是__call__()方法实现了 wsgi 的服务,当请求到了之后会执行__call__()方法。

""" 测试小 demo;
"""

class Server(object):

    def __call__(self, environ, start_response):
        print("请求执行了")
        status = "200 OK-OK"
        response_headers = [('Content-Type', 'text/html')]
        start_response(status, response_headers)
        print("请求字典:", environ)
        print("请求字典:", environ["PATH_INFO"][1:])
        path = environ['PATH_INFO'][1:] or 'hello'  # 路径信息
        return [b'<h1>%s</h1>' % path.encode()]


app = make_server('127.0.0.1', 5001, Server())
app.serve_forever()

image-20231204144043431

image-20231204144114711

源码被引用的类

class WSGIRequest(HttpRequest):
    def __init__(self, environ):
        script_name = get_script_name(environ)
        # If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
        # trailing slash), operate as if '/' was requested.
        path_info = get_path_info(environ) or '/'
        self.environ = environ
        self.path_info = path_info
        # be careful to only replace the first slash in the path because of
        # http://test/something and http://test//something being different as
        # stated in https://www.ietf.org/rfc/rfc2396.txt
        self.path = '%s/%s' % (script_name.rstrip('/'),
                               path_info.replace('/', '', 1))
        self.META = environ
        self.META['PATH_INFO'] = path_info
        self.META['SCRIPT_NAME'] = script_name
        self.method = environ['REQUEST_METHOD'].upper()  # 请求的方法大写
        # Set content_type, content_params, and encoding.
        self._set_content_type_params(environ)  # 设置请求头,和编码格式;
        try:
            content_length = int(environ.get('CONTENT_LENGTH'))
        except (ValueError, TypeError):
            content_length = 0
        self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
        self._read_started = False
        self.resolver_match = None

    def _get_scheme(self):
        return self.environ.get('wsgi.url_scheme')

    @cached_property
    def GET(self):
        # The WSGI spec says 'QUERY_STRING' may be absent.
        raw_query_string = get_bytes_from_wsgi(self.environ, 'QUERY_STRING', '')
        return QueryDict(raw_query_string, encoding=self._encoding)

    def _get_post(self):
        if not hasattr(self, '_post'):
            self._load_post_and_files()
        return self._post

    def _set_post(self, post):
        self._post = post

    @cached_property
    def COOKIES(self):
        raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '')
        return parse_cookie(raw_cookie)

    @property
    def FILES(self):
        if not hasattr(self, '_files'):
            self._load_post_and_files()
        return self._files

    POST = property(_get_post, _set_post)

引用函数的解析

def get_response(self, request):
    """Return an HttpResponse object for the given HttpRequest."""
    # Setup default url resolver for this thread
    set_urlconf(settings.ROOT_URLCONF)
    response = self._middleware_chain(request)  #
    response._resource_closers.append(request.close)
    if response.status_code >= 400:
        log_response(
            '%s: %s', response.reason_phrase, request.path,
            response=response,
            request=request,
        )
    return response

本部分暂时介绍到这里,后期涉及到中间件的以及路由等操作的时候可能都会涉及到本部分,会再次使用本部分的解析;

2. Asgi

梗概: 本部分主要介绍一下 asgi 的相关概念和 djngo 项目中 asgi 部分的代码信息;

2.1 概念

ASGI,全称是 Asynchronous Server Gateway Interface,是 Python Web 应用程序的异步服务器网关接口。它可以将 Web 服务器与应用程序框架连接起来,使之能够处理异步请求。ASGI 是为 Python Web 应用程序中的异步处理而设计的。它允许 Python Web 应用程序使用异步代码而不需要阻塞进程或线程,从而能够更好地处理高并发请求。

相比于WSGI,ASGI提供了更好的异步支持,能够更好地处理Web应用程序的实时性。当然,WSGI也能够处理一些异步请求,但是WSGI的异步处理需要依赖于一些其他的库。ASGI则是直接支持异步处理,不需要额外的库支持。

虽然ASGI是一个协议,但是为了更好地使用它,我们需要一些ASGI框架,常见的有以下的几种:

  • FastAPI

    FastAPI是一个快速、现代、易于使用的Web框架,它使用ASGI协议来处理请求。FastAPI的性能非常出色,因为它使用了异步处理和类型注解。FastAPI还提供了自动化文档生成功能,可以为API生成自动化文档,从而使得API的使用更加容易。

  • Django Channels

    Django Channels 是一个基于 Django 框架的 ASGI 框架,它可以使 Django 应用程序支持异步处理和 WebSockets。Django Channels 还提供了一些有用的功能,例如房间、广播和群组等功能。

2.2 Django 中的应用

在 django 3 的版本中创建 django 的项目的时候会直接创建 asgi.py 文件,内容如下:

"""
ASGI config for djangoProject1 project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoProject1.settings')

application = get_asgi_application()

可以发现内容与 wsgi 中的内容非常的相似,下面继续分析相关的函数信息;

import django
from django.core.handlers.asgi import ASGIHandler


def get_asgi_application():
    """
    The public interface to Django's ASGI support. Return an ASGI 3 callable.

    Avoids making django.core.handlers.ASGIHandler a public API, in case the
    internal implementation changes or moves in the future.
    """
    django.setup(set_prefix=False)  # 加载 Django 的 app
    return ASGIHandler()  # 返回相关的类

ASGIHandler 代码如下:

""" 处理的 asgi 请求的信息
"""
# 也继承了 BaseHandler
class ASGIHandler(base.BaseHandler):
    """Handler for ASGI requests."""
    request_class = ASGIRequest
    # Size to chunk response bodies into for multiple response messages.
    chunk_size = 2 ** 16

    def __init__(self):
        super().__init__()
        self.load_middleware(is_async=True)  # 加载中间件传入异步参数;

    # 同步推理, 请求到了之后会进入到 __call__ 方法, 但是本部分使用的协程异步函数
    async def __call__(self, scope, receive, send): 
        """
        Async entrypoint - parses the request and hands off to get_response.
        """
        # Serve only HTTP connections.
        # FIXME: Allow to override this.
        if scope['type'] != 'http':  # 检查请求是否不是 http 抛出异常
            raise ValueError(
                'Django can only handle ASGI/HTTP connections, not %s.'
                % scope['type']
            )
        # Receive the HTTP request body as a stream object.
        try:
            body_file = await self.read_body(receive)  #  
        except RequestAborted:
            return
        # Request is complete and can be served.
        set_script_prefix(self.get_script_prefix(scope))  # 设置线程的标识
        await sync_to_async(signals.request_started.send, thread_sensitive=True)(sender=self.__class__, scope=scope)
        # Get the request and check for basic issues.
        request, error_response = self.create_request(scope, body_file)  # 返回请求对象
        if request is None:
            await self.send_response(error_response, send)
            return
        # Get the response, using the async mode of BaseHandler.
        response = await self.get_response_async(request)  # 执行父类的异步方法获取响应对象
        response._handler_class = self.__class__
        # Increase chunk size on file responses (ASGI servers handles low-level
        # chunking).
        if isinstance(response, FileResponse):
            response.block_size = self.chunk_size
        # Send the response.
        await self.send_response(response, send)  # 执行 response

    async def read_body(self, receive):
        """Reads a HTTP body from an ASGI connection."""
        # Use the tempfile that auto rolls-over to a disk file as it fills up.
        body_file = tempfile.SpooledTemporaryFile(max_size=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, mode='w+b')
        while True:
            message = await receive()
            if message['type'] == 'http.disconnect':
                # Early client disconnect.
                raise RequestAborted()
            # Add a body chunk from the message, if provided.
            if 'body' in message:
                body_file.write(message['body'])
            # Quit out if that's the end.
            if not message.get('more_body', False):
                break
        body_file.seek(0)
        return body_file

    def create_request(self, scope, body_file):
        """
        Create the Request object and returns either (request, None) or
        (None, response) if there is an error response.
        """
        try:
            return self.request_class(scope, body_file), None
        except UnicodeDecodeError:
            logger.warning(
                'Bad Request (UnicodeDecodeError)',
                exc_info=sys.exc_info(),
                extra={'status_code': 400},
            )
            return None, HttpResponseBadRequest()
        except RequestDataTooBig:
            return None, HttpResponse('413 Payload too large', status=413)

    def handle_uncaught_exception(self, request, resolver, exc_info):
        """Last-chance handler for exceptions."""
        # There's no WSGI server to catch the exception further up
        # if this fails, so translate it into a plain text response.
        try:
            return super().handle_uncaught_exception(request, resolver, exc_info)
        except Exception:
            return HttpResponseServerError(
                traceback.format_exc() if settings.DEBUG else 'Internal Server Error',
                content_type='text/plain',
            )

    async def send_response(self, response, send):
        """Encode and send a response out over ASGI."""
        # Collect cookies into headers. Have to preserve header case as there
        # are some non-RFC compliant clients that require e.g. Content-Type.
        response_headers = []
        for header, value in response.items():
            if isinstance(header, str):
                header = header.encode('ascii')
            if isinstance(value, str):
                value = value.encode('latin1')
            response_headers.append((bytes(header), bytes(value)))
        for c in response.cookies.values():
            response_headers.append(
                (b'Set-Cookie', c.output(header='').encode('ascii').strip())
            )
        # Initial response message.
        await send({
            'type': 'http.response.start',
            'status': response.status_code,
            'headers': response_headers,
        })
        # Streaming responses need to be pinned to their iterator.
        if response.streaming:
            # Access `__iter__` and not `streaming_content` directly in case
            # it has been overridden in a subclass.
            for part in response:
                for chunk, _ in self.chunk_bytes(part):
                    await send({
                        'type': 'http.response.body',
                        'body': chunk,
                        # Ignore "more" as there may be more parts; instead,
                        # use an empty final closing message with False.
                        'more_body': True,
                    })
            # Final closing message.
            await send({'type': 'http.response.body'})
        # Other responses just need chunking.
        else:
            # Yield chunks of response.
            for chunk, last in self.chunk_bytes(response.content):
                await send({
                    'type': 'http.response.body',
                    'body': chunk,
                    'more_body': not last,
                })
        await sync_to_async(response.close, thread_sensitive=True)()

    @classmethod
    def chunk_bytes(cls, data):
        """
        Chunks some data up so it can be sent in reasonable size messages.
        Yields (chunk, last_chunk) tuples.
        """
        position = 0
        if not data:
            yield data, True
            return
        while position < len(data):
            yield (
                data[position:position + cls.chunk_size],
                (position + cls.chunk_size) >= len(data),
            )
            position += cls.chunk_size

    def get_script_prefix(self, scope):
        """
        Return the script prefix to use from either the scope or a setting.
        """
        if settings.FORCE_SCRIPT_NAME:
            return settings.FORCE_SCRIPT_NAME
        return scope.get('root_path', '') or ''

本部分后期会进行相关的补充,本篇文章暂时解析到本部分;

继续努力,终成大器!

posted @ 2024-01-14 22:49  紫青宝剑  阅读(840)  评论(0编辑  收藏  举报