Web框架本質 我們可以這樣理解:所有的Web應用本質上就是一個socket服務端,而用戶的瀏覽器就是一個socket客戶端。 這樣我們就可以自己實現Web框架了。 半成品自定義web框架 可以說Web服務本質上都是在這十幾行代碼基礎上擴展出來的。這段代碼就是它們的祖宗。 用戶的瀏覽器一輸入網址,會 ...
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\nHost: 127.0.0.1:8080\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-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\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nDNT: 1\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: 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()View Code
讓網頁動態起來
這網頁能夠顯示出來了,但是都是靜態的啊。頁面的內容都不會變化的,我想要的是動態網站。
沒問題,我也有辦法解決。我選擇使用字元串替換來實現這個需求。(這裡使用時間戳來模擬動態的數據)
""" 根據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()View Code
好了,在這停頓...