Django 源码WSGI剖析过程详解


Posted in Python onAugust 05, 2019

前言

python 作为一种脚本语言, 已经逐渐大量用于 web 后台开发中, 而基于 python 的 web 应用程序框架也越来越多, Bottle, Django, Flask 等等.

在一个 HTTP 请求到达服务器时, 服务器接收并调用 web 应用程序解析请求, 产生响应数据并返回给服务器. 这里涉及了两个方面的东西: 服务器(server)和应用程序(application). 势必要有一个合约要求服务器和应用程序都去遵守, 如此按照此合约开发的无论是服务器还是应用程序都会具有较大的普遍性. 而这就好像在计算机通信的早期, 各大公司都有属于自己的通信协议, 如此只会让市场杂乱无章, 宁愿只要一种通信协议.

而针对 python 的合约是 WSGI(Python Web Server Gateway Interface). 具体的规定见 PEP 333.

实习的时候一直使用 Django, 下面是结合 Django 学习 WSGI 的笔记.

application/应用程序

在应用程序一方面, 必须提供下面的方法:

def simple_app(environ, start_response):
  """可能是最简单的处理了"""
  status = '200 OK'
  response_headers = [('Content-type', 'text/plain')]
  start_response(status, response_headers)
  return ['Hello world!\n'] # 返回结果必须可迭代

除了方法以外, 还可以用实现了 __call__ 的类实现.

它会被服务器调用, 在这里 environ 是一个字典, 包含了环境变量, REQUEST_METHOD,SCRIPT_NAME,QUERY_STRING 等; start_response 是一个回调函数, 会在 simple_app 中被调用, 主要用来开始响应 HTTP. start_response 原型大概是这样:

def start_response(status, response_headers, exc_info=None):
  ...
  return write # 返回这 write 函数 只是为了兼容之前的 web 框架, 新的框架根本用不到.

参数有 status 即状态码; response_headers HTTP 头, 可以修改; exc_info 是与错误相关的信息, 在产生相应数据过程中可能发生错误, 这时需要更新 HTTP 头部, 通过再次调用 start_response 可以实现. 因此更为详尽的实现写法可能是这种:

def start_response(status, response_headers, exc_info=None):
  if exc_info:
     try:
       # do stuff w/exc_info here
     finally:
       exc_info = None  # Avoid circular ref.
  return write

Server/服务器

在服务器方面, 可以想象最简单的工作就是调用 simple_app(), 然后向客户端发送数据:

result = simple_app(environ, start_response) #名字不一定为 simple_app
try:
  for data in result:
    if data:  # don't send headers until body appears
      write(data)
  if not headers_sent:
    write('')  # send headers now if body was empty
finally:
  if hasattr(result, 'close'):
    result.close()

注意 WSGI 并没有事无巨细规定 web 应用程序和服务器内部的工作方式, 只是是规定了它们之间连接的标准.

python wsgiref 模块

下面看看 Django 是如何实现 WSGI 的. Django 其内部已经自带了一个方便本地测试的小服务器, 所以在刚开始学习 Django 的时候并不需搭建 apache 或者 nginx 服务器. Django 自带的服务器基于 python wsgiref 模块实现, 它自带的测试代码:

# demo_app() 是 application
def demo_app(environ,start_response):
  from StringIO import StringIO
  stdout = StringIO()
  print >>stdout, "Hello world!"
  print >>stdout
  h = environ.items(); h.sort()
  for k,v in h:
    print >>stdout, k,'=', repr(v)
  start_response("200 OK", [('Content-Type','text/plain')])
  return [stdout.getvalue()]

def make_server(
  host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler
):
  """Create a new WSGI server listening on `host` and `port` for `app`"""
  server = server_class((host, port), handler_class)
  server.set_app(app)
  return server

if __name__ == '__main__':
  httpd = make_server('', 8000, demo_app)
  sa = httpd.socket.getsockname()
  print "Serving HTTP on", sa[0], "port", sa[1], "..."
  import webbrowser
  webbrowser.open('http://localhost:8000/xyz?abc')
  httpd.handle_request() # serve one request, then exit

python 的库有好多的工具, 这时可能因为需要的原因, 会生出好多的父类, 为了讲明, 根据 wsgiref 模块和它自带的测试用例得出下面的 UML 图(注意, 这只是 wsgiref, 没有涉及 Django):

Django 源码WSGI剖析过程详解

我读完这些的时候已经晕了, 确实是里边的继承关系有些复杂. 因此, 简要的概括了测试代码的执行关系:

  • make_server() 中 WSGIServer 类已经作为服务器类, 负责接收请求, 调用 application 的处理, 返回相应;
  • WSGIRequestHandler 作为请求处理类, 并已经配置在 WSGIServer 中;
  • 接着还设置了 WSGIServer.application 属性(set_app(app));
  • 返回 server 实例.
  • 接着打开浏览器, 即发起请求. 服务器实例 WSGIServer httpd 调用自身 handle_request() 函数处理请求. handle_request() 的工作流程如下:请求-->WSGIServer 收到-->调用 WSGIServer.handle_request()-->调用 _handle_request_noblock()-->调用 process_request()-->调用 finish_request()-->finish_request() 中实例化 WSGIRequestHandler-->实例化过程中会调用 handle()-->handle() 中实例化 ServerHandler-->调用 ServerHandler.run()-->run() 调用 application() 这才是真正的逻辑.-->run() 中在调用 ServerHandler.finish_response() 返回数据-->回到 process_request() 中调用 WSGIServer.shutdown_request() 关闭请求(其实什么也没做)

