基於python的簡易web框架實現
一 WSGI
今天要實現的是一個簡易的web框架,寫部落格的目的也是為了讓初學者少走彎路,所以這裡會循序漸進的講解。首先我們要明白,web框架,伺服器,前端究竟有什麼樣的關係。難道不能在伺服器之中把功能全部實現嗎?當然可以,但是這違反了軟體開發的原則,叫做單一職責原則。設計模式有23種,一定要有了解。也就是說,所有的設計模式最終的目的就是為了讓工程可擴充套件,可重用。如果把一切東西都放在伺服器裡,首先損耗效能不說,也顯得不易維護。為了解決這種問題,python裡面提供了一種協議,叫做WSGI,首先明白一點,他既不是包,也不是模組,只是一種協議。用這種協議來進行開發,首先來說就是科學,再者易於維護。
WSGI協議其實就是定義了一個介面,def application(env, start_response):。也就是說,WSGI協議最終的體現是什麼呢,就是讓伺服器來處理高併發,連線等一系列的通訊問題,至於資料處理,以及業務邏輯,全放到了application這個方法裡面。只要我們的web框架實現application方法來實現業務邏輯,那麼伺服器也就只剩下轉發的作用了。這裡面再縷一縷思路。首先,對於一個網站而言,我們最直觀的就是在搜尋引擎裡寫入URL來訪問頁面。此時瀏覽器把請求給了伺服器。瀏覽器的職責就是與使用者進行資訊互動,並顯示資訊。而伺服器接收到了這個請求之後,通過WSGI協議把請求進行轉發給web框架。這時的web框架就根據不同的請求,來實現不同的業務邏輯,得到的資料通過伺服器轉發給前端顯示。這就是整個流程。大家想想,這樣不就解決了程式耦合的問題了嗎,面向物件的思想就是在不斷地解耦合。每一個類儘量職責單一,每一個模組職責也要單一,要做到低耦合,高聚合。
二 程式碼第一部分 通訊
import socket import re import multiprocessing import time import socketserver import sys import re class WSGIServer(socketserver.BaseRequestHandler): def handle(self): self.data = self.request.recv(1024).decode('utf-8') data = self.data.splitlines() resource = re.findall(".*?(/.*?) HTTP/.*?",data[0],re.S)[0] self.getConfinfo() if not resource.endswith(".py"): try: f = open("{0}{1}".format(self.conf_info["static_path"], resource), "rb") except: response = "HTTP/1.1 404 NOT FOUND\r\n" response += "\r\n" response += "------file not found-----" self.request.send(response.encode("utf-8")) else: html_content = f.read() f.close() # 2.1 準備傳送給瀏覽器的資料---header response = "HTTP/1.1 200 OK\r\n" response += "\r\n" self.request.send(response.encode("utf-8")) self.request.send(html_content) else: env = dict() env['PATH_INFO'] = resource body = self.application(env, self.set_response_header) header = "HTTP/1.1 %s\r\n" % self.status for temp in self.headers: header += "%s:%s\r\n" % (temp[0], temp[1]) header += "\r\n" response = header + body self.request.send(response.encode('utf-8')) def getConfinfo(self): with open('web_server.conf','r') as f: self.conf_info = eval(f.read()) sys.path.append(self.conf_info['dynamic_path']) self.Frame_name= __import__(self.conf_info["frame_name"]) self.application = getattr(self.Frame_name,self.conf_info["FunctionName"]) def set_response_header(self, status, headers): self.status = status self.headers = [("server", "mini_web v8.8")] self.headers += headers def main(): Soc = socketserver.TCPServer(('localhost', 7840), WSGIServer) Soc.serve_forever() if __name__ == "__main__": main()
先不看程式碼,先從思路上來分析。瀏覽器與伺服器之間的通訊是基於HTTP協議,而HTTP協議是架設在TCP/IP協議之上。所以想要實現HTTP協議,要先進行socket,那麼在這裡博主用的是python裡面本身自帶的模組socketsever模組,這個模組是對socket,select,epoll模組的封裝。先看main()函式,這個函式就是socketsever的用法體現。socketsever類裡面有一個TCPServer類,這個類裡面封裝了TCP伺服器的所有方法,它的初始化首先要傳入一個元組,這個元組裡面分別是IP地址,埠號,接下來的引數,就是回撥函式。在python裡面一切都是物件,那麼函式名就是此函式的引用,也就是C語言的函式指標,所有在python實現回撥不難理解,直接當做引數傳遞。只不過這裡的引數是類,而不是函式。這個類必須繼承於socketserver.BaseRequestHandler這個類,並重寫裡面的handle方法,就可以實現多執行緒。至於怎麼實現,感興趣的可以去看原始碼,博主也稍微看了一下。通過第一步之後,呼叫Soc.server_forever就可以實現開啟伺服器了。
緊接著,HTTP協議有基礎的都該知道最基本的就是一個請求,一個響應。那麼在我們輸入URL的時候,如果是靜態的URL,可以看到有一些是帶有.html字尾的。那麼也就是說,我們的這個請求傳送之後,瀏覽器將這個請求發給了伺服器,伺服器進行轉發,轉發給web框架,web框架根據url指定的地址來找html檔案,此時根據不同的業務邏輯進行不同的處理,最終再把這個html返回給瀏覽器,瀏覽器進行渲染。這就是一個最簡單的過程,我們不考慮伺服器的框架也不考慮ajax,web最簡單最直接的原理,就是瀏覽器給了http請求的url,根據不同的情況,返回不同的頭,不同的報文體。
程式碼的思路也是這樣。
1 首先我們接收完資料之後,進行了正則表示式的解析。此步解析是為了提取url裡面的檔名,此時我們先假定好,該html就存放在了固定目錄裡。
2 解析完檔名之後,進行了判斷,就是當檔名不是以.py結尾的時候,我們認為這是一個靜態檔案,所以直接從配置檔案中讀出路徑,去路徑中去尋找,如果找到了,返回html的內容以及響應頭,注意,響應頭與報文體有一個空行。如果沒找到,則返回前端錯誤資訊。
3 那麼當檔案是以.py結尾的時候,我們呼叫了WSGI協議裡的application函式,這裡講解一下引數,此函式第一個引數為字典,在這裡這個字典要向web框架傳遞的是檔名。第二個引數,是一個回撥函式,此函式的意義在於,接收從框架返回的響應頭,為什麼?因為這部分的邏輯處理交給了框架,伺服器並不知道這個頁面是否存在以及其它的異常,所以,通過這個回撥函式,把框架處理完的頭接收過來。
程式碼的通訊部分大體框架就是這樣。
三 框架部分
import re
import pymysql
"""
URL_FUNC_DICT = {
"/index.py": index,
"/center.py": center
}
"""
URL_FUNC_DICT = dict()
def route(url):
def set_func(func):
# URL_FUNC_DICT["/index.py"] = index
URL_FUNC_DICT[url] = func
def call_func(*args, **kwargs):
return func(*args, **kwargs)
return call_func
return set_func
@route("/index.py") # 相當於 @set_func # index = set_func(index)
def index():
with open("./templates/index.html",encoding="utf-8") as f:
content = f.read()
my_stock_info = "哈哈哈哈 這是你的本月名稱....."
content = re.sub(r"\{%content%\}", my_stock_info, content)
return content
@route("/center.py")
def center():
with open("./templates/center.html",encoding="utf-8") as f:
content = f.read()
db = pymysql.connect(host = 'localhost',port = 3306,user='root',password = 'mysql',database='stock_db',charset='utf8')
cursor = db.cursor()
sql = """select * from info;"""
cursor.execute(sql)
data_from_mysql = cursor.fetchall()
cursor.close()
db.close()
html_template = """
<tr>
<td>%d</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>
<input type="button" value="新增" id="toAdd" name="toAdd" systemidvaule="%s">
</td>
</tr>"""
html = ""
for info in data_from_mysql:
html += html_template % (info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7], info[1])
content = re.sub(r"\{%content%\}", html, content)
return content
@route('/time.py')
def time():
with open("./templates/time.html",encoding="utf-8") as f:
content = f.read()
return content
def application(env, start_response):
start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
file_name = env['PATH_INFO']
try:
return URL_FUNC_DICT[file_name]()
except Exception as ret:
return "產生了異常:%s" % str(ret)
框架部分程式碼還是有點含金量的,首先,伺服器給我們的字典裡包含的是一個檔名,這個檔名就是我們要處理邏輯業務的開始,我們根據不同的檔名,來呼叫不同的處理邏輯。這裡劃上重點!在C語言的專案裡,我們怎麼樣進行函式與字串的對應呢?也就是說我們怎麼實現接收過來特定的字串,來呼叫特定的函式呢。答案是通過輪詢結構體陣列,當找到與接收過來的字串相同的時候,就呼叫對應的函式指標。那麼在python裡我們可以通過字典鍵值對的方式來實現,但是有一點要注意,對於一個web框架而言,邏輯部分是相當多的,如果提前寫好,那是不可能的。所以我們用了帶引數的裝飾器。
帶引數的裝飾器裡面有三層,因為python的執行是從上到下,當我們用裝飾器修飾函式時自動的把函式名和裝飾器的引數寫到了字典裡面。在application裡 我們只需要呼叫字典裡的函式就OK了
四 總結
程式碼裡對於配置檔案的讀寫,這裡就先不表示 ,自己看。但是總體框架就是這樣。一定要掌握裝飾器,框架最精髓的地方就是在於對於多個請求我們怎麼用最優雅的方式來解決冗餘程式碼的問題。帶引數的裝飾器,要重點理解。