捅开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请求过程,中间涉及到的协议等都一一讲解了下,但愿各位明白

相关文章
相关标签/搜索