请求从 Nginx 到 uwsgi 到 django 交互概览
作为python web开发,我们首先要弄清楚,到底一个请求过来,发生了什么事,请求的传递是怎么样完成的,由nginx是怎么转发到uwsgi, uwsgi又是怎样把请求传给到我们的框架(django or falsk)由我们自己写的代码处理,返回数据给客户端的。因此我作了以下一个粗略的流程图:
以下我会逐个步骤从下往上详细讲解,并附上代码和配置,
WSGI 协议
从上面的图看得出 wsgi server (比如uwsgi) 要和 wsgi application(比如django )交互,uwsgi需要将过来的请求转给django 处理,那么uwsgi 和 django的交互和调用就需要一个统一的规范,这个规范就是WSGI WSGI(Web Server Gateway Interface) ,WSGI是 Python PEP333中提出的一个 Web 开发统一规范。
Web 应用的开发通常都会涉及到 Web 框架(django, flask)的使用,各个 Web 框架内部由于实现不同相互不兼容,给用户的学习,使用和部署造成了很多麻烦。
正是有了WSGI这个规范,它约定了wsgi server 怎么调用web应用程序的代码,web 应用程序需要符合什么样的规范,只要 web 应用程序和 wsgi server 都遵守 WSGI 协议,那么,web 应用程序和 wsgi server就可以随意的组合。 比如uwsgi+django , uwsgi+flask, gunicor+django, gunicor+flask 这些的组合都可以任意组合,因为他们遵循了WSGI规范。
WSGI 标准
WSGI 标准中主要定义了两种角色:
- “server” 或 “gateway” 端
- “application” 或 “framework” 端
为了方便理解,我们可以把server具体成 uwsgi, application具体成django
这里可以看到,WSGI 服务器需要调用应用程序的一个可调用对象,这个可调用对象(callable object)可以是一个函数,方法,类或者可调用的实例,总之是可调用的。
下面是一个 callable object 的示例,这里的可调用对象是一个函数:
def simple_app(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/html')]
start_response(status, response_headers)
return ['Hello World']
这里,我们首先要注意,这个对象接收两个参数:
environ
:请求的环境变量,它是一个字典,包含了客户端请求的信息,如 HTTP 请求的首部,方法等信息,可以认为是请求上下文,
start_response
:一个用于发送HTTP响应状态(HTTP status )、响应头(HTTP headers)的回调函数。在返回内容之前必须先调用这个回掉函数
上面的 start_response 这个回调函数的作用是用于让 WSGI Server 返回响应的 HTTP 首部和 HTTP 状态码。这个函数有两个必须的参数,返回的状态码和返回的响应首部组成的元祖列表。返回状态码和首部的这个操作始终都应该在响应 HTTP body 之前执行。
还需要注意的是,最后的返回结果,应该是一个可迭代对象,这里是将返回的字符串放到列表里。如果直接返回字符串可能导致 WSGI 服务器对字符串进行迭代而影响响应速度。
当然,这个函数是一个最简单的可调用对象,它也可以是一个类或者可调用的类实例。
WSGI 实例
- wsgi application 的代码 app.py
def application(env, start_response):
start_response('200 OK', [('Content-Type', 'text/html'), ('X-Coder', 'Cooffeeli')])
return ['<h1>你好!!世界</h1>']
- wsgi server 代码 wsgi_server.py
我们可以借助 python 的 wsgiref 库运行一个 WSGI 服务器(当然这个 WSGI 服务器同时也是 Web 服务器),用它来运行我们的 application
from wsgiref.simple_server import make_server
from app import application
# 启动 WSGI 服务器
httpd = make_server (
'localhost',
9000,
application # 这里指定我们的 application object)
)
# 开始处理请求
httpd.handle_request()
python wsgiref_server.py
运行上面的程序,并访问 http://localhost:9000 , 将返回此次请求所有的首部信息。
这里,我们利用 environ 字典,获取了请求中所有的变量信息,构造成相应的内容返回给客户端。
environ 这个参数中包含了请求的首部,URL,请求的地址,请求的方法等信息。可以参考 PEP3333来查看 environ 字典中必须包含哪些 CGI 变量。
自己实现WSGI Server
既然我们知道了WSGI的规范,我们完全可以自己实现一个WSGI Server
根据这个规范,我们可以总结WSGI Server需要实现以下功能:
- 监听端口,接收请求
- 接受HTTP请求后,解析HTTP协议
- 根据HTTP内容,生成
env
参数,该参数包括HTTP,wsgi信息,可以看作是请求上下文 - 实现一个
start_response
函数,作为调用application的参数,用作application回调函数,负责http相应头
实现代码: WSGIServer.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import sys
import StringIO
from app import application
from datetime import datetime
class WSGIServer(object):
def __init__(self, server_address):
"""初始构造函数, 创建监听socket"""
self.listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.listen_sock.bind(server_address)
self.listen_sock.listen(5)
(host, port) = self.listen_sock.getsockname()
self.server_port = port
self.server_name = socket.getfqdn(host)
def set_application(self, application):
"""设置wsgi application, 供server 调用"""
self.application = application
def get_environ(self):
"""构造WSGI环境变量,传给application的env参数"""
self.env = {
'wsgi.version': (1, 0),
'wsgi.url_scheme': 'http',
'wsgi.errors': sys.stderr,
'wsgi.multithread': False,
'wsgi.run_once': False,
'REQUEST_METHOD': self.request_method,
'PATH_INFO': self.request_path,
'SERVER_NAME': self.server_name,
'SERVER_PORT': str(self.server_port),
'wsgi.input': StringIO.StringIO(self.request_data),
}
return self.env
def start_response(self, http_status, http_headers):
"""构造WSGI响应, 传给application的start_response"""
self.http_status = http_status
self.http_headers = dict(http_headers)
headers = {
'Date': datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
'Server': 'WSGIServer 1.0'
}
self.http_headers.update(headers)
def parse_request(self, text):
"""获取http头信息,用于构造env参数"""
request_line = text.splitlines()[0]
request_info = request_line.split(' ')
(self.request_method,
self.request_path,
self.request_version) = request_info
def get_http_response(self, response_data):
"""完成response 内容"""
res = 'HTTP/1.1 {status}
'.format(status=self.http_status)
for header in self.http_headers.items():
res += '{0}: {1}
'.format(*header)
res += '
'
res_body = ''
for val in response_data:
res_body += val
res += res_body
return res
def handle_request(self):
"""处理请求"""
# 初始版本,只接受一个请求
conn, addr = self.listen_sock.accept()
# 获取http 请求的request内容
self.request_data = conn.recv(1024)
self.parse_request(self.request_data)
# 构造调用application需要的两个参数 env, start_response
env = self.get_environ()
start_response = self.start_response
# 调用application, 并获取需要返回的http response内容
response_data = self.application(env, start_response)
# 获取完整http response header 和 body, 通过socket的sendall返回到客户端
res = self.get_http_response(response_data)
conn.sendall(res)
# 脚本运行完毕也会结束
conn.close()
def make_server(server_address, application):
"""创建WSGI Server 负责监听端口,接受请求"""
wsgi_server = WSGIServer(server_address)
wsgi_server.set_application(application)
return wsgi_server
SERVER_ADDRESS = (HOST, PORT) = '', 8124
wsgi_server = make_server(SERVER_ADDRESS, application)
wsgi_server.handle_request()
上面的 WSGI 服务器运行过程为:
- 初始化,创建套接字,绑定端口
- 接收客户端请求
- 解析 HTTP 协议
- 构造 WSGI 环境变量(environ)
- 调用 application
- 回调函数 start_response 设置好响应的状态码和首部
- 返回信息
至此, wsgi server -> wsgi application 的交互讲解完毕, 下面我们继续看nginx->uwsgi交互过程
启动 uwsgi
上面说了我们自己实现WSGI Server的过程,现在我们用uwsgi 来作为Server
运行监听请求uwsgi
uwsgi --http :9090 --wsgi-file foobar.py --master --processes 4 --threads 2
执行这个命令会产生4个uwsgi进程(每个进程2个线程),1个master进程,当有子进程死掉时再产生子进程,1个 the HTTP router进程,一个6个进程。
这个Http route进程的地位有点类似nginx,(可以认为与nginx同一层)负责路由http请求给worker, Http route进程和worker之间使用的是uwsgi协议
FastCgi协议, uwsgi协议, http协议有什么用?
在构建 Web 应用时,通常会有 Web Server (nginx)和 Application Server(wsgi server eg:uwsgi) 两种角色。其中 Web Server 主要负责接受来自用户的请求,解析 HTTP 协议,并将请求转发给 Application Server,Application Server 主要负责处理用户的请求,并将处理的结果返回给 Web Server,最终 Web Server 将结果返回给用户。
由于有很多动态语言和很多种 Web Server,他们彼此之间互不兼容,给程序员造成了很大的麻烦。因此就有了 CGI/FastCGI ,uwsgi 协议,定义了 Web Server 如何通过输入输出与 Application Server 进行交互,将 Web 应用程序的接口统一了起来。
总而言之, 这些协议就是进程交互的一种沟通方式。
举个例子:美国人和中国人沟通必须要有一个公共的语言:英语, 这时候英语就是两个人沟通的协议, 不然,一个说英语(uwsgi协议), 一个说中文(fastcgi协议)是肯定会乱码的,处理不成功的。用同一个协议,大家都知道该如何解析过来的内容。
所以,nginx 和 uwsgi交互就必须使用同一个协议,而上面说了uwsgi支持fastcgi,uwsgi,http协议,这些都是nginx支持的协议,只要大家沟通好使用哪个协议,就可以正常运行了。
将uwsgi 放在nginx 后面
将uwsgi 放在nginx后面,让nginx反向代理请求到uwsgi
uwsgi 原生支持HTTP, FastCGI, SCGI,以及特定的uwsgi协议, 性能最好的明显时uwsgi, 这个协议已经被nginx支持。
所以uwsgi 配置使用哪个协议,nginx 要使用对应协议
# 使用http协议
uwsgi --http-socket 127.0.0.1:9000 --wsgi-file app.py
# nginx配置
lcation / {
proxy_pass 127.0.0.1:9000;
}
更多协议
[uwsgi]
# 使用uwsgi协议 socket, uwsgi-socket 都是uwsgi协议
# bind to the specified UNIX/TCP socket using default protocol
# UNIX/TCP 意思时可以UNIX: xx.sock, 或者 TCP: 127.0.0.1:9000 他们是都可以的
# UNIX 没有走TCP协议,不是面向连接, 而是直接走文件IO
# nginx 使用uwsgi_pass
socket = 127.0.0.1:9000
socket = /dev/shm/owan_web_uwsgi.sock
uwsgi-socket = /dev/shm/owan_web_uwsgi.sock
# nginx 使用 uwsgi_pass /dev/shm/owan_web_uwsgi.sock;
# 使用fastcgi协议 fastcgi-socket
# bind to the specified UNIX/TCP socket using FastCGI protocol
# nginx 就可以好象PHP那样配置 使用fastcgi_pass
fastcgi-socket = /dev/shm/owan_web_uwsgi.sock
# nginx 使用fastcgi_pass /dev/shm/owan_web_uwsgi.sock;
# 使用http协议 http-socket
# bind to the specified UNIX/TCP socket using HTTP protocol
# nginx 使用proxy_pass
# 原来proxy_pass 是http协议,但不一定要用TCP
# proxy_pass http://unix:/dev/shm/owan_web_uwsgi.sock;
http-socket = /dev/shm/owan_web_uwsgi.sock
# nginx 使用 proxy_pass /dev/shm/owan_web_uwsgi.sock;
chdir = /data/web/advance_python/uwsgi/
wsgi-file = app.py
processes = 4
threads = 2
master = true
...
结束
至此,nginx ->uwsgi ->web 框架 以及 WSGI的相关知识已经讲解完了。 需要补充的是,我们自己实现的WSGI Server只能支持一个请求,在之后的日子,我会再写一些教程,关于socket IO 复用 和线程池 让我们自己写server支持多请求,多并发的功能