一步一步理解 python web 框架,纔不會從入門到放棄 -- 啓程出發

要想清楚地理解 python web 框架,首先要清楚瀏覽器訪問服務器的過程。

用戶經過瀏覽器瀏覽網站的過程:html

  用戶瀏覽器(socket客戶端)python

    3. 客戶端往服務端發消息
    6. 客戶端接收消息
    7. 關閉web

  網站服務器(socket服務端)後端

    1. 啓動,監聽
    2. 等待客戶端鏈接
    4. 服務端收消息
    5. 服務端回消息
    7. 關閉(通常都不會關閉)瀏覽器

下面,咱們先寫一個服務端程序,來模擬瀏覽器服務器訪問過程。服務器

'''
簡單的web服務端示例
'''

import socket

# 生成socket實例對象,默認family=AF_INET, type=SOCK_STREAM, 也就是TCP通訊
sk = socket.socket()
# 綁定IP和端口
sk.bind(("127.0.0.1", 8001))
# 監聽
sk.listen()

# 寫一個死循環,一直等待客戶端來鏈接
while 1:
    # 獲取與客戶端的鏈接,conn爲客戶端鏈接服務器的socket,_爲客戶端的地址
    conn, _ = sk.accept()
    # 接收客戶端發來消息
    data = conn.recv(8096)
    print(data)
    # 給客戶端回覆消息
    conn.send(b'<h1>hello s10!</h1>')
    # 關閉客戶端鏈接服務器的socket
    conn.close()
    # 關閉服務器socket
    sk.close()
View Code

你會發現,運行程序以後而且用瀏覽器訪問 127.0.0.1:8001 ,程序會報錯,瀏覽器顯示「該網頁沒法正常運做」,以下圖框架

 爲何呢?這時候就要引出 HTTP 協議了。socket

HTTP協議

HTTP是一個客戶端終端(用戶)和服務器端(網站)請求和應答的標準(TCP)。ide

HTTP請求/響應步驟:函數

1. 客戶端鏈接到Web服務器
一個HTTP客戶端,一般是瀏覽器,與Web服務器的HTTP端口(默認爲80)創建一個TCP套接字鏈接。

2. 發送HTTP請求
經過TCP套接字,客戶端向Web服務器發送一個文本的請求報文,一個請求報文由請求行、請求頭部、空行和請求數據4部分組成。

3. 服務器接受請求並返回HTTP響應
Web服務器解析請求,定位請求資源。服務器將資源複本寫到TCP套接字,由客戶端讀取。一個響應由狀態行、響應頭部、空行和響應數據4部分組成。

4. 釋放鏈接TCP鏈接
若connection 模式爲close,則服務器主動關閉TCP鏈接,客戶端被動關閉鏈接,釋放TCP鏈接;若connection 模式爲keepalive,則該鏈接會保持一段時間,在該時間內能夠繼續接收請求;

5. 客戶端瀏覽器解析HTML內容
客戶端瀏覽器首先解析狀態行,查看代表請求是否成功的狀態代碼。而後解析每個響應頭,響應頭告知如下爲若干字節的HTML文檔和文檔的字符集。客戶端瀏覽器讀取響應數據HTML,根據HTML的語法對其進行格式化,並在瀏覽器窗口中顯示。

 

瀏覽器和服務端通訊都要遵循一個HTTP協議(消息的格式要求)

關於HTTP協議:

1. 瀏覽器往服務端發的叫 請求(request)
 請求的消息格式:

請求方法 路徑 HTTP/1.1\r\n
k1:v1\r\n
k2:v2\r\n
\r\n
請求數據

2. 服務端往瀏覽器發的叫 響應(response)
 響應的消息格式:

HTTP/1.1 狀態碼 狀態描述符\r\n
k1:v1\r\n
k2:v2\r\n
\r\n
響應正文 <-- html的內容

 

HTTP請求報文格式:

HTTP響應報文格式:

