1. 简介
WSGI
WSGI
:web服务器网关接口,这是python中定义的一个网关协议,规定了Web Server如何跟应用程序交互。可以理解为一个web应用的容器,通过它可以启动应用,进而提供HTTP服务。
它最主要的目的是保证在Python中所有的Web Server程序或者说Gateway程序,能够通过统一的协议跟Web框架或者说Web应用进行交互。
uWSGI
uWGSI
:是一个web服务器,或者wsgi server服务器,他的任务就是接受用户请求,由于用户请求是通过网络发过来的,其中用户到服务器端之间用的是http协议,所以我们uWSGI要想接受并且正确解出相关信息,我们就需要uWSGI实现http协议,没错,uWSGI里面就实现了http协议。所以现在我们uWSGI能准确接受到用户请求,并且读出信息。现在我们的uWSGI服务器需要把信息发给Django,我们就需要用到WSGI协议,刚好uWSGI实现了WSGI协议,所以。uWSGI把接收到的信息作一次简单封装传递给Django,Django接收到信息后,再经过一层层的中间件,于是,对信息作进一步处理,最后匹配url,传递给相应的视图函数,视图函数做逻辑处理......后面的就不叙述了,然后将处理后的数据通过中间件一层层返回,到达Djagno最外层,然后,通过WSGI协议将返回数据返回给uWSGI服务器,uWSGI服务器通过http协议将数据传递给用户。这就是整个流程。
这个过程中我们似乎没有用到uwsgi协议,但是他也是uWSGI实现的一种协议,鲁迅说过,存在即合理,所以说,他肯定在某个地方用到了。我们过一会再来讨论
我们可以用这条命令:python manage.py runserver
,启动Django自带的服务器。DJango自带的服务器(runserver 起来的 HTTPServer 就是 Python 自带的 simple_server)。是默认是单进程单多线程的,对于同一个http请求,总是先执行一个,其他等待,一个一个串行执行。无法并行。而且django自带的web服务器性能也不好,只能在开发过程中使用。于是我们就用uWSGI代替了。
为什么有了WSGI为什么还需要nginx?
因为nginx具备优秀的静态内容处理能力,然后将动态内容转发给uWSGI服务器,这样可以达到很好的客户端响应。支持的并发量更高,方便管理多进程,发挥多核的优势,提升性能。这时候nginx和uWSGI之间的沟通就要用到uwsgi协议。
2. 简单的Web Server
在了解WSGI协议之前,首先看一个通过socker编程实现的Web服务的代码。
import socket EOL1 = b'\n\n' EOL2 = b'\n\r\n' body = """hello world <h1> from test</h1>""" response_params = [ 'HTTP/1.0 200 OK', 'DATE: Sun, 27 may 2019 01:01:01 GMT', 'Content-Type:text/plain; charset=utf-8', 'Content-Length: {}\r\n'.format(len(body.encode())), body, ] response = '\r\n'.join(response_params) def handle_connection(conn, addr): request = b"" print('new conn', conn, addr) import time time.sleep(100) while EOL1 not in request and EOL2 not in request: request += conn.recv(1024) print(request) conn.send(response.encode()) conn.close() def main(): serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('127.0.0.1', 8000)) serversocket.listen(5) print('http://127.0.0.1:8000') try: while True: conn, address = serversocket.accept() handle_connection(conn, address) finally: serversocket.close() if __name__ == '__main__': main()
多线程
import errno import socket import threading import time EOL1 = b'\n\n' EOL2 = b'\n\r\n' body = """hello world <h1> from test</h1>""" response_params = [ 'HTTP/1.0 200 OK', 'DATE: Sun, 27 may 2019 01:01:01 GMT', 'Content-Type:text/plain; charset=utf-8', 'Content-Length: {}\r\n'.format(len(body.encode())), body, ] response = '\r\n'.join(response_params) def handle_connection(conn, addr): print(conn, addr) time.sleep(60) request = b"" while EOL1 not in request and EOL2 not in request: request += conn.recv(1024) print(request) current_thread = threading.currentThread() content_length = len(body.format(thread_name=current_thread.name).encode()) print(current_thread.name) conn.send(response.format(thread_name=current_thread.name, length=content_length).encode()) conn.close() def main(): serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) serversocket.bind(('127.0.0.1', 8000)) serversocket.listen(10) print('http://127.0.0.1:8000') serversocket.setblocking(True) # 设置socket为阻塞模式 try: i = 0 while True: try: conn, address = serversocket.accept() except socket.error as e: if e.args[0] != errno.EAGAIN: raise continue i += 1 print(i) t = threading.Thread(target=handle_connection, args=(conn, address), name='thread-%s'%i) t.start() finally: serversocket.close() if __name__ == '__main__': main()
3. 简单的WSGI Application
该协议分为两个部分:
Web Server 或者Gateway
监听在某个端口上接收外部的请求
Web Application
Web Server接收请求之后,会通过WSGI协议规定的方式把数据传递给Web Application,在Web Application中处理完之后,设置对应的状态和header,之后返回body部分。Web Server拿到返回的数据之后,再进行HTTP协议的封装,最终返回完整的HTTPResponse数据。
下面我们来实现一个简单的应用:
app.py def simple_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [b'Hello world! -by test \n']
我们需要一个脚本运行上面这个应用:
import os import sys from app import simple_app def wsgi_to_bytes(s): return s.encode() def run_with_cgi(application): environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin.buffer environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) environ['wsgi.multithread'] = False environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True if environ.get('HTTPS', 'off') in ('on', '1'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' headers_set = [] headers_sent = [] def write(data): out = sys.stdout.buffer if not headers_set: raise AssertionError('Write() before start_response()') elif not headers_sent: # 在输出第一行数据之前,先发送响应头 status, response_headers = headers_sent[:] = headers_set out.write(wsgi_to_bytes('Status: %s\r\n' % status)) for header in response_headers: out.write(wsgi_to_bytes('%s: %s\r\n' % header)) out.write(wsgi_to_bytes('\r\n')) out.write(data) out.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: # 如果已经发送了header,则重新抛出原始异常信息 raise (exc_info[0], exc_info[1], exc_info[2]) finally: exc_info = None elif headers_set: raise AssertionError('*Headers already set!') headers_set[:] = [status, response_headers] return write result = application(environ, start_response) try: for data in result: if data: write(data) if not headers_sent: write('') finally: if hasattr(result, 'close'): result.close() if __name__ == '__main__': run_with_cgi(simple_app)
运行结果:
Status: 200 OK Content-type: text/plain Hello world! -by test
如果不是windows系统,还可以采用另一种方式运行:
pip install gunicorn
gunicorn app:simle_app
4. 理解
对于上述代码我们只需要关注一点,result = application(environ, start_response)
,我们要实现的Application,只需要能够接收一个环境变量以及一个回调函数即可。但处理完请求之后,通过回调函数(start_response)来设置response的状态和header,最终返回结果,也就是body。
WSGI协议规定,application必须是一个可调用对象,这意味这个对象既可以是Python中的一个函数,也可以是一个实现了__call__方法的类的实例,比如:
样例一
class AppClass(object): status = '200 OK' response_headers = [('Content-type', 'text/plain')] def __call__(self, environ, start_response): print(environ, start_response) start_response(self.status, self.response_headers) return [b'Hello AppClass.__call__\n'] application = AppClass()
gunicorn app: application
运行上述文件
样例二
除此之外,我们还可以通过另一种方式实现WSGI协议,从上面的simple_app和这里的AppClass.__call__的返回值来看,WSGI Server只需要返回一个可迭代的对象就行
class AppClassIter(object): status = '200 OK' response_headers = [('Content-type', 'text/plain')] def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): self.start_response(self.status, self.response_headers) yield b'Hello AppClassIter\n'
gunicorn app: AppClassIter
运行上述文件
这里的启动命令并不是一个类的实例,而是类本身。通过上面两个代码,我们可以看到能够被调用的方法会传environ和start_response过来,而现在这个实现没有可调用的方式,所以就需要在实例化的时候通过参数传递进来,这样在返回body之前,可以先调用start_response方法。
因此,可以推测出WSGI Server是如何调用WSGI Application的,大概代码如下:
def start_response(status, headers): # 伪代码 set_status(status) for k, v in headers: set_header(k, v) def handle_conn(conn): # 调用我们定义的application(也就是上面的simple_app, 或者是AppClass的实例,或者是AppClassIter本身) app = application(environ, start_response) # 遍历返回的结果,生成response for data in app: response += data conn.sendall(response)
5. WSGI中间件和Werkzeug
WSGI中间件可以理解为Python中的一个装饰器,可以在不改变原方法的情况下对方法的输入和输出部分进行处理。
类似这样:
def simple_app(enbiron, start_response): response = Response('Hello World', start_response=start_response) response.set_header('Content-Type', 'text/plain') # 这个函数里面调用start_response return response
这样就看起来更加自然一点。
因此,就存在Werkzeug这样的WSGI工具集,让你能够跟WSGI协议更加友好的交互。从理论上来看,我们可以直接通过WSGI协议的简单实现写一个Web服务。但是有了Werkzeug之后,我们可以写的更加容易。
6. 杂谈
django 的并发能力真的是令人担忧,这里就使用 nginx + uwsgi 提供高并发
nginx 的并发能力超高,单台并发能力过万(这个也不是绝对),在纯静态的 web 服务中更是突出其优越的地方,由于其底层使用 epoll 异步IO模型进行处理,使其深受欢迎
做过运维的应该都知道,
Python需要使用nginx + uWSGI 提供静态页面访问,和高并发
php 需要使用 nginx + fastcgi 提供高并发,
java 需要使用 nginx + tomcat 提供 web 服务
django 原生为单线程序,当第一个请求没有完成时,第二个请求辉阻塞,知道第一个请求完成,第二个请求才会执行。 Django就没有用异步,通过线程来实现并发,这也是WSGI普遍的做法,跟tornado不是一个概念 官方文档解释django自带的server默认是多线程 django开两个接口,第一个接口sleep(20),另一个接口不做延时处理(大概耗时几毫秒) 先请求第一个接口,紧接着请求第二个接口,第二个接口返回数据,第一个接口20秒之后返回数据 证明django的server是默认多线程 启动uWSGI服务器 # 在django项目目录下 Demo工程名 uwsgi --http 0.0.0.0:8000 --file Demo/wsgi.py 经过上述的步骤测试,发现在这种情况下启动django项目,uWSGI也是单线程,访问接口需要"排队" 不给uWSGI加进程,uWSGI默认是单进程单线程 uwsgi --http 0.0.0.0:8000 --file Demo/wsgi.py --processes 4 --threads 2 # processes: 进程数 # processes 和 workers 一样的效果 # threads : 每个进程开的线程数 经过测试,接口可以"同时"访问,uWSGI提供多线程
- Python因为GIL的存在,在一个进程中,只允许一个线程工作,导致单进程多线程无法利用多核
- 多进程的线程之间不存在抢GIL的情况,每个进程有一个自己的线程锁,多进程多GIL