1. 程式人生 > >python之MiniWeb框架

python之MiniWeb框架

本次學習MiniWeb框架的目的並不是能夠完成框架的編寫,而是需要學習框架編寫中的思想,主要是理解路由表的定義和自動維護、檢視函式的作用、前後端分離的開發思想,以及WSGI協議中的application函式的實現與呼叫的思想。

以往,我們多完成的是靜態web伺服器,主要處理的都是一些已經‘寫死’的資料,那麼今天,我們來學習一下動態資料的處理。

說到動態資料,我們就需要了解一個東西,那就是web框架。

所謂web框架簡單地說就是用來處理資料或模板的一個py程式。

那麼接下,我就簡單的給大家簡述一下一個瀏覽器訪問動態資料的整體流程。

WSGI伺服器網管介面(Web Server Gateway Interface)是為python語言定義的一種伺服器與框架進行通訊的簡單介面,其介面原理就是實現application函式。

application函式格式如下:

def application(environ, func):
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
    return 'Hello MiniWeb'

函式的第一個引數environ型別是一個字典,用來接收封裝到字典內的請求資源路徑。

函式的第二個引數func型別是一個函式,用來接收呼叫時傳遞的函式引用。

application函式在是在框架中實現,框架就是處理資料的py檔案;在伺服器端呼叫。

實現application函式並且完成資料處理的py檔案我們就可以稱它是WSGI介面。

首先,我們先看一張圖片

 

這張圖片完美的闡述了從瀏覽器到伺服器到web框架的應用。

接下來我將給大家進行簡單的闡述:

首先,大家需要明確一點本次開發中以.html字尾的檔案為動態資源,其餘均為靜態資源

1.瀏覽器傳送HTTP請求報文給伺服器,

2.伺服器根據HTTP請求報文的請求路徑進行判斷,

如果請求的是靜態資源,那麼伺服器直接將靜態資源的資料讀出來然後拼接成HTTP響應報文返回給瀏覽器;

如果請求的是動態資源,那麼伺服器會將動態資源請求傳送給web框架。

3.web框架接收到伺服器轉發的請求後,先對請求進行判斷,

如果是請求模版那麼web框架將會去模版中查詢請求的模板,如果查詢到請求的模板那就把模板資料返回給web伺服器,再有伺服器返回給瀏覽器,

如果請求的是資料那麼web框架將會去資料庫中進行操作,將查詢等操作的資料或返回值,返回給web伺服器,再有伺服器返回給瀏覽器。

至此一個整體流程就闡述完畢了。

1.web伺服器程式碼實現:

# 1.匯入socket模組
import socket
import threading
import FreamWork


# 建立伺服器類
class HttpServerSocket(object):
    # 給伺服器類的物件設定屬性
    def __init__(self):
        # 2.建立Socket物件
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 3.設定埠複用
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
        # 4.繫結埠
        self.server_socket.bind(('', 8000))
        # 5.設定監聽
        self.server_socket.listen(128)

    def start(self):
        while True:
            # 6.等待客戶端連線
            client_socket, ip_port = self.server_socket.accept()
            # gevent.spawn(self.task, client_socket, ip_port)
            print("上線了", ip_port)
            threading.Thread(target=self.task, args=(client_socket, ip_port), daemon=True).start()

    def task(self, client_socket, ip_port):
        # 7.接收資料
        recv_data = client_socket.recv(1024).decode('utf-8')
        print(recv_data)
        if not recv_data:
            print("客戶端:%s下線了,埠號為:%s" % ip_port)
            return

        # 8.傳送資料
        # 判斷請求資源是否包含引數
        # 請求行格式:GET /index.html HTTP/1.1
        recv_path = recv_data.split()[1]
        # print("第一次分割",recv_path)
        # 如果有引數則以?分割
        if '?' in recv_path:
            real_recv_path = recv_path.split('?')[0]
            # print("?分割",real_recv_path)
        else:
            # 如果沒有引數,則保持請求路徑不變
            real_recv_path = recv_path
            # print("無?分割",real_recv_path)

        # 設定沒指定資源路徑,預設返回index.html
        if real_recv_path == '/':
            real_recv_path = '/index.html'

        # 判斷請求資源是靜態資源還是動態資源
        if real_recv_path.endswith('.html'):
            env = {'PATH_INFO': real_recv_path}
            # 呼叫框架中的application函式
            response_body = FreamWork.application(env, self.start_response)
            response_line = 'HTTP/1.1 %s\r\n' % self.status
            response_header = 'Server: PWS/1.0\r\n'
            # self.response_header 接收的是列表中儲存的元組需要進行解包處理
            response_header += '%s :%s\r\n' % self.response_header[0]
            send_data = (response_line + response_header + '\r\n' + response_body).encode('utf8')
            client_socket.send(send_data)
            client_socket.close()
        else:
            # 判斷請求的資源路徑是否存在
            try:
                with open(f"static{real_recv_path}", "rb") as file:
                    response_body = file.read()
            except  Exception as e:
                # 如果不存在則返回404
                response_line = 'HTTP/1.1 404 NOT FOUND\r\n'
                response_header = 'Server: PWS/1.0\r\n'
                response_body = 'sorry nor found page!\r\n'.capitalize()
                send_data = (response_line + response_header + '\r\n' + response_body).encode('utf-8')
                client_socket.send(send_data)
            else:
                # 如果存在則換回請求的頁面資訊
                response_line = 'HTTP/1.1 200 OK\r\n'
                response_header = 'Server: PWS/1.0\r\n'
                send_data = (response_line + response_header + '\r\n').encode('utf-8') + response_body
                client_socket.send(send_data)
            finally:
                # 斷開與客戶端連線
                client_socket.close()

    def start_response(self, status, response_header):
        self.status = status
        self.response_header = response_header

    def __del__(self):
        # 當服務端程式結束時停止伺服器服務
        self.server_socket.close()


