1. 程式人生 > 實用技巧 >web框架本質 wsgiref模組介紹

web框架本質 wsgiref模組介紹

Web框架本質

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

半成品自定義web框架

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()

輸出:

b'GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nDNT: 1\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: csrftoken=RKBXh1d3M97iz03Rpbojx1bR6mhHudhyX5PszUxxG3bOEwh1lxFpGOgWN93ZH3zv\r\n
\r\n
'

然後我們再看一下我們訪問部落格園官網時瀏覽器收到的響應資訊是什麼。

響應相關資訊可以在瀏覽器除錯視窗的network標籤頁中看到。

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

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

HTTP協議介紹

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

每個HTTP請求和響應都遵循相同的格式,一個HTTP包含Header和Body兩部分,其中Body是可選的。 HTTP響應的Header中有一個Content-Type表明響應的內容格式。如text/html表示HTML網頁。

HTTP GET請求的格式:

HTTP響應的格式:

處女版自定義web框架

  經過上面的補充學習,我們知道了要想讓我們自己寫的web server端正經起來,必須要讓我們的Web server在給客戶端回覆訊息的時候按照HTTP協議的規則加上響應狀態行,這樣我們就實現了一個正經的Web框架了。

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 1:
    # 等待連線
    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(b'HTTP/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路徑返回不同內容的需求。

  但是問題又來了,如果有很多很多路徑要判斷怎麼辦?難道要挨個寫if判斷? 當然不用,我們有更聰明的辦法。

"""
根據URL中不同的路徑返回不同的內容--函式版
"""

import socket
sk = socket.socket()
sk.bind(("127.0.0.1", 8080))  # 繫結IP和埠
sk.listen()  # 監聽


# 將返回不同的內容部分封裝成函式
def index(url):
    s = "這是{}頁面!".format(url)
    return bytes(s, encoding="utf8")


def home(url):
    s = "這是{}頁面!".format(url)
    return bytes(s, encoding="utf8")


while 1:
    # 等待連線
    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(b'HTTP/1.1 200 OK\r\n\r\n')  # 因為要遵循HTTP協議,所以回覆的訊息也要加狀態行
    # 根據不同的路徑返回不同內容,response是具體的響應體
    if url == "/index/":
        response = index(url)
    elif url == "/home/":
        response = home(url)
    else:
        response = b"404 not found!"

    conn.send(response)
    conn.close()

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

  看起來上面的程式碼還是要挨個寫if判斷,怎麼辦?我們還是有辦法!(只要思想不滑坡,方法總比問題多!)

"""
根據URL中不同的路徑返回不同的內容--函式進階版
"""

import socket
sk = socket.socket()
sk.bind(("127.0.0.1", 8080))  # 繫結IP和埠
sk.listen()  # 監聽


# 將返回不同的內容部分封裝成函式
def index(url):
    s = "這是{}頁面!".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 1:
    # 等待連線
    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(b'HTTP/1.1 200 OK\r\n\r\n')  # 因為要遵循HTTP協議,所以回覆的訊息也要加狀態行
    # 根據不同的路徑返回不同內容
    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!"

    # 返回具體的響應訊息
    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 1:
    # 等待連線
    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(b'HTTP/1.1 200 OK\r\n\r\n')  # 因為要遵循HTTP協議,所以回覆的訊息也要加狀態行
    # 根據不同的路徑返回不同內容
    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!"

    # 返回具體的響應訊息
    conn.send(response)
    conn.close()

讓網頁動態起來

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

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

"""
根據URL中不同的路徑返回不同的內容--函式進階版
返回HTML頁面
讓網頁動態起來
"""

import socket
import time

sk = socket.socket()
sk.bind(("127.0.0.1", 8080))  # 繫結IP和埠
sk.listen()  # 監聽


# 將返回不同的內容部分封裝成函式
def index(url):
    with open("index.html", "r", encoding="utf8") as f:
        s = f.read()
        now = str(time.time())
        s = s.replace("@@oo@@", now)  # 在網頁中定義好特殊符號,用動態的資料去替換提前定義好的特殊符號
    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 1:
    # 等待連線
    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(b'HTTP/1.1 200 OK\r\n\r\n')  # 因為要遵循HTTP協議,所以回覆的訊息也要加狀態行
    # 根據不同的路徑返回不同內容
    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!"

    # 返回具體的響應訊息
    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模組版
"""

import time
from wsgiref.simple_server import make_server


# 將返回不同的內容部分封裝成函式
def index(url):
    with open("index.html", "r", encoding="utf8") as f:
        s = f.read()
        now = str(time.time())
        s = s.replace("@@oo@@", now)
    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),
]


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>

index2.html檔案

使用jinja2渲染index2.html檔案:

from wsgiref.simple_server import make_server
from jinja2 import Template


def index():
    with open("index2.html", "r") as f:
        data = f.read()
    template = Template(data)  # 生成模板檔案
    ret = template.render({"name": "Alex", "hobby_list": ["燙頭", "泡吧"]})  # 把資料填充到模板裡面
    return [bytes(ret, encoding="utf8"), ]


def home():
    with open("home.html", "rb") as f:
        data = f.read()
    return [data, ]


# 定義一個url和函式的對應關係
URL_LIST = [
    ("/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 URL_LIST:
        if i[0] == url:
            func = i[1]  # 去之前定義好的url列表裡找url應該執行的函式
            break
    if func:  # 如果能找到要執行的函式
        return func()  # 返回函式的執行結果
    else:
        return [bytes("404沒有該頁面", encoding="utf8"), ]


if __name__ == '__main__':
    httpd = make_server('', 8000, run_server)
    print("Serving HTTP on port 8000...")
    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

Django官網下載頁面

安裝(安裝最新LTS版):

pip3 install django==1.11.9

建立一個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報錯:

Django 啟動時報錯 “UnicodeEncodeError ...”

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

 Django 啟動報錯“SyntaxError: Generator expression must be parenthesized”

報這個錯很大可能是因為使用了Python3.7.0,而目前(2018-06-12)Python3.7.0和Django還有點相容性問題,換回Python3.6的環境即可。