01-02 Web应用 - 图1


一 Web应用的组成

接下来我们的目的是为了开发一个Web应用程序,而Web应用程序是基于B/S架构的,其中B指的是浏览器,负责向S端发送请求信息,而S端会根据接收到的请求信息返回相应的数据给浏览器,需要强调的一点是:S端由server和application两大部分构成,如图所示:

上图:Web应用组成
01-02 Web应用 - 图2

二 开发一个Web应用

我们无需开发浏览器(本质即套接字客户端),只需要开发S端即可,S端的本质就是用套接字实现的,如下

  1. # S端
  2. import socket
  3. def make_server(ip, port, app): # 代表server
  4. sock = socket.socket()
  5. sock.bind((ip, port))
  6. sock.listen(5)
  7. print('Starting development server at http://%s:%s/' %(ip,port))
  8. while True:
  9. conn, addr = sock.accept()
  10. # 1、接收浏览器发来的请求信息
  11. recv_data = conn.recv(1024)
  12. # print(recv_data.decode('utf-8'))
  13. # 2、将请求信息直接转交给application
  14. res = app(recv_data)
  15. # 3、向浏览器返回消息(此处并没有按照http协议返回)
  16. conn.send(res)
  17. conn.close()
  18. def app(environ): # 代表application
  19. # 处理业务逻辑
  20. return b'hello world'
  21. if __name__ == '__main__':
  22. 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协议的格式回复消息

  1. # S端
  2. import socket
  3. def make_server(ip, port, app): # 代表server
  4. sock = socket.socket()
  5. sock.bind((ip, port))
  6. sock.listen(5)
  7. print('Starting development server at http://%s:%s/' %(ip,port))
  8. while True:
  9. conn, addr = sock.accept()
  10. # 1、接收并处理浏览器发来的请求信息
  11. # 1.1 接收浏览器发来的http协议的消息
  12. recv_data = conn.recv(1024)
  13. # 1.2 对http协议的消息加以处理,简单示范如下
  14. ll=recv_data.decode('utf-8').split('\r\n')
  15. head_ll=ll[0].split(' ')
  16. environ={}
  17. environ['PATH_INFO']=head_ll[1]
  18. environ['method']=head_ll[0]
  19. # 2:将请求信息处理后的结果environ交给application,这样application便无需再关注请求信息的处理,可以更加专注于业务逻辑的处理
  20. res = app(environ)
  21. # 3:按照http协议向浏览器返回消息
  22. # 3.1 返回响应首行
  23. conn.send(b'HTTP/1.1 200 OK\r\n')
  24. # 3.2 返回响应头(可以省略)
  25. conn.send(b'Content-Type: text/html\r\n\r\n')
  26. # 3.3 返回响应体
  27. conn.send(res)
  28. conn.close()
  29. def app(environ): # 代表application
  30. # 处理业务逻辑
  31. return b'hello world'
  32. if __name__ == '__main__':
  33. make_server('127.0.0.1', 8008, app)

此时,重启S端后,再在客户端浏览器输入:http://127.0.0.1:8008 便可以看到正常结果hello world了。

我们不仅可以回复hello world这样的普通字符,还可以夹杂html标签,浏览器在接收到消息后会对解析出的html标签加以渲染

  1. # S端
  2. import socket
  3. def make_server(ip, port, app):
  4. sock = socket.socket()
  5. sock.bind((ip, port))
  6. sock.listen(5)
  7. print('Starting development server at http://%s:%s/' %(ip,port))
  8. while True:
  9. conn, addr = sock.accept()
  10. recv_data = conn.recv(1024)
  11. ll=recv_data.decode('utf-8').split('\r\n')
  12. head_ll=ll[0].split(' ')
  13. environ={}
  14. environ['PATH_INFO']=head_ll[1]
  15. environ['method']=head_ll[0]
  16. res = app(environ)
  17. conn.send(b'HTTP/1.1 200 OK\r\n')
  18. conn.send(b'Content-Type: text/html\r\n\r\n')
  19. conn.send(res)
  20. conn.close()
  21. def app(environ):
  22. # 返回html标签
  23. return b'<h1>hello web</h1><img src="https://www.baidu.com/img/bd_logo1.png"></img>'
  24. if __name__ == '__main__':
  25. make_server('127.0.0.1', 8008, app)

