1. 程式人生 > >01 Django web框架的原理

01 Django web框架的原理

cfi 接下來 zh-cn http請求 代碼 sta 提供服務 lac kit

Web框架本質

我們可以這樣理解:所有的Web應用本質上就是一個socket服務端,而用戶的瀏覽器就是一個socket客戶端。 這樣我們就可以自己實現Web框架了。

socket服務端

import socket  
  
sk = socket.socket()  
sk.bind(("127.0.0.1", 80))  
sk.listen()  
  
  
while True:  
    conn, addr = sk.accept()  
    data = conn.recv(8096)  
    conn.send(b"OK")  
    conn.close() 

可以說Web服務本質上都是在這十幾行代碼基礎上擴展出來的。這段代碼就是它們的祖宗。

用戶在瀏覽器中輸入網址,瀏覽器會向服務端發送數據,那瀏覽器會發送什麽數據?怎麽發?這個誰來定? 你這個網站是這個規定,他那個網站按照他那個規定,那互聯網還能玩麽?

所以,必須有一個統一的規則,讓大家發送消息、接收消息的時候都有個格式依據,不能隨便寫。

這個規則就是HTTP協議,以後瀏覽器發送請求信息也好,服務器回復響應信息也罷,都要按照這個規則來。

HTTP協議主要規定了客戶端和服務器之間的通信格式,那HTTP協議是怎麽規定消息格式的呢?

讓我們首先打印下我們在服務端接收到的消息是什麽。

import socket    
    
sk = socket.socket()    
sk.bind((
"127.0.0.1", 80)) sk.listen() while True: conn, addr = sk.accept() data = conn.recv(8096) print(data) # 將瀏覽器發來的消息打印出來 conn.send(b"OK") conn.close()

輸出:

bGET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3355.4 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\r\nCookie: csrftoken=CtHePYARJOKNx5oNVwxIteOJXpNyJ29L4bW4506YoVqFaIFFaHm0EWDZqKmw6Jm8\r\n\r\n

我們將\r\n替換成換行看得更清晰點:

GET / HTTP/1.1  
Host: 127.0.0.1:8080  
Connection: keep-alive  
Cache-Control: max-age=0  
Upgrade-Insecure-Requests: 1  
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3355.4 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  
Cookie: csrftoken=CtHePYARJOKNx5oNVwxIteOJXpNyJ29L4bW4506YoVqFaIFFaHm0EWDZqKmw6Jm8  

然後我們再看一下我們訪問博客園官網時瀏覽器收到的響應信息是什麽。

響應相關信息可以在瀏覽器調試窗口的Network標簽頁中看到。

技術分享圖片

點擊view source之後顯示如下圖:

技術分享圖片

我們發現收發的消息需要按照一定的格式來,這裏就需要了解一下HTTP協議了。

HTTP協議介紹

HTTP協議對收發消息的格式要求

每個HTTP請求和響應都遵循相同的格式,一個HTTP包含Header和Body兩部分,其中Body是可選的。

HTTP響應的Header中有一個 Content-Type表明響應的內容格式。它的值如text/html; charset=utf-8。

text/html則表示是網頁,charset=utf-8則表示編碼為utf-8。

HTTP GET請求的格式:

技術分享圖片

HTTP響應的格式:

技術分享圖片

自定義web框架

經過上面的學習,那我們基於socket服務端的十幾行代碼寫一個我們自己的web框架。我們先不處理瀏覽器發送的請求,先讓瀏覽器能顯示我們web框架返回的信息,那我們就要按照HTTP協議的格式來發送響應。

import socket    
    
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)    
sock.bind((127.0.0.1, 8000))    
sock.listen()    
    
while True:    
    conn, addr = sock.accept()    
    data = conn.recv(8096)    
    # 給回復的消息加上響應狀態行    
    conn.send(b"HTTP/1.1 200 OK\r\n\r\n")    
    conn.send(b"OK")    
    conn.close()    

我們通過十幾行代碼簡單地演示了web 框架的本質。

接下來就讓我們繼續完善我們的自定義web框架吧!

