yokon's blog

理解Python的Web开发

2018.02.01

因为python代码的优雅美观且易于维护这一特点,越来越多的人选择使用Python做Web开发。而PythonWeb框架百花齐放,目前比较流行的框架有大包大揽的Django,小巧灵活的FlaskBottle,还有性能高效的异步框架Tornadosanic。这么多框架只要选择一个,阅读他的文档,就可以很轻松的搭建一个web app,完全不需要去管他实现的原理。

本篇文章意在对一个web开发做一个梳理。

前端网页三剑客

我们打开浏览器输入一个网址yukunweb.com,然后就看到了浏览器给我们显示的页面,这个时候打开浏览器开发者工具,点击Network,刷新页面,会看到下方的请求的url,点击Response,就可以看到服务器返回给浏览器的html文件信息了。如果复制Response响应的内容,保存为index.html并且在浏览器打开,依然可以看到首页的内容,但是似乎缺少了一些页面的样式和功能。

这是因为当浏览器接收到首页的HTML源码后,它会根据HTML的规则去显示页面,然后再根据HTML里的链接,自动发送HTTP请求给服务器,拿到相应的图片,和JavaScriptCSS等资源,最终显示出一个完整的页面。所以我们会在Network下面能看到很多额外的以.js.css等后缀的请求了。

post28_1.jpg

其实我们看到的页面就是浏览器按照HTML的规则,展示给我们的。HTML告诉浏览器那里是导航,那里是主栏,那里是侧栏。而这些信息如何显示,或者是显示的样式,就是CSS文件的功劳。至于比如导航的下拉隐藏上拉显示就是JavaScript的作用。

如果想要做Web开发,就一定得熟悉HTMLCSSJavaScript三剑客的知识,这里推荐W3school的前端教程,也是我学习前端的地方:W3school

客户端和服务器通信

理解了前段三剑客,就知道如何去写一个网页。那么从我们在浏览器的地址栏输入URL,到Web页面呈现出来到底经历了什么。

post28_2.jpg

如图,一般这种通过发送请求获取服务器资源的Web浏览器,都可以称为客户端(client)。首先发送一个请求(request)给服务器,大多是以GET请求方式访问,服务器接收到你的请求,然后取到请求的资源,返回给客户端。

服务器和客户端之间交流是怎么进行的呢,服务器是怎么理解客户端的请求的呢。这里就需要一种协议规范,就是HTTP(HyperText Transfer Protocol,超文本传输协议)。可以说,Web是建立在HTTP协议上通信的。

post28_3.jpg

如图,仍然是之前的例子,打开浏览器访问yukunweb.com,打开浏览器开发者工具,点击图中标记的选项卡(记得点view parsed),可以看到客户端发给服务器的请求头前两行。

GET / HTTP/1.1
Host: www.yukunweb.com

第一行开头的GET表示请求访问服务器的类型,称为方法(method)。随后的字符/指明了请求访问的资源对象,即请求URI。最后的HTTP/1.1,即HTTP的版本号,用来提示客户端使用的HTTP协议功能。

综上所述,第一行请求内容的意思是:请求访问某台HTTP服务器上的/(首页)页面资源。所以第二行的Host表示请求的域名也就是服务器所在地址。

post28_4.jpg

如图,如果是POST请求的话,不仅会有请求头部信息,还有一个Form Data的请求实体内容。

接收到请求的服务器呢,他会将请求内容的处理结果以响应的形式返回,看图中的第一行:

HTTP/1.1 200 OK

开头的部分仍然是服务器对应的HTTP版本,紧接着的200 OK表示请求的处理结果的状态码 (status code) 和原因短语。200状态码就表示响应成功,常见的404表示访问错误,500表示服务器响应错误。这里的OK是没有固定的规则的,你也可以让他返回GOOD啥的。

下一行是服务器信息,本站用的是Nginx服务器,在下一行显示了创建响应的日期时间。在下一行的Content-Type表示内容的类型,客户端会依赖他判断响应的内容是网页还是音频,图片等类型。

这里只是简单的介绍了HTTP协议,即是客户端与服务器之间的通信协议。如果想要深入了解推荐阅读《HTTP权威指南》。

WSGI

如果你浏览一个地址http://www.yukunweb.com/search-result/?keywords=音乐,你会访问到本站的音乐关键词的搜索结果。我们知道客户端发送请求给服务器,那么服务器是怎么拿到资源的呢。其实这是交给后端运行的应用返回的,好比你抓取一个页面到获取到信息,这些逻辑的处理肯定是我们的程序再跑。

