0%

动手实现简单的WSGI服务器

作为一个Django开发时常与uWSGI打交道,接触的项目也都是采用nginx+uWSGI+django的部署方式,此篇文章记录下对WSGI的学习。

什么是WSGI

WSGI,全称为“Web Server Gateway Interface”,即 Web 服务器网关接口。它是一个 Python 标准,定义了 Web 服务器与 Web 应用或框架之间的通信协议(规范)。
容易混淆的概念WSGI、uwsgi、uWSGI对比:

  • WSGI: Web服务器与Web框架间的通信协议(规范)
  • uwsgi:uWSGI服务器自有的协议,它用于定义传输信息的类型,每一个uwsgi packet前4byte为传输信息类型描述,用于与nginx等代理服务器通信,它与WSGI相比是两样东西。
  • uWSGI:uWSGI是实现了uwsgi和WSGI两种协议的Web服务器

alt text
上面可以看出WSGI、uwsgi是通信协议或者叫做规范,uWSGI是Web服务器

现实WSGI服务器demo

为了简化对WSGI的理解这里我们先看一个HTTP到WSGI服务器的实现。
参考Python源码中WSGI实现方式的示例lib/wsgiref/simeple_server.py
alt text

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import sys
import socket
from datetime import datetime, timezone


class WSGIServer():
application = None

def __init__(self, host, port) -> None:
self.client_connection = None
self.headers_set = None
self.server_address = (host, port)
self.request_data = None
self.request_method = None
self.path = None
self.server_name = None
self.server_port = None

def get_environ(self):
env = {}
env['wsgi.version'] = (1, 0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = self.request_data
env['wsgi.errors'] = sys.stderr
env['REQUEST_METHOD'] = self.request_method
env['PATH_INFO'] = self.path
env['SERVER_NAME'] = self.server_name
env['SERVER_PORT'] = self.server_port
return env

def handle_one_request(self):
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(self.server_address)
sk.listen(1)
self.client_connection, client_address = sk.accept()
request_data = self.request_data = self.client_connection.recv(1024)
self.request_method, self.path, self.request_version = self.parse_request(request_data) #提取http协议请求头字段
env = self.get_environ()
result = self.application(env, self.start_response) #调用application
self.finish_response(result) #将python对象转换为http协议二进制数据使用socket发送
self.client_connection.close()

def start_response(self, status, response_headers, exc_info=None):
now = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
server_headers = [('Date', now), ('Server', 'WSGIServerCustomer 0.1')]
self.headers_set = [status, response_headers+server_headers]

def finish_response(self, result):
try:
status, response_headers = self.headers_set
response = f'{self.request_version} {status}\r\n'
for header in response_headers:
response += '{0}: {1}\r\n'.format(*header)
response += '\r\n'
for data in result:
response += data.decode()
print(''.join('<{line}\n'.format(line=line) for line in response.splitlines()))
self.client_connection.sendall(response.encode())
finally:
self.client_connection.close()

def parse_request(self, data):
print(''.join('>{line}\n'.format(line=line.decode()) for line in data.splitlines()))
data = data.splitlines()[0]
return data.decode().split()

def get_app(self):
return self.application

def set_app(self,application):
self.application = application

# 实现一个application
def demo_app(environ, start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
# h = sorted(environ.items())
# for k,v in h:
# print(k,'=',repr(v), file=stdout)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]


def make_server(host, port, app):
server = WSGIServer(host, port)
server.set_app(app)
return server


if __name__ == '__main__':
httpd = make_server('', 8000, demo_app)
# 只处理一次请求
httpd.handle_one_request()

运行效果展示

alt text

实现 WSGI服务器 application分离

下面我们将WSGI服务器与Application分离,Application使用Flask.wsgi_app
alt text

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# !/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : neighbour7
# @File : wsgi_server.py

import sys
import socket
from datetime import datetime, timezone


class WSGIServer():
application = None

def __init__(self, host, port) -> None:
self.client_connection = None
self.headers_set = None
self.server_address = (host, port)
self.request_data = None
self.request_method = None
self.path = None
self.server_name = None
self.server_port = None

def get_environ(self):
env = {}
env['wsgi.version'] = (1, 0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = self.request_data
env['wsgi.errors'] = sys.stderr
env['REQUEST_METHOD'] = self.request_method
env['PATH_INFO'] = self.path
env['SERVER_NAME'] = self.server_name
env['SERVER_PORT'] = self.server_port
return env

def handle_one_request(self):
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sk.bind(self.server_address)
sk.listen(1)
self.client_connection, client_address = sk.accept()
request_data = self.request_data = self.client_connection.recv(1024)
self.request_method, self.path, self.request_version = self.parse_request(request_data) #提取http协议请求头字段
env = self.get_environ()
result = self.application(env, self.start_response) #调用application
self.finish_response(result) #将python对象转换为http协议二进制数据使用socket发送
self.client_connection.close()

def start_response(self, status, response_headers, exc_info=None):
now = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
server_headers = [('Date', now), ('Server', 'WSGIServerCustomer 0.1')]
self.headers_set = [status, response_headers+server_headers]

def finish_response(self, result):
try:
status, response_headers = self.headers_set
response = f'{self.request_version} {status}\r\n'
for header in response_headers:
response += '{0}: {1}\r\n'.format(*header)
response += '\r\n'
for data in result:
response += data.decode()
print(''.join('>{line}\n'.format(line=line) for line in response.splitlines()))
self.client_connection.sendall(response.encode())
finally:
self.client_connection.close()

def parse_request(self, data):
data = data.splitlines()[0]
return data.decode().split()

def get_app(self):
return self.application

def set_app(self,application):
self.application = application

# 实现一个application
def demo_app(environ, start_response):
from io import StringIO
stdout = StringIO()
print("Hello world!", file=stdout)
print(file=stdout)
h = sorted(environ.items())
for k,v in h:
print(k,'=',repr(v), file=stdout)
start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]


def make_server(host, port, app):
server = WSGIServer(host, port)
server.set_app(app)
return server


if __name__ == '__main__':
httpd = make_server('', 8000, demo_app)
# 只处理一次请求
httpd.handle_one_request()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# !/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : neighbour7
# @File : flaskapp.py

from flask import Flask, Response

flask_app = Flask('flaskapp')


@flask_app.route('/hello')
def hello_world():

return "Hello World!"

app = flask_app.wsgi_app

Flask.wsgi_app 中接收两个参数(environ, start_response),源码如下:
alt text

运行效果展示
alt text

总结

通过上面的例子明白WSGI实际上就是将HTTP(FastCGI、uwsgi)等协议转化为Python的可执行对象Application
WSGI 抽象了底层的网络通信细节,让开发者能够专注于编写 Web 应用逻辑,而不必处理网络层面的复杂性。

参考:

自己动手开发网络服务器(二):实现WSGI服务
WSGI,uWSGI和uwsgi区别详解
wsgiref/simple_server.py