捅开web应用的那层纱

编程

疑问

这篇也是Django连接池试验引发来得,考虑到整个web请求流程的复杂和独立性,新起一篇单独讲解

前置

之前搞php,java时,经常提到CGI,FastCGI, 且当时听说FastCGI性能更高,但当时未求深入,不知细节原因。以及一个web请求所经历的生命历程,也是算明白,但不是很深入,此篇会细致讲解“网关接口(协议)”的发展历程,以及web流程的生命周期。

HTTP协议

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议。

HTTP协议基于TCP/IP通信协议来传递数据(文本,图片,json串等)。但这里要注意,他不涉及传输包,只是定义客户端和服务器端的通信格式。

HTTP请求方法

HTTP/0.9 GET

HTTP/1.0 GET、POST、HEAD

HTTP/1.1 GET、POST、HEAD、PUT、PATCH、HEAD、OPTIONS、DELETE

HTTP请求报文

请求报文由以下四部分组成:

  • 请求行:由 请求方法,请求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,以指定请求或响应中的数据格式。

此处只介绍常用的三个

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}

".format(status=self.status)

for header in self.headers:

response += "{0}: {1}

".format(*header)

response += "

"

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

 

 

 

以上是 捅开web应用的那层纱 的全部内容, 来源链接: utcz.com/z/517702.html

回到顶部