要想清楚地理解 python web 框架,首先要清楚瀏覽器訪問服務器的過程。
用戶通過瀏覽器瀏覽網站的過程:
用戶瀏覽器(socket客戶端)
3. 客戶端往服務端發消息
6. 客戶端接收消息
7. 關閉
網站服務器(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()
你會發現,運行程序之后並且用瀏覽器訪問 127.0.0.1:8001 ,程序會報錯,瀏覽器顯示“該網頁無法正常運作”,如下圖
為什么呢?這時候就要引出 HTTP 協議了。
HTTP協議
HTTP是一個客戶端終端(用戶)和服務器端(網站)請求和應答的標准(TCP)。
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()
這時候,在瀏覽器上面就可以看到正確的頁面了,並且可以調出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()
這時候,我們訪問不同的路徑,例如 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()
返回具體的 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()
返回動態 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()

<!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 頁面,都會返回一個當前時間。
小結一下
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()
你會發現,使用了 wsgiref 模塊之后,程序封裝更好了,代碼邏輯也更加清晰了。
WSGI 協議
經過上面的 wsgiref 模塊的示例,在使用通信模塊的方便之余,你可能已經意識到一個問題,類似於 wsgiref 這樣的模塊肯定不止一個,我們自己寫的 url 處理函數需要和這些模塊進行通信,那么,我怎么知道這些模塊傳過來的信息是什么格式?如果各個模塊傳過來的信息結構都不一樣的話,那豈不是說我得根據每一個模塊去定制它專門的 url 處理函數?這不科學,這中間肯定需要一個協議進行約束,這個協議,就叫 WSGI 協議。
下節預告
到了這里,相信聰明的你已經理解清楚整個 瀏覽器 服務器的訪問過程,並且 socket 服務端功能划分有了清晰的認知。
下一節,我們將走進 Django 框架,領略 Django 的魅力。
作者: 守護窗明守護愛
出處: https://www.cnblogs.com/chuangming/p/9072251.html
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出。如有問題,可郵件(1269619593@qq.com)咨詢.