再回到咱們剛纔的程序,程序報錯的緣由是接收到了瀏覽器的訪問報文請求,可是咱們的服務器程序在響應的時候並無按照HTTP響應格式(一個響應由狀態行、響應頭部、空行和響應數據4部分組成)進行迴應,因此瀏覽器在處理服務器的響應的時候就會出錯。

所以,咱們要在發送給瀏覽器的響應中按照HTTP響應格式加上 狀態行、響應頭部、空行和響應數據 這四部分。

'''
簡單的web服務端示例
'''

import socket

# 生成socket實例對象,默認family=AF_INET, type=SOCK_STREAM, 也就是TCP通訊
sk = socket.socket()
# 綁定IP和端口
sk.bind(("127.0.0.1", 8001))
# 監聽
sk.listen()

# 寫一個死循環,一直等待客戶端來鏈接
while 1:
    # 獲取與客戶端的鏈接,conn爲客戶端鏈接服務器的socket,_爲客戶端的地址
    conn, _ = sk.accept()
    # 接收客戶端發來消息
    data = conn.recv(8096)
    print(data)
    # 給客戶端回覆消息,都是以byte的形式傳輸
    # 響應協議版本是 http/1.1,狀態碼是 200,狀態碼描述咱們定義爲 OK,而後加上換行符
    # 響應頭部咱們寫上content-type:text/html; 字符編碼爲 charset=utf-8,加上兩個換行符,這一次send先不傳響應正文
    conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n')
    # 因爲前一個send已經傳過一次響應規定的格式了,這一個send咱們就傳想要在網頁上顯示的正文內容
    conn.send(b'<h1>hello s10!</h1>')
    # 關閉客戶端鏈接服務器的socket
    conn.close()
    # 關閉服務器socket
    sk.close()
View Code

這時候,在瀏覽器上面就能夠看到正確的頁面了,而且能夠調出Chrome的開發者工具查看到咱們傳過來的HTTP響應格式。

 

 根據不一樣的路徑返回不一樣的內容

細心的你可能會發現,如今不管咱們輸出什麼樣的路徑,只要保持 IP 和端口號不變,瀏覽器頁面顯示的都是一樣的內容,這不太符合咱們平常的使用場景。

若是我想根據不一樣的路徑返回不一樣的內容,應該怎麼辦呢?

這時候就須要咱們把服務器收到的請求報文進行解析,讀取到其中的訪問路徑。

觀察收到的HTTP請求,會發現,它們的請求行、請求頭部、請求數據是以 \r\n 進行分隔的,因此咱們能夠根據 \r\n 對收到的請求進行分隔,取出咱們想要的訪問路徑。

"""
完善的web服務端示例
根據不一樣的路徑返回不一樣的內容
"""

import socket

# 生成socket實例對象
sk = socket.socket()
# 綁定IP和端口
sk.bind(("127.0.0.1", 8001))
# 監聽
sk.listen()

# 寫一個死循環,一直等待客戶端來鏈接
while 1:
    # 獲取與客戶端的鏈接
    conn, _ = sk.accept()
    # 接收客戶端發來消息
    data = conn.recv(8096)
    # 把收到的數據轉成字符串類型
    data_str = str(data, encoding="utf-8")  # bytes("str", enconding="utf-8")
    # print(data_str)
    # 用\r\n去切割上面的字符串
    l1 = data_str.split("\r\n")
    # l1[0]得到請求行,按照空格切割上面的字符串
    l2 = l1[0].split()
    # 請求行格式爲:請求方法 URL 協議版本,所以 URL 是 l2[1]
    url = l2[1]
    # 給客戶端回覆消息
    conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n')
    # 想讓瀏覽器在頁面上顯示出來的內容都是響應正文

    # 根據不一樣的url返回不一樣的內容
    if url == "/yimi/":
        response = b'<h1>hello yimi!</h1>'
    elif url == "/xiaohei/":
        response = b'<h1>hello xiaohei!</h1>'
    else:
        response = b'<h1>404! not found!</h1>'
    conn.send(response)
    # 關閉
    conn.close()
    sk.close()