但是,接收并且解析客户端的HTTP请求在发送HTTP响应这些底层操作,后端的程序肯定是不会去处理的。所以,要想只专注于Web业务逻辑,还需要一个服务器和web应用之间的嫁接层————WSGI。

什么是WSGI(Web Server Gateway Interface)?

WSGI翻译过来就是Web服务器网关接口。他只是一个规范,定义了Web服务器如何与Python应用程序进行交互,使得使用Python写的Web应用程序可以和Web服务器(nginx/apache)对接起来。

该规范的地址:PEP 333

WSGIPython的Web开发的基石,有了它你就有了一切,它存在的目的有两个:

  • 描述 Web 服务器如何与 Web 应用程序交互(将客户端请求传给应用程序),
  • 描述 Web 应用程序如何处理请求和如何返回数据给服务器。

由于Python内置的标准库里有一个WSGIwsgiref,我们基于他来写一个体现WSGI目的的例子:

from wsgiref.simple_server import make_server

def application(environ, start_response):
	status = '200 OK'
	response_headers = [('Content-type', 'text/html')]
	start_response(status, response_headers)
	body = '<h1>Hello, {name} !!!</h1>'.format(name=environ['PATH_INFO'][1:] or 'WSGI')
	return [body.encode('utf-8')]

app = make_server('', 8000, application)
app.serve_forever()

运行程序,如果没有报错,此时打开浏览器输入地址127.0.0.1:8000127.0.0.1:8000/GuTianle,就可以看到程序返回的页面了。如图:

post28_5.jpg

我们可以看到一个请求,他的入口只需要一个WSGI的处理函数。因为所有的请求信息都包含在environ中,这样我们就可以根据这些信息去返回不同的数据。

参数:

  • environ:字典类型,存放了所有和客户端相关的信息。如果想知道他里面有哪些参数,可以更改上面的代码在 return 行上面加一个for k, v in environ.items()的循环,打印出字典里的所有参数。
  • start_response:一个可调用对象,接收两个必选参数和一个可选参数:
    • status: 一个字符串,表示 HTTP 响应状态字符串,如 200,404
    • response_headers: 一个列表,包含有如下形式的元组:(header_name, header_value),用来表示 HTTP 响应的 headers ,如(‘Content-type’, ’text/html’)
    • exc_info(可选): 用于出错时,服务器需要返回给浏览器的信息

返回:一个可迭代对象, 服务器通过遍历这个可迭代对象可以获得body的全部内容,内容可以是html也可以是json

这里简单的介绍了WSGI是什么,干什么。如果理解了WSGI,那么写一个Python的Web框架就很简单了。这也是为什么Python有成百上千web框架的原因。

实现基于WSGI的框架

上面我们理解了WSGI是干什么的,那么我们基于它实现一个简单的web框架可以说轻而易举了。

from wsgiref.simple_server import make_server

class Application(object):

	def __init__(self, environ, start_response):
		self.start_response = start_response
		self.path = environ['PATH_INFO']

	def __iter__(self):
		if self.path == '/':
			status = '200 OK'
			response_headers = [('Content-type', 'text/html')]
			self.start_response(status, esponse_headers)
			yield '<h1>Hello,World!</h1>'.encode('utf-8')

		elif self.path == '/wsgi':
			status = '200 OK'
			response_headers = [('Content-type', 'text/html')]
			self.start_response(status, response_headers)
			yield '<h1>Hello,WSGI!</h1>'.encode('utf-8')

		else:
			status = '404 NOT FOUND'
			response_headers = [('Content-type', 'text/html')]
			self.start_response(status, response_headers)
			yield '<h1>404 NOT FOUND</h1>'.encode('utf-8')

if __name__ == "__main__":
	app = make_server('127.0.0.1', 8000, Application)
	print('Serving HTTP on port 8000...')
	app.serve_forever()

这个Application类只不过是对WSGI又做了一层简单的封装而已,由于上面说过WSGI函数返回的是一个可以迭代对象,所以需要实现一个__iter__方法,里面控制了客户端的请求路由并且返回不同的输出。

当然如果你想扩展成一个像样的框架还需要考虑很多,比如像flask那样方便的路由系统,还有对于用户请求方式的处理等等。总之是个很需要折腾的过程,好比flask的0.1版本去掉注释也就 200 多行,而如今最新版本。。。