Python WSGI笔记

Python WSGI笔记

本文转载自花了两个星期,我终于把 WSGI 整明白了

问1:一个HTTP请求到达对应的 application 处理函数要经过怎样的过程?

问2:如何不通过流行的 web 框架来写一个简单的web服务?

一个HTTP请求的过程可以分为两个阶段,第一阶段是从客户端到WSGI Server,第二阶段是从WSGI Server 到 WSGI Application。

在这里插入图片描述

主要内容:

  1. WSGI是什么,因何而生?
  2. HTTP请求是如何到应用程序的?
  3. 实现一个简单的WSGI Server
  4. 实现"高并发"的WSGI Server
  5. 第一次路由:PasteDeploy
  6. PasteDeploy 使用说明
  7. webob.dec.wsgify装饰器
  8. 第二次路由:中间件 routes 路由

1.WSGI是什么?因何而生?

WSGI是 Web Server Gateway Interface 的缩写。介于 web server 和 应用 server 之间的接口规范。

它是 Python 应用程序 (application) 或 框架(如 Django) 和 Web 服务器之间的一种接口,已经被广泛接受。

它是一种协议,一种规范,其是在 PEP 3333 提出的。这个协议旨在解决众多 web 框架和 web server软件的兼容问题。有了 WSGI,你不用再因为你使用的 web 框架而去选择特定的 web server软件。

常见的web应用框架有:Django、Flask等。

常用的web服务器软件有:uWSGI、Gunicorn等。

那这个 WSGI 协议内容是什么呢?知乎上有人将 PEP 3333 翻译成中文,写得非常好,我将这段协议的内容搬运过来。

为了解PEP 333的读者准备的前言

PEP 3333是PEP 333的升级版本,进行了略微修改以提高在 Python 3下的可用性。同时,合并了几点长期存在的、实际的修改。

规范概述

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

除了这些比较纯的服务器/网关 和 应用/框架,我们还可以创建实现了此规范的中间件组件。这个中间件对服务器来说像是应用,对它所包含的应用来说像是服务器。中间件可以用来提供扩展API、内容转换、导航等其他有用的功能。

纵观整个规范,术语[可调用对象]可能代表函数、方法、类或者实现了 __ call __ 方法的实例。这取决于服务器、网关或者应用选择哪种实现技术。相反地,调用这个可调用对象的服务器、网关或者应用不能依赖提供给它的是哪种可调用对象。可调用对象仅仅是被调用,不会内省自己。

字符串类型

一般来说,HTTP处理的是字节,这意味着WSGI规范也要处理字节。然而,这些字节内容往往有某种文本解释。在Python中,字符串是处理文本最方便的方式。

但是在很多Python版本和实现中,字符串是Unicode,不是字节。这就需要小心平衡在HTTP的上下文中如何正确转换字节和文本,并提供有用的API。尤其是需要支持Python实现中不同str类型的转换代码。

因此 WSGI 定义了两种字符串:

  • 原生字符串(总是使用 str 来实现)用于 请求/响应 的头部(headers) 和 元数据(metadata)
  • 字节字符串(在 Python3 中用 bytes实现,其他版本中用 str 实现)用于请求/响应的数据部分(如 POST/PUT的输入数据,HTML 的输出内容等)。

一句话:当你在本文档中看到 string 时,它代表[原生]字符串,例如一个 str 类型的对象,不管它内部实现上用的是字节还是 Unicode。当你看到 bytestring,应该视作一个在 python3 中的 bytes对象,在 python2 中的 str 对象。

应用/框架端

应用对象(application object)就是一个简单的接收两个参数的可调用对象。 不要混淆术语"object"就真的是一个对象实例。Python 中的函数、方法、类、实现了__ call __的实例都是可以接受的。应用对象必须可以被多次调用,因为实际上所有服务器/网关(除了 CGI 网关)都会重复地调用它。

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

  1. 必须是一个可调用的对象
  2. 接收两个必选参数 environ、start_response;
  3. 返回值是可迭代对象,用来表示 http body。

下面是两个应用对象(application object)的示例。一个是函数(function),一个是类(class):

HELLO_WORLD = b'Hello world!/n'

def simple_app(environ, start_response):
  """
  最简单的应用对象
  """
  status = '200 OK'
  response_headers = [('Content-type', 'text/plain')]
  start_response(status, response_headers)
  return [HELLO_WORLD]

