如何搭建一个简易的Web框架
Web框架本質
什么是Web框架, 如何自己搭建一個簡易的Web框架?其實, 只要了解了HTTP協議, 這些問題將引刃而解.
簡單的理解:? 所有的Web應用本質上就是一個socket服務端, 而用戶的瀏覽器就是一個socket客戶端.
用戶在瀏覽器的地址欄輸入網址, 敲下回車鍵便會給服務端發送數據, 這個數據是要遵守統一的規則(格式)的, 這個規則便是HTTP協議. HTTP協議主要規定了客戶端和服務器之間的通信格式
瀏覽器收到的服務器響應的相關信息可以在瀏覽器調試窗口(F12鍵開啟)的Network標簽頁中查看, 點擊view source即可以查看原始響應數據(有些網頁可能并沒有該項)
訪問碼云網站的原始響應數據(節選)
HTTP/1.1 200 OK
Date: Thu, 16 May 2019 13:30:59 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
每個HTTP請求和響應都遵循相同的格式, 一個HTTP包含Header和Body兩部分, 其中Body是可選的. HTTP響應的Header中有一個響應的內容格式. 如text/html表示HTML網頁
? HTTP GET請求的格式??
? HTTP 響應的格式??
以上內容總結為一句話便是:?要使自己寫的Web server端正常運行起來, 必須要使我們自己的Web server端在給客戶端回復消息時按照HTTP協議的規則加上響應狀態行
自定義Web框架
一 響應指定內容的Web框架
瀏覽器訪問127.0.0.1:9001將返回Hello World標題字樣
import socket # 導入socket模塊def main():# 實例化socket對象sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 綁定IP地址與端口sock.bind(('127.0.0.1', 9001))# 監聽 sock.listen()while True:conn, addr = sock.accept()data = conn.recv(1024)str = data.decode("UTF-8").strip(" ")print("瀏覽器請求信息>>>", str)# 如果瀏覽器請求信息非空則進行回復if str:# 給回復的消息加上響應狀態行conn.send(b"HTTP/1.1 200 OK\r\n\r\n")conn.send(b"<h1>Hello World</h1>")conn.close()# 否則跳過本次循環, 開始下一次循環else:continueif __name__ == "__main__":main()二 響應HTML文件的Web框架
? (1)?首先創建一個html文件??
??一個展示標題與當前時間的網頁, 命名為index.html
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><style>#in1{width: 400px;height: 60px;font-size: 26px;font-weight: bloder;line-height: 30px;border: none;}</style><title>index</title> </head> <body><h1>歡迎訪問簡易版Web框架主頁</h1><input type="text" id="in1"/><script>var item;function f(){var time = new Date(); // 實例化時間對象var year = time.getFullYear(); // 獲得年var month = time.getMonth() + 1; // 獲得月 var date = time.getDate(); // 獲得日var hours = time.getHours(); // 獲得小時 var minutes = time.getMinutes(); // 獲得分鐘var seconds = time.getSeconds(); // 獲得秒 // 月份與日期的顯示為兩位數字如01月01日if(month < 10 ){month = "0" + month;}if(date < 10 ){date = "0" + date;}// 時間拼接var dateTime = year + "年" + month + "月" + date + "日" + hours + "時" + minutes + "分" + seconds + "秒";// 利用ID獲取到input元素var inputEle = document.getElementById("in1");// 將input元素的值設置為當前時間 inputEle.value = dateTime;}// 定義啟動函數function start(){// 初始化當前時間 f();// 利用定時器每隔一段時間執行獲取當前時間與賦值函數f item = setInterval(f, 1000);}// 調用啟動函數 start()</script> </body> </html>在該html文件中可添加img標簽, 其src屬性值如果是網絡地址也是可以直接在瀏覽器上現實的
在該html文件中的css樣式與js操作同樣可以直接在瀏覽器上顯示出來
? (2)?準備服務端程序, 文件命名為server.py??
import socket # 導入socket模塊 import os # 導入os模塊def main():# 利用os模塊拼接路徑html_path = os.path.join(os.path.dirname(__file__), "index.html")# 實例化socket對象sk = socket.socket()# 綁定IP地址與端口sk.bind(('127.0.0.1',9001))# 監聽 sk.listen()# 計數i = 1 while True:# 等待瀏覽器連接獲取連接conn, _ = sk.accept()# 接收瀏覽器請求data = conn.recv(1024)# 將瀏覽器請求轉換為字符串并格式化str = data.decode('utf-8').strip(" ")# 打印瀏覽器響應print('瀏覽器請求信息>>>:', str, i)# 計數自加i += 1 # 如果瀏覽器請求內容并不為空, 響應瀏覽器請求if str:# 為響應的數據加上相應狀態行conn.send(b'HTTP/1.1 200 ok \r\n\r\n')# 以bytes數據類型打開html文件with open(html_path,'rb') as f:# 讀取數據data = f.read()# 發送html文件數據 conn.send(data)# 關閉與瀏覽器的連接 conn.close()# 若瀏覽器請求信息為空則關閉連接并跳過本次循環, 開始下一次循環else:conn.close()continueif __name__ == "__main__":main()注意: 該例子使用相對路徑, index.html與server.py需在同一目錄下
三 根據瀏覽器請求響應數據的Web框架
以上簡易的框架基本上都是指定了要給瀏覽器返回什么數據, 這樣肯定滿足不了我們的需求, 那么如何才能根據瀏覽器的請求, 響應相對應的數據呢?
CSS, JS, 圖片等文件都叫做網站的靜態文件
??(1) 為了測試, 首先創建一個html文件, 命名為index.html??
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>index</title><!-- 引入外部CSS文件 --><link rel="stylesheet" href="css.css"><!-- 引入外部JS文件 --><script src="js.js"></script> </head> <body><h1>歡迎訪問Web框架首頁</h1><!-- 綁定事件 --><div onmouseover="mOver(this)"; onmouseout="mOut(this)">把鼠標移到上面</div> </body> </html>? ??(2) 接著創建一個CSS文件, 命名為css.css??
div {/* 初始化元素背景色為綠色 */background-color:green;/* 初始化元素寬200px */width:200px;/* 初始化元素高200px */height:200px;/* 初始化元素內填充40px */padding:40px;/* 初始化字體顏色為白色 */color:#ffffff; }?? (3)?再創建一個JS文件, 命名為js.js??
// 定義鼠標覆蓋事件觸發函數 function mOver(obj) {// 文字替換為"謝謝"obj.innerHTML="謝謝"// 背景顏色更改為紅obj.style.backgroundColor= "red"; } // 定義鼠標非覆蓋狀態事件觸發函數 function mOut(obj) { // 文字替換為"把鼠標以到上面"obj.innerHTML="把鼠標移到上面"// 背景顏色更改為綠obj.style.backgroundColor= "green"; }? ???(4)?準備服務端程序, 文件命名為server.py??
import os # 導入os模塊 import socket # 導入socket模塊 # 導入線程模塊 from threading import Thread # 實例化socket對象 server = socket.socket() # 綁定IP及端口 server.bind(("127.0.0.1", 9001)) server.listen()# 路徑拼接 html_path = os.path.join(os.path.dirname(__file__), "index.html") css_path = os.path.join(os.path.dirname(__file__), "css.css") js_path = os.path.join(os.path.dirname(__file__), "js.js")def html(conn):"""響應"/"請求"""conn.send(b'HTTP/1.1 200 ok \r\n\r\n')with open(html_path, mode="rb") as f:content = f.read()conn.send(content)conn.close()def css(conn):"""響應"/css.css"請求"""conn.send(b"HTTP/1.1 200 ok \r\n\r\n")with open(css_path, mode="rb") as f:content = f.read()conn.send(content)conn.close()def js(conn):"""響應"/js.js"請求"""conn.send(b"HTTP/1.1 200 ok \r\n\r\n")with open(js_path, mode="rb") as f:content = f.read()conn.send(content)conn.close()def NotFound(conn):conn.send(b"HTTP/1.1 200 ok \r\n\r\n")conn.send(b"<h1>404NotFound!</h1>")# 請求列表 request_list = [("/", html),("/css.css", css),("/js.js", js) ]def get(conn):"""處理響應函數"""try: # 異常處理req = conn.recv(1024).decode("UTF-8")req = req.split("\r\n")[0].split()[1]# 打印瀏覽器請求print(req)except IndexError:pass# 遍歷請求列表進行響應for request in request_list:# 若瀏覽器請求信息等于請求列表中的項,則進行響應# 判斷服務端是否能夠進行響應if req == request[0]:# 獲取線程對象, 實現并發t = Thread(target=request[1], args=(conn, ))# 啟動線程 t.start()# 響應后結束遍歷breakelse: # 若本次循環未匹配則跳過本次循環開始下一次continueelse: # 若所有請求皆不匹配則調用NotFound函數, 表示無法響應 NotFound(conn)def main():while True:# 利用線程實現并發# 獲取TCP連接conn, _ = server.accept()t = Thread(target=get, args=(conn,))t.start()if __name__ == "__main__":main()? 注意: 該例子使用相對路徑, index.html, css.css, js.js與server.py需在同一目錄下
四 進階版Web框架
以上的幾版Web框架比較基礎, 一些定義的函數使用起來也比較繁瑣, 可定制性很差, 修改起來也比較困難.?
利用Python提供的一些模塊可以簡化一些步驟, 并且使框架的可定制性更好, 可以方便其他人進行定制使用
? 結構示意圖??
?
??文件結構??
構建Web框架
??(1) 構建目錄??
新建文件夾frame
1)?文件夾內創建__init__.py文件(內容為空)
2) 文件夾內新建文件夾file
? (2) 準備html文件??
index.html文件
<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><style>/* 時間展示樣式 */#in1{width: 400px;height: 60px;font-size: 26px;font-weight: bloder;line-height: 30px;border: none;}</style><title>index</title></head><body><!-- 標題 --><h1>歡迎訪問簡易版Web框架主頁</h1><!-- 動態替換(模板渲染), 刷新頁面動態刷新 --><h2>@</h2><input type="text" id="in1"/><!-- 認證表單 --><form action="http://127.0.0.1:9001/auth/" method="post"><label for="username">用戶名</label><input type="text" id="username" name="username"/><label for="password">密碼</label><input type="password" id="password" name="password"/><input type="submit"></form><script>var item;function f(){var time = new Date();var year = time.getFullYear();var month = time.getMonth() + 1;var date = time.getDate();var hours = time.getHours();var minutes = time.getMinutes();var seconds = time.getSeconds();// 月份與日期的顯示為兩位數字如01月01日if(month < 10 ){month = "0" + month;}if(date < 10 ){date = "0" + date;}// 時間拼接var dateTime = year + "年" + month + "月" + date + "日" + hours + "時" + minutes + "分" + seconds + "秒";var inputEle = document.getElementById("in1");inputEle.value = dateTime;}function start(){f();item = setInterval(f, 1000);}start()</script></body> </html>success.html文件
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>success</title> </head> <body><h1>登陸成功</h1> </body> </html>? 將以上兩個html文件保存到file文件夾內
? (3) models.py文件??
首先需要創建一個數據庫, 這里使用MySQL
-- 登錄MySQL mysql -u用戶名 -p密碼-- 查看數據庫 SHOW DATABASES;-- 創建數據庫 CREATE DATABASE 庫名;/*這里創建一個名為dbf的數據庫CREATE DATABASE dbf; */利用pymysql模塊操作數據庫, 建表插入數據
models.py文件
import pymysql # 導入pymysql模塊, 需要下載 # pip install pymysqldef main():conn = pymysql.connect(host = "127.0.0.1", # mysql主機地址port = 3306, # mysql端口user = "root", # mysql遠程連接用戶名password = "123", # mysql遠程連接密碼database = "dbf", # mysql使用的數據庫名charset = "UTF8" # mysql使用的字符編碼,默認為utf8 )# 實例化游標對象cursor = conn.cursor(pymysql.cursors.DictCursor)# 創建表格sql1 = """CREATE TABLE userinfo(id int PRIMARY KEY auto_increment,username char(12) NOT NULL UNIQUE,password char(20) NOT NULL);"""# 向創建的表格中插入數據sql2 = """INSERT INTO userinfo(username, password) VALUES("a", "1"),("b", "2");"""# 將sql指令提交到緩存 cursor.execute(sql1)cursor.execute(sql2)# 提交并執行sql指令 conn.commit()# 關閉游標 cursor.close()# 關閉與數據庫的連接 conn.close()if __name__ == "__main__":main()? (4) auth.py文件??
用于驗證用戶登錄信息
auth.py文件
import pymysql # 導入pymysql模塊def auth(username, password):conn = pymysql.connect(host = "127.0.0.1", # mysql主機地址port = 3306, # mysql端口user = "root", # mysql遠程連接用戶名password = "123", # mysql遠程連接密碼database = "dbf", # mysql使用的數據庫名charset = "UTF8" # mysql使用的字符編碼,默認為utf8 )# 打印用戶信息: 用戶名, 密碼print("userinfo", username, password)# 實例化游標對象cursor = conn.cursor(pymysql.cursors.DictCursor)# sql查詢指令sql = "SELECT * FROM userinfo WHERE username=%s AND password=%s"# res獲取影響行數res = cursor.execute(sql, [username, password])if res: # 數據庫中存在該數據, 返回Truereturn Trueelse: # 數據庫中不存在該數據, 返回Falsereturn False? (5) views.py文件??
用于處理數據
views.py文件
"""該模塊存放瀏覽器請求對應的網頁與urls模塊中url_list列表中的項存在映射關系若要添加新的內容, 只需要定義相應的函數, 并將函數名以字符串的形式加入到__all__列表中 """import os # 導入os模塊 import time # 導入time模塊 import auth # 導入auth.py from urllib.parse import parse_qs # 導入parse_qs用于解析數據# 展示所有可用方法 __all__ = ["index","authed"# "css" ]# 路徑拼接(針對windows"/", linu需要把"/"改為"\") index_path = os.path.join( os.path.dirname(__file__), "file/index.html") success_path = os.path.join( os.path.dirname(__file__), "file/success.html")def index(environ):with open(index_path, mode="rb") as f:data = f.read().decode("UTF-8")# 將特殊符號@替換為當前時間, 實現動態網站data = data.replace("@", time.strftime(("%Y-%m-%d %H:%M:%S")))return data.encode("UTF-8")def authed(environ):if environ.get("REQUEST_METHOD") == "POST":try:request_body_size = int(environ.get("CONTENT_LENGTH", 0))except (ValueError):request_body_size = 0request_data = environ["wsgi.input"].read(request_body_size)print(">>>", request_data) # bytes數據類型print("????", environ["QUERY_STRING"]) # "空的" - post請求只能按照以上方式獲取數據# parse_qs負責解析數據# 不管是POST還是GET請求都不能直接拿到數據, 拿到的數據仍需要進行分解提取# 所以引入urllib模塊中的parse_qs方法request_data = parse_qs(request_data.decode("UTF-8"))print("拆解后的數據", request_data) # {"username": ["a"], "password": ["1"]}username = request_data["username"][0]password = request_data["password"][0]status = auth.auth(username, password)if status:with open(success_path, mode="rb") as f:data = f.read()else:# 如果直接返回中文, 沒有給瀏覽器指定編碼格式, 默認是gbk, 需要進行gbk編碼, 使瀏覽器能夠識別# 這里已經指定了編碼# start_response("200 OK", [("Content-Type", "text/html;charset=UTF8")])data = "<h1>用戶名或密碼錯誤, 登陸失敗</h1>".encode("UTF-8")return dataif environ.get("REQUEST_METHOD") == "GET":print("????", environ["QUERY_STRING"]) # "username='a'&password='1'"字符出數據類型 request_data = environ["QUERY_STRING"]# parse_qs負責解析數據# 不管是POST還是GET請求都不能直接拿到數據, 拿到的數據仍需要進行分解提取# 所以引入urllib模塊中的parse_qs方法request_data = parse_qs(request_data)print("拆解后的數據", request_data) # {"username": ["a"], "password": ["1"]}username = request_data["username"][0]password = request_data["password"][0]print(username, password)status = auth.auth(username, password)if status:with open(success_path, mode="rb") as f:data = f.read()else:# 如果直接返回中文, 沒有給瀏覽器指定編碼格式, 默認使gbk, 需要進行gbk編碼, 是瀏覽器能夠識別# 這里已經指定了編碼# start_response("200 OK", [("Content-Type", "text/html;charset=UTF8")])data = "<h1>用戶名或密碼錯誤, 登陸失敗</h1>".encode("UTF-8")return data# def css(environ): # with open("css.css", mode="rb") as f: # data = f.read() # return data? (6) urls.py文件??
映射表
urls.py文件
from views import index, authed """可在此處按照類似格式添加任意內容例如再向url_list列表中添加一項, 按照如下格式("/css.css", css), 只需要再在views.py文件中創建一個對應的函數即可 """ url_list = [("/", index),("/auth/", authed)# ("/css.css", css) ]? (7) manage.py文件??
? 主邏輯
manage.py文件
from urls import url_list from wsgiref.simple_server import make_serverdef application(environ, start_response):""":param environ: 包含所有請求信息的字典:param start_response: 封裝響應信息(相應行與響應頭):return: [響應主體]"""# 封裝響應信息start_response("200 OK", [("Content-Type", "text/html;charset=UTF8")])# 打印包含所有請求信息的字典print(environ)# 打印請求路徑信息print(environ["PATH_INFO"])path = environ["PATH_INFO"]for p in url_list:if path == p[0]:data = p[1](environ)breakelse:continueelse:data = b"<h1>Sorry 404!, NOT Found The Page</h1>"# 返回響應主體# 必須遵守此格式[內容]return [data]if __name__ == "__main__":# 綁定服務器IP地址與端口號, 調用函數frame = make_server("127.0.0.1", 9001, application)# 開始監聽HTTP請求frame.serve_forever()??至此一個簡易的Web框架就搭建好了, 我再來簡單介紹一下啟動步驟??
啟動步驟
(1) 首先按照步驟,?執行(3) models.py文件
1) 創建數據庫
2) 執行models.py
(2) 執行manage.py啟動服務器
(3) 根據指定IP及端口, 使用瀏覽器訪問
這里指定127.0.0.1:9001
效果演示
index頁面
登錄成功
登錄失敗
錯誤請求
包/模塊解析
以上的框架中用到了兩個比較重要的包/模塊: wsgiref模塊與urllib包, 下面介紹一下
wsgiref模塊
WSGI簡介引用
WSGI(Web?Server?Gateway?Interface)是一種規范, 它定義了使用Python編寫的web應用程序與web服務器程序之間的接口格式, 實現web應用程序與web服務器程序間的解耦
常用的WSGI服務器有uwsgi、Gunicorn. 而Python標準庫提供的獨立WSGI服務器叫做wsgiref, Django開發環境用的就是這個模塊來做服務器
wsgire模塊簡介引用
wsgiref模塊其實就是將整個請求信息給封裝了起來, 比如它將所有請求信息封裝成了一個叫做request的對象, 那么直接利用request.path就能獲取到本次請求的路徑. request.method就能獲取到本次請求的請求方式(GET/POST)等
?urllib包
urllib簡介《Python參考手冊(第4版)》
urllib包提供了一個高級接口, 用于編寫需要與HTTP服務器、FTP服務器和本地文件交互的客戶端. 典型的應用程序包括從網頁抓取數據、自動化、代理、Web爬蟲等. 這是可配置程度最高的庫模塊之一
由于urllib包中功能模塊眾多且功能強大, 在此不做過多介紹, 僅介紹本框架所用模塊
在views.py中我們通過?from urllib.parse import parse_qs?導入了urllib包下的parser模塊中的parse_qs方法
parse模塊《Python參考手冊(第4版)》
urllib.parser模塊用于操作URL字符串, 如"http://www.python.org"
其中parse_qs方法:
parse_qs(qs [, keep_blank_values [, strict_parsing]])
解析URL編碼的(MIME類型為application/x-www-form-urlencoded)查詢字符串qs, 并返回字典, 其中鍵是查詢變量名稱, 值是為每個名稱定義的值列表. keep_blank_values是一個布爾值標志,控制如何處理空白值. 如果為True, 則它們包含在字典中, 值設置為空字符串; 如果為False(默認值), 則將其丟棄。strict_parsing是一個布爾值標志, 如果為True, 則將解析錯誤轉換為ValueError異常. 默認情況下會忽略錯誤
?
以上就是本人在學習Django框架前的學習總結, 可供學習參考
?
轉載于:https://www.cnblogs.com/dmcs95/p/10886462.html
總結
以上是生活随笔為你收集整理的如何搭建一个简易的Web框架的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 背景图片随着浏览器拖动而变化
- 下一篇: LeetCode 52.N-Queens