1. web框架本質
通過對網絡編程的學習,我們可以這樣理解:所有的Web應用本質上就是一個socket服務端,而用戶所使用的瀏覽器就是一個socket客戶端,用戶使用瀏覽器按照http協議發送請求,服務端再按照http協議做出響應。
下面我們就基於socket來自己實現一個半成品web框架,寫一個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()
打開瀏覽器地址欄,輸入127.0.0.1:80訪問
可以說Web服務本質上都是在這十幾行代碼基礎上擴展出來的。這段代碼就是它們的祖宗。
2. HTTP協議
用戶的瀏覽器一輸入網址,會給服務端發送數據,那瀏覽器會發送什麼數據?怎麼發?這個誰來定? 你這個網站是這個規定,他那個網站按照他那個規定,這互聯網還能玩麼?所以,必須有一個統一的規則,讓大家發送消息、接收消息的時候有個格式依據,不能隨便寫。
這個規則就是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\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n\r\n’
然後我們再看一下我們訪問博客園官網時瀏覽器收到的響應信息是什麼。響應相關信息可以在瀏覽器調試窗口的network標籤頁中看到。
點擊view source之後顯示如下圖:
我們發現收發的消息需要按照一定的格式來,這裏就需要了解一下HTTP協議了。
(在前端中會補充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框架吧!
3. 完善自定義的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中不同的路徑返回不同的內容--函數版
"""
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()
根據不同路徑返回不同內容-函數進階版 |
"""
根據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頁面
"""
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()
好了,在這停頓…
4. 服務器程序和應用程序
對於真實開發中的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
html文件
<!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的語法規則寫上,其內部就會按照指定的語法進行相應的替換,從而達到動態的返回內容。
5. python中主流web框架分類
- a.收發socket消息,按照HTTP協議解析消息
- b.字符串替換
- c.業務邏輯處理
tornado: 自己實現a,b,c
Django: 自己實現b,c 使用別人的a
Flask: 自己實現c 使用別人的a,b
另一個維度的分類:
- Django
- 其他