class APPClass:
  """
  产生相同的输出,但是用类实现。
  """
  def __init__(self, environ, start_response):
    self.environ = environ
    self.start = start_response
    
	def __iter__(self):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    self.start(status, response_headers)
    yield HELLO_WORLD
服务器/网关端

服务器或者网关每次从 HTTP 客户端收到一个请求,就调用一次应用对象。

中间件:可以与两端交互的组件

中间件就是一个简单对象:既可以作为服务端角色,响应应用对象;也可以作为应用对象,与服务器交互。除此之外,还有一些其他功能:

  • 重写environ,然后基于 URL,将请求对象路由给不同的应用对象。
  • 支持多个应用或者框架顺序地运行于同一个进程中。
  • 通过转发请求和响应,支持负载均衡和远程处理。
  • 支持对内容做后处理(postprocessing),比如处理一个 XSL 样式表文件。

中间件的灵魂是:对 WSGI 接口的服务器/网关端 和 应用/框架端是透明的,不需要其他条件。

希望将中间件合并进应用的用户,将这个中间件传递给服务器即可,就好像这个中间件是一个应用对象;或者让中间件去调用应用对象,好像这个中间件就是服务器。当然,被中间件包装(wrap)的应用对象,实际上可能是另一个包装了另一个应用的中间件,以此类推,就创建了一个中间件栈(middleware stack)。

最重要的,中间件必须同时满足服务端和应用端的限制和条件。然而,在有些情况下,中间件需要的条件比单纯的服务端或者应用端更严格,这些点会在下面予以说明。

HTTP请求是如何到应用程序的?

当客户端发出一个 HTTP 请求后,是如何转到我们的应用程序处理并返回的呢?
关于这个过程,细节的点这里没法细讲,只能讲个大概。

根据其架构组成的不同将这个过程的实现分为两种:

在这里插入图片描述

1、两级结构

在这种结构里,uWSGI作为服务器,它用到了HTTP协议以及wsgi协议,flask应用作为application,实现了wsgi协议。当有客户端发来请求,uWSGI接收请求,调用flask app得到响应,之后响应给客户端。

这里说一点,通常来说,Flask等web框架会自己附带一个wsgi服务器(这就是flask应用可以直接启动的原因),但是这只是在开发阶段用到的,在生产环境是不够用的,所以用到了 uWSGI 这个性能高的wsgi服务器。

2、三级结构

这种结构里,uWSGI作为中间件,它用到了uWSGI协议(与 nginx 通信),wsgi协议(调用Flask app)。当有客户端发来请求,nginx先做处理(静态资源是 nginx 的强项),无法处理的请求(uWSGI),最后的响应也是ngin回复给客户端的。多了一层反向代理有什么好处?

提高web server性能(uWSGI处理静态资源不如nginx;nginx会在收到一个完整的http请求后再转发给uWSGI);

nginx可以做负载均衡(前提是有多个服务器),保护了实际的web服务器(客户端是在和nginx交互而不是uWSGI);

uWSGI

uWSGI 是一个实现了 wsgi、uwsgi、http协议的服务器。

它有两种模式,http模式对应上面的两级结构,socket模式对应上面的三层结构。

3、实现一个简单的WSGI Server
# hello.py
# 函数式 WSGI 服务器调用的对象是 app
def app(environ, start_response):		# 提供两个必须变量
	start_response('200 OK', [('Content-Type', 'text/html')])		# 调用start_response发送头部
	body = ''
	for i, j in environ.items():
		body += '<p>' + (str(i) + ':::::' + str(j) + '<br><p>')
	return [body.encode('utf-8')]		# 返回byte的元组表示 response body

确实很简单,我们再来看看别的方法实现的app。

