Django WSGI Server

Django WSGI Server

复习

  • WSGI 协议主要包括 server 和 application 两部分:
    • WSGI server 负责从客户端接收请求,将 request 转发给 application,将 application 返回的 response 返回给客户端
    • WSGI application 接收由 server 转发的 request,处理请求,并将处理结果返回给 server。application中可以包括多个栈式的中间件(middlewares),这些中间件需要同时实现 server 与 application,因此可以在 WSGI 服务器与 WSGI 应用之间起调节作用:对服务器来说,中间件扮演应用程序,对应用程序来说,中间件扮演服务器。
  • WSGI 协议其实是定义了一种 server 与 application 解耦的规范,即可以有多个实现 WSGI server 的服务器,也可以有多个实现 WSGI application 的框架,那么就可以选择任意的 server 和 application 组合实现自己的web应用。例如uWSGI和Gunicorn都是实现了 WSGI server 协议的服务器,Django,Flask是实现了WSGI application 协议的web框架,可以根据项目实际情况搭配使用。

Django 中自带的 WSGI 实现

WSGI Server

找到代码,位于 django/core/servers/basehttp.py

This is a simple server for use in testing or debugging Django apps. It hasn’t been reviewed for security issues. DON’T USE IT FOR PRODUCTION USE!

Django 其内部已经自带了一个方便本地测试的小服务器, 所以在刚开始学习 Django 的时候并不需搭建 apache 或者 nginx 服务器. Django 自带的服务器基于 python wsgiref 模块实现的, 其百分之七八十的代码都是 wsgiref 中的代码, 只重写了一部分, 所以 Django 自带的服务器测试写个 helloworld 就好了,生产时千万不要用。

新增的要点的简析

水平有限,选了一些重要的重写部分来详细学习

异常的处理和日志记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 省略部分代码

logger = logging.getLogger('django.server') # 很眼熟的一个日志模块中的内置logger

def is_broken_pipe_error():
exc_type, exc_value = sys.exc_info()[:2]
return issubclass(exc_type, socket.error) and exc_value.args[0] == 32


class WSGIServer(simple_server.WSGIServer):

def handle_error(self, request, client_address):
if is_broken_pipe_error():
logger.info("- Broken pipe from %s\n", client_address)
else:
super().handle_error(request, client_address)

class WSGIRequestHandler(simple_server.WSGIRequestHandler):

def log_message(self, format, *args):
extra = {
'request': self.request,
'server_time': self.log_date_time_string(),
}
if args[1][0] == '4':
# 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x
if args[0].startswith('\x16\x03'):
extra['status_code'] = 500
logger.error(
"You're accessing the development server over HTTPS, but "
"it only supports HTTP.\n", extra=extra,
)
return

if args[1].isdigit() and len(args[1]) == 3:
status_code = int(args[1])
extra['status_code'] = status_code

if status_code >= 500:
level = logger.error
elif status_code >= 400:
level = logger.warning
else:
level = logger.info
else:
level = logger.info

level(format, *args, extra=extra)

  • 比原先多处理了一个Broken PIPE Error, 这是啥?
  • 可以参考这个博客:https://www.cnblogs.com/yaowen/p/9726357.html
  • 日志根据服务器的状态码来记录,不难理解 log_message 函数,回避 HTTPS 请求,500+ 的是服务器错误,400+ 为警告,其余为正常,状态码非数字用 info 等级(这是啥情况)
  • TODO: 搞明白 args 传进来了啥

HTTP1.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 省略部分代码

class ServerHandler(simple_server.ServerHandler):
http_version = '1.1'

def cleanup_headers(self):
super().cleanup_headers()
# HTTP/1.1 requires support for persistent connections. Send 'close' if
# the content length is unknown to prevent clients from reusing the
# connection.
if 'Content-Length' not in self.headers:
self.headers['Connection'] = 'close'
# Mark the connection for closing if it's set as such above or if the
# application sent the header.
if self.headers.get('Connection') == 'close':
self.request_handler.close_connection = True

  • 注释写的很详细了,关于 Content-Length 为什么会没有?
    • 客户端在 http 头加 Connection:keep-alive时,服务器的 response 是 Transfer-Encoding:chunked 的形式,通知页面数据是否接收完毕,例如长连接或者程序运行中可以动态的输出内容。
    • 在Http 1.0及之前版本中,Content-Length 字段可有可无。
    • 在http1.1及之后版本。如果是 keep alive,则 Content-Length 和 chunked 只能在头里出现一个