View Code

這時候,咱們訪問不一樣的路徑,例如 http://127.0.0.1:8001/yimi/   http://127.0.0.1:8001/xiaohei/ 會在瀏覽器上顯示不同的內容

能夠看到,咱們如今的程序邏輯不是很清晰,咱們能夠改一下,url 用一個列表存起來,url 對應的響應分別寫成一個個函數,經過函數調用進行 url 訪問,你會發現,這跟某個框架的處理方式很像很像(偷笑罒ω罒~~~)

"""
完善的web服務端示例
函數版根據不一樣的路徑返回不一樣的內容
進階函數版 不寫if判斷了,用url名字去找對應的函數名
"""

import socket

# 生成socket實例對象
sk = socket.socket()
# 綁定IP和端口
sk.bind(("127.0.0.1", 8001))
# 監聽
sk.listen()

# 定義一個處理/yimi/的函數
def yimi(url):
    ret = '<h1>hello {}</h1>'.format(url)
    # 由於HTTP傳的是字節,因此要把上面的字符串轉成字節
    return bytes(ret, encoding="utf-8")


# 定義一個處理/xiaohei/的函數
def xiaohei(url):
    ret = '<h1>hello {}</h1>'.format(url)
    return bytes(ret, encoding="utf-8")


# 定義一個專門用來處理404的函數
def f404(url):
    ret = "<h1>你訪問的這個{} 找不到</h1>".format(url)
    return bytes(ret, encoding="utf-8")


url_func = [
    ("/yimi/", yimi),
    ("/xiaohei/", xiaohei),
]


# 寫一個死循環,一直等待客戶端來連我
while 1:
    # 獲取與客戶端的鏈接
    conn, _ = sk.accept()
    # 接收客戶端發來消息
    data = conn.recv(8096)
    # 把收到的數據轉成字符串類型
    data_str = str(data, encoding="utf-8")  # bytes("str", enconding="utf-8")
    # print(data_str)
    # 用\r\n去切割上面的字符串
    l1 = data_str.split("\r\n")
    # print(l1[0])
    # 按照空格切割上面的字符串
    l2 = l1[0].split()
    url = l2[1]
    # 給客戶端回覆消息
    conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n')
    # 想讓瀏覽器在頁面上顯示出來的內容都是響應正文

    # 根據不一樣的url返回不一樣的內容
    # 去url_func裏面找對應關係
    for i in url_func:
        if i[0] == url:
            func = i[1]
            break
    # 找不到對應關係就默認執行f404函數
    else:
        func = f404

    # 拿到函數的執行結果
    response = func(url)
    # 將函數返回的結果發送給瀏覽器
    conn.send(response)
    # 關閉鏈接
    conn.close()
View Code

 返回具體的 HTML 頁面

如今,你可能會在想,目前咱們想要返回的內容是經過函數進行返回的,返回的都是一些簡單地字節,若是我想要返回一個已經寫好的精美的 HTML 頁面應該怎麼辦呢?

咱們能夠把寫好的 HTML 頁面以二進制的形式讀取進來,返回給瀏覽器,瀏覽器再進行解析,這就能夠啦!

"""
完善的web服務端示例
函數版根據不一樣的路徑返回不一樣的內容
進階函數版 不寫if判斷了,用url名字去找對應的函數名
返回html頁面
"""

import socket

# 生成socket實例對象
sk = socket.socket()
# 綁定IP和端口
sk.bind(("127.0.0.1", 8001))
# 監聽
sk.listen()


# 定義一個處理/yimi/的函數
def yimi(url):
    # 以二進制的形式讀取
    with open("yimi.html", "rb") as f:
       ret = f.read()
    return ret


# 定義一個處理/xiaohei/的函數
def xiaohei(url):
    with open("xiaohei.html", "rb") as f:
       ret = f.read()
    return ret


# 定義一個專門用來處理404的函數
def f404(url):
    ret = "<h1>你訪問的這個{} 找不到</h1>".format(url)
    return bytes(ret, encoding="utf-8")


