實現一個簡單的靜態(tài)web網站,只需將寫好的html頁面上傳到特定的web服務器軟件即可,但靜態(tài)網頁其實和圖片沒什么區(qū)別,每次更新網站內容,都需要重新制作html頁面,然后上傳給提供web服務的軟件,替換原來的html頁面,也就完成了更新,以一個正常人的思維方式,每次更新內容都要重新生成html的工作實在太蛋疼了!那么能不能讓程序自己生成html呢?當然可以,程序就是為將人類從重復繁雜的工作中解放出來而生的!
如果我們提供一個動態(tài)網站服務,至少應考慮以下四點:
1.要有穩(wěn)定的web服務程序(可以使用知名的apache,nginx,這里為了探究原理,我們自己用多進程寫一個簡單的web服務);
2.要有可用的web網頁模板(網絡上web模版的數量堪比ppt模板,當然我們可以自己畫一張, 10分鐘后...算了-_-///,作為后端人員,我們這里用樸素的信息顯示就好)
3.要有可填充html模板的內容(內容一般從自己的數據庫里取,篇幅所限,我們這里用time.ctime()函數,模擬數據庫動態(tài)數據);
4.要有處理填寫內容的邏輯(這個就是我們要今天主要研究的按照wsgi標準實現的簡單的web框架);
一個優(yōu)秀的動態(tài)web框架應該是這樣的:
1.web框架要和 web服務器軟件 分離開;如果把大量的邏輯處理語句,和html放到一個文件中,后期會難以維護(這也是現在多人開發(fā)推崇MVC的原因);
2.一個優(yōu)秀的web框架要和web服務器軟件 有良好的交互通信;這時就需要一個數據交互的標準WSGI(python為自身web框架制定了WSGI標準);
3.一個優(yōu)秀的web框架要實現,和數據庫有良好的讀寫通信方法;
關于WSGI標準
為了方便表述,先舉一個栗子:
import time
def app(environ, start_response):
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return str(environ) + '==Hello world from a simple WSGI application!--->%s\n' % time.ctime()
WIGS模型的要點:
1.在web框架模塊,以上面的栗子為例,web服務器軟件會向web框架傳遞一個列表(environ)和一個函數(函數體在web服務器軟件中實現)的引用(start_response),然后web框架要實現一個app函數,并將 "一個列表"和"一個函數的引用",作為兩個參數;
2.傳遞過來的列表內部存儲了N個元組,這些元組包含了web服務器接收到的客戶端瀏覽器的請求信息, 傳遞過來的函數參數的引用,可以用來返回請求資源的狀態(tài)反饋(如果請求的資源可以訪問,就會返回200,如果資源無法訪問,就返回404或502之類的錯誤;
3.傳遞過來的函數引用的調用比return更靠前,這樣可以在返回正式的網頁之前的這段時間,讓web服務器軟件做好接收數據的準備;(其實可以將函數的引用作為web框架與web服務器軟件傳遞數據的的一種快捷方式);
擴展:
其實雙重返回的設計思路很常見,比如在tcp四次揮手的過程中,第二次和第三次揮手都是服務器發(fā)送數據,客戶端接收數據;
第二次服務端向客戶端說("客戶端,我收到你主動關閉本次連接的消息了!"),第三次服務端向客戶端說("客戶端,我已經關閉了這次的發(fā)送連接,不會給你發(fā)數據了,收到了記得回我個消息哈!");
也許有人會認為,既然第二次和第三次都是服務端向客戶端發(fā)送數據,那應該可以將兩條消息一起發(fā)送,但實際上,服務器關閉發(fā)送數據的通道是需要一定的時間的,如果第二次和第三次一起發(fā)送,客戶端瀏覽器就不能在發(fā)送第一次消息后,及時確認消息是否送達,而在tcp連接中,及時"確認送達"是一件非常重要的事情!
在web服務器軟件模塊,至少要實現三個功能:
1.創(chuàng)建 包含客戶端請求頭消息的列表(作為第一個參數傳遞);
2.創(chuàng)建一個可以解析返回狀態(tài)信息的函數(作為第二個參數傳遞);
3.接收web框架內app函數返回的body,并將body與作為第二個參數的引用的函數的返回狀態(tài)值組合,一同發(fā)送給客戶端瀏覽器;
實現源碼
1.作者自己編寫小型web服務器(以上篇 gevent實現靜態(tài)web服務器為基礎改寫)
web_server.py
import socket
from sys import argv
import gevent
from gevent import monkey
import time
import random
import re
# 服務器類
class WISG(object):
def __init__(self, port, app):
self.port = port
self.root_dir = "./HTML"
# 創(chuàng)建主套接字
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 允許端口重用
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 主套接字綁定端口
self.server_socket.bind(("", self.port))
# 主套接字轉為被動模式
self.server_socket.listen(128)
# 獲取web框架中的函數引用
self.app = app
pass
# 啟動服務器對象的入口函數
def run_forever(self):
self.create_new_socket()
pass
# 創(chuàng)建新的套接字,使用gevent,使新的套接字以消耗少量資源的協程方式運行
def create_new_socket(self):
while True:
new_client_socket, new_client_socket_addr = self.server_socket.accept()
gevent.spawn(self.deal_accept_data, new_client_socket)
# 處理接收到的數據
def deal_accept_data(self, new_client_socket):
recv_data = new_client_socket.recv(1024)
# 接收到的請求為utf-8格式,解析數據
recv_data = recv_data.decode("utf-8")
# 如果收到客戶端發(fā)送的空字符,則關閉連接
if not recv_data:
return
# 將接收到的數據轉換為列表
recv_data_list = recv_data.splitlines()
# 獲取請求頭信息
the_request_header = recv_data_list[0]
file_name = self.get_file_name(the_request_header)
if file_name.endswith(".html"):
print("請求的文件名為%s"%(file_name))
# 向客戶端發(fā)送文件
self.send_html(file_name, new_client_socket)
new_client_socket.close()
else:
print("進入動態(tài)選擇模塊...")
print("請求的文件名為%s"%(file_name))
# 得到web框架返回的數據
# 創(chuàng)建一個字典
environ = dict()
environ["PATH_INFO"] = file_name
#獲得內容
dynamic_content = self.app(environ, self.set_response_header)
new_client_socket.send(self.dynamic_response_headers_info + dynamic_content.encode("utf-8"))
new_client_socket.close()
# 根據請求頭信息,獲得本地對應以.html或.py尾綴的文件名
def get_file_name(self, the_request_header):
"""GET /index.html HTTP/1.1"""
file_name = re.match(r"[^/]+([^ ]+).*", the_request_header).group(1)
if file_name == "/":
file_name = "/index.html"
return file_name
pass
# 發(fā)送靜態(tài)文件的html到客戶端
def send_html(self, file_name, new_client_socket):
try:
f = open(self.root_dir+file_name, "rb")
except Exception as res:
print(res)
print("無法找到網頁404")
else:
content = f.read()
respond_body = content
respond_header = "HTTP/1.1 200 OK \r\n"
respond_header += "Content-Type: text/html; charset=utf-8\r\n"
respond_header = respond_header + "\r\n"
# 發(fā)送回應
new_client_socket.send(respond_header.encode("utf-8"))
new_client_socket.send(respond_body)
print("內容發(fā)送成功!")
pass
def set_response_header(self, status, headers):
#將從web框架收到的狀態(tài)碼,和返回的頭信息存儲到一個列表里面
self.dynamic_respond_header = [status, headers]
# 組建返回頭信息
dynamic_respond_header = "HTTP/1.1 %s \r\n"
dynamic_respond_header += "%s:%s\r\n"%(headers[0][0], headers[0][1])
dynamic_respond_header += "\r\n"
# 將列表中的數據進行整理,轉為可直接使用的"返回頭"信息,然后存到類變量dynamic_response_headers_info
self.dynamic_response_headers_info = dynamic_respond_header.encode("utf-8")
pass
def main():
monkey.patch_all()
# 創(chuàng)建web服務器
if len(argv) == 3:
port = int(argv[1])
# web框架名稱
frame_name = re.match(r"([^:]+):(.+)", argv[2]).group(1)
# web框架中主調函數的名稱
app_name = re.match(r"([^:]+):(.+)", argv[2]).group(2)
# 動態(tài)導入框架函數app
web_frame_module = __import__(frame_name)
# 獲得框架中的主調函數
app = getattr(web_frame_module, app_name)
# 傳入端口號,和來自web框架的函數app
web_server = WISG(port, app)
print("app的名字為%s,框架的名字為%s,端口號為%s"%(frame_name, app_name, port))
print("請在地址欄訪問 127.0.0.1:%d"%(port))
# 啟動web服務器
web_server.run_forever()
pass
if __name__ == "__main__":
main()
2.按照wsgi標準實現的web框架
web_frame.py
import time
import re
import codecs
template_root = "./HTML"
file_name = None
def read_file(file_name):
try:
file_name = template_root+file_name
f = codecs.open(file_name, "r", "utf-8")
except Exception as e:
print(e)
print("無法打開%s"%file_name)
else:
content = f.read()
# 這里我們假裝從mysql獲得了數據
data_from_mysql = "我是來自數據庫的動態(tài)數據......"+'當前的時間為--->%s\n' % time.ctime()
# 將模板中的{content}換成我們的"動態(tài)數據"
content = str(re.sub(r"{content}", data_from_mysql, content))
f.close()
return content
# web框架入口
def app(environ, start_response):
"""environ包含需要訪問的.py文件(模板)的名稱, start_response代表來自web框架的函數的引用"""
# 設置返回的狀態(tài)碼信息
status = "200 OK"
# 設置返回的網頁類型
response_headers = [('Content-Type', 'text/html;charset=utf-8')]
# 向web框架中定義的函數start_response中傳入頭信息(狀態(tài)碼,網頁類型)
start_response(status, response_headers)
file_name = environ["PATH_INFO"]
content = read_file(file_name)
# 將動態(tài)的數據返回給服務器框架
return content
README
終端運行:
python3 web_server.py 8888 web_frame:app
目錄樣式