• Neutron — API Service Web 开发框架


    目录

    WSGI

    WSGI(Web Server Gateway Interface,Web 服务器网关接口)是一个 Python Web Application 和 Web Server 之间的标准交互接口规范,定义了 Web Application 如何集成到不同的 Web Server(e.g. Apache、Nginx 等)、或高并发的网络框架(e.g. Eventlet)中的交互标准,包括调用接口函数、请求和响应的数据结构以及环境变量等等,使得它们能够协同工作。类似于 Java 的 Servlet。

    WSGI 的核心思想就是将 Web Application 和 Web Server 进行解耦,在解耦之后,还可以在它们两者之间再加入一个 “中间层" 来完成灵活的功能组合:

    • WSGI Server:本质是一个 HTTP 服务器,提供 HTTP 协议的处理,例如:IP、Port、虚拟主机等。HTTP Request 首先到达 WSGI Server,然后再被转发到 WSGI Middleware 或 WSGI Application。
    • WSGI Middleware:作为一个中间层,可以在传递 HTTP Request 到 WSGI Application 的过程中完成一系列可组合的预先处理。例如:身份验证、日志记录等。
    • WSGI Application:是 Web Application 的核心业务实现,可以使用不同的 Web 框架(e.g. Flask、Django 等)进行开发,根据功能需求提供多样的 API 资源类型和处理逻辑。

    通过这样的解耦,使得开发者可以选用理想的 Web 框架进行开发而无需关注 Web Server 的实现细节,同时也能够将这些 Web Application 部署到不同类型的 Web Server 上运行,例如:在 Apache 上安装 wsgi_mod 模块、在 Nginx 上安装 uWSGI 模块以支持 WSGI 规范。定义这些规范的目的是为了定义统一的标准,提升程序的可移植性。

    WSGI 的工作原理

    在这里插入图片描述

    从 WSGI Server 的角度看,它首先需要知道如何调用 WSGI Application,并将 HTTP Request 传递给它,然后再从 Application 处接收到返回的 HTTP Response,再最终返回给 Client。

    所以 WSGI 规范为 Server 和 Application 之间的交互定义了一个唯一且统一的 application() 接口,并由 Application 实现、由 Server 调用。接口的原型如下:

    def application(environ, start_response):
          pass
    
    • 1
    • 2

    Application 通常会以 Python Module 或 app 实例对象的方式来提供这个 application() 接口。以 web.py 框架为例,它本身是一个 Python Module 实现了 application(),可以被 Server 导入并调用。application() 作为 Application 的唯一入口,所以 Server 会将所有 HTTP Requests 都调用这个接口来进行处理。

    import web
    
    urls = (
        '/.*', 'hello',
    )
    
    class hello(object):
        def GET(self):
            return "Hello, world."
    
    app = web.application(urls, globals()).wsgifunc()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    environ 参数

    environ(环境参数)是一个 Dict 类型对象,用于存放下列数据:

    1. HTTP Request 信息;
    2. CGI(通用网管接口规范)环境变量信息;
    3. WSGI 环境变量信息;
    4. OS 环境变量信息;
    5. Server 环境变量信息。

    其中,CGI 环境变量信息包括:

    • SERVER_NAME 和 SERVER_PORT:HTTP Request 的 URL 中的 Hostname 和 Port。
    • PATH_INFO(URL 路径信息):HTTP Request 的 URL 中核心的 API Resource 路径信息。
    • QUERY_STRING(查询字符串): HTTP Request 的 URL 中可能会包含查询字符串,使用 ? 号标识。
    • SCRIPT_NAME(脚本名称):HTTP Request 的 URL 中可能会包含一些脚本的名称。
    • REQUEST_METHOD(请求方法):HTTP Request 的 Methods,包括:GET、POST、PUT、DELETE 等;
    • CONTENT_TYPE:HTTP Headers 中的 content-type 信息。
    • CONTENT_LENGTH:HTTP Headers 中的 content-length 信息。
    • SERVER_PROTOCOL:Server 的 HTTP 协议版本信息。

    WSGI 环境变量信息包括:

    • wsgi.version:WSGI 规范的版本;
    • wsgi.url_scheme:HTTP 或 HTTPS;
    • wsgi.input:一个类文件的输入流,Application 通过这个获取 HTTP Request Body;
    • wsgi.errors: 一个类文件的输出流,当 Application 出错时,可以将错误信息写入这里;
    • wsgi.multithread:当 Application 可能被多个线程同时调用时,这个值需要为 True;
    • wsgi.multiprocess: 当 Application 对象可能被多个进程同时调用时,这个值需要为 True;
    • wsgi.run_once:当 Server 期望 Application 在进程的生命周期内只被调用一次时,该值需要为 True。

    start_resposne 参数

    start_resposne(回调函数参数)指向一个 Server 实现的回调函数,Application 通过这个回调函数返回 HTTP Response 给 Server。

    WSGI 规范定义了这个由 Server 实现的回调函数可以接受 2 个必选参数和 1 个可选参数:

    start_resposne(status, resposne_headers, exc_info=None)
    
    • 1
    • status:必选,String 类型,返回 HTTP Response Status Code;
    • resposne_headers:必选,Dict 类型,返回 HTTP Response Headers;
    • exc_info:可选,返回可能的 HTTP Response Error Message 或 Body 信息。
    def application(environ, start_response):
          status = '200 OK'
          response_headers = [('Content-type', 'text/plain')]
          start_response(status, response_headers)
          return ['hello, world']
    
    • 1
    • 2
    • 3
    • 4
    • 5

    WSGI 的中间件

    如下图所示,WSGI Middleware 本身兼具了 WSGI Applicant 和 WSGI Server 的角色。因此它可以在两端之间起协调作用,经过不同的 Middleware,便拥有了不同的功能,例如:URL 路由转发、身份权限认证。

    1. Server 收到 HTTP Request 后,生成 environ_s,并且定义了 start_response_s 回调函数。
    2. Server 调用 Middleware 的 application(),传递 environ_s 和 start_response_s。
    3. Middleware 根据 environ_s 执行业务逻辑,生成 environ_m,并且定义了 start_response_m 回调函数。
    4. Middleware 再调用 Application 的 application(),传递 environ_m 和 start_response_m。
    5. Application 的 application() 处理完成后,调用 start_response_m 并且返回结果给 Middleware。Middleware 将结果存放在 result_m 中。
    6. Middleware 处理 result_m,然后生成 result_s,接着调用 start_response_s 返回结果 result_s 给 Server。
    7. Server 获取到 result_s 后封装 HTTP Response 返回给 Client。

    这里写图片描述

    WSGI Web 开发框架

    Paste + PasteDeploy + Routes + WebOb 是一个常见的 WSGI Web 开发框架。

    • Paste:是一个 WSGI 工具库,用于处理 Python WSGI Application 和 WSGI Server 之间的通信标准,同时提供了许多 WSGI Middleware 组件,用于处理 Request 和 Response,包括:会话管理,身份验证等等。满足 WSGI 规范的解耦架构设计。

    • PasteDeploy:是 Paste 的一个用于加载 WSGI Application 到 WSGI Server 的插件模块,支持通过 api-paste.ini 配置文件的方式和机制来定义与管理 WSGI Middleware 和 WSGI Application 在 Web Server 上的部署形态。通过 api-paste.ini 配置文件,开发者可以通过可插拔的方式来组装一个 Web Application。

    • Routes:是一个基于 URL 的 HTTP Request 路由分发库,用于将不同的 HTTP URLs/Methods(API Resources)与 API Controllers/Functions(资源表现层状态转移控制器)映射起来,实现将 HTTP Request 转发到对应的 Functions 进行处理并返回。

    • WebOb:是一个将 HTTP 协议中的 Request 和 Response 标准进行 Python Object 封装的库。以 Python Object 的风格和形式提供了对 Request URL、Request Header、Request Body、Request Filter & Query Parameter 以及 Response Header、Response Status Codes、Response Message、Response Body 的操作方式。此外,WebOb 还提供了一些简便的方法来处理 Cookie、会话管理、文件上传等常见的 Web 功能。

    Paste + PasteDeploy + Routes + WebOb 能够开发符合 WSGI 的 Web Application。但该框架的弊端是实现复杂,代码量大,所以在 OpenStack 中只有 Neutron 等几个初始项目在使用。后来的新项目都采用了更简单高效的 Pecan 框架。

    OpenStack 中的应用案例

    NOTE:下文中以早期的 neutron-icehouse-eol 版本的代码来进行说明,介绍 Neutron Server 启动 API Service(Web Application)的流程。

    进程入口

    # /Users/fanguiju/workspace/neutron-icehouse-eol/setup.cfg
    [entry_points]
    console_scripts =
    ......
        neutron-server = neutron.server:main
    
    
    # /Users/fanguiju/workspace/neutron-icehouse-eol/neutron/server/__init__.py
            # 实例化 API Service
            neutron_api = service.serve_wsgi(service.NeutronApiService)
    ......
                # 实例化 RPC Worker
                neutron_rpc = service.serve_rpc()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    WSGI Application 加载

    # /Users/fanguiju/workspace/neutron-icehouse-eol/neutron/common/config.py
    def load_paste_app(app_name):
        """Builds and returns a WSGI app from a paste config file.
    
        :param app_name: Name of the application to load
        :raises ConfigFilesNotFoundError when config file cannot be located
        :raises RuntimeError when application cannot be loaded from config file
        """
    
        config_path = cfg.CONF.find_file(cfg.CONF.api_paste_config)
        ......
            app = deploy.loadapp("config:%s" % config_path, name=app_name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Paste/PasteDeploy

    此时,PasteDeploy 通过 api-paste.ini 文件来配置 WSGI Middleware 和 WSGI Application 的组合。

    Neutron 的 api-paste.ini 文件如下:

    # /Users/fanguiju/workspace/neutron-icehouse-eol/etc/api-paste.ini
    
    [composite:neutron]
    use = egg:Paste#urlmap
    /: neutronversions
    /v2.0: neutronapi_v2_0
    
    [composite:neutronapi_v2_0]
    use = call:neutron.auth:pipeline_factory
    noauth = request_id catch_errors extensions neutronapiapp_v2_0
    keystone = request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0
    
    [filter:request_id]
    paste.filter_factory = neutron.openstack.common.middleware.request_id:RequestIdMiddleware.factory
    
    [filter:catch_errors]
    paste.filter_factory = neutron.openstack.common.middleware.catch_errors:CatchErrorsMiddleware.factory
    
    [filter:keystonecontext]
    paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory
    
    [filter:authtoken]
    paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
    
    [filter:extensions]
    paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory
    
    [app:neutronversions]
    paste.app_factory = neutron.api.versions:Versions.factory
    
    [app:neutronapiapp_v2_0]
    paste.app_factory = neutron.api.v2.router:APIRouter.factory
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    入口是 composite Section,用于将 HTTP Request URL 分发到指定的 Middleware 和 Application。

    • use:指定了根据 URL 分发 Request 的方式。
    • egg:Paste#urlmap:表示使用 paste/urlmap.py 模块来根据 URL 分发 Request。
    • /v2.0、/v3、/:作为传递给 urlmap_factory() 函数进行 Request 分发的实参。urlmap_factory() 返回下一个 composite Section 或 app Section。

    这里以 /v2.0 为例,urlmap_factory() 返回了 neutronapi_v2_0 composite Section,它包含了一个 Middleware Pipeline。Pipeline 用于把一系列 Middleware 和最终的 Application 串联起来,并遵守以下 2 点要求:

    1. 最后一个名字对应的 Section,一定要是一个 app 类型的。
    2. 非最后一个名字对应的 Sections,一定要是一个 filter 类型的。

    在 filter 和 app Section 中定义了 paste.filter_factory 实际的入口函数。

    根据上述配置,Neutron Server 中间件的执行顺序将如下所示:

    1. request_id:该中间件为每个请求生成一个唯一的请求 ID,并将其添加到请求的环境中,以便追踪和日志记录。
    2. catch_errors:该中间件捕获在管道中后续中间件执行过程中出现的任何异常,并进行适当的处理。它可以确保错误被捕获并返回适当的错误响应,而不会导致应用程序崩溃。
    3. authtoken:该中间件负责对请求进行身份验证和授权。它通常与身份验证服务(如 Keystone)进行交互,验证请求的凭据,并将与身份验证相关的信息添加到请求的环境中。
    4. keystonecontext:该中间件从请求的环境中提取身份验证信息,并将其转换为一个上下文对象,以供后续中间件或应用程序使用。这个上下文对象通常包含有关用户、项目等身份验证相关的信息。
    5. journal:该中间件用于将请求的信息记录到日志系统中,以便后续的监控和审计。它可以将请求的详细信息发送到一个日志服务或存储库中,用于记录请求的元数据和操作。
    6. extensions:该中间件用于处理与 Neutron API 相关的扩展功能。它可以根据请求中的扩展信息来调用相应的扩展处理逻辑,以满足客户端的定制需求。
    7. neutronapiapp_v2_0:最后一个中间件是真正的应用程序中间件。它代表了 Neutron API 的实际应用程序处理逻辑。这个中间件可能是一个 WSGI callable,它接收请求并生成相应的响应。

    Routes

    Routes 的常规用法是首先构建一个 Mapper 实例对象,然后调用 Mapper 对象的 connect() 或 collection() 方法把相应的 URL、HTTP Method 映射到一个 Controller 的某个 Action 上。

    Controller 是一个自定义的类实例,每一种 API Resource 都实现了一个 Controller,内含了多个与 HTTP Methods 对应的 Actions 成员方法,例如:list、show、create、update、delete 等。

    通过 Paste/PasteDeploy 的配置处理之后进入到 neutronapiapp_v2_0 Application 的 factory() 函数,Paste 最终会调用它。在 APIRouter 类的实例化过程中会完成 Routes Mapper 的映射工作。

    class APIRouter(wsgi.Router):
    
        @classmethod
        def factory(cls, global_config, **local_config):
            return cls(**local_config)
    
        def __init__(self, **local_config):
            mapper = routes_mapper.Mapper()
            plugin = manager.NeutronManager.get_plugin()  # Core Plugin
            ext_mgr = extensions.PluginAwareExtensionManager.get_instance()  # Service Plugins
            ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP)
    
            col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,  # 集合操作类型:index、create
                              member_actions=MEMBER_ACTIONS)  # 成员操作类型:show、update、delete
    
            def _map_resource(collection, resource, params, parent=None):
                allow_bulk = cfg.CONF.allow_bulk
                allow_pagination = cfg.CONF.allow_pagination
                allow_sorting = cfg.CONF.allow_sorting
                controller = base.create_resource(
                    collection, resource, plugin, params, allow_bulk=allow_bulk,
                    parent=parent, allow_pagination=allow_pagination,
                    allow_sorting=allow_sorting)
                path_prefix = None
                if parent:
                    path_prefix = "/%s/{%s_id}/%s" % (parent['collection_name'],
                                                      parent['member_name'],
                                                      collection)
                mapper_kwargs = dict(controller=controller,
                                     requirements=REQUIREMENTS,
                                     path_prefix=path_prefix,
                                     **col_kwargs)
                return mapper.collection(collection, resource,
                                         **mapper_kwargs)
    
    		# 顶级资源
            mapper.connect('index', '/', controller=Index(RESOURCES))
    
            for resource in RESOURCES:
                _map_resource(RESOURCES[resource], resource,
                              attributes.RESOURCE_ATTRIBUTE_MAP.get(
                                  RESOURCES[resource], dict()))
    
            for resource in SUB_RESOURCES:
                _map_resource(SUB_RESOURCES[resource]['collection_name'], resource,
                              attributes.RESOURCE_ATTRIBUTE_MAP.get(
                                  SUB_RESOURCES[resource]['collection_name'],
                                  dict()),
                              SUB_RESOURCES[resource]['parent'])
    
            # Certain policy checks require that the extensions are loaded
            # and the RESOURCE_ATTRIBUTE_MAP populated before they can be
            # properly initialized. This can only be claimed with certainty
            # once this point in the code has been reached. In the event
            # that the policies have been initialized before this point,
            # calling reset will cause the next policy check to
            # re-initialize with all of the required data in place.
            policy.reset()
            super(APIRouter, self).__init__(mapper)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    WebOb

    WebOb 有 3 个重要的对象:

    1. webob.Request:对 HTTP Request 中的 environ 参数进行封装。
    2. webob.Response:对 HTTP Response 的所有元素进行封装。
    3. webob.exc:对 HTTP Error Code 和 Msg 进行封装。

    此外,WebOb 还提供了一个 webob.dec.wsgify 装饰器,用于将一个普通的 WSGI callable(可调用对象)转换为一个接受 request 对象和 start_response 函数作为参数的 WSGI middleware。

    在 Python Web 应用程序中,WSGI 是一种定义了 Web 服务器和 Web 应用程序之间通信协议的标准接口。WSGI callable 是一个符合 WSGI 规范的可调用对象,可以被 Web 服务器调用来处理 HTTP 请求并生成相应的响应。

    webob.dec.wsgify 装饰器的作用是简化编写符合 WSGI 规范的中间件的过程。它接受一个普通的 WSGI callable,并将其转换为一个接受 request 对象和 start_response 函数的中间件。这样,你可以在中间件中直接操作 request 对象,对请求进行处理,并使用 start_response 函数来发送响应。

    • WSGI 原始的调用方式
    app_iter = myfunc(environ, start_response)
    
    • 1
    • WebOb 的调用方式
    @wsgify
    def myfunc(req):
        return webob.Response('Hey!')
        
    resp = myfunc(req)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    示例:

    from webob import Request, Response
    from webob.dec import wsgify
    
    @wsgify
    def my_middleware(request):
        # 处理请求
        response = Response()
        response.text = "Hello, World!"
        return response
    
    # 使用 my_middleware 作为中间件
    app = my_middleware
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在上面的例子中,my_middleware 是一个普通的 WSGI callable,通过 @wsgify 装饰器将其转换为一个符合 WSGI 规范的中间件。在 my_middleware 中,你可以直接操作 request 对象,并返回一个 Response 对象作为响应。

    WSGI Server 启动

    # /Users/fanguiju/workspace/neutron-icehouse-eol/neutron/service.py
    
    def _run_wsgi(app_name):
        ......
        server = wsgi.Server("Neutron")
        server.start(app, cfg.CONF.bind_port, cfg.CONF.bind_host,
                     workers=cfg.CONF.api_workers)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 相关阅读:
    SQL 注入复习总结
    bugku-web-安慰奖
    平面点云,边界提取
    Codeforces Round #909 (Div. 3)
    Virtual Serial Port Driver Pro 10.0.992 安装以及使用教程
    【论文精读5】MVSNet系列论文详解-Point-MVSNet
    网络安全(黑客)-高效自学
    【BOOST C++ 20 设计模式】(3)库 Boost.MetaStateMachine
    C++ Qt开发:Charts与数据库组件联动
    使用 AIGC ,ChatGPT 快速合并Excel工作薄
  • 原文地址:https://blog.csdn.net/Jmilk/article/details/131573854