更进一步我们还可以返回一个文件,例如timer.html,内容如下

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h2>{{ time }}</h2>
  9. </body>
  10. </html>

S端程序如下

  1. # S端
  2. import socket
  3. def make_server(ip, port, app): # 代表server
  4. sock = socket.socket()
  5. sock.bind((ip, port))
  6. sock.listen(5)
  7. print('Starting development server at http://%s:%s/' %(ip,port))
  8. while True:
  9. conn, addr = sock.accept()
  10. recv_data = conn.recv(1024)
  11. ll=recv_data.decode('utf-8').split('\r\n')
  12. head_ll=ll[0].split(' ')
  13. environ={}
  14. environ['PATH_INFO']=head_ll[1]
  15. environ['method']=head_ll[0]
  16. res = app(environ)
  17. conn.send(b'HTTP/1.1 200 OK\r\n')
  18. conn.send(b'Content-Type: text/html\r\n\r\n')
  19. conn.send(res)
  20. conn.close()
  21. def app(environ):
  22. # 处理业务逻辑:打开文件,读取文件内容并返回
  23. with open('timer.html', 'r', encoding='utf-8') as f:
  24. data = f.read()
  25. return data.encode('utf-8')
  26. if __name__ == '__main__':
  27. make_server('127.0.0.1', 8008, app)

上述S端为浏览器返回的都是静态页面(内容都固定的),我们还可以返回动态页面(内容是变化的)

  1. # S端
  2. import socket
  3. def make_server(ip, port, app): # 代表server
  4. sock = socket.socket()
  5. sock.bind((ip, port))
  6. sock.listen(5)
  7. print('Starting development server at http://%s:%s/' %(ip,port))
  8. while True:
  9. conn, addr = sock.accept()
  10. recv_data = conn.recv(1024)
  11. ll=recv_data.decode('utf-8').split('\r\n')
  12. head_ll=ll[0].split(' ')
  13. environ={}
  14. environ['PATH_INFO']=head_ll[1]
  15. environ['method']=head_ll[0]
  16. res = app(environ)
  17. conn.send(b'HTTP/1.1 200 OK\r\n')
  18. conn.send(b'Content-Type: text/html\r\n\r\n')
  19. conn.send(res)
  20. conn.close()
  21. def app(environ):
  22. # 处理业务逻辑
  23. with open('timer.html', 'r', encoding='utf-8') as f:
  24. data = f.read()
  25. import time
  26. now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  27. data = data.replace('{{ time }}', now) # 字符串替换
  28. return data.encode('utf-8')
  29. if __name__ == '__main__':
  30. make_server('127.0.0.1', 8008, app) # 在浏览器输入http://127.0.0.1:8008,每次刷新都会看到不同的时间

三 Web框架的由来