def main():
    http_socket = HttpServerSocket()
    http_socket.start()


if __name__ == '__main__':
    main()

核心程式碼(程式碼段中顏色為紅色並加粗的程式碼)解讀:

1.通過分解後的請求資源路徑的字尾判斷請求的是否是html頁面,如果是則認為請求的是動態資源;

2.將動態資源路徑封裝到一個字典中,並將字典和函式的引用傳遞給application函式,

3.web框架(application函式)根據傳遞的資源路徑去模板中查詢是否含有請求的模板,如果有則讀取模版資料並返回;

4.接收到web框架(application函式)返回的資料,並拼接HTTP響應報文,然後通過瀏覽器套接字將HTTP響應報文傳送給瀏覽器,最後關閉與瀏覽器連線。

2.MiniWeb框架程式碼實現:

import db
import json


# 定義一個路由表
router_table = {}


# 編寫介面函式
# environ引數用來接收請求資源路徑
# start_response引數用來接收傳遞過來的函式
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
    func = error
    if environ['PATH_INFO'] in router_table:
        func = router_table[environ['PATH_INFO']]
    body = func()
    return body


# 定義一個裝飾器,用來實現路由表的自動維護
def router(url):
    def decorator(func):
        def inner(*args, **kwargs):
            body = func(*args, **kwargs)
            return body

        router_table[url] = func
        return inner

    return decorator


# 前後端不分離技術
@router('/index.html')
def index():
    db_data = db.db_select()
    with open('templates/index.html', 'r') as file:
        body = file.read()
    row_str = """
            <tr>
                <td>%s</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>
    """
    body_data = ''
    for data in db_data:
        body_data += row_str % (data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[1])
    body = body.replace('{%content%}', body_data)
    return body


# 前後端分離技術
@router('/center.html')
def center():
    with open('templates/center.html', 'r', encoding='utf-8') as file:
        body = file.read()
    return body



# 編寫介面函式
def center_data():
    result = db.db_select_interface()
    # 編寫JSON資料
    # JSON型別資料格式:[{"key":"value"},{"key":"value"},{"key":"value"}]
    data = []
    for i in result:
        dd = {}
        dd['code'] = i[0]
        dd['short'] = i[1]
        dd['chg'] = i[2]
        dd['turnover'] = i[3]
        dd['price'] = i[4]
        dd['highs'] = i[5]
        dd['info'] = i[6]
        data.append(dd)
    # 將資料裝換成JSON型別資料
    # data:要轉換的字串
    # ensure_ascii設定是否用ASCII碼解析,False為否,True為是
    josn_data = json.dumps(data,ensure_ascii=False)
    return josn_data



@router('/update.html')
def update():
    with open('templates/update.html', 'r', encoding='utf-8') as file:
        body = file.read()
    return body


def error():
    return '<h1>你的頁面迷路了。。。</h1>'

JSON的資料格式:

[{"key":"value"},{"key":"value"},{"key":"value"}]

在JSON資料轉換時需要注意,json.dumps()會預設採用ASCII碼進項解碼轉換,想要不使用ASCII進行轉碼就需要設定ensure_ascii引數,將它設定成False即可解決問題。

這裡給大家簡單闡述一下路由表的概念:

路由表就是一個用請求地址作為Key,裝飾器內層函式的引用作為Value而形成的鍵值對組成的字典。

其格式為:

{
'/index.html':inner,
'/error.html':inner
}

對於路由表的自動維護,我們可以通過編寫帶引數的裝飾器來實現:

router_table = {}


def router(url):
    def decorator(func):
        def inner(*args, **kwargs):
            func(*args, **kwargs)

        router_table[url] = inner
        return inner

    return decorator

通過@router('/index.html')語法糖來給路由表中新增資料。

其主要流程:

1.先執行router('/index.html'),然後將裝飾器decorator函式的引用返回;

2.在通過@decorator的引用,將檢視函式傳遞給func,將inner的引用及url組成鍵值對儲存到路由表中;

這裡再給大家擴充套件一個概念,檢視函式是MVT開發模式中的V也就是View,其主要功能是處理資料。

3.資料庫操作程式碼實現:

import pymysql

# 定義一個數據庫連線函式
def db_connect():
    db = pymysql.connect(host='localhost', port=3306, user='root', password='mysql', database='stock_db', charset='utf8')
    # 返回資料庫連線物件
    return db

# 定義一個index頁面查詢資料的函式
def db_select():
    db = db_connect()
    cur = db.cursor()
    sql = '''
    select * from info;
    '''
    cur.execute(sql)
    result = cur.fetchall()
    cur.close()
    db.close()
    return result


# 定義一個介面查詢資料的函式
def db_select_interface():
    db = db_connect()
    cur = db.cursor()
    sql = '''select i.code, i.short, i.chg, i.turnover,i.price, i.highs, f.note_info from info i inner join focus f on i.id = f.info_id '''
    cur.execute(sql)
    result = cur.fetchall()
    cur.close()
    db.close()
    return result

最後,總結一下MiniWeb開發的大概流程:

1.在原有的TCP服務端的基礎上加上一個請求資源的判斷,即判斷是否是動態資源;

2.如果是動態資源,就將動態資源的請求路徑傳送給框架,

3.框架在進行判斷,如果動態資源是模版那就讀出模版的資料並返回給伺服器,最後再由伺服器拼接響應報文給客戶端,

4.如果動態資源請求的是資料,那麼框架就回到資料庫中將資料查詢出來,並且拼接成JSON資料格式的字串,再將JSON資料格式的字串轉換成JSON資料,最後在返回給伺服器,最後再由伺服器拼接響應報文併發送給客戶