
一 Web应用的组成
接下来我们的目的是为了开发一个Web应用程序,而Web应用程序是基于B/S架构的,其中B指的是浏览器,负责向S端发送请求信息,而S端会根据接收到的请求信息返回相应的数据给浏览器,需要强调的一点是:S端由server和application两大部分构成,如图所示:
二 开发一个Web应用
我们无需开发浏览器(本质即套接字客户端),只需要开发S端即可,S端的本质就是用套接字实现的,如下
# S端import socketdef make_server(ip, port, app): # 代表serversock = 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、将请求信息直接转交给applicationres = 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协议的格式回消息才行
S端修订版本:处理HTTP协议的请求消息,并按照HTTP协议的格式回复消息
# S端import socketdef make_server(ip, port, app): # 代表serversock = 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 socketdef 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 socketdef make_server(ip, port, app): # 代表serversock = 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 socketdef make_server(ip, port, app): # 代表serversock = 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 timenow = 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_serverfrom wsgiref.simple_server import make_serverdef 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 timenow = 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) # 代表serverprint('监听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 datetimenow = 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_serverdef 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)breakelse: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 datetimenow = 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 内容如下:
from wsgiref.simple_server import make_serverfrom mysite.urls import url_patterns # 需要导入urls中的url_patternsdef 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)breakelse: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中新增一条映射关系
url_patterns = [('/index', index),('/timer', timer),('/home', home), # 新增的映射关系]
步骤三:在views.py中新增一个名为home的函数
def home(environ):with open('templates/home.html', 'r',encoding='utf-8') as f:data = f.read()return data.encode('utf-8')
我们自定义的框架功能有限,在Python中我们可以使用别人开发的、功能更强大的Django框架
四 Django框架的使用
4.1 Django项目目录结构
mysitemysite文件夹 # 项目同名文件夹settings.py # django暴露给用户可以配置的配置文件urls.py # 路由与视图函数(可以是函数也可是类)对应关系(路由层)wsgi.py # 忽略app01文件夹 # 应用(可以有多个)migrations文件夹 # 存储数据库记录相关(类似于操作日志)admin.py # django后台管理apps.py # 注册appmodels.py # 数据库相关(模型层)tests.py # 测试文件views.py # 视图函数(视图层)db.sqlite3 # django自带的小型数据库manage.py # django入口文件templates # 模板文件(存储html文件)(模板层)
4.2 基于Django实现的一个简单示例
(1)url.py
from django.contrib import adminfrom django.conf.urls import url#导入views模块from app01 import viewsurlpatterns = [url(r'^admin/', admin.site.urls),# r'^index/$' 会正则匹配url地址的路径部分url(r'^index/$',views.index), # 新增地址http://127.0.0.1:8001/index/与index函数的映射关系]
(2)视图
from django.shortcuts import render# 必须定义一个request形参,request相当于我们自定义框架时的environ参数def index(request):import datetimenow=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
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title></head><body><h4>当前时间:{{ ctime }}</h4></body></html>
测试:
# 命令行终端python manage.py runserver 8001# 浏览器在浏览器输入:http://127.0.0.1:8001/index/ 会看到当前时间。
4.3 Django框架的分层与请求生命周期
综上,我们使用Django框架就是为了开发application,而application的工作过程本质就是根据不同的请求返回不同的数据,Django框架将这个工作过程细分为如下四层去实现
1、路由层(根据不同的地址执行不同的视图函数,详见urls.py)
2、视图层(定义处理业务逻辑的视图函数,详见views.py)
3、模型层 (跟数据库打交道的,详解models.py)
4、模板层(待返回给浏览器的html文件,详见templates)
django请求生命周期
这体现了一种解耦合的思想,下面我们开始详细介绍每一层