综上案例我们可以发现一个规律,在开发S端时,server的功能是复杂且固定的(处理socket消息的收发和http协议的处理),而app中的业务逻辑却各不相同(不同的软件就应该有不同的业务逻辑),重复开发复杂且固定的server是毫无意义的,有一个wsgiref模块帮我们写好了server的功能,这样我们便只需要专注于app功能的编写即可

  1. # wsgiref实现了server,即make_server
  2. from wsgiref.simple_server import make_server
  3. def app(environ, start_response): # 代表application
  4. # 1、返回http协议的响应首行和响应头信息
  5. start_response('200 OK', [('Content-Type', 'text/html')])
  6. # 2、处理业务逻辑:根据请求url的不同返回不同的页面内容
  7. if environ.get('PATH_INFO') == '/index':
  8. with open('index.html','r', encoding='utf-8') as f:
  9. data=f.read()
  10. elif environ.get('PATH_INFO') == '/timer':
  11. with open('timer.html', 'r', encoding='utf-8') as f:
  12. data = f.read()
  13. import time
  14. now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  15. data = data.replace('{{ time }}', now) # 字符串替换
  16. else:
  17. data='<h1>Hello, web!</h1>'
  18. # 3、返回http响应体信息,必须是bytes类型,必须放在列表中
  19. return [data.encode('utf-8')]
  20. if __name__ == '__main__':
  21. # 当接收到请求时,wsgiref模块会对该请求加以处理,然后后调用app函数,自动传入两个参数:
  22. # 1 environ是一个字典,存放了http的请求信息
  23. # 2 start_response是一个功能,用于返回http协议的响应首行和响应头信息
  24. s = make_server('', 8011, app) # 代表server
  25. print('监听8011')
  26. s.serve_forever() # 在浏览器输入http://127.0.0.1:8011/index和http://127.0.0.1:8011/timer会看到不同的页面内容

timer.html已经存在了,新增的index.html页面内容如下:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h1>主页</h1>
  9. </body>
  10. </html>

上述案例中app在处理业务逻辑时需要根据不同的url地址返回不同的页面内容,当url地址越来越多,需要写一堆if判断,代码不够清晰,耦合程度高,所以我们做出以下优化

  1. # 处理业务逻辑的函数
  2. def index(environ):
  3. with open('index.html', 'r', encoding='utf-8') as f:
  4. data = f.read()
  5. return data.encode('utf-8')
  6. def timer(environ):
  7. import datetime
  8. now = datetime.datetime.now().strftime('%y-%m-%d %X')
  9. with open('timer.html', 'r', encoding='utf-8') as f:
  10. data = f.read()
  11. data = data.replace('{{ time }}', now)
  12. return data.encode('utf-8')
  13. # 路径跟函数的映射关系
  14. url_patterns = [
  15. ('/index', index),
  16. ('/timer', timer),
  17. ]
  18. from wsgiref.simple_server import make_server
  19. def app(environ, start_response):
  20. start_response('200 OK', [('Content-Type', 'text/html')])
  21. # 拿到请求的url并根据映射关系url_patters执行相应的函数
  22. reuqest_url = environ.get('PATH_INFO')
  23. for url in url_patterns:
  24. if url[0] == reuqest_url:
  25. data = url[1](environ)
  26. break
  27. else:
  28. data = b'404'
  29. return [data]
  30. if __name__ == '__main__':
  31. s = make_server('', 8011, app)
  32. print('监听8011')
  33. s.serve_forever()

随着业务逻辑复杂度的增加,处理业务逻辑的函数以及url_patterns中的映射关系都会不断地增多,此时仍然把所有代码都放到一个文件中,程序的可读性和可扩展性都会变得非常差,所以我们应该将现有的代码拆分到不同文件中

插图:
01-02 Web应用 - 图3

  1. mysite # 文件夹
  2. ├── app01 # 文件夹
  3. └── views.py
  4. ├── mysite # 文件夹
  5. └── urls.py
  6. └── templates # 文件夹
  7. ├── index.html
  8. └── timer.html
  9. ├── main.py

views.py 内容如下:

  1. # 处理业务逻辑的函数
  2. def index(environ):
  3. with open('templates/index.html', 'r',encoding='utf-8') as f: # 注意文件路径
  4. data = f.read()
  5. return data.encode('utf-8')
  6. def timer(environ):
  7. import datetime
  8. now = datetime.datetime.now().strftime('%y-%m-%d %X')
  9. with open('templates/timer.html', 'r',encoding='utf-8') as f: # 注意文件路径
  10. data = f.read()
  11. data=data.replace('{{ time }}',now)
  12. return data.encode('utf-8')

urls.py内容如下:

  1. # 路径跟函数的映射关系
  2. from app01.views import * # 需要导入views中的函数
  3. url_patterns = [
  4. ('/index', index),
  5. ('/timer', timer),
  6. ]

