数字营销 · Web开发 · 基础设施

Web服务器网关接口实现原理分析

从简单的 Python Web 应用出发,分析 WSGI 如何连接 Web 服务器与应用程序,并通过实现一个简单的 WSGI 服务器理解请求、环境数据和响应之间的处理过程。

Web 服务器网关接口(WSGI),又叫 Python Web Server Gateway Interface,是用来描述 Web 服务器与 Web 应用程序之间如何通信,以及应用程序如何处理请求的一套规范。它的第一个版本于 2003 年发布,此后 WSGI 逐渐成为 Python Web 应用开发中的重要标准,目前规范版本为 1.0.1。

使用 Python 做过 Web 开发的,对下面的例子应该非常熟悉,运行之后访问网站会显示 Hello, web!

from wsgiref.simple_server import make_server

def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return '<h1>Hello, web!</h1>'

httpd = make_server('', 80, application)
print "Serving HTTP on port 80..."
httpd.serve_forever()

在上面的例子中,几行代码就可以搭建出一个 HTTP 服务器,并没有使用 Apache、Nginx 这样的专用 Web 服务器,而是使用了 wsgiref 中的模块。

对于这个服务来说,application 是用户层代码,而 WSGI 则在 Web 服务器和应用程序之间提供了一套统一的接口。

WSGI流程
标题

也就是说,WSGI 本身并不负责真正的业务处理,它更像是一条标准化的管道。最终处理用户请求的,还是用户层的代码。

这里就有两个问题:

  1. 如何把用户请求交给 application 程序来处理?
  2. 如何把用户的请求数据等信息一并交给 application

WSGI 的处理方式很简单:为每个请求调用一个回调函数,这个回调函数接受两个参数。第一个参数为用户请求和运行环境的数据,第二个参数为 HTTP 响应头的回调函数。

由于这个回调函数就是用户层程序,因此就很容易通过参数的形式获取前端的请求数据等信息:

def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return "Your IP Addr is %s, Your Request Path is %s" % (
environ['REMOTE_ADDR'],
environ['PATH_INFO'][1:] or '/'
)

from wsgiref.simple_server import make_server

httpd = make_server('', 80, application)
print "Serving HTTP on port 80..."
httpd.serve_forever()

这样一来,网站应用程序就可以通过 WSGI 传入的 environ 参数获取用户输入以及其它请求信息,从而给不同的用户返回不同的内容。

实现一个简单的WSGI服务器

只有真正动手实现一个 WSGI 服务器,才能对它有更深入的认识。废话不多说,直接上代码:

#!/usr/bin/env python
# -*- coding:utf-8 -*-

# 对WSGI的理解并对其进行实验
# https://www.python.org/dev/peps/pep-3333/

import socket,re,os

httpre=re.compile(r"^(\w*)\s*([^\s]*)\s*HTTP\/([.\d]*)[\r\n]*([\s\S]+)[\r\n]*[\r\n]*([\s\S]?)$")
headerre=re.compile(r"([^\s]*):\s*([^\r\n]*)[\r\n]")

def recvall(link,buflen):
buf = link.recv(buflen)
if len(buf) < buflen:
return buf
else:
return "%s%s"%(buf,recvall(link,buflen))

def headerdic(str):
mydic = {}
matches = headerre.findall(str)
for line in matches:
mydic[line[0]]=line[1]
return mydic

# http request return
# method, uri, httpversion, header, body
def httppareser(request):
try:
raw_re = httpre.findall(request.strip())[0]
return raw_re[0], raw_re[1], raw_re[2], raw_re[3], raw_re[4]
except:
return None

class WSGIServer:
def __init__(self,host="127.0.0.1",port=8080,keepalive=5):
self.host = host
self.port = port
self.keepalive = keepalive
self.environ = os.environ

def run(self,app):
sockt = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sockt.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sockt.bind((self.host,self.port))
sockt.listen(5)

while True:
con,addr = sockt.accept()
raw_request = recvall(con,1024)
method,uri,httpversion,header,body = httppareser(raw_request)

environ = header
global response_header

# start_response
def start_response(status,response_header_list):
global response_header
headers = ""

for header in response_header_list:
headers = headers+"%s:%s\r\n"%(header[0],header[1])

response_header = "HTTP/1.1 %s\r\n%s\r\n"%(status,headers)

response_body = app(environ,start_response)
con.send(response_header+response_body)
con.close()

# This is the application
def app(environ,start_response):
start_response(
"404 Not Found",
[
("Content-Type","text/html"),
("User-Agent","Mybody")
]
)

return """<!DOCTYPE HTML>
<html>
<head lang="zh-cn">
<meta charset="utf-8">
<title>你好世界</title>
<body>
<h1>Hello World</h1>
</body>
</html>"""

srv = WSGIServer()
srv.run(app)

当然,Python 对静态文件的处理效率还是比较低的,所以通常生产环境的做法是在 WSGI 前增加一个反向代理服务器。静态文件由 Web 服务器直接处理,动态请求则交给 Python 来处理:

生产环境的WSGI搭建

我们注意到,在应用程序被调用的时候,WSGI 给我们传递了两个参数,一个是组合后的字典类型数据 environ,另一个是可调用的函数 start_response

应用程序通过调用传入的函数来生成 HTTP 响应头部信息,再通过返回值生成 HTTP Body。

从网络传输的角度来看,HTTP 头部数据和 Body 最终都是需要通过网络发送的数据,只是它们在 HTTP 协议中承担的作用不同。为什么不直接返回头部和 Body 信息,而要额外传递一个函数?

实际上,这正是 WSGI 接口设计的一部分。通过 environ 传递请求环境,通过 start_response 设置响应头,再通过返回值返回响应内容,可以把 Web 服务器和应用程序之间的职责划分清楚。

对此,Python 后来也提出了新的 Web 网关接口方案,相关介绍可以参考 PEP 0444

整体来看,WSGI 提供的其实就是一套标准接口,让 Web 服务器不需要关心 Python 应用程序内部是如何实现的,Python 应用程序也不需要关心底层 Web 服务器具体是什么。

这也是 WSGI 能够成为 Python Web 开发基础设施的重要原因。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。