# 实现__call__方法的类的实例变量 WSGI 服务器调用的对象是 app1()
class app1:
  def __call__(self, environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
    body = ''
    for i, j in environ.items():
      body += '<p>' + (str(i) + ':::::' + str(j) + '<br><p>')
    return [body.encode('utf-8')]
# 迭代法 WSGI服务器调用的对象是app2
class app2def __init__(self, environ, start_response):
    self.environ = environ
    self.start_response = start_response
  def __iter__(self):
    self.start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
    body = ''
    for i, j in self.environ.items():
      body += '<p>' + (str(i) + ':::::' + str(j) + '<br><p>')
    yield body.encode('utf-8')

这三种写法不同,但效果是一致的。

看起来很简单,那么怎么调用呢?这两个参数又是怎么提供呢?答案在下边。

WSGI服务器

在上面的架构图里,不知道你发现没有,有个库叫做 wsgiref, 它是 Python 自带的一个 wsgi 服务器模块。

从其名字上就可以看出,它是用纯 Python 编写的WSGI服务器的参考实现。所谓"参考实现"是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。

有了 wsgiref 这个模块,你就可以很快速的启动一个 wsgi server。

# ser.py 同 hello.py 同一层
from wsgiref.simple_server import make_server
from hello import app

# 创建一个服务器,第三个参数是处理函数
# 监听端口及绑定的ip一级请求来时使用的app
httpd = make_server('0.0.0.0', 8000, app)
print('Serving HTTP on port 8000...')
# 开始不断监听HTTP请求
httpd.serve_forever()

当你运行这段代码后,就会开启一个 wsgi server,监听 0.0.0.0:8000,并接收请求。

打开浏览器,键入http://127.0.0.1:8000,就能看到我们app的效果了。

第一次路由:PasteDeploy

安装:
使用PasteDeploy部署,需要安装:

  • pip install PasteDeploy
  • pip install Paste

上面我们提到 WSGI Server 的创建要传入一个 Application,用来处理接收到的请求,对于一个由多个 app 的项目。

比如,你有一个个人网站提供了如下几个模块:

/blog # 博客 app
/wiki # wiki app

如何根据 请求的 url 地址,将请求转发到对应的 application 上呢?

答案是,使用 PasteDeploy 这个库。

PasteDeploy 到底是做什么的呢?

根据 官方文档 的说明,翻译如下:

  • PasteDeploy 是用来寻找和配置WSGI应用和服务的系统。PasteDeploy给开发者提供了一个简单的函数loadapp,通过这个函数,可以从一个配置文件或者Python egg中加载一个WSGI应用。

使用PasteDeploy的其中一个重要意义在于,系统管理员可以安装和管理WSGI应用,而无需掌握与Python与WSGI相关知识。

由于 PasteDeploy 原来是属于 Paste 的,现在独立出来了,但是安装的时候还是会安装到 paste 目录(site-packages\paste\deploy)下。

PasteDeploy 使用说明

考虑到很多人事第一次接触 PasteDeploy,所以这里结合网上博客做了以下总结。对你入门会有帮助。

掌握 PasteDeploy,你只要按照以下三个步骤逐个完成即可:

  1. 配置 PasteDeploy使用的ini文件;
  2. 定义WSGI应用;
  3. 通过loadapp函数加载WSGI应用;
第一步:写 paste.ini 文件

在写之前,咱得知道 ini 文件的格式吧。

首先,像下面这样一个段叫做 section

[type:name]
key = value
...

其上的 type,主要有如下几种:

  1. composite (组合):多个 app 的路由分发;

    [composite:main]
    user = egg:Paste#urlmap
    / = home
    /blog = blog
    /wiki = wiki
    
  2. app (应用):指明 WSGI 应用的路径;

    [app:home]
    paste.app_factory = example:Home.factory
    
  3. pipeline(管道):给一个 app 绑定多个过滤器;将多个filter和最后一个 WSGI 应用串联起来。

    [pipeline:main]
    pipeline = filter1 filter2 filter3 myapp
    
    [filter:filter1]
    ...
    
    [filter:filter2]
    ...
    
    [app:myapp]
    ...
    
  4. filter(过滤器):以 app 作为唯一参数的函数,并返回一个 “过滤” 后的app。通过键值 next 可以指定需要将请求传递给谁。next 指定的可以是一个普通的 WSGI应用,也可以是另一个过滤器。虽然名称上是过滤器,但是功能上不局限于过滤功能,可以是其它功能,例如日志功能,即将认为重要的请求数据记录下来。

    [app-filter:filter_name]
    use = egg:...
    next = next_app
    
    [app:next_app]
    ...
    

对 ini 文件有了一定的了解后,就可以看懂下面这个 ini 配置文件了

[composite:main]
use = egg:Paste#urlmap
/blog = blog
/wiki = wiki

[app:blog]
paste.app_factory = example:Blog.factory

[app:wiki]
paste.app_factory = example:Wiki.factory
第二步是定义一个符合 WSGI 规范的 application 对象。

符合 WSGI 规范的 application 对象,可以有多种形式,函数、方法、类、实例对象,这里仅以实例对象为例(需要实现 __ call __ 方法),做一个演示。

import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
  def __init__(self):
    print('Init Blog.')
  
  def __call__(self, environ, start_response):
    status_code = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    response_body = "This is Blog's response body.".encode('utf-8')
    
    start_response(status_code, response_headers)
    return [response_body]
  
  @classmethod
  def factory(cls, global_conf, **kwargs):
    print('Blog factory.')
   	return Blog()

最后,第三步是使用 loadapp 函数加载 WSGI 应用。

loadapp 是 PasteDeploy 提供的一个函数,使用它可以很方便地从第一步的ini配置文件里加载 app;

loadapp 函数可以接收两个实参:

  1. WSGI 对于 application 对象有如下三点要求;
  2. URI:“config:<配置文件的全路径>”
conf_path = os.path.abspath('paste.ini')

# 加载 app
applications = deploy.loadapp('config:{}'.format(conf_path) 'main')

applications 是 URLMap 对象。

完善并整合第二步和第三步的内容,写成一个 python 文件(wsgi_server.py)。

内容如下:

import os
from paste import deploy
from wsgiref.simple_server import make_server

class Blog(object):
  def __init__(self):
    print('Init Blog.')
    
  def __call__(self, environ, start_response):
    status_code = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    response_body = "This is Blog's response body.".encode('utf-8')
    
    start_response(status_code, response_headers)
    return [response_body]
  
  @classmethod
  def factory(cls, global_conf, **kwargs):
    print('Blog factory.')
    return Blog()
  
class Wiki(object):
  def __init__(self):
    print("Init Wiki.")
  
  def __call__(self, environ, start_response):
    status_code = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    response_body = "This is Wiki's response body.".encode('utf-8')
    
    start_response(status_code, response_headers)
    return [response_body]
  
  @classmethod
  def factory(cls, global_conf, **kwargs):
    print('Wiki factory.')
    return Wiki()
  

if __name__ == "__main__":
  app = "main"
  port = 22800
  conf_path = os.path.abspath('paste.ini')
  
  # 加载 app
  applications = deploy.loadapp('config:{}'.format(conf_path), app)
  server = make_server('localhost', port, applications)
  
  print('Started web server at port {}'.format(port))
  server.serve_forever()

一切都准备好后,在终端执行 python wsgi_server.py 来启动 web server;
在这里插入图片描述
如果像上图一样一切正常,那么打开浏览器

  • 访问http://127.0.0.1:8000/blog,应该显示:This is Blog’s response body.
  • 访问http://127.0.0.1:8000/wiki,应该显示:This is Wiki’s response body.。

注意:urlmap对url的大小写是敏感的,例如如果访问http://127.0.0.1:8000/BLOG,在url映射中未能找到大写的BLOG。

到此,你学会了使用 PasteDeploy 的简单使用。

本文转载自花了两个星期,我终于把 WSGI 整明白了

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python WSGIPython Web服务器网关接口的缩写,它是一种用于连接Web服务器和Python应用程序的规范。WSGI允许开发人员使用统一的接口来开发Web应用程序,无论使用哪种Web服务器。WSGI通过定义一组规则和约定,使得编写Web应用程序变得更加简单和可扩展。 在Python中,可以使用uWSGI作为基于WSGI协议的功能强大的Web服务器,同时也支持Python WSGI协议。uWSGI性能很好,但配置相对复杂。 通常,一个WSGI应用程序由一个可调用的对象组成,该对象接收两个参数,分别是environ和start_response。environ包含与请求相关的环境变量,而start_response是一个用于发送响应头的函数。通过调用start_response函数,应用程序可以发送响应状态码和响应头给Web服务器,然后将返回的数据作为可迭代对象返回给Web服务器。 当Web服务器收到请求时,它将环境变量和回调函数传递给WSGI应用程序,应用程序处理请求并生成响应。然后,Web服务器将响应返回给客户端。这种方式可以使不同的Web服务器与不同的应用程序框架进行交互,实现了服务器和应用程序的解耦。[2, 3] 总结来说,Python WSGI是一种连接Web服务器和Python应用程序的规范,它提供了一种统一的接口来开发Web应用程序。uWSGI是一个基于WSGI协议的强大的Web服务器。通过WSGI,可以实现Web服务器和应用程序的解耦,使得开发和部署Web应用程序变得更加灵活和可扩展。[1, 2, 3]<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Python基础入门教程:WSGI](https://blog.csdn.net/weixin_33798152/article/details/91396901)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Python WSGI的深入理解](https://download.csdn.net/download/weixin_38746515/14869908)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值