main.py 内容如下:

  1. from wsgiref.simple_server import make_server
  2. from mysite.urls import url_patterns # 需要导入urls中的url_patterns
  3. def app(environ, start_response):
  4. start_response('200 OK', [('Content-Type', 'text/html')])
  5. # 拿到请求的url并根据映射关系url_patters执行相应的函数
  6. reuqest_url = environ.get('PATH_INFO')
  7. for url in url_patterns:
  8. if url[0] == reuqest_url:
  9. data = url[1](environ)
  10. break
  11. else:
  12. data = b'404'
  13. return [data]
  14. if __name__ == '__main__':
  15. s = make_server('', 8011, app)
  16. print('监听8011')
  17. s.serve_forever()

至此,我们就针对application的开发自定义了一个框架,所以说框架的本质就是一系列功能的集合体、不同的功能放到不同的文件中。有了该框架,可以让我们专注于业务逻辑的编写,极大的提高了开发web应用的效率(开发web应用的框架可以简称为web框架),比如我们新增一个业务逻辑,要求为:浏览器输入http://127.0.0.1:8011/home 就能访问到home.html页面,在框架的基础上具体开发步骤如下:

步骤一:在templates文件夹下新增home.html

步骤二:在urls.py的url_patterns中新增一条映射关系

  1. url_patterns = [
  2. ('/index', index),
  3. ('/timer', timer),
  4. ('/home', home), # 新增的映射关系
  5. ]

步骤三:在views.py中新增一个名为home的函数

  1. def home(environ):
  2. with open('templates/home.html', 'r',encoding='utf-8') as f:
  3. data = f.read()
  4. return data.encode('utf-8')

我们自定义的框架功能有限,在Python中我们可以使用别人开发的、功能更强大的Django框架

四 Django框架的使用

4.1 Django项目目录结构

  1. mysite
  2. mysite文件夹 # 项目同名文件夹
  3. settings.py # django暴露给用户可以配置的配置文件
  4. urls.py # 路由与视图函数(可以是函数也可是类)对应关系(路由层)
  5. wsgi.py # 忽略
  6. app01文件夹 # 应用(可以有多个)
  7. migrations文件夹 # 存储数据库记录相关(类似于操作日志)
  8. admin.py # django后台管理
  9. apps.py # 注册app
  10. models.py # 数据库相关(模型层)
  11. tests.py # 测试文件
  12. views.py # 视图函数(视图层)
  13. db.sqlite3 # django自带的小型数据库
  14. manage.py # django入口文件
  15. templates # 模板文件(存储html文件)(模板层)

4.2 基于Django实现的一个简单示例

(1)url.py

  1. from django.contrib import admin
  2. from django.conf.urls import url
  3. #导入views模块
  4. from app01 import views
  5. urlpatterns = [
  6. url(r'^admin/', admin.site.urls),
  7. # r'^index/$' 会正则匹配url地址的路径部分
  8. url(r'^index/$',views.index), # 新增地址http://127.0.0.1:8001/index/与index函数的映射关系
  9. ]

(2)视图

  1. from django.shortcuts import render
  2. # 必须定义一个request形参,request相当于我们自定义框架时的environ参数
  3. def index(request):
  4. import datetime
  5. now=datetime.datetime.now()
  6. ctime=now.strftime("%Y-%m-%d %X")
  7. return render(request,"index.html",{"ctime":ctime}) # render会读取templates目录下的index.html文件的内容并且用字典中的ctime的值替换模版中的{{ ctime }}

(3)模版

在templates目录下新建文件index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h4>当前时间:{{ ctime }}</h4>
  9. </body>
  10. </html>

测试:

  1. # 命令行终端
  2. python manage.py runserver 8001
  3. # 浏览器
  4. 在浏览器输入: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请求生命周期
01-02 Web应用 - 图4
这体现了一种解耦合的思想,下面我们开始详细介绍每一层