重写了单次请求处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class WSGIRequestHandler(simple_server.WSGIRequestHandler):


def handle(self):
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
try:
self.connection.shutdown(socket.SHUT_WR)
except (socket.error, AttributeError):
pass

def handle_one_request(self):
"""Copy of WSGIRequestHandler.handle() but with different ServerHandler"""
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(414)
return

if not self.parse_request(): # An error code has been sent, just exit
return

handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ()
)
handler.request_handler = self # backpointer for logging & connection closing
handler.run(self.server.get_app())
  • //TODO: 理解 close_connection 会因什么而发生变化

RUN 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
server_address = (addr, port)
if threading:
httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {})
else:
httpd_cls = server_cls
httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
if threading:
# ThreadingMixIn.daemon_threads indicates(指出) how threads will behave on an
# abrupt shutdown; like quitting the server by the user or restarting
# by the auto-reloader. True means the server will not wait for thread
# termination before it quits. This will make auto-reloader faster
# and will prevent the need to kill the server manually if a thread
# isn't terminating correctly.
httpd.daemon_threads = True # daemon_thread 守护进程
httpd.set_app(wsgi_handler)
httpd.serve_forever()
  • type() 函数
    • 只有第一个参数则返回对象的类型,三个参数返回新的类型对象。
    • 不会认为子类是一种父类类型,不考虑继承关系。
    • type(name:类名, bases:基类的元组, dict:类内定义的命名空间变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# type不会认为子类是父类的类型,不会考虑继承关系
class A:

def foo(self):
print("You Called A.foo")


class B(A):

def foo(self):
print("You Called B.foo")


class C:

def bar(self):
print("You Called C.bar")


if __name__ == "__main__":
clazz_A = type("A", (A, C), {})
t = clazz_A()
t.bar()
t.foo()
print(type(t))
clazz_B = type("B", (A, C), {})
t = clazz_B()
t.bar()
t.foo()
print(type(t))
clazz_A = type("A", (B, C), {})
t = clazz_A()
t.bar()
t.foo()
print(type(t))

"""
You Called C.bar
You Called A.foo
<class '__main__.A'>
You Called C.bar
You Called A.foo
<class '__main__.B'>
You Called C.bar
You Called B.foo
<class '__main__.A'>
"""
  • 这个就是启动函数,会创建一个 WSGI Server 的实例,和我们在上一篇中的流程图唯一有一个不同就是使用的是 WSGI Handler类,而不是模块里写的一个 demo_app。

  • 参数解析

    • add: 地址,可传入 ip 地址,一般是 127.0.0.1
    • port: 端口,自定义端口
    • wsgi_handler: 上节提到的 application, 在 django.core.handlers 中定义
    • ipv6: 如果为 true, 会将协议地址族换成是 AF_INET6
    • threading: 如果为 true, 服务器类会多继承一个类,这样能多线程处理请求
  • run() 什么时候会被调用呢?当然是在 runserver 时。

get_wsgi_application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.core.wsgi import get_wsgi_application
def get_internal_wsgi_application():

from django.conf import settings
app_path = getattr(settings, 'WSGI_APPLICATION')
if app_path is None:
return get_wsgi_application()

try:
return import_string(app_path)
except ImportError as err:
raise ImproperlyConfigured(
"WSGI application '%s' could not be loaded; "
"Error importing module." % app_path
) from err

# wsgi.py
from django.core.handlers.wsgi import WSGIHandler
def get_wsgi_application():
django.setup(set_prefix=False)
return WSGIHandler()
  • 当你在你的项目中配置了 WSGI_APPLICATION,那么使用你选的。否则用 Django 自己的,就是 WSGIHandler
  • import_string 不难猜测,用字符串来引入一个模块,可以学习一下 Django 对于 Python 模块导入的包装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import copy
import os
from importlib import import_module
from importlib.util import find_spec as importlib_find


def import_string(dotted_path):
"""
Import a dotted module path and return the attribute/class designated by the
last name in the path. Raise ImportError if the import failed.
"""
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError as err:
raise ImportError("%s doesn't look like a module path" % dotted_path) from err

module = import_module(module_path)

try:
return getattr(module, class_name)
except AttributeError as err:
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
module_path, class_name)
) from err

总结

Django-WSGI.png

WSGI Application

  • 篇幅太长了,放到下一篇里吧

参考

Author

Ctwo

Posted on

2020-02-11

Updated on

2020-10-25

Licensed under

Comments