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

Web服务器网关接口(WSGI),又叫Python Web服务器网关接口(Python Web Server Gateway Interface),是用来描述web服务器与网站应用程序通信和应用程序如何处理请求的规范。它的第一个版本于2003年发布,从那时起,wsgi就成为了python web应用的开发标准,目前最新的版本是v1.0.1,于2010年9月26日发布。

使用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()
			#con.settimeout(self.keepalive)
			raw_request = recvall(con,1024)
			method,uri,httpversion,header,body = httppareser(raw_request)
			environ = header#,**os.environ)
			global response_header

			#start_response
			#gen http response header
			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前添加一个反向代理服务器,静态文件由服务器直接获取,动态请求则交给python来处理,如下:
生产环境的WSGI搭建

我们注意到,在应用程序被调用的时候,wsgi给我们传递了两个参数,一个是组合的字典类型数据environ,另一个是可调用的函数,应用程序通过调用传入的函数来生成HTTP返回的头部信息,通过返回值生成HTTP的body信息。从网络传输层的角度来看,HTTP头部数据和Body数据是一样的数据,只是用两个”\r\n”分隔而已,何不直接返回头部和body信息,徒增一个函数不仅没有带来什么帮助,反而给开发带来更多麻烦。对此python开发了第二代web网关标准,应用程序只传入一个environ参数即可,相关介绍,可以访问PEP0444查看。

发表评论