Web框架原理
Web框架原理
我们可以这样理解:所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端。 这样我们就可以自己实现Web框架了。
web框架 就是一个socket服务端
功能:
a. socket收发消息
b. 根据不同的路径返回不同的内容
c. 可以返回动态页面(字符串的替换 - 模板的渲染)
分类:
Django b c
flask b
tornado a b c
另类分类:
Django
其他
socket服务端
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(8096)
conn.send(b'OK')
conn.close()
可以说Web服务本质上都是在这十几行代码基础上扩展出来的。这段代码就是它们的祖宗。
用户在浏览器中输入网址,浏览器会向服务端发送数据,那浏览器会发送什么数据?怎么发?这个谁来定? 你这个网站是这个规定,他那个网站按照他那个规定,那互联网还能玩么?
所以,必须有一个统一的规则,让大家发送消息、接收消息的时候都有个格式依据,不能随便写。
这个规则就是HTTP协议,以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。
HTTP协议主要规定了客户端和服务器之间的通信格式,那HTTP协议是怎么规定消息格式的呢?
让我们首先打印下我们在服务端接收到的消息是什么。
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(8096)
print(data)
conn.send(b'OK')
conn.close()
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n\r\n'
把\r\n换做换行后
GET / HTTP/1.1
Host: 127.0.0.1:8000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
我们发现收发的消息需要按照一定的格式来,这里就需要了解一下HTTP协议了。
HTTP协议对收发消息的格式要求
每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。
HTTP GET请求的格式:
HTTP响应的格式:
自定义web框架
经过上面的学习,那我们基于socket服务端的十几行代码写一个我们自己的web框架。我们先不处理浏览器发送的请求,先让浏览器能显示我们web框架返回的信息,那我们就要按照HTTP协议的格式来发送响应。
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(8096)
print(data)
conn.send(b'HTTP/1.1 200 OK\r\n\r\n<h1>OK</h1>')
conn.close()
根据不同的路径返回不同的内容
这样就结束了吗? 如何让我们的Web服务根据用户请求的URL不同而返回不同的内容呢?
小事一桩,我们可以从请求相关数据里面拿到请求URL的路径,然后拿路径做一个判断...
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(8096).decode('utf-8')
url = data.split('\r\n')[0].split()[1].strip('/') # 提取出路径
# print(url)
print(data)
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
if url == 'index':
response = b'index'
elif url == 'home':
response = b'home'
else:
response = b'404 not found!'
conn.send(response)
conn.close()
根据不同的路径返回不同的内容--函数版
上面的代码解决了不同URL路径返回不同内容的需求。
我们返回的内容是简单的几个字符,那如果我可以将返回的结果封装成一个函数呢?
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
def func(url):
return 'This is {} page.'.format(url).encode('utf-8')
while True:
conn, addr = server.accept()
data = conn.recv(8096).decode('utf-8')
url = data.split('\r\n')[0].split()[1].strip('/') # 提取出路径
# print(url)
print(data)
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
if url == 'index':
response = func(url)
elif url == 'home':
response = func(url)
else:
response = b'404 not found!'
conn.send(response)
conn.close()
根据不同的路径返回不同的内容--函数进阶版
看起来上面的代码写了一个函数,那肯定可以写多个函数,不同的路径对应执行不同的函数拿到结果,但是我们要一个个判断路径,是不是很麻烦?我们有简单的办法来解决。
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
# 将返回不同的内容部分封装成不同的函数
def index(url):
return 'This is {} page.\nhahahahahahaha'.format(url).encode('utf-8')
def home(url):
return 'This is {} page.'.format(url).encode('utf-8')
# 定义一个url和实际要执行的函数的对应关系
list_ = [
('index', index),
('home', home)
]
while True:
conn, addr = server.accept()
data = conn.recv(8096).decode('utf-8')
url = data.split('\r\n')[0].split()[1].strip('/') # 提取出路径
# print(url)
print(data)
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
func = None # 定义一个保存将要执行的函数名的变量
for i in list_:
if i[0] == url:
func = i[1]
break
if func:
response = func(url)
else:
response = b'404 not found!'
conn.send(response)
conn.close()
返回具体的HTML文件
完美解决了不同URL返回不同内容的问题。 但是我不想仅仅返回几个字符串,我想给浏览器返回完整的HTML内容,这又该怎么办呢?
没问题,不管是什么内容,最后都是转换成字节数据发送出去的。 我们可以打开HTML文件,读取出它内部的二进制数据,然后再发送给浏览器。
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
# 将返回不同的内容部分封装成不同的函数
def index(url):
with open('index.html', 'rb') as f:
text = f.read()
return text
def home(url):
with open('home.html', 'rb') as f:
text = f.read()
return text
# 定义一个url和实际要执行的函数的对应关系
list_ = [
('index', index),
('home', home)
]
while True:
conn, addr = server.accept()
data = conn.recv(8096).decode('utf-8')
url = data.split('\r\n')[0].split()[1].strip('/') # 提取出路径
# print(url)
print(data)
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
func = None # 定义一个保存将要执行的函数名的变量
for i in list_:
if i[0] == url:
func = i[1]
break
if func:
response = func(url)
else:
response = b'404 not found!'
conn.send(response)
conn.close()
返回动态的HTML文件
这网页能够显示出来了,但是都是静态的啊。页面的内容都不会变化的,我想要的是动态网站。
没问题,我也有办法解决。我选择使用字符串替换来实现这个需求。(这里使用时间戳来模拟动态的数据)
import socket
import time
server = socket.socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
# 将返回不同的内容部分封装成不同的函数
def index(url):
with open('index.html', 'rb') as f:
text = f.read()
return text
def home(url):
with open('home.html', 'rb') as f:
text = f.read()
return text
def timer(url):
with open('time.html', 'r', encoding='utf-8') as f:
text = f.read()
text = text.replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S"))
return text.encode('utf-8')
# 定义一个url和实际要执行的函数的对应关系
list_ = [
('index', index),
('home', home),
('time', timer)
]
while True:
conn, addr = server.accept()
data = conn.recv(8096).decode('utf-8')
url = data.split('\r\n')[0].split()[1].strip('/') # 提取出路径
# print(url)
print(data)
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
func = None # 定义一个保存将要执行的函数名的变量
for i in list_:
if i[0] == url:
func = i[1]
break
if func:
response = func(url)
else:
response = b'404 not found!'
conn.send(response)
conn.close()
服务器程序和应用程序
对于真实开发中的python web程序来说,一般会分为两部分:服务器程序和应用程序。
服务器程序负责对socket服务端进行封装,并在请求到来时,对请求的各种数据进行整理。
应用程序则负责具体的逻辑处理。为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。
这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。
这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。
WSGI(Web Server Gateway Interface)就是一种规范,它定义了使用Python编写的web应用程序与web服务器程序之间的接口格式,实现web应用程序与web服务器程序间的解耦。
常用的WSGI服务器有uwsgi、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgiref,Django开发环境用的就是这个模块来做服务器
wsgiref
我们利用wsgiref模块来替换我们自己写的web框架的socket server部分:
from wsgiref.simple_server import make_server
import time
# 将返回不同的内容部分封装成不同的函数
def index(url):
with open('index.html', 'rb') as f:
text = f.read()
return text
def home(url):
with open('home.html', 'rb') as f:
text = f.read()
return text
def timer(url):
with open('time.html', 'r', encoding='utf-8') as f:
text = f.read()
text = text.replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S"))
return text.encode('utf-8')
# 定义一个url和实际要执行的函数的对应关系
list_ = [
('index', index),
('home', home),
('time', timer)
]
def run_server(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息
url = environ['PATH_INFO'] # 取到用户输入的url
func = None
for i in list_:
if i[0] == url.strip('/'):
func = i[1]
break
if func:
response = func(url)
else:
response = b"404 not found!"
return [response, ]
if __name__ == '__main__':
httpd = make_server('127.0.0.1', 8000, run_server)
httpd.serve_forever()
jinja2
上面的代码实现了一个简单的动态,我完全可以从数据库中查询数据,然后去替换我html中的对应内容,然后再发送给浏览器完成渲染。 这个过程就相当于HTML模板渲染数据。 本质上就是HTML内容中利用一些特殊的符号来替换要展示的数据。 我这里用的特殊符号是我定义的,其实模板渲染有个现成的工具: jinja2
下载jinja2:pip install jinja2
index2.html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>姓名:{{name}}</h1>
<h1>爱好:</h1>
<ul>
{% for hobby in hobby_list %}
<li>{{hobby}}</li>
{% endfor %}
</ul>
</body>
</html>
from wsgiref.simple_server import make_server
from jinja2 import Template
import time
# 将返回不同的内容部分封装成不同的函数
def index(url):
with open('index.html', 'rb') as f:
text = f.read()
return text
def home(url):
with open('home.html', 'rb') as f:
text = f.read()
return text
def timer(url):
with open('time.html', 'r', encoding='utf-8') as f:
text = f.read()
text = text.replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S"))
return text.encode('utf-8')
def index2(url):
with open('index2.html', 'r', encoding='utf-8') as f:
data = f.read()
template = Template(data) # 生成模板文件
ret = template.render({
'name': 'pig',
'hobby_list': ['吃', '喝', '睡']
})
# 把数据填充到模板中,在这一步中,可以调用数据库,选出对应信息返回。
return ret.encode('utf-8')
# 定义一个url和实际要执行的函数的对应关系
list_ = [
('index', index),
('home', home),
('time', timer),
('index2', index2),
]
def run_server(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息
url = environ['PATH_INFO'] # 取到用户输入的url
func = None
for i in list_:
if i[0] == url.strip('/'):
func = i[1]
break
if func:
response = func(url)
else:
response = b"404 not found!"
return [response, ]
if __name__ == '__main__':
httpd = make_server('127.0.0.1', 8000, run_server)
httpd.serve_forever()
模板的原理就是字符串替换,我们只要在HTML页面中遵循jinja2的语法规则写上,其内部就会按照指定的语法进行相应的替换,从而达到动态的返回内容。
Django
-
下载
安装最新LTS版,长期支持。- 命令行
pip install django1.11.18
pip install django1.11.18 -i https://pypi.doubanio.com/simple/ - pycharm
- 命令行
-
创建项目
- 命令行
django-admin startproject mysite - pycharm
- 命令行
-
启动项目
-
命令行
cd 项目目录下 manage.py
python36 manage.py runserver # 127.0.0.1:8000
python36 manage.py runserver 80 # 127.0.0.1:80
python36 manage.py runserver 0.0.0.0:80 # 0.0.0.0:80 -
pycharm
-
-
目录介绍
mysite/
├── manage.py # 管理文件
└── mysite # 项目目录
├── __init__.py
├── settings.py # 配置
├── urls.py # 路由 --> URL和函数的对应关系
└── wsgi.py # runserver命令就使用wsgiref模块做简单的web server
- 配置
TEMPLATES
'DIRS': [os.path.join(BASE_DIR, 'templates')]
DATABASES 数据库
静态文件的配置
STATIC_URL = '/static/' # 别名
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
- Django基础必备三件套:
from django.shortcuts import HttpResponse, render, redirect
HttpResponse
内部传入一个字符串参数,返回给浏览器。
def index(request):
# 业务逻辑代码
return HttpResponse("OK")
render
除request参数外还接受一个待渲染的模板文件和一个保存具体数据的字典参数。
将数据填充进模板文件,最后把结果返回给浏览器。(类似于我们上面用到的jinja2)
redirect
接受一个URL参数,表示跳转到指定的URL。
def index(request):
# 业务逻辑代码
return redirect("/home/")