根據不同的路徑返回不同的內容

這樣就結束了嗎? 如何讓我們的Web服務根據用戶請求的URL不同而返回不同的內容呢?

小事一樁,我們可以從請求相關數據裏面拿到請求URL的路徑,然後拿路徑做一個判斷...

""" 
根據URL中不同的路徑返回不同的內容 
"""  
  
import socket  
  
sk = socket.socket()  
sk.bind(("127.0.0.1", 8080))  # 綁定IP和端口  
sk.listen()  # 監聽  
  
while True:  
    # 等待連接  
    conn, add = sk.accept()  
    data = conn.recv(8096)  # 接收客戶端發來的消息  
    # 從data中取到路徑  
    data = str(data, encoding="utf8")  # 把收到的字節類型的數據轉換成字符串  
    # 按\r\n分割  
    data1 = data.split("\r\n")[0]  
    url = data1.split()[1]  # url是我們從瀏覽器發過來的消息中分離出的訪問路徑  
    conn.send(bHTTP/1.1 200 OK\r\n\r\n)  # 因為要遵循HTTP協議,所以回復的消息也要加狀態行  
    # 根據不同的路徑返回不同內容  
    if url == "/index/":  
        response = b"index"  
    elif url == "/home/":  
        response = b"home"  
    else:  
        response = b"404 not found!"  
  
    conn.send(response)  
    conn.close()

根據不同的路徑返回不同的內容--函數版

上面的代碼解決了不同URL路徑返回不同內容的需求。

我們返回的內容是簡單的幾個字符,那如果我可以將返回的結果封裝成一個函數呢?

""" 
根據URL中不同的路徑返回不同的內容--函數版 
"""  
  
import socket  
  
sk = socket.socket()  
sk.bind(("127.0.0.1", 8080))  # 綁定IP和端口  
sk.listen()  # 監聽  
  
  
# 將返回不同的內容部分封裝成函數  
def func(url):  
    s = "這是{}頁面!".format(url)  
    return bytes(s, encoding="utf8")  
  
  
while True:  
    # 等待連接  
    conn, add = sk.accept()  
    data = conn.recv(8096)  # 接收客戶端發來的消息  
    # 從data中取到路徑  
    data = str(data, encoding="utf8")  # 把收到的字節類型的數據轉換成字符串  
    # 按\r\n分割  
    data1 = data.split("\r\n")[0]  
    url = data1.split()[1]  # url是我們從瀏覽器發過來的消息中分離出的訪問路徑  
    conn.send(bHTTP/1.1 200 OK\r\n\r\n)  # 因為要遵循HTTP協議,所以回復的消息也要加狀態行  
    # 根據不同的路徑返回不同內容,response是具體的響應體  
    if url == "/index/":  
        response = func(url)  
    elif url == "/home/":  
        response = func(url)  
    else:  
        response = b"404 not found!"  
  
    conn.send(response)  
    conn.close() 

根據不同的路徑返回不同的內容--函數進階版

看起來上面的代碼寫了一個函數,那肯定可以寫多個函數,不同的路徑對應執行不同的函數拿到結果,但是我們要一個個判斷路徑,是不是很麻煩?我們有簡單的辦法來解決。

""" 
根據URL中不同的路徑返回不同的內容--函數進階版 
"""  
  
import socket  
  
sk = socket.socket()  
sk.bind(("127.0.0.1", 8080))  # 綁定IP和端口  
sk.listen()  # 監聽  
  
  
# 將返回不同的內容部分封裝成不同的函數  
def index(url):  
    s = "這是{}頁面XX!".format(url)  
    return bytes(s, encoding="utf8")  
  
  
def home(url):  
    s = "這是{}頁面。。!".format(url)  
    return bytes(s, encoding="utf8")  
  
  
# 定義一個url和實際要執行的函數的對應關系  
list1 = [  
    ("/index/", index),  
    ("/home/", home),  
]  
  
