一 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協議
HTTP協議詳解鏈接地址:http://www.cnblogs.com/linhaifeng/articles/8243379.html
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 內容如下:
from 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中新增一條映射關系
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框架的安裝與使用
在使用Django框架開發web應用程序時,開發階段同樣依賴wsgiref模塊來實現Server的功能,我們使用Django框架是為了快速地開發application
4.1 安裝
目前在企業開發中Django框架使用的主流版本為1.11.x版本,最新版本為2.x,我們主要講解1.11版本,同時會涉及2.x的新特性
pip3 install django==1.11.18 # 在命令行執行該命令
4.2 使用
4.2.1 快速創建並啟動Django項目
如果使用的是我們自定義的框架來開發web應用,需要事先生成框架包含的一系列基礎文件,然后在此基礎上進行開發。
如果使用的是Django框架來開發web應用,同樣需要事先生成Django框架包含的一系列基礎文件,然后在此基礎上進行開發。
但Django框架更為方便的地方在於它已經為我們提供了一系列命令來幫我們快速地生成這一系列基礎文件
# 在命令行執行以下指令,會在當前目錄生成一個名為mysite的文件夾,該文件夾中包含Django框架的一系列基礎文件
django-admin startproject mysite
創建功能模塊
cd mysite # 切換到mysite目錄下,執行以下命令
python manage.py startapp app01 # 創建功能模塊app01,此處的startapp代表創建application下的一個功能模塊。例如我們要開發application是京東商城,京東商城這個大項目下有一個訂單管理模塊,我們可以將其命名為app01
運行
python manage.py runserver 8001 # 在瀏覽器輸入:http://127.0.0.1:8001 會看到Django的歡迎頁面。
4.2.2 Django項目目錄結構
截目錄樹的圖(按照下述目錄截圖)
mysite # 文件夾
├── app01 # 文件夾
│ └── migrations # 文件夾
│ └── admin.py
│ └── apps.py
│ └── models.py
│ └── tests.py
│ └── views.py
├── mysite # 文件夾
│ └── settings.py
│ └── urls.py
│ └── wsgi.py
└── templates # 文件夾
├── manage.py
關鍵文件介紹
-manage.py---項目入口,執行一些命令
-項目名
-settings.py 全局配置信息
-urls.py 總路由,請求地址跟視圖函數的映射關系
-app名字
-migrations 數據庫遷移的記錄
-models.py 數據庫表模型
-views.py 處理業務邏輯的函數,簡稱視圖函數
4.2.3 基於Pycharm創建Django項目
4.2.4 基於Django實現的一個簡單示例
(1)url.py
from 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)視圖
from 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
<!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.2.5 Django框架的分層與請求生命周期
綜上,我們使用Django框架就是為了開發application,而application的工作過程本質就是根據不同的請求返回不同的數據,Django框架將這個工作過程細分為如下四層去實現
1、路由層(根據不同的地址執行不同的視圖函數,詳見urls.py)
2、視圖層(定義處理業務邏輯的視圖函數,詳見views.py)
3、模型層 (跟數據庫打交道的,詳解models.py)
4、模板層(待返回給瀏覽器的html文件,詳見templates)
django請求生命周期
這體現了一種解耦合的思想,下面我們開始詳細介紹每一層