咱們能夠這樣理解:全部的Web應用本質上就是一個socket服務端,而用戶的瀏覽器就是一個socket客戶端。 這樣咱們就能夠本身實現Web框架了。html
socket服務端
- 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服務本質上都是在這十幾行代碼基礎上擴展出來的。這段代碼就是它們的祖宗。python
用戶在瀏覽器中輸入網址,瀏覽器會向服務端發送數據,那瀏覽器會發送什麼數據?怎麼發?這個誰來定? 你這個網站是這個規定,他那個網站按照他那個規定,那互聯網還能玩麼?mysql
因此,必須有一個統一的規則,讓你們發送消息、接收消息的時候都有個格式依據,不能隨便寫。web
這個規則就是HTTP協議,之後瀏覽器發送請求信息也好,服務器回覆響應信息也罷,都要按照這個規則來。sql
HTTP協議主要規定了客戶端和服務器之間的通訊格式,那HTTP協議是怎麼規定消息格式的呢?數據庫
讓咱們首先打印下咱們在服務端接收到的消息是什麼。django
- 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\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3355.4 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\r\nCookie: csrftoken=CtHePYARJOKNx5oNVwxIteOJXpNyJ29L4bW4506YoVqFaIFFaHm0EWDZqKmw6Jm8\r\n\r\n'
咱們將\r\n替換成換行看得更清晰點:服務器
- GET / HTTP/1.1
- Host: 127.0.0.1:8080
- Connection: keep-alive
- Cache-Control: max-age=0
- Upgrade-Insecure-Requests: 1
- User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3355.4 Safari/537.36
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
- Accept-Encoding: gzip, deflate, br
- Accept-Language: zh-CN,zh;q=0.9
- Cookie: csrftoken=CtHePYARJOKNx5oNVwxIteOJXpNyJ29L4bW4506YoVqFaIFFaHm0EWDZqKmw6Jm8
-
-
而後咱們再看一下咱們訪問博客園官網時瀏覽器收到的響應信息是什麼。session
響應相關信息能夠在瀏覽器調試窗口的Network標籤頁中看到。
點擊view source以後顯示以下圖:
咱們發現收發的消息須要按照必定的格式來,這裏就須要瞭解一下HTTP協議了。
HTTP協議介紹
HTTP協議對收發消息的格式要求
每一個HTTP請求和響應都遵循相同的格式,一個HTTP包含Header和Body兩部分,其中Body是可選的。
HTTP響應的Header中有一個 Content-Type
代表響應的內容格式。它的值如text/html; charset=utf-8。
text/html則表示是網頁,charset=utf-8則表示編碼爲utf-8。
HTTP GET請求的格式:
HTTP響應的格式:
自定義web框架
通過上面的學習,那咱們基於socket服務端的十幾行代碼寫一個咱們本身的web框架。咱們先不處理瀏覽器發送的請求,先讓瀏覽器能顯示咱們web框架返回的信息,那咱們就要按照HTTP協議的格式來發送響應。
- 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的路徑,而後拿路徑作一個判斷...
-
- import socket
-
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080))
- sk.listen()
-
- while True:
-
- conn, add = sk.accept()
- data = conn.recv(8096)
-
- data = str(data, encoding="utf8")
-
- data1 = data.split("\r\n")[0]
- url = data1.split()[1]
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
-
- 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))
- sk.listen()
-
-
- def func(url):
- s = "這是{}頁面!".format(url)
- return bytes(s, encoding="utf8")
-
-
- while True:
-
- conn, add = sk.accept()
- data = conn.recv(8096)
-
- data = str(data, encoding="utf8")
-
- data1 = data.split("\r\n")[0]
- url = data1.split()[1]
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
-
- if url == "/index/":
- response = func(url)
- elif url == "/home/":
- response = func(url)
- else:
- response = b"404 not found!"
-
- conn.send(response)
- conn.close()
根據不一樣的路徑返回不一樣的內容--函數進階版
看起來上面的代碼寫了一個函數,那確定能夠寫多個函數,不一樣的路徑對應執行不一樣的函數拿到結果,可是咱們要一個個判斷路徑,是否是很麻煩?咱們有簡單的辦法來解決。
-
- import socket
-
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080))
- sk.listen()
-
-
- def index(url):
- s = "這是{}頁面XX!".format(url)
- return bytes(s, encoding="utf8")
-
-
- def home(url):
- s = "這是{}頁面。。!".format(url)
- return bytes(s, encoding="utf8")
-
-
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ]
-
- while True:
-
- conn, add = sk.accept()
- data = conn.recv(8096)
-
- data = str(data, encoding="utf8")
-
- data1 = data.split("\r\n")[0]
- url = data1.split()[1]
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
-
- func = None
- for item in list1:
- if item[0] == url:
- func = item[1]
- break
- if func:
- response = func(url)
- else:
- response = b"404 not found!"
-
-
- conn.send(response)
- conn.close()
返回具體的HTML文件
完美解決了不一樣URL返回不一樣內容的問題。 可是我不想僅僅返回幾個字符串,我想給瀏覽器返回完整的HTML內容,這又該怎麼辦呢?
沒問題,不論是什麼內容,最後都是轉換成字節數據發送出去的。 咱們能夠打開HTML文件,讀取出它內部的二進制數據,而後再發送給瀏覽器。
-
- import socket
-
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080))
- sk.listen()
-
-
- def index(url):
-
- 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")
-
-
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ]
-
- while True:
-
- conn, add = sk.accept()
- data = conn.recv(8096)
-
- data = str(data, encoding="utf8")
-
- data1 = data.split("\r\n")[0]
- url = data1.split()[1]
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
-
- func = None
- for item in list1:
- if item[0] == url:
- func = item[1]
- break
- if func:
- response = func(url)
- else:
- response = b"404 not found!"
-
-
- conn.send(response)
- conn.close()
index.html
home.html
讓網頁動態起來
這網頁可以顯示出來了,可是都是靜態的啊。頁面的內容都不會變化的,我想要的是動態網站。
沒問題,我也有辦法解決。我選擇使用字符串替換來實現這個需求。(這裏使用時間戳來模擬動態的數據)
-
- import socket
-
- sk = socket.socket()
- sk.bind(("127.0.0.1", 8080))
- sk.listen()
-
-
- def index(url):
-
- 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")
-
-
- def timer(url):
- import time
- with open("time.html", "r", encoding="utf8") as f:
- s = f.read()
- s = s.replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S"))
- return bytes(s, encoding="utf8")
-
-
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ("/time/", timer),
- ]
-
- while True:
-
- conn, add = sk.accept()
- data = conn.recv(8096)
-
- data = str(data, encoding="utf8")
-
- data1 = data.split("\r\n")[0]
- url = data1.split()[1]
- conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
-
- func = None
- for item in list1:
- if item[0] == url:
- func = item[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部分:
-
- from wsgiref.simple_server import make_server
-
-
- def index(url):
-
- 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")
-
-
- def timer(url):
- import time
- with open("time.html", "r", encoding="utf8") as f:
- s = f.read()
- s = s.replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S"))
- return bytes(s, encoding="utf8")
-
-
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ("/time/", timer),
- ]
-
-
- def run_server(environ, start_response):
- start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ])
- url = environ['PATH_INFO']
- 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:
index2.html文件
使用jinja2渲染index2.html文件:
- from wsgiref.simple_server import make_server
- from jinja2 import Template
-
-
- def index(url):
-
- with open("index2.html", "r", encoding="utf8") as f:
- data = f.read()
- template = Template(data)
- ret = template.render({'name': 'alex', 'hobby_list': ['抽菸', '喝酒', '燙頭']})
- return bytes(ret, encoding="utf8")
-
-
- def home(url):
- with open("home.html", "r", encoding="utf8") as f:
- s = f.read()
- return bytes(s, encoding="utf8")
-
-
- list1 = [
- ("/index/", index),
- ("/home/", home),
- ]
-
-
- def run_server(environ, start_response):
- start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ])
- url = environ['PATH_INFO']
- 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()
如今的數據是咱們本身手寫的,那可不能夠從數據庫中查詢數據,來填充頁面呢?
使用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
Django官網下載頁面
安裝(安裝最新LTS版):
pip3 install django==1.11.15
建立一個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 啓動時報錯 UnicodeEncodeError ...
報這個錯誤一般是由於計算機名爲中文,改爲英文的計算機名重啓下電腦就能夠了。