一 Web应用的组成
接下来我们学习的目的是为了开发一个Web应用程序,而Web应用程序是基于B/S架构的,其中B指的是浏览器,负责向S端发送请求信息,而S端会根据接收到的请求信息返回相应的数据给浏览器,需要强调的一点是:S端由server和application两大部分构成,如图所示:
上图:Web应用组成
二 开发一个Web应用
我们无需开发浏览器(本质即套接字客户端),只需要开发S端即可,S端的本质就是用套接字实现的,如下
# S端
import socket
def make_server(ip, port, app): # 代表server
sock = socket.socket()
sock.bind((ip, port))
sock.listen(5)
print('Starting development server at http://%s:%s/' %(ip,port))
while True:
conn, addr = sock.accept()
# 1、接收浏览器发来的请求信息
recv_data = conn.recv(1024)
# print(recv_data.decode('utf-8'))
# 2、将请求信息直接转交给application
res = app(recv_data)
# 3、向浏览器返回消息(此处并没有按照http协议返回)
conn.send(res)
conn.close()
def app(environ): # 代表application
# 处理业务逻辑
return b'hello world'
if __name__ == '__main__':
make_server('127.0.0.1', 8008, app) # 在客户端浏览器输入:http://127.0.0.1:8008 会报错(注意:请使用谷歌浏览器)
目前S端已经可以正常接收浏览器发来的请求消息了,但是浏览器在接收到S端回复的响应消息b’hello world’时却无法正常解析 ,因为浏览器与S端之间收发消息默认使用的应用层协议是HTTP,浏览器默认会按照HTTP协议规定的格式发消息,而S端也必须按照HTTP协议的格式回消息才行,所以接下来我们详细介绍HTTP协议
S端修订版本:处理HTTP协议的请求消息,并按照HTTP协议的格式回复消息
# S端
import socket
def make_server(ip, port, app): # 代表server
sock = socket.socket()
sock.bind((ip, port))
sock.listen(5)
print('Starting development server at http://%s:%s/' %(ip,port))
while True:
conn, addr = sock.accept()
# 1、接收并处理浏览器发来的请求信息
# 1.1 接收浏览器发来的http协议的消息
recv_data = conn.recv(1024)
# 1.2 对http协议的消息加以处理,简单示范如下
ll=recv_data.decode('utf-8').split('\r\n')
head_ll=ll[0].split(' ')
environ={}
environ['PATH_INFO']=head_ll[1]
environ['method']=head_ll[0]
# 2:将请求信息处理后的结果environ交给application,这样application便无需再关注请求信息的处理,可以更加专注于业务逻辑的处理
res = app(environ)
# 3:按照http协议向浏览器返回消息
# 3.1 返回响应首行
conn.send(b'HTTP/1.1 200 OK\r\n')
# 3.2 返回响应头(可以省略)
conn.send(b'Content-Type: text/html\r\n\r\n')
# 3.3 返回响应体
conn.send(res)
conn.close()
def app(environ): # 代表application
# 处理业务逻辑
return b'hello world'
if __name__ == '__main__':
make_server('127.0.0.1', 8008, app)
此时,重启S端后,再在客户端浏览器输入:http://127.0.0.1:8008 便可以看到正常结果hello world了。
我们不仅可以回复hello world这样的普通字符,还可以夹杂html标签,浏览器在接收到消息后会对解析出的html标签加以渲染
# S端
import socket
def make_server(ip, port, app):
sock = socket.socket()
sock.bind((ip, port))
sock.listen(5)
print('Starting development server at http://%s:%s/' %(ip,port))
while True:
conn, addr = sock.accept()
recv_data = conn.recv(1024)
ll=recv_data.decode('utf-8').split('\r\n')
head_ll=ll[0].split(' ')
environ={}
environ['PATH_INFO']=head_ll[1]
environ['method']=head_ll[0]
res = app(environ)
conn.send(b'HTTP/1.1 200 OK\r\n')
conn.send(b'Content-Type: text/html\r\n\r\n')
conn.send(res)
conn.close()
def app(environ):
# 返回html标签
return b'<h1>hello web</h1><img src="https://www.baidu.com/img/bd_logo1.png"></img>'
if __name__ == '__main__':
make_server('127.0.0.1', 8008, app)
更进一步我们还可以返回一个文件,例如timer.html,内容如下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h2>{{ time }}</h2>
</body>
</html>
S端程序如下
# S端
import socket
def make_server(ip, port, app): # 代表server
sock = socket.socket()
sock.bind((ip, port))
sock.listen(5)
print('Starting development server at http://%s:%s/' %(ip,port))
while True:
conn, addr = sock.accept()
recv_data = conn.recv(1024)
ll=recv_data.decode('utf-8').split('\r\n')
head_ll=ll[0].split(' ')
environ={}
environ['PATH_INFO']=head_ll[1]
environ['method']=head_ll[0]
res = app(environ)
conn.send(b'HTTP/1.1 200 OK\r\n')
conn.send(b'Content-Type: text/html\r\n\r\n')
conn.send(res)
conn.close()
def app(environ):
# 处理业务逻辑:打开文件,读取文件内容并返回
with open('timer.html', 'r', encoding='utf-8') as f:
data = f.read()
return data.encode('utf-8')
if __name__ == '__main__':
make_server('127.0.0.1', 8008, app)
上述S端为浏览器返回的都是静态页面(内容都固定的),我们还可以返回动态页面(内容是变化的)
# S端
import socket
def make_server(ip, port, app): # 代表server
sock = socket.socket()
sock.bind((ip, port))
sock.listen(5)
print('Starting development server at http://%s:%s/' %(ip,port))
while True:
conn, addr = sock.accept()
recv_data = conn.recv(1024)
ll=recv_data.decode('utf-8').split('\r\n')
head_ll=ll[0].split(' ')
environ={}
environ['PATH_INFO']=head_ll[1]
environ['method']=head_ll[0]
res = app(environ)
conn.send(b'HTTP/1.1 200 OK\r\n')
conn.send(b'Content-Type: text/html\r\n\r\n')
conn.send(res)
conn.close()
def app(environ):
# 处理业务逻辑
with open('timer.html', 'r', encoding='utf-8') as f:
data = f.read()
import time
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
data = data.replace('{{ time }}', now) # 字符串替换
return data.encode('utf-8')
if __name__ == '__main__':
make_server('127.0.0.1', 8008, app) # 在浏览器输入http://127.0.0.1:8008,每次刷新都会看到不同的时间
三 Web框架的由来
综上案例我们可以发现一个规律,在开发S端时,server的功能是复杂且固定的(处理socket消息的收发和http协议的处理),而app中的业务逻辑却各不相同(不同的软件就应该有不同的业务逻辑),重复开发复杂且固定的server是毫无意义的,有一个wsgiref模块帮我们写好了server的功能,这样我们便只需要专注于app功能的编写即可
# wsgiref实现了server,即make_server
from wsgiref.simple_server import make_server
def app(environ, start_response): # 代表application
# 1、返回http协议的响应首行和响应头信息
start_response('200 OK', [('Content-Type', 'text/html')])
# 2、处理业务逻辑:根据请求url的不同返回不同的页面内容
if environ.get('PATH_INFO') == '/index':
with open('index.html','r', encoding='utf-8') as f:
data=f.read()
elif environ.get('PATH_INFO') == '/timer':
with open('timer.html', 'r', encoding='utf-8') as f:
data = f.read()
import time
now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
data = data.replace('{{ time }}', now) # 字符串替换
else:
data='<h1>Hello, web!</h1>'
# 3、返回http响应体信息,必须是bytes类型,必须放在列表中
return [data.encode('utf-8')]
if __name__ == '__main__':
# 当接收到请求时,wsgiref模块会对该请求加以处理,然后后调用app函数,自动传入两个参数:
# 1 environ是一个字典,存放了http的请求信息
# 2 start_response是一个功能,用于返回http协议的响应首行和响应头信息
s = make_server('', 8011, app) # 代表server
print('监听8011')
s.serve_forever() # 在浏览器输入http://127.0.0.1:8011/index和http://127.0.0.1:8011/timer会看到不同的页面内容
timer.html已经存在了,新增的index.html页面内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>主页</h1>
</body>
</html>
上述案例中app在处理业务逻辑时需要根据不同的url地址返回不同的页面内容,当url地址越来越多,需要写一堆if判断,代码不够清晰,耦合程度高,所以我们做出以下优化
# 处理业务逻辑的函数
def index(environ):
with open('index.html', 'r', encoding='utf-8') as f:
data = f.read()
return data.encode('utf-8')
def timer(environ):
import datetime
now = datetime.datetime.now().strftime('%y-%m-%d %X')
with open('timer.html', 'r', encoding='utf-8') as f:
data = f.read()
data = data.replace('{{ time }}', now)
return data.encode('utf-8')
# 路径跟函数的映射关系
url_patterns = [
('/index', index),
('/timer', timer),
]
from wsgiref.simple_server import make_server
def app(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
# 拿到请求的url并根据映射关系url_patters执行相应的函数
reuqest_url = environ.get('PATH_INFO')
for url in url_patterns:
if url[0] == reuqest_url:
data = url[1](environ)
break
else:
data = b'404'
return [data]
if __name__ == '__main__':
s = make_server('', 8011, app)
print('监听8011')
s.serve_forever()
随着业务逻辑复杂度的增加,处理业务逻辑的函数以及url_patterns中的映射关系都会不断地增多,此时仍然把所有代码都放到一个文件中,程序的可读性和可扩展性都会变得非常差,所以我们应该将现有的代码拆分到不同文件中
mysite # 文件夹
├── app01 # 文件夹
│ └── views.py
├── mysite # 文件夹
│ └── urls.py
└── templates # 文件夹
│ ├── index.html
│ └── timer.html
├── main.py
views.py 内容如下:
# 处理业务逻辑的函数
def index(environ):
with open('templates/index.html', 'r',encoding='utf-8') as f: # 注意文件路径
data = f.read()
return data.encode('utf-8')
def timer(environ):
import datetime
now = datetime.datetime.now().strftime('%y-%m-%d %X')
with open('templates/timer.html', 'r',encoding='utf-8') as f: # 注意文件路径
data = f.read()
data=data.replace('{{ time }}',now)
return data.encode('utf-8')
urls.py内容如下:
# 路径跟函数的映射关系
from app01.views import * # 需要导入views中的函数
url_patterns = [
('/index', index),
('/timer', timer),
]
main.py 内容如下:
Copyfrom wsgiref.simple_server import make_server
from mysite.urls import url_patterns # 需要导入urls中的url_patterns
def app(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
# 拿到请求的url并根据映射关系url_patters执行相应的函数
reuqest_url = environ.get('PATH_INFO')
for url in url_patterns:
if url[0] == reuqest_url:
data = url[1](environ)
break
else:
data = b'404'
return [data]
if __name__ == '__main__':
s = make_server('', 8011, app)
print('监听8011')
s.serve_forever()
至此,我们就针对application的开发自定义了一个框架,所以说框架的本质就是一系列功能的集合体、不同的功能放到不同的文件中。有了该框架,可以让我们专注于业务逻辑的编写,极大的提高了开发web应用的效率(开发web应用的框架可以简称为web框架),比如我们新增一个业务逻辑,要求为:浏览器输入http://127.0.0.1:8011/home 就能访问到home.html页面,在框架的基础上具体开发步骤如下:
步骤一:在templates文件夹下新增home.html
步骤二:在urls.py的url_patterns中新增一条映射关系
Copyurl_patterns = [
('/index', index),
('/timer', timer),
('/home', home), # 新增的映射关系
]
步骤三:在views.py中新增一个名为home的函数
Copydef home(environ):
with open('templates/home.html', 'r',encoding='utf-8') as f:
data = f.read()
return data.encode('utf-8')
我们自定义的框架功能有限,在Python中我们可以使用别人开发的、功能更强大的Django框架
四 Django框架的安装与使用
在使用Django框架开发web应用程序时,开发阶段同样依赖wsgiref模块来实现Server的功能,我们使用Django框架是为了快速地开发application
4.1 安装
目前在企业开发中Django框架使用的主流版本为1.11.x版本,最新版本为2.x,我们主要讲解1.11版本,同时会涉及2.x的新特性
Copypip3 install django==1.11.18 # 在命令行执行该命令
4.2 使用
4.2.1 快速创建并启动Django项目
如果使用的是我们自定义的框架来开发web应用,需要事先生成框架包含的一系列基础文件,然后在此基础上进行开发。
如果使用的是Django框架来开发web应用,同样需要事先生成Django框架包含的一系列基础文件,然后在此基础上进行开发。
但Django框架更为方便的地方在于它已经为我们提供了一系列命令来帮我们快速地生成这一系列基础文件
Copy# 在命令行执行以下指令,会在当前目录生成一个名为mysite的文件夹,该文件夹中包含Django框架的一系列基础文件
django-admin startproject mysite
创建功能模块
Copycd mysite # 切换到mysite目录下,执行以下命令
python manage.py startapp app01 # 创建功能模块app01,此处的startapp代表创建application下的一个功能模块。例如我们要开发application是京东商城,京东商城这个大项目下有一个订单管理模块,我们可以将其命名为app01
运行
Copypython manage.py runserver 8001 # 在浏览器输入:http://127.0.0.1:8001 会看到Django的欢迎页面。
4.2.2 Django项目目录结构
截目录树的图(按照下述目录截图)
Copymysite # 文件夹
├── app01 # 文件夹
│ └── migrations # 文件夹
│ └── admin.py
│ └── apps.py
│ └── models.py
│ └── tests.py
│ └── views.py
├── mysite # 文件夹
│ └── settings.py
│ └── urls.py
│ └── wsgi.py
└── templates # 文件夹
├── manage.py
关键文件介绍
Copy-manage.py---项目入口,执行一些命令
-项目名
-settings.py 全局配置信息
-urls.py 总路由,请求地址跟视图函数的映射关系
-app名字
-migrations 数据库迁移的记录
-models.py 数据库表模型
-views.py 处理业务逻辑的函数,简称视图函数
4.2.3 基于Pycharm创建Django项目
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A20tcI9z-1606399294023)(C:\Users\lekaiyu\AppData\Roaming\Typora\typora-user-images\1606360310435.png)]
4.2.4 基于Django实现的一个简单示例
(1)url.py
Copyfrom django.contrib import admin
from django.conf.urls import url
#导入views模块
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
# r'^index/$' 会正则匹配url地址的路径部分
url(r'^index/$',views.index), # 新增地址http://127.0.0.1:8001/index/与index函数的映射关系
]
(2)视图
Copyfrom django.shortcuts import render
# 必须定义一个request形参,request相当于我们自定义框架时的environ参数
def index(request):
import datetime
now=datetime.datetime.now()
ctime=now.strftime("%Y-%m-%d %X")
return render(request,"index.html",{"ctime":ctime}) # render会读取templates目录下的index.html文件的内容并且用字典中的ctime的值替换模版中的{{ ctime }}
(3)模版
在templates目录下新建文件index.html
Copy<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h4>当前时间:{{ ctime }}</h4>
</body>
</html>
测试:
Copypython manage.py runserver 8001 # 在浏览器输入:http://127.0.0.1:8001/index/ 会看到当前时间。
4.2.5 Django框架的分层与请求生命周期
综上,我们使用Django框架就是为了开发application,而application的工作过程本质就是根据不同的请求返回不同的数据,Django框架将这个工作过程细分为如下四层去实现
1、路由层(根据不同的地址执行不同的视图函数,详见urls.py)
2、视图层(定义处理业务逻辑的视图函数,详见views.py)
3、模型层 (跟数据库打交道的,详解models.py)
4、模板层(待返回给浏览器的html文件,详见templates)
django请求生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JTQiPa1b-1606399294026)(C:\Users\lekaiyu\AppData\Roaming\Typora\typora-user-images\1606360386932.png)]
django是如何通过网络socket层接收数据并将请求转发给django的urls层
就是通过wsgi(Web Server Gateway Interface)
Django框架完全遵循wsgi协议,底层采用socket、socketserver、select网络模型实现,可以利用操作系统的非堵塞和线程池等特性
Django本身是用python代码实现的wsgi服务,并发非常低,默认6个
而线上部署django项目时一般采用C语言实现的uWSGI
看源码,找到程序的入口是第一步,很简单,我们怎么启动django来着
python3.6 manage.py runserver 8088
# ps:
# python解释器版本:3.6
# django版本:2.2.7
好了,就它manage.py,我们来看看它里面都干了些啥(读源码不必面面俱到,找到关键代码即可)
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Foo.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
执行了 django.core.management下的execute_from_command_line ,关键代码是execute_from_command_line,看它就好
但在这之前需要提一嘴django.core.management是一个包,在导入时会执行其下的__init__.py,这里面不仅有我们将要看的execute_from_command_line,其实还做了很多其他事
#在导入from django.apps import apps时会运行django.apps.__init__.py文件,这是整个django程序的开端,它其实做了非常多的事情(例如:初始化日志模块、加载INSTALL_APP、检查各APP是否正常、检查缓存模块是否正常等),当一切无误时才会往下走,否则将会报错退出程序。
我们来看一下django.core.management.init.py中的execute_from_command_line
def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv) # 调用当前文件中的类ManagementUtility产生对象,这个类就在该函数的上方,一找就能找到
utility.execute() # 调用类ManagementUtility中的方法execute
关键代码utility.execute() ,去类ManagementUtility中可以找到,该方法特别长,就不列举了,一连串if条件就是判断参数是否合法
关键代码在self.fetch_command(subcommand).run_from_argv(self.argv)
,链式调用,我们一点一点来看
先看fetch_command(subcommand),即fetch_command(‘runserver’),就在类ManagementUtility中往上翻可以找到该方法,关键代码在注释里我都标注好了
def fetch_command(self, subcommand):
"""self.fetch_command
是利用django内置的命令管理工具去匹配到具体的模块,例如self.fetch_command(subcommand)其实就相当于是self.fetch_command('runserver'),它最终找到了==django.contrib.staticfiles.management.commands.runserver.Command==这个命令工具。
django中的命令工具代码组织采用的是策略模式+接口模式,也就是说django.core.management.commands这个目录下面存在各种命令工具,每个工具下面都有一个Command接口,当匹配到'runserver'时调用'runserver'命令工具的Command接口,当匹配到'migrate'时调用'migrate'命令工具的Command接口。
"""
commands = get_commands() # 关键代码1
try:
app_name = commands[subcommand] # 关键代码2
except KeyError:
if os.environ.get('DJANGO_SETTINGS_MODULE'):
settings.INSTALLED_APPS
else:
sys.stderr.write("No Django settings specified.\n")
possible_matches = get_close_matches(subcommand, commands)
sys.stderr.write('Unknown command: %r' % subcommand)
if possible_matches:
sys.stderr.write('. Did you mean %s?' % possible_matches[0])
sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name)
sys.exit(1)
if isinstance(app_name, BaseCommand):
klass = app_name
else:
klass = load_command_class(app_name, subcommand) # 关键代码3
return klass # 关键代码4
关键代码1:get_comands()会返回一个字典
关键代码2:app_name = commands[subcommand]取值操作即app_name=‘django.core’
关键代码3:klass = load_command_class(app_name, subcommand)即klass = load_command_class(‘django.core’ ,’runserver‘),自己去看很简单,klass=django.core.management.commands.runserver.Command类
好啦,此时我们得知self.fetch_command(subcommand)得到的是类Command,好多人就在这懵逼了,接下来链式调用应该去找run_from_argv(self.argv)了,但是在Command类中怎么也找不到,傻逼了吧,去Command的父类BaseCommand
里找啊,傻叉(这就是好多人看源码的心态,看着看着就崩了,崩几次就疯了)
class BaseCommand:
def run_from_argv(self, argv):
"""
run_from_argv的作用是初始化中间件、启动服务,也就是拉起wgsi(但实际上并不是由它来直接完成,而是由后续很多其他代码来完成),直观上看它应该是runserver.Command对象的一个方法,但实际上要稍微更复杂一些,因为没有列出关联代码,所以在下一个代码块中进行说明。
"""
self._called_from_command_line = True
parser = self.create_parser(argv[0], argv[1])
options = parser.parse_args(argv[2:])
cmd_options = vars(options)
args = cmd_options.pop('args', ())
handle_default_options(options)
try:
self.execute(*args, **cmd_options) # 关键代码
except Exception as e:
if options.traceback or not isinstance(e, CommandError):
raise
if isinstance(e, SystemCheckError):
self.stderr.write(str(e), lambda x: x)
else:
self.stderr.write('%s: %s' % (e.__class__.__name__, e))
sys.exit(1)
finally:
try:
connections.close_all()
except ImproperlyConfigured:
pass
关键代码self.execute(*args, **cmd_options),注意了,这个execute应该去Command类里找啊,因为该self是Command类的对象啊,让我们回到Command类中,找execute
class Command(BaseCommand):
。。。。。。。
def execute(self, *args, **options):
if options['no_color']:
super().execute(*args, **options) # 关键代码1
def get_handler(self, *args, **options):
"""Return the default WSGI handler for the runner."""
return get_internal_wsgi_application()
def handle(self, *args, **options):
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')
self.use_ipv6 = options['use_ipv6']
if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.')
self._raw_ipv6 = False
if not options['addrport']:
self.addr = ''
self.port = self.default_port
else:
m = re.match(naiveip_re, options['addrport'])
if m is None:
raise CommandError('"%s" is not a valid port number '
'or address:port pair.' % options['addrport'])
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
if not self.port.isdigit():
raise CommandError("%r is not a valid port number." % self.port)
if self.addr:
if _ipv6:
self.addr = self.addr[1:-1]
self.use_ipv6 = True
self._raw_ipv6 = True
elif self.use_ipv6 and not _fqdn:
raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
if not self.addr:
self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
self._raw_ipv6 = self.use_ipv6
self.run(**options) # 关键代码2
def run(self, **options):
use_reloader = options['use_reloader']
if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options) # 关键代码3
def inner_run(self, *args, **options):
autoreload.raise_last_exception()
threading = options['use_threading']
shutdown_message = options.get('shutdown_message', '')
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
self.stdout.write("Performing system checks...\n\n")
self.check(display_num_errors=True)
self.check_migrations()
now = datetime.now().strftime('%B %d, %Y - %X')
self.stdout.write(now)
self.stdout.write((
"Django version %(version)s, using settings %(settings)r\n"
"Starting development server at %(protocol)s://%(addr)s:%(port)s/\n"
"Quit the server with %(quit_command)s.\n"
) % {
"version": self.get_version(),
"settings": settings.SETTINGS_MODULE,
"protocol": self.protocol,
"addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
"port": self.port,
"quit_command": quit_command,
})
try:
handler = self.get_handler(*args, **options)
run(self.addr, int(self.port), handler,
ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls) # 关键代码4
except socket.error as e:
ERRORS = {
errno.EACCES: "You don't have permission to access that port.",
errno.EADDRINUSE: "That port is already in use.",
errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
}
try:
error_text = ERRORS[e.errno]
except KeyError:
error_text = e
self.stderr.write("Error: %s" % error_text)
os._exit(1)
except KeyboardInterrupt:
if shutdown_message:
self.stdout.write(shutdown_message)
sys.exit(0)
关键代码1:super().execute(*args, **options)会去父类BaseCommand中找到excute方法,该方法中的关键代码为output = self.handle(*args, **options),该self是Command类的对象,所以接着去Command类中找到handle方法
关键代码2->关键代码3->关键代码4->定位到一个run方法,该方法就在本文件开头位置导入过
from django.core.servers.basehttp import (
WSGIServer, get_internal_wsgi_application, run,
)
截止到该部分,实际上就是一个初始化过程,全部都为’runserver’服务,虽然很多代码我没有列出来,但是它确实做了一些,例如参数解析、端口指定检测、ipv4检测、ipv6检测、端口是否占用、线程检查等工作。
接下来我把注意力放在django.core.servers.basehttp下的run函数上,代码如下
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer): # 形参wsgi_handler的值为StaticFilesHandler
"""知会各个对象启动wsgi服务"""
server_address = (addr, port)
if threading:
httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {}) # 关键代码1
else:
httpd_cls = server_cls
httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) # 关键代码2
if threading:
httpd.daemon_threads = True
httpd.set_app(wsgi_handler) # 关键代码3
httpd.serve_forever() # 关键代码4
关键代码1:调用内置元类type创建一个类WSGIServer,该类继承了(socketserver.ThreadingMixIn, WSGIServer),去代码块WSGIServer类中查看它本身只继承了wsgiref.simple_server.WSGIServer、object这两个类,通过type重新创建一下是给类WSGIServer强行添加了一个爹socketserver.ThreadingMixIn,这么做的意义是每次调用类WSGIServer的时候都会单独启用一个线程来处理,说完了WSGIServer的第一个基类,我们再来说它的第二个基类WSGIServer完整的继承家族
django.core.servers.basehttp.WSGIServer
wsgiref.simple_server.WSGIServer、 socketserver.ThreadingMixIn
http.server.HTTPServer
socketserver.TCPServer
socketserver.BaseServer
object
httpd_cls
这个变量被定义完成之后,由于大量的继承关系,它其实已经不单纯的属于django,它是一个传统意义上的WSGI服务对象了。
关键代码2:httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
这行代码非常重要,因为它是WSGI服务器与django之间相互通信的唯一枢纽通道,也就是说,当WSGI服务对象收到socket请求后,会将这个请求传递给django的WSGIRequestHandler(下节会列出WSGIRequestHandler是如何工作的)。
关键代码3:httpd.set_app(wsgi_handler)
是将django.contrib.staticfiles.handlers.StaticFilesHandler
传递给WSGIServer当作一个application,当WSGIServer收到网络请求后,可以将数据分发给django.core.servers.basehttp.WSGIRequestHandler
,最终由django.core.servers.basehttp.WSGIRequestHandler
将数据传递给application(即:django.contrib.staticfiles.handlers.StaticFilesHandler
)。
关键代码4:httpd.serve.forever()
启动非堵塞网络监听服务。
总结:综上所述其实都是在为启动django服务而做准备,大致内容如下
#1、解析运行 python manage.py 所提供的参数,例如: runserver.
#2、根据参数 找到相对应的 命令管理工具。
#3、加载所有的app。
#4、检查端口、ipv4检测、ipv6检测、端口是否占用、线程检查、orm对象检查(表是否创建)。
#5、实例化WSGIRequestHandler,并且将它注册到python Lib库中的WSGIServer中。
#6、最后启动python Lib库中的WSGIServer
三 httpd.serve.forever()后续事宜
承接上一小节httpd.serve_forever我们接着聊
,httpd.serve_forever调用的是socketserver.BaseServer.serve_forever
方法(关于socketserver的源码解析点击这里,下面我直接说流程,原理不再累述)。
#1、socketserver.BaseServer.serve_forever方法采用了selector网络模型进行等待数据,每0.5秒遍历一次文件描述符,当有数据进来时,ready变量会是一个socket请求对象,这时会将后续工作转交给self._handler_request_noblock方法(即:socketserver.BaseServer._handler_request_noblock)去处理。
#2、socketserver.BaseServer._handler_request_noblock方法基本没做什么事情(self.verify_request压根就没有检查任何东西),直接就把后续工作转交给 socketserver.BaseServer.process_request 方法。
#3、socketserver.BaseServer.process_request也没做什么事情,直接就将后续工作转交给socketserver.BaseServer.finish_request方法,只不过在最后加了一条关闭请求的命令。
#4、socketserver.BaseServer.finish_request也没做什么事情,直接就将后续工作转交给socketserver.BaseServer.RequestHandlerClass。
#5、socketserver.BaseServer.RequestHandlerClass是由上一节httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)传递过来的参数django.core.servers.basehttp.WSGIRequestHandler。 也就是说当执行self.RequestHandler(request, client_address, self)时等同于执行django.core.servers.basehttp.WSGIRequestHandler(request, client_address, self)。
serve_forever就是开启了一个while来无限监听网络层的scoket请求,当一条请求过来时,就层层转交到django.core.servers.basehttp.WSGIRequestHandler代码如下
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
protocol_version = 'HTTP/1.1'
def address_string(self):
return self.client_address[0]
def log_message(self, format, *args):
extra = {
'request': self.request,
'server_time': self.log_date_time_string(),
}
if args[1][0] == '4':
if args[0].startswith('\x16\x03'):
extra['status_code'] = 500
logger.error(
"You're accessing the development server over HTTPS, but "
"it only supports HTTP.\n", extra=extra,
)
return
if args[1].isdigit() and len(args[1]) == 3:
status_code = int(args[1])
extra['status_code'] = status_code
if status_code >= 500:
level = logger.error
elif status_code >= 400:
level = logger.warning
else:
level = logger.info
else:
level = logger.info
level(format, *args, extra=extra)
def get_environ(self):
for k in self.headers:
if '_' in k:
del self.headers[k]
return super().get_environ()
def handle(self): # 关键代码
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
try:
self.connection.shutdown(socket.SHUT_WR)
except (socket.error, AttributeError):
pass
def handle_one_request(self):
"""Copy of WSGIRequestHandler.handle() but with different ServerHandler"""
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
if not self.parse_request():
return
handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
handler.request_handler = self
handler.run(self.server.get_app())
关键代码:方法handle,至于如何调用到它,需要从WSGIRequestHandler的实例化说起,上面我们提到当执行self.RequestHandler(request, client_address, self)
时等同于执行django.core.servers.basehttp.WSGIRequestHandler(request, client_address, self),而WSGIRequestHandler的继承的父类们如下
#1、django.core.servers.basehttp.WSGIRequestHandler
#2、wsgiref.simple_server.WSGIRequestHandler
#3、http.server.BaseHTTPRequestHandler
#4、socketserver.StreamRequestHandler
#5、socketserver.BaseRequestHandler
#6、object
实例化类WSGIRequestHandler时发现它并没有__init__和__call__方法,需要去父类中找,最终在socketserver.BaseRequestHandler中找到,它调用了self.hande方法,注意self.handle并不是直接调用BaseRequestHandler中的handle,根据对象属性的查找关系,会去django.core.servers.basehttp.WSGIRequestHandler类中找,找到了handle,其实是相当于回调了handle,代码如下
def handle(self):
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request() # 关键代码
try:
self.connection.shutdown(socket.SHUT_WR)
except (socket.error, AttributeError):
pass
关键代码:self.handle_one_request()直接在当前类中找到,代码如下
def handle_one_request(self):
"""Copy of WSGIRequestHandler.handle() but with different ServerHandler"""
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return
if not self.parse_request():
return
# 关键代码1
handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
# 关键代码2
handler.request_handler = self
handler.run(self.server.get_app())
关键代码1:实例化了ServerHandler对象。
关键代码2:意思是将django.contrib.staticfiles.handlers.StaticFilesHandler
转交给ServerHandler去运行,而ServerHandler对象并没有run方法,去它的父类们中去找,
#1、django.core.servers.basehttp.ServerHandler
#2、wsgiref.simple_server.ServerHandler
#3、wsgiref.handlers.SimpleHandler
#4、wsgiref.handlers.BaseHandler # 在此处找到run方法
#5、object
最终在 wsgiref.handlers.BaseHandler
中找到了run方法,代码如下
class BaseHandler:
............
def run(self, application):
try:
self.setup_environ()
self.result = application(self.environ, self.start_response) # 关键代码
self.finish_response()
except:
try:
self.handle_error()
except:
self.close()
raise
关键代码:application(self.environ, self.start_response)
也就相当于是django.contrib.staticfiles.handlers.StaticFilesHandler.__call__(self.environ, lf.start_response)
。
class StaticFilesHandler(WSGIHandler): # django专门用来处理静态文件的类
"""
WSGI middleware that intercepts calls to the static files directory, as
defined by the STATIC_URL setting, and serves those files.
"""
# May be used to differentiate between handler types (e.g. in a
# request_finished signal)
handles_files = True
def __init__(self, application):
self.application = application
self.base_url = urlparse(self.get_base_url())
super().__init__()
def load_middleware(self):
# Middleware are already loaded for self.application; no need to reload
# them for self.
pass
def get_base_url(self):
utils.check_settings()
return settings.STATIC_URL
def _should_handle(self, path):
"""
Check if the path should be handled. Ignore the path if:
* the host is provided as part of the base_url
* the request's path isn't under the media path (or equal)
"""
return path.startswith(self.base_url[2]) and not self.base_url[1]
def file_path(self, url):
"""
Return the relative path to the media file on disk for the given URL.
"""
relative_url = url[len(self.base_url[2]):]
return url2pathname(relative_url)
def serve(self, request):
"""Serve the request path."""
return serve(request, self.file_path(request.path), insecure=True)
def get_response(self, request):
from django.http import Http404
if self._should_handle(request.path):
try:
return self.serve(request)
except Http404 as e:
return response_for_exception(request, e)
return super().get_response(request)
def __call__(self, environ, start_response):
if not self._should_handle(get_path_info(environ)):
return self.application(environ, start_response) # 关键代码1
return super().__call__(environ, start_response)
关键代码1:self.application(environ, start_response) ,先说self.application是个啥呢,可以看到在该类的__init__方法中执行了一个self.application = application,那它的值到底是啥呢???
教你一招,源码读到这里,不必再回头,读源码的窍门在于读一点记录一点,遇到看不懂的变量打印一下值看一下即可,最好不要重复回头,那样只会让你更晕,例如我们用管理用户(修改django源码需要权限)修改文件django.contrib.staticfiles.handlers.StaticFilesHandler加一行打印代码,
def __init__(self, application):
self.application = application
print('django源码打印--->self.application值为',self.application) # 打印
self.base_url = urlparse(self.get_base_url())
super().__init__()
然后重启django可以看到self.application的值为<django.core.handlers.wsgi.WSGIHandler object at 0x106cf0278>,去查看类django.core.handlers.wsgi.WSGIHandler 的实例化发现加载了中间件self.load_middleware(),至此我们完成分析如何从wsgi服务到将url请求信息转交给django,剩下的就是django的内部流程啦,我们有机会再继续剖析吧
return self.serve(request)
except Http404 as e:
return response_for_exception(request, e)
return super().get_response(request)
def __call__(self, environ, start_response):
if not self._should_handle(get_path_info(environ)):
return self.application(environ, start_response) # 关键代码1
return super().__call__(environ, start_response)
关键代码1:self.application(environ, start_response) ,先说self.application是个啥呢,可以看到在该类的__init__方法中执行了一个self.application = application,那它的值到底是啥呢???
教你一招,源码读到这里,不必再回头,读源码的窍门在于读一点记录一点,遇到看不懂的变量打印一下值看一下即可,最好不要重复回头,那样只会让你更晕,例如我们用管理用户(修改django源码需要权限)修改文件django.contrib.staticfiles.handlers.StaticFilesHandler加一行打印代码,
def __init__(self, application):
self.application = application
print('django源码打印--->self.application值为',self.application) # 打印
self.base_url = urlparse(self.get_base_url())
super().__init__()
然后重启django可以看到self.application的值为<django.core.handlers.wsgi.WSGIHandler object at 0x106cf0278>,去查看类django.core.handlers.wsgi.WSGIHandler 的实例化发现加载了中间件self.load_middleware(),至此我们完成分析如何从wsgi服务到将url请求信息转交给django,剩下的就是django的内部流程啦,我们有机会再继续剖析吧
可以用同样的手法查看envion变量,该变量非常重要,http协议的请求信息都被放入了environ变量中。我们分析流程中的WSGIServer类主要用于处理socket请求和对接WSGIRequestHandler,WSGIRequestHandler类主要针对environ进行预处理和对接WSGIServerHandler,而ServerHandler类则主要用于执行应用程序(application)和返回响应给WSGIServer