while True:  
    # 等待連接  
    conn, add = sk.accept()  
    data = conn.recv(8096)  # 接收客戶端發來的消息  
    # 從data中取到路徑  
    data = str(data, encoding="utf8")  # 把收到的字節類型的數據轉換成字符串  
    # 按\r\n分割  
    data1 = data.split("\r\n")[0]  
    url = data1.split()[1]  # url是我們從瀏覽器發過來的消息中分離出的訪問路徑  
    conn.send(bHTTP/1.1 200 OK\r\n\r\n)  # 因為要遵循HTTP協議,所以回復的消息也要加狀態行  
    # 根據不同的路徑返回不同內容  
    func = None  # 定義一個保存將要執行的函數名的變量  
    for item in list1:  
        if item[0] == url:  
            func = item[1]  
            break  
    if func:  
        response = func(url)  
    else:  
        response = b"404 not found!"  
  
    # 返回具體的響應消息  
    conn.send(response)  
    conn.close()

返回具體的HTML文件

完美解決了不同URL返回不同內容的問題。 但是我不想僅僅返回幾個字符串,我想給瀏覽器返回完整的HTML內容,這又該怎麽辦呢?

沒問題,不管是什麽內容,最後都是轉換成字節數據發送出去的。 我們可以打開HTML文件,讀取出它內部的二進制數據,然後再發送給瀏覽器。

""" 
根據URL中不同的路徑返回不同的內容--函數進階版 
返回獨立的HTML頁面 
"""  
  
import socket  
  
sk = socket.socket()  
sk.bind(("127.0.0.1", 8080))  # 綁定IP和端口  
sk.listen()  # 監聽  
  
  
# 將返回不同的內容部分封裝成不同的函數  
def index(url):  
    # 讀取index.html頁面的內容  
    with open("index.html", "r", encoding="utf8") as f:  
        s = f.read()  
    # 返回字節數據  
    return bytes(s, encoding="utf8")  
  
  
def home(url):  
    with open("home.html", "r", encoding="utf8") as f:  
        s = f.read()  
    return bytes(s, encoding="utf8")  
  
  
# 定義一個url和實際要執行的函數的對應關系  
list1 = [  
    ("/index/", index),  
    ("/home/", home),  
]  
  
while True:  
    # 等待連接  
    conn, add = sk.accept()  
    data = conn.recv(8096)  # 接收客戶端發來的消息  
    # 從data中取到路徑  
    data = str(data, encoding="utf8")  # 把收到的字節類型的數據轉換成字符串  
    # 按\r\n分割  
    data1 = data.split("\r\n")[0]  
    url = data1.split()[1]  # url是我們從瀏覽器發過來的消息中分離出的訪問路徑  
    conn.send(bHTTP/1.1 200 OK\r\n\r\n)  # 因為要遵循HTTP協議,所以回復的消息也要加狀態行  
    # 根據不同的路徑返回不同內容  
    func = None  # 定義一個保存將要執行的函數名的變量  
    for item in list1:  
        if item[0] == url:  
            func = item[1]  
            break  
    if func:  
        response = func(url)  
    else:  
        response = b"404 not found!"  
  
    # 返回具體的響應消息  
    conn.send(response)  
    conn.close()  
技術分享圖片
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>index</title>
</head>
<body>
<div>這是index頁面</div>
</body>
</html>
index.html 技術分享圖片
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>index</title>
</head>
<body>
<div>這是home頁面</div>
</body>
</html>
home.html

讓網頁動態起來

這網頁能夠顯示出來了,但是都是靜態的啊。頁面的內容都不會變化的,我想要的是動態網站。

沒問題,我也有辦法解決。我選擇使用字符串替換來實現這個需求。(這裏使用時間戳來模擬動態的數據)

""" 
根據URL中不同的路徑返回不同的內容--函數進階版 
返回獨立的HTML頁面 
"""  
  
import socket  
  
sk = socket.socket()  
sk.bind(("127.0.0.1", 8080))  # 綁定IP和端口  
sk.listen()  # 監聽  
  
  
# 將返回不同的內容部分封裝成不同的函數  
def index(url):  
    # 讀取index.html頁面的內容  
    with open("index.html", "r", encoding="utf8") as f:  
        s = f.read()  
    # 返回字節數據  
    return bytes(s, encoding="utf8")  
  
  