ps: 明明 application 是 WSGIServer 的属性, 为什么会在 ServerHandler 中调用? 因为在实例化 WSGIRequestHandler 的时候 WSGIServer 把自己搭进去了, 所以在 WSGIRequestHandler 中实例化 ServerHandler 时候可以通过 WSGIRequestHandler.server.get_app() 得到真正的 application.

总结

从上面可以得到, 启动服务器的时候, 无论以什么方式都要给它传递一个 application(), 是一个函数也好, 一个实现了 __call__ 的类也好; 当请求到达服务器的时候, 服务器自会调用 application(), 从而得到相应数据. 至于, 对请求的数据如何相应, application() 中可以细化.

确实, 其中的调用链太过长, 这期间还没有加入 HTTP 头的分析(提取 Cookie等). 如果只为响应一个 "helloworld", 在 WSGIServer.finish_request() 中直接相应数据就好了, WSGIRequestHandler 和 ServerHandler 类可以直接省去, 而只需要你提供一个 application()! 但事实上, 并不只是相应 "helloworld" 那样简单...

关于 Django 中的 WSGI 如何, 下一节再说. Django 源码剖析从这里开始! 我已经在 github 备份了 Django 源码的注释: Decode-Django, 有兴趣的童鞋 fork 吧. 本文结合 python wsgiref, BaseHTTPServer.py, SocketServer.py 模块源码看更好.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
举例区分Python中的浅复制与深复制
Jul 02 Python
python自定义异常实例详解
Jul 11 Python
Python编程pygal绘图实例之XY线
Dec 09 Python
python使用ddt过程中遇到的问题及解决方案【推荐】
Oct 29 Python
使用pycharm设置控制台不换行的操作方法
Jan 19 Python
Python 正则表达式 re.match/re.search/re.sub的使用解析
Jul 22 Python
Python 异常处理Ⅳ过程图解
Oct 18 Python
Python延迟绑定问题原理及解决方案
Aug 04 Python
python进度条显示之tqmd模块
Aug 22 Python
python 下载m3u8视频的示例代码
Nov 11 Python
Python-OpenCV实现图像缺陷检测的实例
Jun 11 Python
详解Python+OpenCV进行基础的图像操作
Feb 15 Python
Python使用itchat 功能分析微信好友性别和位置
Aug 05 #Python
Python队列RabbitMQ 使用方法实例记录
Aug 05 #Python
Python 通过微信控制实现app定位发送到个人服务器再转发微信服务器接收位置信息
Aug 05 #Python
基于python框架Scrapy爬取自己的博客内容过程详解
Aug 05 #Python
基于python实现的百度音乐下载器python pyqt改进版(附代码)
Aug 05 #Python
使用coverage统计python web项目代码覆盖率的方法详解
Aug 05 #Python
基于python实现的百度新歌榜、热歌榜下载器(附代码)
Aug 05 #Python
You might like
利用PHP制作简单的内容采集器的原理分析
2008/10/01 PHP
高质量PHP代码的50个实用技巧必备(下)
2016/01/22 PHP
JS类定义原型方法的两种实现的区别评论很多
2007/09/12 Javascript
javascript cookies 设置、读取、删除实例代码
2010/04/12 Javascript
js控制CSS样式属性语法对照表
2012/12/11 Javascript
javascript游戏开发之《三国志曹操传》零部件开发(五)可移动地图的实现
2013/01/23 Javascript
javascript阻止浏览器后退事件防止误操作清空表单
2013/11/22 Javascript
jquery修改网页背景颜色通过css方法实现
2014/06/06 Javascript
程序员必知35个jQuery 代码片段
2015/11/05 Javascript
JS实现字符串转日期并比较大小实例分析
2015/12/09 Javascript
ES6的新特性概览
2016/03/10 Javascript
简单实现js间歇或无缝滚动效果
2016/06/29 Javascript
node+experss实现爬取电影天堂爬虫
2016/11/20 Javascript
基于AngularJS实现的工资计算器实例
2017/06/16 Javascript
让nodeJS支持ES6的词法----babel的安装和使用方法
2017/07/31 NodeJs
JavaScript输入分钟、秒倒计时技巧总结(附代码)
2017/08/17 Javascript
jquery手机触屏滑动拼音字母城市选择器的实例代码
2017/12/11 jQuery
js将键值对字符串转为json字符串的方法
2018/03/30 Javascript
修改node.js默认的npm安装目录实例
2018/05/15 Javascript
如何使用 vue + d3 画一棵树
2018/12/03 Javascript
微信小程序开发的基本流程步骤
2019/01/31 Javascript
js删除指定位置超链接中含有百度与360的标题
2021/01/06 Javascript
[48:48]VGJ.T vs Liquid 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
Python爬虫实现简单的爬取有道翻译功能示例
2018/07/13 Python
详解如何在Apache中运行Python WSGI应用
2019/01/02 Python
我用Python抓取了7000 多本电子书案例详解
2019/03/25 Python
TensorFLow 变量命名空间实例
2020/02/11 Python
python GUI库图形界面开发之PyQt5图片显示控件QPixmap详细使用方法与实例
2020/02/27 Python
使用Pyhton 分析酒店针孔摄像头
2020/03/04 Python
Python classmethod装饰器原理及用法解析
2020/10/17 Python
CSS3实现多背景模拟动态边框的效果
2016/11/08 HTML / CSS
办公自动化毕业生求职信
2014/03/09 职场文书
感情真挚的毕业生求职信
2014/07/19 职场文书
机械工程及其自动化专业求职信
2014/08/08 职场文书
离婚律师函范本
2015/05/27 职场文书
redis客户端实现高可用读写分离的方式详解
2021/07/04 Redis