HTTP協議簡介
1. 使用谷歌/火狐瀏覽器分析
在Web應用中,服務器把網頁傳給瀏覽器,實際上就是把網頁的HTML代碼發送給瀏覽器,讓瀏覽器顯示出來。而瀏覽器和服務器之間的傳輸協議是HTTP,所以:
-
HTML是一種用來定義網頁的文本,會HTML,就可以編寫網頁;
-
HTTP是在網絡上傳輸HTML的協議,用於瀏覽器和服務器的通信。
Chrome瀏覽器提供了一套完整地調試工具,非常適合Web開發。
安裝好Chrome瀏覽器后,打開Chrome,在菜單中選擇“視圖”,“開發者”,“開發者工具”,就可以顯示開發者工具:
說明
- Elements顯示網頁的結構
- Network顯示瀏覽器和服務器的通信
我們點Network,確保第一個小紅燈亮着,Chrome就會記錄所有瀏覽器和服務器之間的通信:
2. http協議的分析
當我們在地址欄輸入www.sina.com時,瀏覽器將顯示新浪的首頁。在這個過程中,瀏覽器都干了哪些事情呢?通過Network的記錄,我們就可以知道。在Network中,找到www.sina.com那條記錄,點擊,右側將顯示Request Headers,點擊右側的view source,我們就可以看到瀏覽器發給新浪服務器的請求:
2.1 瀏覽器請求
說明
最主要的頭兩行分析如下,第一行:
GET / HTTP/1.1
GET表示一個讀取請求,將從服務器獲得網頁數據,/表示URL的路徑,URL總是以/開頭,/就表示首頁,最后的HTTP/1.1指示采用的HTTP協議版本是1.1。目前HTTP協議的版本就是1.1,但是大部分服務器也支持1.0版本,主要區別在於1.1版本允許多個HTTP請求復用一個TCP連接,以加快傳輸速度。
從第二行開始,每一行都類似於Xxx: abcdefg:
Host: www.sina.com
表示請求的域名是www.sina.com。如果一台服務器有多個網站,服務器就需要通過Host來區分瀏覽器請求的是哪個網站。
2.2 服務器響應
繼續往下找到Response Headers,點擊view source,顯示服務器返回的原始響應數據:
HTTP響應分為Header和Body兩部分(Body是可選項),我們在Network中看到的Header最重要的幾行如下:
HTTP/1.1 200 OK
200表示一個成功的響應,后面的OK是說明。
如果返回的不是200,那么往往有其他的功能,例如
- 失敗的響應有404 Not Found:網頁不存在
- 500 Internal Server Error:服務器內部出錯
...等等...
Content-Type: text/html
Content-Type指示響應的內容,這里是text/html表示HTML網頁。
請注意,瀏覽器就是依靠Content-Type來判斷響應的內容是網頁還是圖片,是視頻還是音樂。瀏覽器並不靠URL來判斷響應的內容,所以,即使URL是
http://www.baidu.com/meimei.jpg
,它也不一定就是圖片。
HTTP響應的Body就是HTML源碼,我們在菜單欄選擇“視圖”,“開發者”,“查看網頁源碼”就可以在瀏覽器中直接查看HTML源碼:
瀏覽器解析過程
當瀏覽器讀取到新浪首頁的HTML源碼后,它會解析HTML,顯示頁面,然后,根據HTML里面的各種鏈接,再發送HTTP請求給新浪服務器,拿到相應的圖片、視頻、Flash、JavaScript腳本、CSS等各種資源,最終顯示出一個完整的頁面。所以我們在Network下面能看到很多額外的HTTP請求。
3. 總結
3.1 HTTP請求
跟蹤了新浪的首頁,我們來總結一下HTTP請求的流程:
3.1.1 步驟1:瀏覽器首先向服務器發送HTTP請求,請求包括:
方法:GET還是POST,GET僅請求資源,POST會附帶用戶數據;
路徑:/full/url/path;
域名:由Host頭指定:Host: www.sina.com
以及其他相關的Header;
如果是POST,那么請求還包括一個Body,包含用戶數據
3.1.1 步驟2:服務器向瀏覽器返回HTTP響應,響應包括:
響應代碼:200表示成功,3xx表示重定向,4xx表示客戶端發送的請求有錯誤,5xx表示服務器端處理時發生了錯誤;
響應類型:由Content-Type指定;
以及其他相關的Header;
通常服務器的HTTP響應會攜帶內容,也就是有一個Body,包含響應的內容,網頁的HTML源碼就在Body中。
3.1.1 步驟3:如果瀏覽器還需要繼續向服務器請求其他資源,比如圖片,就再次發出HTTP請求,重復步驟1、2。
Web采用的HTTP協議采用了非常簡單的請求-響應模式,從而大大簡化了開發。當我們編寫一個頁面時,我們只需要在HTTP請求中把HTML發送出去,不需要考慮如何附帶圖片、視頻等,瀏覽器如果需要請求圖片和視頻,它會發送另一個HTTP請求,因此,一個HTTP請求只處理一個資源(此時就可以理解為TCP協議中的短連接,每個鏈接只獲取一個資源,如需要多個就需要建立多個鏈接)
HTTP協議同時具備極強的擴展性,雖然瀏覽器請求的是http://www.sina.com
的首頁,但是新浪在HTML中可以鏈入其他服務器的資源,比如<img src="http://i1.sinaimg.cn/home/2013/1008/U8455P30DT20131008135420.png">
,從而將請求壓力分散到各個服務器上,並且,一個站點可以鏈接到其他站點,無數個站點互相鏈接起來,就形成了World Wide Web,簡稱WWW。
3.2 HTTP格式
每個HTTP請求和響應都遵循相同的格式,一個HTTP包含Header和Body兩部分,其中Body是可選的。
HTTP協議是一種文本協議,所以,它的格式也非常簡單。
3.2.1 HTTP GET請求的格式:
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
每個Header一行一個,換行符是\r\n。
3.2.2 HTTP POST請求的格式:
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
當遇到連續兩個\r\n時,Header部分結束,后面的數據全部是Body。
3.2.3 HTTP響應的格式:
200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
HTTP響應如果包含body,也是通過\r\n\r\n來分隔的。
請再次注意,Body的數據類型由Content-Type頭來確定,如果是網頁,Body就是文本,如果是圖片,Body就是圖片的二進制數據。
當存在Content-Encoding時,Body數據是被壓縮的,最常見的壓縮方式是gzip,所以,看到Content-Encoding: gzip時,需要將Body數據先解壓縮,才能得到真正的數據。壓縮的目的在於減少Body的大小,加快網絡傳輸。
Web靜態服務器-1-顯示固定的頁面
#coding=utf-8 import socket from multiprocessing import Process def handleClient(clientSocket): '用一個新的進程,為一個客戶端進行服務' recvData = clientSocket.recv(2014) requestHeaderLines = recvData.splitlines() for line in requestHeaderLines: print(line) responseHeaderLines = "HTTP/1.1 200 OK\r\n" responseHeaderLines += "\r\n" responseBody = "hello world" response = responseHeaderLines + responseBody clientSocket.send(response) clientSocket.close() def main(): '作為程序的主控制入口' serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serverSocket.bind(("", 7788)) serverSocket.listen(10) while True: clientSocket,clientAddr = serverSocket.accept() clientP = Process(target = handleClient, args = (clientSocket,)) clientP.start() clientSocket.close() if __name__ == '__main__': main()
服務器端
客戶端
Web靜態服務器-2-顯示需要的頁面
#coding=utf-8 import socket from multiprocessing import Process import re def handleClient(clientSocket): '用一個新的進程,為一個客戶端進行服務' recvData = clientSocket.recv(2014) requestHeaderLines = recvData.splitlines() for line in requestHeaderLines: print(line) httpRequestMethodLine = requestHeaderLines[0] getFileName = re.match("[^/]+(/[^ ]*)", httpRequestMethodLine).group(1) print("file name is ===>%s"%getFileName) #for test if getFileName == '/': getFileName = documentRoot + "/index.html" else: getFileName = documentRoot + getFileName print("file name is ===2>%s"%getFileName) #for test try: f = open(getFileName) except IOError: responseHeaderLines = "HTTP/1.1 404 not found\r\n" responseHeaderLines += "\r\n" responseBody = "====sorry ,file not found====" else: responseHeaderLines = "HTTP/1.1 200 OK\r\n" responseHeaderLines += "\r\n" responseBody = f.read() f.close() finally: response = responseHeaderLines + responseBody clientSocket.send(response) clientSocket.close() def main(): '作為程序的主控制入口' serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serverSocket.bind(("", 7788)) serverSocket.listen(10) while True: clientSocket,clientAddr = serverSocket.accept() clientP = Process(target = handleClient, args = (clientSocket,)) clientP.start() clientSocket.close() #這里配置服務器 documentRoot = './html' if __name__ == '__main__': main()
服務器端
客戶端
Web靜態服務器-3-使用類
#coding=utf-8 import socket import sys from multiprocessing import Process import re class WSGIServer(object): addressFamily = socket.AF_INET socketType = socket.SOCK_STREAM requestQueueSize = 5 def __init__(self, server_address): #創建一個tcp套接字 self.listenSocket = socket.socket(self.addressFamily,self.socketType) #允許重復使用上次的套接字綁定的port self.listenSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #綁定 self.listenSocket.bind(server_address) #變為被動,並制定隊列的長度 self.listenSocket.listen(self.requestQueueSize) def serveForever(self): '循環運行web服務器,等待客戶端的鏈接並為客戶端服務' while True: #等待新客戶端到來 self.clientSocket, client_address = self.listenSocket.accept() #方法2,多進程服務器,並發服務器於多個客戶端 newClientProcess = Process(target = self.handleRequest) newClientProcess.start() #因為創建的新進程中,會對這個套接字+1,所以需要在主進程中減去依次,即調用一次close self.clientSocket.close() def handleRequest(self): '用一個新的進程,為一個客戶端進行服務' recvData = self.clientSocket.recv(2014) requestHeaderLines = recvData.splitlines() for line in requestHeaderLines: print(line) httpRequestMethodLine = requestHeaderLines[0] getFileName = re.match("[^/]+(/[^ ]*)", httpRequestMethodLine).group(1) print("file name is ===>%s"%getFileName) #for test if getFileName == '/': getFileName = documentRoot + "/index.html" else: getFileName = documentRoot + getFileName print("file name is ===2>%s"%getFileName) #for test try: f = open(getFileName) except IOError: responseHeaderLines = "HTTP/1.1 404 not found\r\n" responseHeaderLines += "\r\n" responseBody = "====sorry ,file not found====" else: responseHeaderLines = "HTTP/1.1 200 OK\r\n" responseHeaderLines += "\r\n" responseBody = f.read() f.close() finally: response = responseHeaderLines + responseBody self.clientSocket.send(response) self.clientSocket.close() #設定服務器的端口 serverAddr = (HOST, PORT) = '', 8888 #設置服務器服務靜態資源時的路徑 documentRoot = './html' def makeServer(serverAddr): server = WSGIServer(serverAddr) return server def main(): httpd = makeServer(serverAddr) print('web Server: Serving HTTP on port %d ...\n'%PORT) httpd.serveForever() if __name__ == '__main__': main()v
服務器動態資源請求
1. 瀏覽器請求動態頁面過程
2. WSGI
怎么在你剛建立的Web服務器上運行一個Django應用
和Flask應用
,如何不做任何改變而適應不同的web架構呢?
在以前,選擇 Python web 架構
會受制於可用的web服務器
,反之亦然。如果架構和服務器可以協同工作,那就好了:
但有可能面對(或者曾有過)下面的問題,當要把一個服務器和一個架構結合起來時,卻發現他們不是被設計成協同工作的:
那么,怎么可以不修改服務器和架構代碼而確保可以在多個架構下運行web服務器呢?答案就是 Python Web Server Gateway Interface (或簡稱 WSGI,讀作“wizgy”)。
WSGI允許開發者將選擇web框架和web服務器分開。可以混合匹配web服務器和web框架,選擇一個適合的配對。比如,可以在Gunicorn 或者 Nginx/uWSGI 或者 Waitress上運行 Django, Flask, 或 Pyramid。真正的混合匹配,得益於WSGI同時支持服務器和架構:
web服務器必須具備WSGI接口,所有的現代Python Web框架都已具備WSGI接口,它讓你不對代碼作修改就能使服務器和特點的web框架協同工作。
WSGI由web服務器支持,而web框架允許你選擇適合自己的配對,但它同樣對於服務器和框架開發者提供便利使他們可以專注於自己偏愛的領域和專長而不至於相互牽制。其他語言也有類似接口:java有Servlet API,Ruby 有 Rack。
3.定義WSGI接口
WSGI接口定義非常簡單,它只要求Web開發者實現一個函數,就可以響應HTTP請求。我們來看一個最簡單的Web版本的“Hello World!”:
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return 'Hello World!'
上面的application()
函數就是符合WSGI標准的一個HTTP處理函數,它接收兩個參數:
- environ:一個包含所有HTTP請求信息的dict對象;
- start_response:一個發送HTTP響應的函數。
整個application()
函數本身沒有涉及到任何解析HTTP的部分,也就是說,把底層web服務器解析部分和應用程序邏輯部分進行了分離,這樣開發者就可以專心做一個領域了
不過,等等,這個application()
函數怎么調用?如果我們自己調用,兩個參數environ和start_response我們沒法提供,返回的str也沒法發給瀏覽器。
所以application()
函數必須由WSGI服務器來調用。有很多符合WSGI規范的服務器。而我們此時的web服務器項目的目的就是做一個極可能解析靜態網頁還可以解析動態網頁的服務器
Web動態服務器-1
#coding=utf-8 import socket import sys from multiprocessing import Process import re class WSGIServer(object): addressFamily = socket.AF_INET socketType = socket.SOCK_STREAM requestQueueSize = 5 def __init__(self, serverAddress): #創建一個tcp套接字 self.listenSocket = socket.socket(self.addressFamily,self.socketType) #允許重復使用上次的套接字綁定的port self.listenSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #綁定 self.listenSocket.bind(serverAddress) #變為被動,並制定隊列的長度 self.listenSocket.listen(self.requestQueueSize) self.servrName = "localhost" self.serverPort = serverAddress[1] def serveForever(self): '循環運行web服務器,等待客戶端的鏈接並為客戶端服務' while True: #等待新客戶端到來 self.clientSocket, client_address = self.listenSocket.accept() #方法2,多進程服務器,並發服務器於多個客戶端 newClientProcess = Process(target = self.handleRequest) newClientProcess.start() #因為創建的新進程中,會對這個套接字+1,所以需要在主進程中減去依次,即調用一次close self.clientSocket.close() def setApp(self, application): '設置此WSGI服務器調用的應用程序入口函數' self.application = application def handleRequest(self): '用一個新的進程,為一個客戶端進行服務' self.recvData = self.clientSocket.recv(2014) requestHeaderLines = self.recvData.splitlines() for line in requestHeaderLines: print(line) httpRequestMethodLine = requestHeaderLines[0] getFileName = re.match("[^/]+(/[^ ]*)", httpRequestMethodLine).group(1) print("file name is ===>%s"%getFileName) #for test if getFileName[-3:] != ".py": if getFileName == '/': getFileName = documentRoot + "/index.html" else: getFileName = documentRoot + getFileName print("file name is ===2>%s"%getFileName) #for test try: f = open(getFileName) except IOError: responseHeaderLines = "HTTP/1.1 404 not found\r\n" responseHeaderLines += "\r\n" responseBody = "====sorry ,file not found====" else: responseHeaderLines = "HTTP/1.1 200 OK\r\n" responseHeaderLines += "\r\n" responseBody = f.read() f.close() finally: response = responseHeaderLines + responseBody self.clientSocket.send(response) self.clientSocket.close() else: #根據接收到的請求頭構造環境變量字典 env = {} #調用應用的相應方法,完成動態數據的獲取 bodyContent = self.application(env, self.startResponse) #組織數據發送給客戶端 self.finishResponse(bodyContent) def startResponse(self, status, response_headers): serverHeaders = [ ('Date', 'Tue, 31 Mar 2016 10:11:12 GMT'), ('Server', 'WSGIServer 0.2'), ] self.headers_set = [status, response_headers + serverHeaders] def finishResponse(self, bodyContent): try: status, response_headers = self.headers_set #response的第一行 response = 'HTTP/1.1 {status}\r\n'.format(status=status) #response的其他頭信息 for header in response_headers: response += '{0}: {1}\r\n'.format(*header) #添加一個換行,用來和body進行分開 response += '\r\n' #添加發送的數據 for data in bodyContent: response += data self.clientSocket.send(response) finally: self.clientSocket.close() #設定服務器的端口 serverAddr = (HOST, PORT) = '', 8888 #設置服務器靜態資源的路徑 documentRoot = './html' #設置服務器動態資源的路徑 pythonRoot = './wsgiPy' def makeServer(serverAddr, application): server = WSGIServer(serverAddr) server.setApp(application) return server def main(): if len(sys.argv) < 2: sys.exit('請按照要求,指定模塊名稱:應用名稱,例如 module:callable') #獲取module:callable appPath = sys.argv[1] #根據冒號切割為module和callable module, application = appPath.split(':') #添加路徑套sys.path sys.path.insert(0, pythonRoot) #動態導入module變量中指定的模塊 module = __import__(module) #獲取module變量中指定的模塊的,application變量指定的屬性 application = getattr(module, application) httpd = makeServer(serverAddr, application) print('WSGIServer: Serving HTTP on port %d ...\n'%PORT) httpd.serveForever() if __name__ == '__main__': main()
應用程序示例
import time def app(environ, start_response): status = '200 OK' response_headers = [('Content-Type', 'text/plain')] start_response(status, response_headers) return [str(environ)+'==Hello world from a simple WSGI application!--->%s\n'%time.ctime()]
Web動態服務器-2-傳遞數據給應用
#coding=utf-8 import socket import sys from multiprocessing import Process import re class WSGIServer(object): addressFamily = socket.AF_INET socketType = socket.SOCK_STREAM requestQueueSize = 5 def __init__(self, serverAddress): #創建一個tcp套接字 self.listenSocket = socket.socket(self.addressFamily,self.socketType) #允許重復使用上次的套接字綁定的port self.listenSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #綁定 self.listenSocket.bind(serverAddress) #變為被動,並制定隊列的長度 self.listenSocket.listen(self.requestQueueSize) self.servrName = "localhost" self.serverPort = serverAddress[1] def serveForever(self): '循環運行web服務器,等待客戶端的鏈接並為客戶端服務' while True: #等待新客戶端到來 self.clientSocket, client_address = self.listenSocket.accept() #方法2,多進程服務器,並發服務器於多個客戶端 newClientProcess = Process(target = self.handleRequest) newClientProcess.start() #因為創建的新進程中,會對這個套接字+1,所以需要在主進程中減去依次,即調用一次close self.clientSocket.close() def setApp(self, application): '設置此WSGI服務器調用的應用程序入口函數' self.application = application def handleRequest(self): '用一個新的進程,為一個客戶端進行服務' self.recvData = self.clientSocket.recv(2014) requestHeaderLines = self.recvData.splitlines() for line in requestHeaderLines: print(line) httpRequestMethodLine = requestHeaderLines[0] getFileName = re.match("[^/]+(/[^ ]*)", httpRequestMethodLine).group(1) print("file name is ===>%s"%getFileName) #for test if getFileName[-3:] != ".py": if getFileName == '/': getFileName = documentRoot + "/index.html" else: getFileName = documentRoot + getFileName print("file name is ===2>%s"%getFileName) #for test try: f = open(getFileName) except IOError: responseHeaderLines = "HTTP/1.1 404 not found\r\n" responseHeaderLines += "\r\n" responseBody = "====sorry ,file not found====" else: responseHeaderLines = "HTTP/1.1 200 OK\r\n" responseHeaderLines += "\r\n" responseBody = f.read() f.close() finally: response = responseHeaderLines + responseBody self.clientSocket.send(response) self.clientSocket.close() else: #處理接收到的請求頭 self.parseRequest() #根據接收到的請求頭構造環境變量字典 env = self.getEnviron() #調用應用的相應方法,完成動態數據的獲取 bodyContent = self.application(env, self.startResponse) #組織數據發送給客戶端 self.finishResponse(bodyContent) def parseRequest(self): '提取出客戶端發送的request' requestLine = self.recvData.splitlines()[0] requestLine = requestLine.rstrip('\r\n') self.requestMethod, self.path, self.requestVersion = requestLine.split(" ") def getEnviron(self): env = {} env['wsgi.version'] = (1, 0) env['wsgi.input'] = self.recvData env['REQUEST_METHOD'] = self.requestMethod # GET env['PATH_INFO'] = self.path # /index.html return env def startResponse(self, status, response_headers, exc_info=None): serverHeaders = [ ('Date', 'Tue, 31 Mar 2016 10:11:12 GMT'), ('Server', 'WSGIServer 0.2'), ] self.headers_set = [status, response_headers + serverHeaders] def finishResponse(self, bodyContent): try: status, response_headers = self.headers_set #response的第一行 response = 'HTTP/1.1 {status}\r\n'.format(status=status) #response的其他頭信息 for header in response_headers: response += '{0}: {1}\r\n'.format(*header) #添加一個換行,用來和body進行分開 response += '\r\n' #添加發送的數據 for data in bodyContent: response += data self.clientSocket.send(response) finally: self.clientSocket.close() #設定服務器的端口 serverAddr = (HOST, PORT) = '', 8888 #設置服務器靜態資源的路徑 documentRoot = './html' #設置服務器動態資源的路徑 pythonRoot = './wsgiPy' def makeServer(serverAddr, application): server = WSGIServer(serverAddr) server.setApp(application) return server def main(): if len(sys.argv) < 2: sys.exit('請按照要求,指定模塊名稱:應用名稱,例如 module:callable') #獲取module:callable appPath = sys.argv[1] #根據冒號切割為module和callable module, application = appPath.split(':') #添加路徑套sys.path sys.path.insert(0, pythonRoot) #動態導入module變量中指定的模塊 module = __import__(module) #獲取module變量中制定的模塊的application變量指定的屬性 application = getattr(module, application) httpd = makeServer(serverAddr, application) print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT)) httpd.serveForever() if __name__ == '__main__': main()