def home(url):  
    with open("home.html", "r", encoding="utf8") as f:  
        s = f.read()  
    return bytes(s, encoding="utf8")  
  
  
def timer(url):  
    import time  
    with open("time.html", "r", encoding="utf8") as f:  
        s = f.read()  
        s = s.replace(@@time@@, time.strftime("%Y-%m-%d %H:%M:%S"))  
    return bytes(s, encoding="utf8")  
  
  
# 定義一個url和實際要執行的函數的對應關系  
list1 = [  
    ("/index/", index),  
    ("/home/", home),  
    ("/time/", timer),  
]  
  
while True:  
    # 等待連接  
    conn, add = sk.accept()  
    data = conn.recv(8096)  # 接收客戶端發來的消息  
    # 從data中取到路徑  
    data = str(data, encoding="utf8")  # 把收到的字節類型的數據轉換成字符串  
    # 按\r\n分割  
    data1 = data.split("\r\n")[0]  
    url = data1.split()[1]  # url是我們從瀏覽器發過來的消息中分離出的訪問路徑  
    conn.send(bHTTP/1.1 200 OK\r\n\r\n)  # 因為要遵循HTTP協議,所以回復的消息也要加狀態行  
    # 根據不同的路徑返回不同內容  
    func = None  # 定義一個保存將要執行的函數名的變量  
    for item in list1:  
        if item[0] == url:  
            func = item[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部分:

"""  
根據URL中不同的路徑返回不同的內容--函數進階版  
返回HTML頁面  
讓網頁動態起來  
wsgiref模塊版  
"""   
     
from wsgiref.simple_server import make_server   
     
     
# 將返回不同的內容部分封裝成函數   
def index(url):   
    # 讀取index.html頁面的內容   
    with open("index.html", "r", encoding="utf8") as f:   
        s = f.read()   
    # 返回字節數據   
    return bytes(s, encoding="utf8")   
     
     
def home(url):   
    with open("home.html", "r", encoding="utf8") as f:   
        s = f.read()   
    return bytes(s, encoding="utf8")   
     
     
def timer(url):   
    import time   
    with open("time.html", "r", encoding="utf8") as f:   
        s = f.read()   
        s = s.replace(@@time@@, time.strftime("%Y-%m-%d %H:%M:%S"))   
    return bytes(s, encoding="utf8")   
     
     
# 定義一個url和實際要執行的函數的對應關系   
list1 = [   
    ("/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 list1:   
        if i[0] == url:   
            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, 8090, run_server)   
    print("我在8090等你哦...")   
    httpd.serve_forever() 

jinja2

上面的代碼實現了一個簡單的動態,我完全可以從數據庫中查詢數據,然後去替換我html中的對應內容,然後再發送給瀏覽器完成渲染。 這個過程就相當於HTML模板渲染數據。 本質上就是HTML內容中利用一些特殊的符號來替換要展示的數據。 我這裏用的特殊符號是我定義的,其實模板渲染有個現成的工具: jinja2

下載jinja2:

pip install jinja2
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Title</title>
</head>
<body>
    <h1>姓名:{{name}}</h1>
    <h1>愛好:</h1>
    <ul>
        {% for hobby in hobby_list %}
        <li>{{hobby}}</li>
        {% endfor %}
    </ul>
</body>
</html>

使用jinja2渲染index2.html文件:

from wsgiref.simple_server import make_server  
from jinja2 import Template  
  
  
def index(url):  
    # 讀取HTML文件內容  
    with open("index2.html", "r", encoding="utf8") as f:  
        data = f.read()  
        template = Template(data)   # 生成模板文件  
        ret = template.render({name: alex, hobby_list: [抽煙, 喝酒, 燙頭]})   # 把數據填充到模板中  
    return bytes(ret, encoding="utf8")  
  
  
def home(url):  
    with open("home.html", "r", encoding="utf8") as f:  
        s = f.read()  
    return bytes(s, encoding="utf8")  
  
  
# 定義一個url和實際要執行的函數的對應關系  
list1 = [  
    ("/index/", index),  
    ("/home/", home),  
]  
  
  
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 list1:  
        if i[0] == url:  
            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, 8090, run_server)  
    print("我在8090等你哦...")  
    httpd.serve_forever() 

現在的數據是我們自己手寫的,那可不可以從數據庫中查詢數據,來填充頁面呢?

使用pymysql連接數據庫:

conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", passwd="xxx", db="xxx", charset="utf8")
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
cursor.execute("select name, age, department_id from userinfo")
user_list = cursor.fetchall()
cursor.close()
conn.close()

創建一個測試的user表:

CREATE TABLE user(
  id int auto_increment PRIMARY KEY,
  name CHAR(10) NOT NULL,
  hobby CHAR(20) NOT NULL
)engine=innodb DEFAULT charset=UTF8;

模板的原理就是字符串替換,我們只要在HTML頁面中遵循jinja2的語法規則寫上,其內部就會按照指定的語法進行相應的替換,從而達到動態的返回內容。

Django

安裝(安裝最新LTS版):

pip3 install django==1.11.15

創建一個django項目:

下面的命令創建了一個名為"mysite"的Django 項目:

django-admin startproject mysite

目錄介紹:

mysite/
├── manage.py  # 管理文件
└── mysite  # 項目目錄
    ├── __init__.py
    ├── settings.py  # 配置
    ├── urls.py  # 路由 --> URL和函數的對應關系
    └── wsgi.py  # runserver命令就使用wsgiref模塊做簡單的web server

運行Django項目:

python manage.py runserver 127.0.0.1:8000

模板文件配置:

TEMPLATES = [
    {
        BACKEND: django.template.backends.django.DjangoTemplates,
        DIRS: [os.path.join(BASE_DIR, "template")],  # template文件夾位置
        APP_DIRS: True,
        OPTIONS: {
            context_processors: [
                django.template.context_processors.debug,
                django.template.context_processors.request,
                django.contrib.auth.context_processors.auth,
                django.contrib.messages.context_processors.messages,
            ],
        },
    },
]

靜態文件配置:

STATIC_URL = /static/  # HTML中使用的靜態文件夾前綴
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),  # 靜態文件存放位置
]

