捅開web應用的那層紗

疑問

這篇也是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()

這裏能夠看出服務器程序是如何與應用程序配合完成用戶請求的。大概作了如下幾件事:

  1. 初始化,創建套接字,綁定監聽端口;

  2. 設置加載的 web app;

  3. 開始持續運行 server;

  4. 處理訪問請求(self.handle_request());

  5. 獲取請求信息及環境信息(self.get_environ());

  6. environ運行加載的 web app 獲得返回信息(app_data = self.application(env, self.start_response));

  7. 構造返回信息頭部;

  8. 返回信息;

總結

至此,整個HTTP請求過程,中間涉及到的協議等都一一講解了下,但願各位明白

相關文章
相關標籤/搜索