# 用戶訪問的路徑和後端要執行的函數的對應關係
url_func = [
    ("/yimi/", yimi),
    ("/xiaohei/", xiaohei),
]


# 寫一個死循環,一直等待客戶端來連我
while 1:
    # 獲取與客戶端的鏈接
    conn, _ = sk.accept()
    # 接收客戶端發來消息
    data = conn.recv(8096)
    # 把收到的數據轉成字符串類型
    data_str = str(data, encoding="utf-8")  # bytes("str", enconding="utf-8")
    # print(data_str)
    # 用\r\n去切割上面的字符串
    l1 = data_str.split("\r\n")
    # print(l1[0])
    # 按照空格切割上面的字符串
    l2 = l1[0].split()
    url = l2[1]
    # 給客戶端回覆消息
    conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n')
    # 想讓瀏覽器在頁面上顯示出來的內容都是響應正文

    # 根據不一樣的url返回不一樣的內容
    # 去url_func裏面找對應關係
    for i in url_func:
        if i[0] == url:
            func = i[1]
            break
    # 找不到對應關係就默認執行f404函數
    else:
        func = f404
    # 拿到函數的執行結果
    response = func(url)
    # 將函數返回的結果發送給瀏覽器
    conn.send(response)
    # 關閉鏈接
    conn.close()
View Code

 返回動態 HTML 頁面

這時候,你可能又會納悶,如今返回的都是些靜態的、固定的 HTML 頁面,若是我想返回一個動態的 HTML 頁面,應該怎麼辦?

動態的網頁,本質上都是字符串的替換,字符串替換髮生服務端,替換完再返回給瀏覽器。

這裏,咱們經過返回一個當前時間,來模擬動態 HTML 頁面的返回過程。

"""
完善的web服務端示例
函數版根據不一樣的路徑返回不一樣的內容
進階函數版 不寫if判斷了,用url名字去找對應的函數名
返回html頁面
返回動態的html頁面
"""

import socket
import time

# 生成socket實例對象
sk = socket.socket()
# 綁定IP和端口
sk.bind(("127.0.0.1", 8001))
# 監聽
sk.listen()

# 定義一個處理/yimi/的函數
def yimi(url):
    with open("yimi.html", "r", encoding="utf-8") as f:
       ret = f.read()
    # 獲得替換後的字符串
    ret2 = ret.replace("@@xx@@", str(time.ctime()))
    return bytes(ret2, encoding="utf-8")


# 定義一個處理/xiaohei/的函數
def xiaohei(url):
    with open("xiaohei.html", "rb") as f:
       ret = f.read()
    return ret


# 定義一個專門用來處理404的函數
def f404(url):
    ret = "你訪問的這個{} 找不到".format(url)
    return bytes(ret, encoding="utf-8")


url_func = [
    ("/yimi/", yimi),
    ("/xiaohei/", xiaohei),
]


# 寫一個死循環,一直等待客戶端來連我
while 1:
    # 獲取與客戶端的鏈接
    conn, _ = sk.accept()
    # 接收客戶端發來消息
    data = conn.recv(8096)
    # 把收到的數據轉成字符串類型
    data_str = str(data, encoding="utf-8")  # bytes("str", enconding="utf-8")
    # print(data_str)
    # 用\r\n去切割上面的字符串
    l1 = data_str.split("\r\n")
    # print(l1[0])
    # 按照空格切割上面的字符串
    l2 = l1[0].split()
    url = l2[1]
    # 給客戶端回覆消息
    conn.send(b'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n')
    # 想讓瀏覽器在頁面上顯示出來的內容都是響應正文

    # 根據不一樣的url返回不一樣的內容
    # 去url_func裏面找對應關係
    for i in url_func:
        if i[0] == url:
            func = i[1]
            break
    # 找不到對應關係就默認執行f404函數
    else:
        func = f404
    # 拿到函數的執行結果
    response = func(url)
    # 將函數返回的結果發送給瀏覽器
    conn.send(response)
    # 關閉鏈接
    conn.close()