看不明白?有圖有真相:

技術分享圖片

剛開始學習時可在配置文件中暫時禁用csrf中間件,方便表單提交測試。

MIDDLEWARE = [
    django.middleware.security.SecurityMiddleware,
    django.contrib.sessions.middleware.SessionMiddleware,
    django.middleware.common.CommonMiddleware,
    # ‘django.middleware.csrf.CsrfViewMiddleware‘,
    django.contrib.auth.middleware.AuthenticationMiddleware,
    django.contrib.messages.middleware.MessageMiddleware,
    django.middleware.clickjacking.XFrameOptionsMiddleware,
]

Django基礎必備三件套:

from django.shortcuts import HttpResponse, render, redirect

HttpResponse

內部傳入一個字符串參數,返回給瀏覽器。

例如:

def index(request):
    # 業務邏輯代碼
    return HttpResponse("OK")

render

除request參數外還接受一個待渲染的模板文件和一個保存具體數據的字典參數。

將數據填充進模板文件,最後把結果返回給瀏覽器。(類似於我們上面用到的jinja2)

例如:

def index(request):
    # 業務邏輯代碼
    return render(request, "index.html", {"name": "alex", "hobby": ["燙頭", "泡吧"]})

redirect

接受一個URL參數,表示跳轉到指定的URL。

例如

def index(request):
    # 業務邏輯代碼
    return redirect("/home/")

重定向是怎麽回事?

技術分享圖片


啟動Django報錯:

Django 啟動時報錯 UnicodeEncodeError ...

報這個錯誤通常是因為計算機名為中文,改成英文的計算機名重啟下電腦就可以了。

01 Django web框架的原理