首先咱們今天要作的事是開發一個Web框架。可能聽到這你就會想、是否是很難啊?這東西本身能寫出來?css
若是你有這種疑惑的話,那就繼續看下去吧。相信看完今天的內容你也能寫出一個本身的Web框架。html
要知道什麼是Web框架首先要知道Web服務器的概念。Web服務器是一個無情的收發機器,對它來講,接收和發送是最主要的工做。在咱們用瀏覽器打開網頁時,若是不考慮複雜狀況,咱們能夠理解爲咱們在向服務器要東西,而服務器接到了你的請求後,根據一些判斷,再給你發送一些內容。python
仔細一想,其實這一個套接字(Socket)。git
那Web框架是什麼呢?Web框架其實就是對Web服務器的一個封裝,最原始的服務器只有一個原生的Socket,它能夠作一些基本的工做。可是想用原生Socket作Web開發,那你的事情就多了去了。github
而Web框架就是對Socket的高級封裝,不一樣的Web框架封裝程度不一樣。像Django是封裝地比較完善的一個框架,而Flask則要輕便得多。web
那他們只會封裝Socket嗎?咱們接着往下看!正則表達式
如今大多數框架都是MCV模式或者類MCV模式的。那MCV的含義是什麼呢?具體含義以下:數據庫
下面咱們來具體解釋一下:瀏覽器
模型很好理解,就是咱們常說的類,咱們一般會將模型和數據庫表對應起來。服務器
視圖層關注的是咱們展現的頁面效果,主要就是html、css、js等。
控制層,其實把它稱做業務邏輯層要更好理解。也就是決定我要顯示什麼數據。
若是拿登陸的業務來看。數據庫中用戶表對應的類就是屬於模型層,咱們看到的登陸頁面就是視圖層,而咱們處理判斷登陸的用戶名密碼等一系列內容就是業務邏輯層的內容。
那MTV又是什麼呢?其實MTV就是MCV的另外一種形式,model是同樣的,而T的含義是Template,對應View。比較奇怪的就是MTV中的View,它對應Controller。
其實MVC和MTV沒有本質區別。
在大多數框架中咱們都不會去關注Socket自己,而更多的是去關注MTV三個部分。在本文,咱們會去本身實現Template和View兩個部分。
Template部分很好理解,就是咱們一般的html頁面。可是咱們最終要實現的是動態頁面(頁面中的數據是動態生成的),所以咱們須要對傳統的html頁面進行一些改造。這部分的工做須要咱們定義一些特徵標記,以及對html進行一些渲染工做。
而View部分咱們會實現兩個功能,一個是路由分發,另外一個是視圖函數。
路由分發的工做就是讓咱們對應不一樣的url,響應不一樣的內容。好比我請求http://www.test.com/login.html
會返回登陸頁面,若是請求http://www.test.com/register.html
則返回註冊頁面。
而視圖函數則是針對每一個請求的處理。後面咱們會再提到。
知道了上面這些知識後,咱們就能夠着手開發咱們的Web框架了。
服務器是Web框架的基礎,而Socket是服務器的基礎。所以咱們還須要瞭解一下Socket的使用。
在python中socket的操做封裝在socket.socket類中。咱們先看下面這段代碼,如何再來逐一解釋:
import socket
# 建立一個服務端socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定ip和端口
server.bind(('127.0.0.1', 8000))
# 監聽是否有請求
server.listen(1)
# 接收請求
conn, addr = server.accept()
# 接收數據
data = conn.recv(1024)
print(addr)
print(data.decode('utf-8'))
複製代碼
在咱們作操做前,咱們須要建立一個socket對象。在建立是咱們傳入了兩個參數,他們規定了以下內容:
有了socket對象後,咱們使用bind方法綁定ip和端口。其中127.0.0.1表示本機ip,綁定後咱們就能夠經過指定ip和端口訪問了。
由於是服務器,因此咱們須要使用listen監聽是否有請求,listen方法是阻塞的,他會一直等待。
當監聽到請求後,咱們能夠經過accept方法接收請求。accept方法會返回鏈接和請求的地址信息(ip和端口)。
而後經過conn.recv就能夠獲取客戶端發來的數據了。recv方法中傳入的參數是接收的最大字節數。
在網絡傳輸過程當中,數據都是二進制形式傳輸的,所以咱們須要對數據進行解碼。
咱們能夠編寫一個client來測試一下:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
client.send("你好".encode('utf-8'))
複製代碼
咱們依次運行服務端,和客戶端。會發現客戶端輸出以下內容:
('127.0.0.1', 49992)
你好
複製代碼
能夠看到客戶端成功將數據發送給了服務端。
上面只是簡單看一下socket的使用,那種程度的服務器還不能知足咱們網站的需求。咱們來看看它有些上面問題:
關於沒有響應的問題很好解決,咱們只須要在服務端加下面兩句代碼:
conn.send('你好'.encode('utf-8'))
conn.close()
複製代碼
如今咱們運行服務端,客戶端你已經能夠刪除了。由於微軟已經幫咱們實現了一個客戶端,就是鼎鼎大名的IE瀏覽器。咱們打開IE瀏覽器在url輸入:http://127.0.0.1:8000/
就能夠看到以下頁面:
可能有些人就發現了,這個實際上是用utf-8編碼而後用gbk解碼的「你好」。這個其實就是咱們編寫的服務器返回的內容。
可是若是你再次訪問這個頁面,瀏覽器就會無情地告訴你「沒法訪問此頁面」。由於咱們服務端已經中止了,咱們能夠給咱們的服務器加個while循環:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
data = conn.recv(1024)
conn.send("你好".encode('gbk'))
conn.close()
複製代碼
這樣咱們就能夠一直訪問了。可是實際上它仍是有問題,由於它同一時間只能接收一個鏈接。想要能夠同時接收多個鏈接,就須要使用多線程了,因而我我把服務端修改成以下:
import socket
from threading import Thread
def connect(conn):
data = conn.recv(1024)
conn.send("你好".encode('gbk'))
conn.close()
def run_server():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
t = Thread(target=connect, args=(conn, ))
t.start()
run_server()
複製代碼
咱們在accept部分不停地接收鏈接,而後開啓一個線程給請求返回數據。這樣咱們就解決了一次請求服務器就會中止的問題了。爲了方便使用,我用面向對象的方式重寫了服務器的代碼:
import socket
from threading import Thread
class Server(socket.socket):
""" 定義服務器的類 """
def __init__(self, ip, host, connect_num, *args, **kwargs):
super(Server, self).__init__(*args, **kwargs)
self.ip = ip
self.host = host
self.connect_num = connect_num
@staticmethod
def conn(conn):
# 獲取請求參數
request = conn.recv(1024)
conn.send("你好".encode('gbk'))
conn.close()
def run(self):
""" 運行服務器 :return: """
# 綁定ip和端口
self.bind((self.ip, self.host))
self.listen(self.connect_num)
while True:
conn, addr = self.accept()
t = Thread(target=Server.conn, args=(conn,))
t.start()
複製代碼
這樣咱們只須要編寫下面的代碼就能運行咱們的服務器了:
import socket
from server import Server
my_server = Server('127.0.0.1', 8000, 5, socket.AF_INET, socket.SOCK_STREAM)
my_server.run()
複製代碼
如今咱們的服務端寫好了,咱們再來關注一下Template和View部分。
在上面咱們的服務端只返回了一個簡單的字符串,下面咱們來看看如何讓服務器返回一個html頁面。
其實想要返回html很是簡單,咱們只須要先準備一個html頁面,咱們建立一個template模板,並在目錄下建立index.html文件,內容以下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Do not go gentle into that good night!</h1>
</body>
</html>
複製代碼
咱們只寫了一個h標籤,而後在Server類中的conn方法作一點簡單的修改:
@staticmethod
def conn(conn):
# 獲取請求參數
request = conn.recv(1024)
with open('template/index.html', 'rb') as f:
conn.send(f.read())
conn.close()
複製代碼
咱們把本來發送固定字符串改爲了發送從文件中讀取的內容,咱們再次在IE中訪問http://127.0.0.1:8000/
能夠看到以下頁面:
這樣咱們想要返回不一樣的頁面只須要修改html文件就行了。可是上面的方式還不能讓咱們動態地顯示數據,所以咱們還須要繼續修改。
想要動態顯示數據,咱們確定須要對html的內容進行二次處理。爲了方便咱們二次處理,咱們能夠定義一些特殊標記,咱們把它們稱做【模板標記】。好比下面這個html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>username: %%username%%</h1>
<h1>password: %%password%%</h1>
</body>
</html>
複製代碼
其中%%username%%
就是咱們定義的模板標記。咱們只須要在服務端找到這些標記,而後替換就行了。因而我將conn方法修改成以下:
@staticmethod
def conn(conn):
# 獲取請求參數
request = conn.recv(1024)
html = render('template/index.html', {'username': 'zack', 'password': '123456'})
conn.send(html.encode('gbk'))
conn.close()
複製代碼
此次咱們再也不是直接把html的內容發送出去了,而是把模板的路徑交由render函數進行讀取並渲染。咱們來看看render函數:
def render(template_path, params={}):
with open(template_path, 'r') as f:
html = f.read()
# 找到全部模板標記
if params:
markers = re.findall('%%(.*?)%%', html)
for marker in markers:
tag = re.findall('%%' + marker + '%%', html)[0]
if params.get('%s' % marker):
html = html.replace(tag, params.get('%s' % marker))
else:
html = html.replace(tag, '')
return html
複製代碼
咱們的render函數接收兩個參數,分別是模板路徑代碼,和用來渲染的參數。咱們使用正則表達式找出特殊標記,而後用對應的變量進行替換。最後再把渲染後的結果返回,咱們來訪問一下:http://127.0.0.0:8000/
能夠看到以下頁面:
能夠看到咱們渲染成功了。在咱們知道如何渲染頁面後,咱們就能夠從數據庫取數據,而後再渲染到頁面上了。不過這裏就再也不細說下去了。
在上面的例子中,咱們都是隻能返回一個頁面。接下來咱們就來實現一個能夠根據url來返回不一樣頁面的框架。
其實路由系統就是一個url和頁面的對應關係,爲了方便修改,咱們另外建立一個urls.py文件,內容大體以下:
from views import *
urlpatterns = {
'/index': index,
'/login': login,
'/register': register,
'/error': error
}
複製代碼
在裏面咱們寫了一個字典。並且還導入了一個views模塊,這個模塊咱們稍後會建立。
咱們來看看它的做用,首先咱們須要知道,字典的值是一個個函數。知道這點後咱們就能很簡單地猜想到,其實這個urlpatterns就是url和函數地對應關係。下面咱們來把views模塊建立一下。
咱們的視圖視圖函數一般須要一個參數,就是咱們的請求內容。咱們能夠封裝成一個request類,我爲了方便就直接接收字符串:
import re
def render(template_path, params={}):
with open(template_path, 'r') as f:
html = f.read()
# 找到全部模板標記
if params:
markers = re.findall('%%(.*?)%%', html)
for marker in markers:
tag = re.findall('%%' + marker + '%%', html)[0]
if params.get('%s' % marker):
html = html.replace(tag, params.get('%s' % marker))
else:
html = html.replace(tag, '')
return html
def index(request):
return render('template/index.html', {'username': 'zack', 'password': '123456'})
def login(request):
return render('template/login.html')
def register(request):
return render('template/register.html')
def error(request):
return render('template/error.html')
複製代碼
咱們在視圖函數定義了一系列函數,這樣咱們就能夠針對不一樣的url發送不一樣的響應了。另外我把render函數移到了views模塊。
那咱們要怎樣才能讓視圖函數來處理不一樣的請求呢?這個時候咱們就須要想一下誰是第一個拿到請求的。我想你應該也想到了,就是咱們的Socket服務器,全部咱們還要回到Server類。
咱們到如今尚未看到IE給咱們服務器發的東西,如今咱們來看一看:
b'GET / HTTP/1.1\r\nAccept: text/html, application/xhtml+xml, image/jxr, */*\r\nAccept-Language: zh-Hans-CN,zh-Hans;q=0.5\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko\r\nAccept-Encoding: gzip, deflate\r\nHost: 127.0.0.1:8000\r\nConnection: Keep-Alive\r\n\r\n'
複製代碼
把上面的內容整理後:
b'GET / HTTP/1.1 \r\n Accept: text/html, application/xhtml+xml, image/jxr, */* \r\n Accept-Language: zh-Hans-CN,zh-Hans;q=0.5 \r\n User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko \r\ nAccept-Encoding: gzip, deflate \r\n Host: 127.0.0.1:8000 \r\n Connection: Keep-Alive \r\n\r\n'
複製代碼
能夠看到它們都是由\r\n
拆分的字符串,並且在第一行就有咱們的請求url的信息。可能你看不出,可是經驗豐富的我一眼就看出了,由於我請求的url是http://127.0.0.1:8000/
,因此咱們的請求url是/。
知道這些後,那接下來的工做就是字符串處理了。我把Server的conn函數修改成以下:
@staticmethod
def conn(conn):
# 獲取請求參數
request = conn.recv(1024)
# 在request中提取method和url
method, url, _ = request.decode('gbk').split('\r\n')[0].split(' ')
# 在路由系統中找到對應的視圖函數,並把請求參數傳遞過去
html = urls.urlpatterns.get(url)(request)
conn.send(html.encode('gbk'))
conn.close()
複製代碼
咱們先是經過字符串分割的方式提取出url,而後在路由系統中匹配視圖函數,把請求參數傳遞給視圖函數,視圖函數就會幫咱們渲染一個html頁面,咱們把html返回給瀏覽器。這樣咱們就實現了一個相對完整的web框架了!
固然,這個框架是不能使用到生成中的,你們能夠經過這個案例來理解Web框架的各個部分。
可能有些機智的讀者嘗試用Chrome或者Edge瀏覽器訪問上面的服務器,可是卻被拒絕了。
由於咱們的響應信息只是並無包含響應頭,Chrome認爲咱們響應的東西是不正規的,所以不讓咱們訪問。你們能夠嘗試着本身解決一下這個問題。
今天的內容就到這裏了!感興趣的讀者能夠關注公衆號「新建文件夾X」,感謝閱讀。
項目代碼已上傳GitHub:github.com/IronSpiderM…