服務端.py
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>yimi</title>
</head>
<body>
    <h1>Hello yimi</h1>
    <h3 style="background-color: pink">這是yimi的小站!</h3>
    <h4>@@xx@@</h4>
</body>
</html>
yimi.html

能夠看到,如今咱們每一次訪問 yimi 頁面,都會返回一個當前時間。

 小結一下

1. web 框架的本質:
  socket 服務端 與 瀏覽器的通訊
2. socket 服務端功能劃分:
  a. 負責與瀏覽器收發消息( socket 通訊) --> wsgiref/uWsgi/gunicorn...
  b. 根據用戶訪問不一樣的路徑執行不一樣的函數
  c. 從 HTML 讀取出內容,而且完成字符串的替換 --> jinja2 (模板語言)
3. Python 中 Web 框架的分類:
  1. 按上面三個功能劃分:
    1. 框架自帶 a,b,c --> Tornado
    2. 框架自帶 b 和 c,使用第三方的 a --> Django
    3. 框架自帶 b,使用第三方的 a 和 c --> Flask
  2. 按另外一個維度來劃分:
    1. Django --> 大而全(你作一個網站能用到的它都有)
    2. 其餘 --> Flask 輕量級

引入 wsgiref 模塊實現 socket 通訊

不知道你會不會以爲以前的程序中,socket 通訊特別麻煩,並且還都是同樣的套路,完徹底全能夠獨立出來作成一個模塊,要用的時候再直接引進來用就能夠了。

沒錯,有你這種想法的人還不在少數(吃鯨......),特別是一些大牛們,就 socket 通訊這一塊,作出了一些特別好用的模塊,例如咱們下面要用的 wsgiref 模塊。

"""
根據URL中不一樣的路徑返回不一樣的內容--函數進階版
返回HTML頁面
讓網頁動態起來
wsgiref模塊負責與瀏覽器收發消息(socket通訊)
"""

import time
from wsgiref.simple_server import make_server


# 將返回不一樣的內容部分封裝成函數
def yimi(url):
    with open("yimi.html", "r", encoding="utf8") as f:
        s = f.read()
        now = str(time.ctime())
        s = s.replace("@@xx@@", now)
    return bytes(s, encoding="utf8")


def xiaohei(url):
    with open("xiaohei.html", "r", encoding="utf8") as f:
        s = f.read()
    return bytes(s, encoding="utf8")


# 定義一個url和實際要執行的函數的對應關係
list1 = [
    ("/yimi/", yimi),
    ("/xiaohei/", xiaohei),
]


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"<h1>404 not found!</h1>"
    return [response, ]


if __name__ == '__main__':
    httpd = make_server('127.0.0.1', 8090, run_server)
    print("我在8090等你哦...")
    httpd.serve_forever()
View Code

你會發現,使用了 wsgiref 模塊以後,程序封裝更好了,代碼邏輯也更加清晰了。

 WSGI 協議

通過上面的 wsgiref 模塊的示例,在使用通訊模塊的方便之餘,你可能已經意識到一個問題,相似於 wsgiref 這樣的模塊確定不止一個,咱們本身寫的 url 處理函數須要和這些模塊進行通訊,那麼,我怎麼知道這些模塊傳過來的信息是什麼格式?若是各個模塊傳過來的信息結構都不同的話,那豈不是說我得根據每個模塊去定製它專門的 url 處理函數?這不科學,這中間確定須要一個協議進行約束,這個協議,就叫 WSGI 協議

下節預告

到了這裏,相信聰明的你已經理解清楚整個 瀏覽器 服務器的訪問過程,而且 socket 服務端功能劃分有了清晰的認知。

下一節,咱們將走進 Django 框架,領略 Django 的魅力。

 

做者: 守護窗明守護愛

出處: http://www.javashuo.com/article/p-qodskqkk-d.html

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出。若有問題,可郵件(1269619593@qq.com)諮詢.

相關文章
相關標籤/搜索