疑問
這篇也是Django鏈接池試驗引起來得,考慮到整個web請求流程的複雜和獨立性,新起一篇單獨講解php
前置
以前搞php,java時,常常提到CGI,FastCGI, 且當時據說FastCGI性能更高,但當時未求深刻,不知細節緣由。以及一個web請求所經歷的生命歷程,也是算明白,但不是很深刻,此篇會細緻講解「網關接口(協議)」的發展歷程,以及web流程的生命週期。css
HTTP協議
HTTP協議是Hyper Text Transfer Protocol(超文本傳輸協議)的縮寫,是用於從萬維網服務器傳輸超文本到本地瀏覽器的傳送協議。html
HTTP協議基於TCP/IP通訊協議來傳遞數據(文本,圖片,json串等)。但這裏要注意,他不涉及傳輸包,只是定義客戶端和服務器端的通訊格式。java
HTTP請求方法
HTTP/0.9 GET HTTP/1.0 GET、POST、HEAD HTTP/1.1 GET、POST、HEAD、PUT、PATCH、HEAD、OPTIONS、DELETE
HTTP請求報文
請求報文由如下四部分組成:python
- 請求行:由 請求方法,請求URL(不包括域名),HTTP協議版本 組成
- 請求頭(Request Header):由 key/vaue的形式組成
- 空行:請求頭之下是一個空行,通知服務器再也不有請求頭(有請求體時纔有)
- 請求體:通常post纔有,但get也能夠經過body傳遞
GET /books/?sex=man&name=Professional HTTP/1.1 // 請求行 Host: www.example.com User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 Connection: Keep-Alive // 以上是請求頭 POST / HTTP/1.1 // 請求行 Host: www.example.com User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 40 Connection: Keep-Alive // 以上是請求頭 (此處空行) // 表明請求頭結束 sex=man&name=Professional // 請求體
注意:比較重要的Content-Type字段,GET方法裏沒有,是由於沒設置請求體,若是要設置請求體則必須指定Content-Type,以指定請求或響應中的數據格式。web
此處只介紹經常使用的三個json
application/json:JSON數據格式 - 接口經常使用
application/x-www-form-urlencoded:表單提交時指定這個
multipart/form-data : 須要在表單中進行文件上傳時,就須要使用該格式瀏覽器
HTTP響應報文
與請求報文相似,也是由四部分組成:服務器
- 狀態行:由 HTTP協議,狀態碼,狀態描述 組成
- 響應頭(Response Header):key/value的形式
- 空行:請求頭之下是一個空行,通知服務器再也不有請求頭
- 響應正文:
HTTP/1.1 200 OK // 狀態行 Server: Apache-Coyote/1.1 Content-Type: text/html;charset=UTF-8 Content-Length: 624 Date: Mon, 03 Nov 2014 06:37:28 GMT // 以上爲響應頭 (此處爲一空行) // 表明響應頭的終結 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> // 如下爲響應正文,此處指定爲text/html數據格式 <html> ......
HTTP狀態碼
1xx:指示信息,表示請求已接收,繼續處理併發
2xx:成功,表示請求已被成功接受,處理
3xx:重定向
4xx:客戶端錯誤
5xx:服務器端錯誤
HTTP請求流程
即瀏覽器輸入地址回車到顯示返回的過程
簡單來說就是:域名解析 --> 發起TCP的3次握手 --> 創建TCP鏈接後發起http請求 --> 服務器響應http請求,瀏覽器獲得html代碼 --> 瀏覽器解析html代碼,並請求html代碼中的資源(如js、css、圖片等) --> 瀏覽器對頁面進行渲染呈現給用戶 --> 四次揮手結束
畫了個示意圖,複雜細節暫不在此解釋
網關接口(協議)
爲何須要這個,要從CGI的起源提及,好久好久之前.....
CGI
最開始的互聯網只有靜態內容,web server只須要實現HTTP協議解析與靜態資源定位便可,但隨着用戶交互性的加強,服務端業務邏輯逐漸增長,這時純靜態內容已經知足不了了,但動態內容的東西都交給web server去作顯然不合適,這時CGI應聲出現,CGI 協議定義了 web server 與 cgi 程序之間通訊的規範, web server 一收到動態資源的請求就 fork 一個子進程調用 cgi 程序處理這個請求, 同時將和此請求相關的 context 傳給 cgi 程序, 像是 path_info, script path, request method, remote ip 等等...
但正由於每次都fork一個進程去處理,在併發比較多的時候對資源的消耗仍是很是大的,同時響應速度也會變慢,因此CGI的升級版本FastCGI就出現了。
FastCGI
FastCGI致力於減小網頁服務器與CGI程序之間交互的開銷,從而使服務器能夠同時處理更多的網頁請求。
與CGI爲每一個請求建立一個新的進程不一樣,FastCGI使用持續的進程來處理一連串的請求。簡單來講,其本質就是一個常駐內存的進程池技術,由調度器負責將傳遞過來的CGI請求發送給處理CGI的handler進程來處理。在一個請求處理完成以後,該處理進程不銷燬,繼續等待下一個請求的到來。
WSGI
事情繼續發展,回到python上來,python也是做爲「cgi application」一員出現,可是那時的Python應用程序一般是爲CGI,FastCGI,mod_python中的一個而設計,甚至是爲特定Web服務器的自定義的API接口而設計的。如何選擇合適的Web應用程序框架成爲困擾Python初學者的一個問題。
WSGI是做爲Web服務器與Web應用程序或應用框架之間的一種低級別的接口,以提高可移植Web應用開發的共同點。WSGI是基於現存的CGI標準而設計的。
WSGI內容概要
WSGI協議主要包括server(服務器程序)和application(應用程序)兩部分
application(應用程序)
WSGI規定
1. 應用程序應爲可調用對象,須要接收2個參數
- environ,一個字典,該字典能夠包含了客戶端請求的信息以及其餘信息,能夠認爲是請求上下文
- start_response,一個用於發送HTTP響應狀態(HTTP status )、響應頭(HTTP headers)的回調函數,第一個參數爲HTTP響應狀態,第二個參數爲[(key, value),...]
2. 可調用對象要返回一個值,這個值是可迭代的。
經過回調函數將響應狀態和響應頭返回給server,同時返回響應正文(response body),響應正文是可迭代的、幷包含了多個字符串。
看起來應用程序代碼像下面這樣:
# callable function def application(environ, start_response): status = '200 OK' response_headers = [('Content-Type', 'text/plain')] start_response(status, response_headers) return ['Hello world'] or # callable class class Application: def __init__(self, environ, start_response): pass def __iter__(self): yield ['Hello world'] or # callable object class ApplicationObj: def __call__(self, environ, start_response): return ['Hello world']
server(服務器程序)
服務器程序要求監聽HTTP請求,在每次客戶端的請求傳來時,調用咱們寫好的應用程序,並將處理好的結果返回給客戶端。
3. 服務器程序須要調用應用程序
手擼一個server端
# server.py # coding: utf-8 from __future__ import unicode_literals import socket import StringIO import sys import datetime class WSGIServer(object): socket_family = socket.AF_INET socket_type = socket.SOCK_STREAM request_queue_size = 10 def __init__(self, address): self.socket = socket.socket(self.socket_family, self.socket_type) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind(address) self.socket.listen(self.request_queue_size) host, port = self.socket.getsockname()[:2] self.host = host self.port = port def set_application(self, application): self.application = application def serve_forever(self): while 1: self.connection, client_address = self.socket.accept() self.handle_request() def handle_request(self): self.request_data = self.connection.recv(1024) self.request_lines = self.request_data.splitlines() try: self.get_url_parameter() env = self.get_environ() app_data = self.application(env, self.start_response) self.finish_response(app_data) print '[{0}] "{1}" {2}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), self.request_lines[0], self.status) except Exception, e: pass def get_url_parameter(self): self.request_dict = {'Path': self.request_lines[0]} for itm in self.request_lines[1:]: if ':' in itm: self.request_dict[itm.split(':')[0]] = itm.split(':')[1] self.request_method, self.path, self.request_version = self.request_dict.get('Path').split() def get_environ(self): env = { 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': StringIO.StringIO(self.request_data), 'wsgi.errors': sys.stderr, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'REQUEST_METHOD': self.request_method, 'PATH_INFO': self.path, 'SERVER_NAME': self.host, 'SERVER_PORT': self.port, 'USER_AGENT': self.request_dict.get('User-Agent') } return env def start_response(self, status, response_headers): headers = [ ('Date', datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')), ('Server', 'RAPOWSGI0.1'), ] self.headers = response_headers + headers self.status = status def finish_response(self, app_data): try: response = 'HTTP/1.1 {status}\r\n'.format(status=self.status) for header in self.headers: response += '{0}: {1}\r\n'.format(*header) response += '\r\n' for data in app_data: response += data self.connection.sendall(response) finally: self.connection.close() if __name__ == '__main__': port = 8888 if len(sys.argv) < 2: sys.exit('請提供可用的wsgi應用程序, 格式爲: 模塊名.應用名 端口號') elif len(sys.argv) > 2: port = sys.argv[2] def generate_server(address, application): server = WSGIServer(address) server.set_application(TestMiddle(application)) return server app_path = sys.argv[1] module, application = app_path.split('.') module = __import__(module) application = getattr(module, application) httpd = generate_server(('', int(port)), application) print 'RAPOWSGI Server Serving HTTP service on port {0}'.format(port) print '{0}'.format(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')) httpd.serve_forever()
這裏能夠看出服務器程序是如何與應用程序配合完成用戶請求的。大概作了如下幾件事:
-
初始化,創建套接字,綁定監聽端口;
-
設置加載的 web app;
-
開始持續運行 server;
-
處理訪問請求(self.handle_request());
-
獲取請求信息及環境信息(self.get_environ());
-
用environ運行加載的 web app 獲得返回信息(app_data = self.application(env, self.start_response));
-
構造返回信息頭部;
-
返回信息;
總結
至此,整個HTTP請求過程,中間涉及到的協議等都一一講解了下,但願各位明白