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包含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()
讓網頁動態起來
這網頁能夠顯示出來了,但是都是靜態的啊。頁面的內容都不會變化的,我想要的是動態網站。
沒問題,我也有辦法解決。我選擇使用字符串替換來實現這個需求。(這里使用時間戳來模擬動態的數據)
""" 根據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()
好了,在這停頓...
服務器程序和應用程序
對於真實開發中的python web程序來說,一般會分為兩部分:服務器程序和應用程序。
服務器程序負責對socket服務器進行封裝,並在請求到來時,對請求的各種數據進行整理。
應用程序則負責具體的邏輯處理。為了方便應用程序的開發,就出現了眾多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的開發方式,但是無論如何,開發出的應用程序都要和服務器程序配合,才能為用戶提供服務。
這樣,服務器程序就需要為不同的框架提供不同的支持。這樣混亂的局面無論對於服務器還是框架,都是不好的。對服務器來說,需要支持各種不同框架,對框架來說,只有支持它的服務器才能被開發出的應用使用。
這時候,標准化就變得尤為重要。我們可以設立一個標准,只要服務器程序支持這個標准,框架也支持這個標准,那么他們就可以配合使用。一旦標准確定,雙方各自實現。這樣,服務器可以支持更多支持標准的框架,框架也可以使用更多支持標准的服務器。
WSGI(Web Server Gateway Interface)就是一種規范,它定義了使用Python編寫的web應用程序與web服務器程序之間的接口格式,實現web應用程序與web服務器程序間的解耦。
常用的WSGI服務器有uwsgi、Gunicorn。而Python標准庫提供的獨立WSGI服務器叫wsgiref,Django開發環境用的就是這個模塊來做服務器。
從這繼續...
wsgiref
我們利用wsgiref模塊來替換我們自己寫的web框架的socket server部分:
""" 根據URL中不同的路徑返回不同的內容--函數進階版 返回HTML頁面 讓網頁動態起來 wsgiref模塊版 """ import time from wsgiref.simple_server import make_server # 將返回不同的內容部分封裝成函數 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), ] def run_server(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 設置HTTP響應的狀態碼和頭信息 url = environ['PATH_INFO'] # 取到用戶輸入的url 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!" return [response, ] if __name__ == '__main__': httpd = make_server('127.0.0.1', 8090, run_server) print("我在8090等你哦...") httpd.serve_forever()
jinja2
上面的代碼實現了一個簡單的動態,我完全可以從數據庫中查詢數據,然后去替換我html中的對應內容,然后再發送給瀏覽器完成渲染。 這個過程就相當於HTML模板渲染數據。 本質上就是HTML內容中利用一些特殊的符號來替換要展示的數據。 我這里用的特殊符號是我定義的,其實模板渲染有個現成的工具: jinja2
下載jinja2:
pip install jinja2

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <h1>姓名:{{name}}</h1> <h1>愛好:</h1> <ul> {% for hobby in hobby_list %} <li>{{hobby}}</li> {% endfor %} </ul> </body> </html>
使用jinja2渲染index2.html文件:
from wsgiref.simple_server import make_server from jinja2 import Template def index(): with open("index2.html", "r") as f: data = f.read() template = Template(data) # 生成模板文件 ret = template.render({"name": "Alex", "hobby_list": ["燙頭", "泡吧"]}) # 把數據填充到模板里面 return [bytes(ret, encoding="utf8"), ] def home(): with open("home.html", "rb") as f: data = f.read() return [data, ] # 定義一個url和函數的對應關系 URL_LIST = [ ("/index/", index), ("/home/", home), ] def run_server(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 設置HTTP響應的狀態碼和頭信息 url = environ['PATH_INFO'] # 取到用戶輸入的url func = None # 將要執行的函數 for i in URL_LIST: if i[0] == url: func = i[1] # 去之前定義好的url列表里找url應該執行的函數 break if func: # 如果能找到要執行的函數 return func() # 返回函數的執行結果 else: return [bytes("404沒有該頁面", encoding="utf8"), ] if __name__ == '__main__': httpd = make_server('', 8000, run_server) print("Serving HTTP on port 8000...") httpd.serve_forever()
現在的數據是我們自己手寫的,那可不可以從數據庫中查詢數據,來填充頁面呢?
使用pymysql連接數據庫:
conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", passwd="xxx", db="xxx", charset="utf8") cursor = conn.cursor(cursor=pymysql.cursors.DictCursor) cursor.execute("select name, age, department_id from userinfo") user_list = cursor.fetchall() cursor.close() conn.close()
創建一個測試的user表:
CREATE TABLE user( id int auto_increment PRIMARY KEY, name CHAR(10) NOT NULL, hobby CHAR(20) NOT NULL )engine=innodb DEFAULT charset=UTF8;
模板的原理就是字符串替換,我們只要在HTML頁面中遵循jinja2的語法規則寫上,其內部就會按照指定的語法進行相應的替換,從而達到動態的返回內容。
Django
安裝(安裝最新LTS版):
pip3 install django==1.11.9
創建一個django項目:
下面的命令創建了一個名為"mysite"的Django 項目:
django-admin startproject mysite
目錄介紹:
mysite/ ├── manage.py # 管理文件 └── mysite # 項目目錄 ├── __init__.py ├── settings.py # 配置 ├── urls.py # 路由 --> URL和函數的對應關系 └── wsgi.py # runserver命令就使用wsgiref模塊做簡單的web server
運行Django項目:
python manage.py runserver 127.0.0.1:8000
模板文件配置:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, "template")], # template文件夾位置 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
靜態文件配置:
STATIC_URL = '/static/' # HTML中使用的靜態文件夾前綴 STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), # 靜態文件存放位置 ]
看不明白?有圖有真相:
剛開始學習時可在配置文件中暫時禁用csrf中間件,方便表單提交測試。
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
Django基礎必備三件套:
from django.shortcuts import HttpResponse, render, redirect
HttpResponse
內部傳入一個字符串參數,返回給瀏覽器。
例如:
def index(request): # 業務邏輯代碼 return HttpResponse("OK")
render
除request參數外還接受一個待渲染的模板文件和一個保存具體數據的字典參數。
將數據填充進模板文件,最后把結果返回給瀏覽器。(類似於我們上面用到的jinja2)
例如:
def index(request): # 業務邏輯代碼 return render(request, "index.html", {"name": "alex", "hobby": ["燙頭", "泡吧"]})
redirect
接受一個URL參數,表示跳轉到指定的URL。
例如:
def index(request): # 業務邏輯代碼 return redirect("/home/")
重定向是怎么回事?
課后練習:
Django版登錄
啟動Django報錯:
Django 啟動時報錯 “UnicodeEncodeError ...”
報這個錯誤通常是因為計算機名為中文,改成英文的計算機名重啟下電腦就可以了。
Django 啟動報錯“SyntaxError: Generator expression must be parenthesized”
報這個錯很大可能是因為使用了Python3.7.0,而目前(2018-06-12)Python3.7.0和Django還有點兼容性問題,換回Python3.6的環境即可。